mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-18 17:16:46 +03:00
Refine product and visual work lifecycle
This commit is contained in:
+81
-29
@@ -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 !!(
|
||||
|
||||
Reference in New Issue
Block a user