Refine product and visual work lifecycle

This commit is contained in:
Paul Bakaus
2026-07-17 16:10:13 -07:00
parent bbed6eef08
commit 77c7d8e0fc
53 changed files with 1464 additions and 1761 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"craft": {
"description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.",
"description": "Deprecated compatibility alias for an ordinary Impeccable new-work request. It adds no behavior; natural build and redesign requests use the same flow.",
"argumentHint": "[feature description]"
},
"init": {
+43 -16
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env node
/**
* Surface-concept seed: the dice half of new-work's task-composition procedure.
* External concept seed: the dice half of new-work's world and surface
* selection procedures.
*
* The model derives a grounded shortlist of candidate FORMS from the
* audience's world and the subject's cultural home (see
@@ -22,8 +23,8 @@
* material and win over thin categories, which is the intended shape.
*
* Usage:
* node scripts/concept-seed.mjs # roll at random
* node scripts/concept-seed.mjs --from <key> # deterministic (hash key)
* node scripts/concept-seed.mjs --scope surface
* node scripts/concept-seed.mjs --scope world --from <key>
*
* Env vars:
* IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs.
@@ -39,6 +40,12 @@ const pool = JSON.parse(readFileSync(join(here, 'concept-ingredients.json'), 'ut
const args = process.argv.slice(2);
const fromIdx = args.indexOf('--from');
const scopeIdx = args.indexOf('--scope');
const scope = scopeIdx !== -1 ? args[scopeIdx + 1] : 'surface';
if (scope !== 'surface' && scope !== 'world') {
process.stderr.write('concept-seed: --scope must be world or surface\n');
process.exit(1);
}
// When no key is supplied, generate one and print it: a user reporting a
// bad outcome can hand us the key and we replay the exact roll.
const key = fromIdx !== -1
@@ -46,7 +53,7 @@ const key = fromIdx !== -1
: (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'));
function hashUnit(k, salt) {
const h = crypto.createHash('sha256').update(`${salt}:${k}`).digest();
const h = crypto.createHash('sha256').update(`${scope}:${salt}:${k}`).digest();
return h.readUInt32BE(0) / 0xffffffff;
}
const unit = (salt) => hashUnit(key, salt);
@@ -66,24 +73,44 @@ for (let i = 0; picks.length < 3 && i < 60; i++) {
}
}
process.stdout.write(`CONCEPT SEED (key: ${key}; rerun with --from ${key} to reproduce this roll)
PROMOTED INDEX: ${buildIndex}
After ordering the task's grounded structural candidates by resonance,
const promotedInstruction = scope === 'world'
? `After ordering the grounded visual-world candidates by product fit, promote
candidate ${buildIndex} into the serious shortlist. Present it beside the
strongest materially different candidates and let the user select or revise
the durable world. It must survive navigation, quiet and dense content,
interaction and state, and a surface unlike the current request.`
: `After ordering the task's grounded structural candidates by resonance,
promote candidate ${buildIndex} into the serious shortlist. In an attended
run, present it beside the strongest materially different candidates and
let the user select or revise the surface concept. In a truly unattended
run, use it when it survives audience identification and product clarity.
run, use it when it survives audience identification and product clarity.`;
const challengerInstruction = scope === 'world'
? `A challenger enters the world shortlist only when its structure can become
reusable identity grammar across the product, not a one-page costume. Weigh
product identification, product clarity, and cross-surface system breadth.`
: `A challenger wins only when it beats the grounded list on both audience
identification and product clarity. It may change task topology or
interaction, but never the committed visual identity.`;
const authorityInstruction = scope === 'world'
? `PRODUCT.md and explicit incumbent brand commitments constrain every world.
The seed never chooses exact colors, fonts, tokens, or a user preference.`
: `PRODUCT.md and DESIGN.md constrain every surface candidate's identity
vocabulary; they do not cancel task-level composition. The seed never
authorizes a new palette, type system, material world, or unfamiliar control
behavior.`;
process.stdout.write(`${scope.toUpperCase()} CONCEPT SEED (key: ${key}; rerun with --scope ${scope} --from ${key} to reproduce this roll)
PROMOTED INDEX: ${buildIndex}
${promotedInstruction}
The promotion exists to refuse the model's ranking rut, not to outrank the
user or the brief.
CHALLENGERS (weigh against your derived candidates on the same two axes,
audience identification and product clarity; a challenger wins only when
it beats the grounded list on both):
CHALLENGERS:
1. ${picks[0]}
2. ${picks[1]}
3. ${picks[2]}
If a challenger survives, it may enter the shortlist as a structural option.
PRODUCT.md and DESIGN.md constrain every candidate's identity vocabulary;
they do not cancel task-level composition. A user- or brief-pinned surface
concept beats the roll, always. The seed never authorizes a new palette,
type system, material world, or unfamiliar control behavior.
${challengerInstruction}
${authorityInstruction}
A user- or brief-pinned decision beats the roll, always.
`);
+1 -1
View File
@@ -11,7 +11,7 @@
* output is always valid JSON.
*
* Signals:
* - setup: PRODUCT.md / DESIGN.md presence, register, whether code exists
* - setup: PRODUCT.md / DESIGN.md presence and whether code exists
* - critique: the latest cached critique score (.impeccable/critique)
* - git: branch + files changed vs the default branch (a scope hint)
* - devServer: whether a local dev server answers on a common port (gates live)
+81 -29
View File
@@ -1,8 +1,10 @@
/**
* Context loader: prints PRODUCT.md (and DESIGN.md if present) as one
* markdown block on stdout, or prints a `NO_PRODUCT_MD:` message when no
* Context loader: prints PRODUCT.md, DESIGN.md when present, the matching
* persisted surface brief when one can be resolved, and native-platform
* guidance selected from PRODUCT.md. It prints a
* `NO_PRODUCT_MD:` message when no
* PRODUCT.md is found anywhere. The skill keys off that message to branch:
* from-scratch build commands (init / teach / craft / shape) and clear
* from-scratch build requests (plus init / teach / shape) and clear
* build/shape intent divert into the init flow, while scoped commands proceed
* using the existing code as context.
*
@@ -24,9 +26,11 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseTargetOptions } from './lib/target-args.mjs';
import { IMPECCABLE_COMMAND } from './lib/provider.mjs';
import { resolveSurfaceBrief } from './lib/surface-briefs.mjs';
const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md'];
const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md'];
const SKILL_REFERENCE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'reference');
const FALLBACK_DIRS = ['.agents/context', 'docs'];
const MONOREPO_MARKER_FILES = ['pnpm-workspace.yaml', 'turbo.json', 'nx.json', 'lerna.json'];
const MONOREPO_FALLBACK_PROJECT_DIRS = ['apps', 'packages'];
@@ -75,6 +79,12 @@ export function loadContext(cwd = process.cwd(), options = {}) {
const designPath = resolved.designPath;
const product = productPath ? safeRead(productPath) : null;
const design = designPath ? safeRead(designPath) : null;
const platform = extractPlatform(product);
const surfaceResolution = resolveSurfaceBrief(
resolved.projectRoot,
hasTargetOption(options) ? options.targetPath : null,
);
const surfaceBrief = surfaceResolution.brief;
return {
hasProduct: !!product,
product,
@@ -85,7 +95,18 @@ export function loadContext(cwd = process.cwd(), options = {}) {
contextDir: resolved.contextDir,
productContextDir: productPath ? path.dirname(productPath) : null,
designContextDir: designPath ? path.dirname(designPath) : null,
hasSurfaceBrief: !!surfaceBrief,
surfaceBrief: surfaceBrief?.text ?? null,
surfaceBriefPath: surfaceBrief?.path ? path.relative(absCwd, surfaceBrief.path) : null,
surfaceBriefReason: surfaceResolution.reason,
surfaceBriefCandidates: surfaceResolution.candidates.map((brief) => ({
slug: brief.slug,
path: path.relative(absCwd, brief.path),
primaryTarget: brief.primaryTarget,
relatedTargets: brief.relatedTargets,
})),
hasVisualImplementation: hasVisualImplementation(resolved.projectRoot),
platform,
projectRoot: resolved.projectRoot,
repoRoot: resolved.repoRoot,
isMonorepo: resolved.isMonorepo,
@@ -698,6 +719,19 @@ function safeRead(p) {
}
}
function loadNativePlatformReferences(platform) {
const names = platform === 'adaptive'
? ['ios', 'android']
: platform === 'ios' || platform === 'android'
? [platform]
: [];
return names.flatMap((name) => {
const filePath = path.join(SKILL_REFERENCE_DIR, `${name}.md`);
const content = safeRead(filePath);
return content ? [{ name, filePath, content }] : [];
});
}
/**
* Best-effort evidence that the project already has an incumbent visual
* implementation. DESIGN.md is documentation, not the only source of design
@@ -989,13 +1023,13 @@ async function cli() {
const parts = ctx.hasVisualImplementation
? [
'NO_PRODUCT_MD: This project has no PRODUCT.md yet, but it does have an incumbent visual implementation. ' +
'For `init`, `teach`, `craft`, or `shape`, load reference/init.md and create PRODUCT.md with the user first. ' +
'For extension, init documents the incumbent system; for redesign/rebrand, init replaces it through a new ' +
'visual-world choice. Other ' +
'For `init`, `teach`, `shape`, or any request to create a new surface or replacement visual world, load reference/init.md and create PRODUCT.md with the user first. ' +
'After init writes PRODUCT.md, reference/new-work.md preserves and documents the incumbent system for an ' +
'extension or replaces it with the user for a redesign/rebrand. Other ' +
'narrow refinement commands may read the CSS, tokens, components, and assets and proceed without blocking, then ' +
`offer \`${IMPECCABLE_COMMAND} init\` as a follow-up.`,
'BUILD_INIT_REQUIRED: Before `craft` or `shape`, init must capture PRODUCT.md with the human or structured ' +
'simulated user. A redesign then replaces the visual world; an extension documents it.',
'BUILD_INIT_REQUIRED: Before shape or any new-surface/redesign flow, init must capture PRODUCT.md with the human or structured ' +
'simulated user. Init writes product truth only; reference/new-work.md owns every visual decision.',
'SCOPED_EXISTING_ALLOWED: Narrow refinement commands may use the incumbent implementation as authority without ' +
'blocking on context setup; they must preserve it and offer init afterward.',
'EXISTING_VISUAL_SYSTEM: For refinement or extension, code and assets are incumbent design authority and missing ' +
@@ -1004,17 +1038,18 @@ async function cli() {
]
: [
'NO_PRODUCT_MD: This project has no PRODUCT.md yet. ' +
'For `init`, `teach`, `craft`, `shape`, ' +
'For `init`, `teach`, `shape`, ' +
'or wording that clearly maps to a from-scratch build/shape flow, load ' +
'reference/init.md, complete its human or structured simulated-user interview, and write PRODUCT.md plus the ' +
'user-chosen seed DESIGN.md before building. If no answer mechanism truly exists, init may infer only from the ' +
'explicit brief, label its assumptions, and still write both files. For any other ' +
'reference/init.md, complete its human or structured simulated-user interview, and write PRODUCT.md before ' +
'designing. If no answer mechanism truly exists, init may infer only from the explicit brief and must label its ' +
'assumptions. It never writes DESIGN.md. For any other ' +
'(scoped) command against existing code, proceed using the code as ' +
`context and offer \`${IMPECCABLE_COMMAND} init\` as a suggestion (do not block).`,
'IDENTITY_INIT_REQUIRED: No committed product or visual world was found. New builds and redesigns ' +
'must finish reference/init.md before reference/new-work.md develops the task-specific surface concept. Scoped ' +
'PRODUCT_INIT_REQUIRED: No product context or visual authority was found. New builds and redesigns ' +
'must finish reference/init.md for PRODUCT.md, then reference/new-work.md establishes the world and surface. Scoped ' +
'fixes to existing code do not need the new-surface flow.',
];
appendSurfaceBriefContext(parts, ctx);
parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists }));
if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) {
parts.push(buildMissingTargetDirective());
@@ -1027,30 +1062,28 @@ async function cli() {
if (ctx.hasDesign) {
parts.push(`# DESIGN.md\n\n${ctx.design.trim()}`);
}
appendSurfaceBriefContext(parts, ctx);
parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists }));
if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) {
parts.push(buildMissingTargetDirective());
}
if (!ctx.hasDesign) {
parts.push(ctx.hasVisualImplementation
? 'BUILD_DESIGN_DOCUMENT_REQUIRED: PRODUCT.md exists and DESIGN.md is missing, but code contains incumbent visual decisions. ' +
'Before `craft` or `shape`, load reference/init.md Step 5. For extension, document CSS, tokens, components, and ' +
'assets as the incumbent world. For redesign/rebrand, replace the visual world with the user and treat the old ' +
'look only as evidence and anti-reference. Narrow refinement commands may proceed using the implementation directly.'
: 'IDENTITY_INIT_REQUIRED: PRODUCT.md exists but no DESIGN.md or incumbent visual implementation was found. ' +
'A new build or redesign must complete reference/init.md Step 5 with the human or structured simulated ' +
'user before reference/new-work.md develops the task-specific surface concept. Scoped fixes to existing code do not need it.');
? 'INCUMBENT_WORLD_UNDOCUMENTED: PRODUCT.md exists and DESIGN.md is missing, but code contains incumbent visual decisions. ' +
'For shape or a new-surface/redesign request, load reference/new-work.md: an extension documents and preserves the code-defined world; ' +
'a redesign replaces it with the user and uses the old look only as evidence and anti-reference. Narrow refinement ' +
'commands may proceed using the implementation directly.'
: 'WORLD_DISCOVERY_REQUIRED: PRODUCT.md exists but no DESIGN.md or incumbent visual implementation was found. ' +
'For a new build or redesign, load reference/new-work.md and establish the visual world with the human or structured ' +
'simulated user before developing the task concept. Scoped fixes to existing code do not need this flow.');
}
const platform = extractPlatform(ctx.product);
const nativeRefs =
platform === 'adaptive' ? ['ios', 'android'] : platform === 'ios' || platform === 'android' ? [platform] : [];
if (nativeRefs.length) {
const refList = nativeRefs.map(p => `\`reference/${p}.md\``).join(' and ');
const label = platform === 'adaptive' ? '`adaptive` (both iOS and Android)' : `\`${platform}\``;
const platformReferences = loadNativePlatformReferences(ctx.platform);
for (const reference of platformReferences) {
parts.push(
`NEXT STEP: This project targets ${label}. Also read ${refList} for native conventions, in addition to SKILL.md's mode guidance.`,
`# NATIVE PLATFORM REFERENCE: ${reference.name.toUpperCase()} (reference/${reference.name}.md)\n\n${reference.content.trim()}`,
);
} else if (!platform) {
}
if (!ctx.platform) {
// A `## Platform` section that names something we don't recognize (a
// toolchain like `flutter`, a typo) would otherwise silently fall back to
// web — the wrong default exactly when the user tried to say "native".
@@ -1087,10 +1120,29 @@ function buildResolvedContextDirective(ctx, options, { targetExists = null } = {
repoRoot: ctx.repoRoot,
productPath: ctx.productPath,
designPath: ctx.designPath,
surfaceBriefPath: ctx.surfaceBriefPath,
surfaceBriefReason: ctx.surfaceBriefReason,
surfaceBriefCandidates: ctx.surfaceBriefCandidates,
hasVisualImplementation: ctx.hasVisualImplementation,
platform: ctx.platform,
}, null, 2)}`;
}
function appendSurfaceBriefContext(parts, ctx) {
if (ctx.hasSurfaceBrief && ctx.surfaceBrief) {
parts.push(`# SURFACE BRIEF (${ctx.surfaceBriefPath})\n\n${ctx.surfaceBrief.trim()}`);
return;
}
if (!ctx.surfaceBriefCandidates?.length) return;
const helper = path.join(path.dirname(fileURLToPath(import.meta.url)), 'surface-brief.mjs');
parts.push(
'SURFACE_CONTEXT_AVAILABLE: Persisted surface briefs exist, but none was selected unambiguously for this invocation. ' +
'Resolve the requested surface to its concrete primary or related source path, then run ' +
`\`node ${helper} read <path>\` once before changing that surface. Candidates:\n` +
JSON.stringify(ctx.surfaceBriefCandidates, null, 2),
);
}
function shouldWarnMissingTarget(ctx, targetProvided, targetExists = null) {
if (ctx.isMonorepo && targetProvided && targetExists === false) return true;
return !!(
+2 -41
View File
@@ -29,8 +29,9 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { getCritiqueDir } from './lib/impeccable-paths.mjs';
import { slugFromTarget } from './lib/target-slug.mjs';
const SLUG_MAX = 50;
export { slugFromTarget } from './lib/target-slug.mjs';
/**
* Mechanically derive a slug from a resolved target. Returns null if the
@@ -40,46 +41,6 @@ const SLUG_MAX = 50;
* concrete artifact before calling this — we never slug a natural-language
* phrase.
*/
export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
if (!resolved || typeof resolved !== 'string') return null;
const trimmed = resolved.trim();
if (!trimmed) return null;
// URL
if (/^https?:\/\//i.test(trimmed)) {
let url;
try { url = new URL(trimmed); } catch { return null; }
const hostPath = `${url.hostname}${url.pathname}`;
return kebab(hostPath);
}
// File path. Make it project-relative so two devs critiquing the same
// checkout get the same slug regardless of where their repo is cloned.
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
let rel = path.relative(cwd, abs);
// If the target is outside cwd, fall back to the basename so we still
// produce a stable slug (vs the absolute path, which would include
// home dirs / usernames).
if (rel.startsWith('..') || path.isAbsolute(rel)) {
rel = path.basename(abs);
}
if (!rel || rel === '.' || rel === '') return null;
return kebab(rel);
}
function kebab(s) {
const slug = s
.toLowerCase()
.replace(/[/\\.]+/g, '-')
.replace(/[^a-z0-9-]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
if (!slug) return null;
// Cap from the tail — the tail (filename) is more identifying than the
// top-level directory.
return slug.length <= SLUG_MAX ? slug : slug.slice(slug.length - SLUG_MAX).replace(/^-/, '');
}
/**
* Filename-safe UTC ISO timestamp: hyphens for separators, trailing Z.
* Plain colons aren't allowed on Windows filesystems.
+151
View File
@@ -0,0 +1,151 @@
import fs from 'node:fs';
import path from 'node:path';
import { slugFromTarget } from './target-slug.mjs';
export const SURFACE_BRIEF_VERSION = 1;
export function getSurfaceBriefDir(projectRoot) {
return path.join(projectRoot, '.impeccable', 'surfaces');
}
export function normalizeSurfaceTarget(target, { projectRoot = process.cwd() } = {}) {
if (!target || typeof target !== 'string' || !target.trim()) return null;
const trimmed = target.trim();
if (/^https?:\/\//i.test(trimmed)) {
try {
const url = new URL(trimmed);
url.hash = '';
url.search = '';
return url.toString().replace(/\/$/, '') || url.origin;
} catch {
return null;
}
}
if (/^route:/i.test(trimmed)) {
const route = trimmed.slice(trimmed.indexOf(':') + 1).trim();
if (!route.startsWith('/') || route.includes('..')) return null;
const normalizedRoute = route.split(/[?#]/, 1)[0].replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/';
return `route:${normalizedRoute}`;
}
if (trimmed === '/') return 'route:/';
if (trimmed.startsWith('/')) {
const absolute = path.resolve(trimmed);
const relativeToProject = path.relative(projectRoot, absolute);
const isProjectFile = relativeToProject && !relativeToProject.startsWith('..') && !path.isAbsolute(relativeToProject);
if (!isProjectFile && !fs.existsSync(absolute) && !trimmed.includes('..')) {
const normalizedRoute = trimmed.split(/[?#]/, 1)[0].replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/';
return `route:${normalizedRoute}`;
}
}
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(projectRoot, trimmed);
const rel = path.relative(projectRoot, abs);
if (!rel || rel === '.' || rel.startsWith('..') || path.isAbsolute(rel)) return null;
return rel.split(path.sep).join('/');
}
export function surfaceBriefPathForTarget(target, { projectRoot = process.cwd() } = {}) {
const normalized = normalizeSurfaceTarget(target, { projectRoot });
if (!normalized) return null;
const slugInput = normalized.startsWith('route:') ? `route${normalized.slice('route:'.length)}` : normalized;
const slug = slugFromTarget(slugInput, { cwd: projectRoot });
return slug ? path.join(getSurfaceBriefDir(projectRoot), `${slug}.md`) : null;
}
export function parseSurfaceBrief(text, filePath = null) {
const match = String(text || '').match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
const meta = {};
if (match) {
for (const line of match[1].split(/\r?\n/)) {
const colon = line.indexOf(':');
if (colon < 0) continue;
const key = line.slice(0, colon).trim();
const raw = line.slice(colon + 1).trim();
if (!key) continue;
if (/^(?:\[|\{|\")/.test(raw) || /^(?:true|false|null|-?\d+(?:\.\d+)?)$/.test(raw)) {
try { meta[key] = JSON.parse(raw); continue; } catch { /* keep string */ }
}
meta[key] = raw.replace(/^['"]|['"]$/g, '');
}
}
const primaryTarget = typeof meta.primary_target === 'string' ? meta.primary_target : null;
const relatedTargets = Array.isArray(meta.related_targets)
? meta.related_targets.filter((value) => typeof value === 'string')
: [];
return {
path: filePath,
text: String(text || ''),
body: match ? String(text || '').slice(match[0].length).trim() : String(text || '').trim(),
meta,
slug: typeof meta.slug === 'string' ? meta.slug : filePath ? path.basename(filePath, '.md') : null,
primaryTarget,
relatedTargets,
targets: [primaryTarget, ...relatedTargets].filter(Boolean),
};
}
export function listSurfaceBriefs(projectRoot = process.cwd()) {
const dir = getSurfaceBriefDir(projectRoot);
let names;
try {
names = fs.readdirSync(dir).filter((name) => name.endsWith('.md')).sort();
} catch {
return [];
}
return names.flatMap((name) => {
const filePath = path.join(dir, name);
try {
return [parseSurfaceBrief(fs.readFileSync(filePath, 'utf-8'), filePath)];
} catch {
return [];
}
});
}
export function resolveSurfaceBrief(projectRoot = process.cwd(), target = null) {
const briefs = listSurfaceBriefs(projectRoot);
if (!target) {
return {
brief: briefs.length === 1 ? briefs[0] : null,
candidates: briefs,
reason: briefs.length === 1 ? 'only-brief' : briefs.length > 1 ? 'ambiguous' : 'none',
};
}
const normalized = normalizeSurfaceTarget(target, { projectRoot });
if (!normalized) return { brief: null, candidates: briefs, reason: 'invalid-target' };
const exactPath = surfaceBriefPathForTarget(normalized, { projectRoot });
const exact = briefs.find((brief) => brief.path === exactPath && (!brief.targets.length || brief.targets.includes(normalized)));
if (exact) return { brief: exact, candidates: briefs, reason: 'slug' };
const mapped = briefs.filter((brief) => brief.targets.includes(normalized));
return {
brief: mapped.length === 1 ? mapped[0] : null,
candidates: mapped.length > 1 ? mapped : briefs,
reason: mapped.length === 1 ? 'mapping' : mapped.length > 1 ? 'ambiguous-target' : 'not-found',
};
}
export function writeSurfaceBrief({
projectRoot = process.cwd(),
primaryTarget,
relatedTargets = [],
body,
}) {
const normalizedPrimary = normalizeSurfaceTarget(primaryTarget, { projectRoot });
if (!normalizedPrimary) throw new Error('surface brief requires a concrete project-relative primary target or URL');
const normalizedRelated = [...new Set(relatedTargets
.map((target) => normalizeSurfaceTarget(target, { projectRoot }))
.filter((target) => target && target !== normalizedPrimary))];
const slug = slugFromTarget(normalizedPrimary, { cwd: projectRoot });
const filePath = surfaceBriefPathForTarget(normalizedPrimary, { projectRoot });
fs.mkdirSync(path.dirname(filePath), { recursive: true });
const frontmatter = [
'---',
`version: ${SURFACE_BRIEF_VERSION}`,
`slug: ${JSON.stringify(slug)}`,
`primary_target: ${JSON.stringify(normalizedPrimary)}`,
`related_targets: ${JSON.stringify(normalizedRelated)}`,
'---',
].join('\n');
fs.writeFileSync(filePath, `${frontmatter}\n\n${String(body || '').trim()}\n`, 'utf-8');
return filePath;
}
+33
View File
@@ -0,0 +1,33 @@
import path from 'node:path';
const SLUG_MAX = 50;
/** Derive one clone-stable slug from a concrete file path or URL. */
export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
if (!resolved || typeof resolved !== 'string') return null;
const trimmed = resolved.trim();
if (!trimmed) return null;
if (/^https?:\/\//i.test(trimmed)) {
let url;
try { url = new URL(trimmed); } catch { return null; }
return kebab(`${url.hostname}${url.pathname}`);
}
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
let rel = path.relative(cwd, abs);
if (rel.startsWith('..') || path.isAbsolute(rel)) rel = path.basename(abs);
if (!rel || rel === '.') return null;
return kebab(rel);
}
export function kebab(value) {
const slug = String(value || '')
.toLowerCase()
.replace(/[/\\.]+/g, '-')
.replace(/[^a-z0-9-]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
if (!slug) return null;
return slug.length <= SLUG_MAX ? slug : slug.slice(slug.length - SLUG_MAX).replace(/^-/, '');
}
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { resolveProjectRoot } from './context.mjs';
import {
listSurfaceBriefs,
resolveSurfaceBrief,
surfaceBriefPathForTarget,
writeSurfaceBrief,
} from './lib/surface-briefs.mjs';
function summary(brief, projectRoot) {
return {
slug: brief.slug,
path: path.relative(projectRoot, brief.path).split(path.sep).join('/'),
primaryTarget: brief.primaryTarget,
relatedTargets: brief.relatedTargets,
};
}
function main(argv) {
const [command, target, bodyFile, ...relatedTargets] = argv;
const projectRoot = resolveProjectRoot(process.cwd(), target ? { targetPath: target } : {});
if (command === 'path') {
const filePath = surfaceBriefPathForTarget(target, { projectRoot });
if (!filePath) throw new Error('surface brief path requires a concrete target');
process.stdout.write(`${path.relative(process.cwd(), filePath) || filePath}\n`);
return;
}
if (command === 'list') {
process.stdout.write(`${JSON.stringify(listSurfaceBriefs(projectRoot).map((brief) => summary(brief, projectRoot)), null, 2)}\n`);
return;
}
if (command === 'read') {
const result = resolveSurfaceBrief(projectRoot, target || null);
if (result.brief) {
process.stdout.write(result.brief.text);
return;
}
if (result.candidates.length) process.stderr.write(`${JSON.stringify(result.candidates.map((brief) => summary(brief, projectRoot)), null, 2)}\n`);
process.exit(2);
}
if (command === 'write') {
if (!target || !bodyFile) throw new Error('usage: surface-brief.mjs write <primary-target> <body-file>');
const filePath = writeSurfaceBrief({
projectRoot,
primaryTarget: target,
relatedTargets,
body: fs.readFileSync(bodyFile, 'utf-8'),
});
process.stdout.write(`${path.relative(process.cwd(), filePath) || filePath}\n`);
return;
}
throw new Error('usage: surface-brief.mjs <path|list|read|write> [target] [body-file] [related-target ...]');
}
function isMainModule() {
if (!process.argv[1]) return false;
try {
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(process.argv[1]);
} catch {
return import.meta.url === pathToFileURL(process.argv[1]).href;
}
}
if (isMainModule()) {
try {
main(process.argv.slice(2));
} catch (error) {
process.stderr.write(`${error?.message || error}\n`);
process.exit(1);
}
}