mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-21 18:47:02 +03:00
Sync generated provider output
This commit is contained in:
@@ -434,7 +434,7 @@ export function renderConceptSeed({
|
||||
lead carrying kicker THE ROLL, with steer and re-roll, and let the user
|
||||
lock one in; the world is already settled, so this choice is composition.
|
||||
Visualize every dealt card: with image generation available and a
|
||||
comp-led default (.impeccable/settings.json buildPath; the page toggle
|
||||
comp-led default (.impeccable/config.json buildPath; the page toggle
|
||||
handles the exception), declare a comp per card and generate after
|
||||
serving, lead first; otherwise author each card's wireframe field (see
|
||||
serve-question --schema) and the page draws the schematic. Carry the
|
||||
|
||||
@@ -1013,14 +1013,22 @@ async function fetchLatestSkillVersion() {
|
||||
}
|
||||
}
|
||||
|
||||
// Two instructions used to sit in one directive: ask, and "if they agree, run
|
||||
// it". Nothing gated the second on an answer, and the same sentence said to
|
||||
// continue without waiting, so a run that could never establish agreement was
|
||||
// still spelled out as the next command. The offer stays; the command leaves
|
||||
// this turn entirely, because installing over the skill mid-session changes
|
||||
// files the session is reading and only takes effect in the next one anyway.
|
||||
function buildUpdateDirective(localVersion, latestVersion) {
|
||||
return (
|
||||
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
||||
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
||||
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
||||
`Mention it once, in this form: "A newer Impeccable (v${latestVersion}) is available. ` +
|
||||
`Update now? It runs \`npx impeccable update\`." ` +
|
||||
`If they agree, run \`npx impeccable update\` (the update applies to the next session, not this one). ` +
|
||||
`Either way, continue the current task without waiting, and do not raise this again.`
|
||||
`Do not run \`npx impeccable update\` in this turn, whatever the user answers: it rewrites the skill files ` +
|
||||
`this session is reading, and the update only takes effect in the next session, so there is nothing to gain now. ` +
|
||||
`Run it in a later turn, only after the user has asked for it in their own words. ` +
|
||||
`Continue the current task now without waiting, and do not raise this again.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1142,7 +1150,7 @@ async function cli() {
|
||||
parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists }));
|
||||
appendDetectorFallback(parts, ctx);
|
||||
appendImageGenDirective(parts);
|
||||
appendBuildPathDirective(parts);
|
||||
appendBuildPathDirective(parts, ctx);
|
||||
appendAutonomyCounterDirective(parts);
|
||||
appendSubagentAuthorizationDirective(parts);
|
||||
if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) {
|
||||
@@ -1162,7 +1170,7 @@ async function cli() {
|
||||
parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists }));
|
||||
appendDetectorFallback(parts, ctx);
|
||||
appendImageGenDirective(parts);
|
||||
appendBuildPathDirective(parts);
|
||||
appendBuildPathDirective(parts, ctx);
|
||||
appendAutonomyCounterDirective(parts);
|
||||
appendSubagentAuthorizationDirective(parts);
|
||||
if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) {
|
||||
@@ -1271,17 +1279,51 @@ function automaticHookMode(ctx) {
|
||||
}
|
||||
|
||||
|
||||
// Build-path preference: a workflow setting (comp-led vs code-led), read
|
||||
// here so every session starts knowing it without a file hunt. Absence
|
||||
// stays silent; new-work's own default applies, and the decision page
|
||||
// toggle can flip the value for a single session.
|
||||
function appendBuildPathDirective(parts) {
|
||||
try {
|
||||
const settings = JSON.parse(fs.readFileSync(path.join(process.cwd(), '.impeccable', 'settings.json'), 'utf8'));
|
||||
if (settings.buildPath === 'comp' || settings.buildPath === 'code') {
|
||||
parts.push(`BUILD_PATH_DEFAULT: ${settings.buildPath} (from .impeccable/settings.json). Author direction and surface rounds with this as buildPath.value and toggle: true; a flip on the page binds that session only and is never written back to settings.`);
|
||||
// Build-path preference: a workflow setting (comp-led vs code-led), read here
|
||||
// so every session starts knowing it without a file hunt. It rides the unified
|
||||
// config beside the hook and detector settings, and the gitignored local file
|
||||
// wins, because whether a machine has an image tool is a property of that
|
||||
// machine, not of the team's committed default. Absence stays silent;
|
||||
// new-work's own default applies, and the decision page toggle can flip the
|
||||
// value for a single session.
|
||||
function readBuildPathAt(root) {
|
||||
let value = null;
|
||||
let source = null;
|
||||
for (const name of ['config.json', 'config.local.json']) {
|
||||
const raw = readJson(path.join(root, '.impeccable', name));
|
||||
if (raw?.buildPath === 'comp' || raw?.buildPath === 'code') {
|
||||
value = raw.buildPath;
|
||||
source = `.impeccable/${name}`;
|
||||
}
|
||||
} catch { /* no settings file */ }
|
||||
}
|
||||
return value ? { value, source } : null;
|
||||
}
|
||||
|
||||
// Roots in precedence order, nearest first: the resolved project decides, and
|
||||
// the repo root is the fallback a monorepo commits once for every app in it.
|
||||
// `checkBuildPathUnset` reads exactly these two, and the pair has to match:
|
||||
// when they disagree the finding goes silent because a value exists while the
|
||||
// directive never names it, which is the one combination nobody can debug.
|
||||
//
|
||||
// The invoking directory is deliberately not in the chain. With `--target`
|
||||
// selecting another workspace, cwd is the caller's app, not the target's, and
|
||||
// letting it rank above the repo root hands one workspace another's workflow.
|
||||
// It stands in only when no project resolved at all.
|
||||
function appendBuildPathDirective(parts, ctx) {
|
||||
const roots = [...new Set(
|
||||
[ctx?.projectRoot || process.cwd(), ctx?.repoRoot].filter(Boolean).map((root) => path.resolve(root)),
|
||||
)];
|
||||
for (const root of roots) {
|
||||
const found = readBuildPathAt(root);
|
||||
if (!found) continue;
|
||||
// "Never written back" is scoped by the fact that this directive exists at
|
||||
// all: it is emitted only where a value is already recorded, which is the
|
||||
// case where a flip really is session-only. Saying so inline because the
|
||||
// bare absolute reads as a rule that overrides new-work's one-time offer,
|
||||
// which is exactly how the same wording misfired in serve-question.
|
||||
parts.push(`BUILD_PATH_DEFAULT: ${found.value} (from ${found.source}). Author direction and surface rounds with this as buildPath.value and toggle: true; a flip on the page binds that session only and is never written back, because a default is already recorded here. New-work's one-time offer to record a flipped value applies only where no default exists, which is why you are not seeing this line on those projects.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Image generation availability: harness-native tools always win, but when the
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
stampProductSchema,
|
||||
} from './lib/artifact-schema.mjs';
|
||||
import {
|
||||
checkBuildPathUnset,
|
||||
checkConfig,
|
||||
checkDesignSidecar,
|
||||
checkNativePlatformEvidence,
|
||||
@@ -120,6 +121,7 @@ async function collect(cwd, targetOptions) {
|
||||
...checkDesignDrift({ designPath: absDesignPath, projectRoot }),
|
||||
...checkDesignCoverage({ design: ctx.design, designPath: ctx.designPath, parseDesignMd }),
|
||||
...checkConfig({ projectRoot, repoRoot: ctx.repoRoot }),
|
||||
...checkBuildPathUnset({ projectRoot, repoRoot: ctx.repoRoot, product: ctx.product }),
|
||||
...checkDetectorIgnores({ projectRoot, knownRuleIds }),
|
||||
...checkSurfaceBriefs({ candidates: ctx.surfaceBriefCandidates, projectRoot }),
|
||||
...checkHookInstallation({
|
||||
|
||||
@@ -47,18 +47,33 @@ import {
|
||||
|
||||
// Top-level keys any reader honors: `hook` and `detector` subtrees (hook-lib's
|
||||
// readConfig), `updateCheck` (context.mjs), `projectRoots` (context.mjs's
|
||||
// monorepo resolution), plus `stalenessCheck` below. `$schema` and `version`
|
||||
// are allowed as conventional metadata nobody reads.
|
||||
// monorepo resolution), `buildPath` (context.mjs's build-path directive), plus
|
||||
// `stalenessCheck` below. `$schema` and `version` are allowed as conventional
|
||||
// metadata nobody reads.
|
||||
const KNOWN_CONFIG_KEYS = new Set([
|
||||
'hook',
|
||||
'detector',
|
||||
'updateCheck',
|
||||
'stalenessCheck',
|
||||
'projectRoots',
|
||||
'buildPath',
|
||||
'$schema',
|
||||
'version',
|
||||
]);
|
||||
|
||||
// The only two values context.mjs and new-work honor. A near miss reads as a
|
||||
// working preference and silently rides the opposite path, so it is worth
|
||||
// reporting rather than coercing.
|
||||
const BUILD_PATH_VALUES = Object.freeze(['comp', 'code']);
|
||||
|
||||
// Evidence that this project does the kind of work `buildPath` governs. A
|
||||
// project that only ever ran polish or audit has no use for the setting and
|
||||
// should never be told it exists. Two stats, so Tier 1 can afford it.
|
||||
const DIRECTION_WORK_PATHS = Object.freeze([
|
||||
path.join('.impeccable', 'surfaces'),
|
||||
path.join('.impeccable', 'mocks', 'decision'),
|
||||
]);
|
||||
|
||||
// `detector` is a closed set, so a typo here is worth reporting. `hook` is not
|
||||
// checked: it carries runtime settings from several writers and the false
|
||||
// positive rate would outweigh the catch.
|
||||
@@ -325,6 +340,20 @@ export function checkConfig({ projectRoot, repoRoot }) {
|
||||
}));
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(raw, 'buildPath')
|
||||
&& !BUILD_PATH_VALUES.includes(raw.buildPath)) {
|
||||
findings.push(finding({
|
||||
id: 'config-invalid-build-path',
|
||||
artifact: 'config.json',
|
||||
filePath: rel,
|
||||
severity: 'mention',
|
||||
summary: `${rel} sets \`buildPath\` to ${JSON.stringify(raw.buildPath)}, which nothing reads. `
|
||||
+ `The values are ${BUILD_PATH_VALUES.map((value) => `\`${value}\``).join(' and ')}.`,
|
||||
fix: 'Report the value. An unread `buildPath` does not fall back to the other path; '
|
||||
+ 'it falls back to the default, so a project meaning `code` has been building comp-led.',
|
||||
}));
|
||||
}
|
||||
|
||||
const detector = raw.detector;
|
||||
if (detector && typeof detector === 'object' && !Array.isArray(detector)) {
|
||||
const unknownDetector = Object.keys(detector).filter((key) => !KNOWN_DETECTOR_KEYS.has(key));
|
||||
@@ -345,6 +374,47 @@ export function checkConfig({ projectRoot, repoRoot }) {
|
||||
return findings;
|
||||
}
|
||||
|
||||
/**
|
||||
* No recorded build-path preference on a project that plainly does visual
|
||||
* direction work. Not drift in the usual sense: the setting is newer than the
|
||||
* project, so every project that predates it lands here at once. That is why
|
||||
* it is gated twice, on a product record and on evidence of the work the
|
||||
* setting governs, and why it says the choice rather than assuming a harness
|
||||
* can make it. Image generation is the real precondition and this module
|
||||
* cannot see it: a harness-native image tool leaves no trace on disk, so the
|
||||
* finding hands the question to the one reader that knows.
|
||||
*/
|
||||
export function checkBuildPathUnset({ projectRoot, repoRoot, product }) {
|
||||
if (!projectRoot || !product) return [];
|
||||
const roots = [...new Set([projectRoot, repoRoot].filter(Boolean).map((root) => path.resolve(root)))];
|
||||
|
||||
for (const root of roots) {
|
||||
for (const name of ['config.json', 'config.local.json']) {
|
||||
const raw = readJson(path.join(root, '.impeccable', name));
|
||||
// Any declared value ends this, valid or not: an invalid one already has
|
||||
// its own finding and two reports of one key is noise.
|
||||
if (raw && Object.prototype.hasOwnProperty.call(raw, 'buildPath')) return [];
|
||||
}
|
||||
}
|
||||
|
||||
const evidence = DIRECTION_WORK_PATHS.filter((rel) => fs.existsSync(path.join(projectRoot, rel)));
|
||||
if (!evidence.length) return [];
|
||||
|
||||
return [finding({
|
||||
id: 'config-build-path-unset',
|
||||
artifact: 'config.json',
|
||||
filePath: '.impeccable/config.json',
|
||||
severity: 'mention',
|
||||
summary: 'This project has run visual direction work but records no `buildPath`, '
|
||||
+ 'so every direction round takes the comp-first default without anyone having chosen it.',
|
||||
fix: 'Only when image generation exists in your tool surface, offer the choice once: '
|
||||
+ '**comp-first** (an image sets the bar before any code; bolder composition, slower) or '
|
||||
+ '**code-first** (build directly; ambition carried by the direction contract; leaner, faster). '
|
||||
+ 'Write the answer to `.impeccable/config.json` as `"buildPath": "comp"` or `"buildPath": "code"`, '
|
||||
+ 'merging with the keys already there. Without image generation there is no choice to record: stay silent.',
|
||||
})];
|
||||
}
|
||||
|
||||
// ─── Surface briefs ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -446,6 +516,7 @@ export function collectBootFindings(ctx, extras = {}) {
|
||||
projectRoot,
|
||||
}),
|
||||
...checkConfig({ projectRoot, repoRoot: ctx.repoRoot }),
|
||||
...checkBuildPathUnset({ projectRoot, repoRoot: ctx.repoRoot, product: ctx.product }),
|
||||
...checkSurfaceBriefs({ candidates: ctx.surfaceBriefCandidates, projectRoot }),
|
||||
...(extras.projectRootPatterns
|
||||
? checkProjectRoots({
|
||||
|
||||
@@ -161,8 +161,12 @@ function printAnswer(raw) {
|
||||
console.log('FOLLOWUP OPEN: the table stays open and the page is showing a loading hand. Deliver the next round now with --update --key <key> --payload <file>, then collect it with --wait; never leave the page waiting on a round you have not sent.');
|
||||
}
|
||||
if (a.buildPath === 'comp' || a.buildPath === 'code') {
|
||||
// The page never writes the flip itself, but "never write it" overstated
|
||||
// that into a rule the agent then applied to new-work's one-time offer,
|
||||
// which exists for exactly this case: a flip on a project that had no
|
||||
// recorded default is the only moment the preference is ever asked for.
|
||||
const origin = a.buildPathFlipped
|
||||
? 'flipped on the page, so it binds this session only; never write it to settings'
|
||||
? 'flipped on the page, so it binds this session only, and the page never writes it back; the sole exception is new-work’s one-time offer, on a project that had no recorded default at all, which asks after the round closes and writes the answer to .impeccable/config.json'
|
||||
: 'the round’s recorded default';
|
||||
console.log(`BUILD PATH: ${a.buildPath} (${origin}). ${a.buildPath === 'comp'
|
||||
? 'Comp-led: the chosen card’s comp is law; generate it before building when it does not exist yet, and the finish review audits the build against it.'
|
||||
@@ -199,7 +203,7 @@ if (hasFlag('schema')) {
|
||||
canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' },
|
||||
steer: true,
|
||||
}, null, 2));
|
||||
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register <value> for the next round; offer the registers on direction rounds, and never pre-select one. buildPath rides the payload as { "value": "comp"|"code", "toggle": true }: the value is the recorded default (.impeccable/settings.json, or a PRODUCT.md standing commitment as fallback) and the toggle renders a footer switch whose flip binds that session only; the ANSWER then carries buildPath plus buildPathFlipped. On a code-led round each card still declares its comp path as a flip reserve: wireframes render, and a flip to comp makes --wait return once with BUILD PATH FLIPPED so you generate the comps into the declared slots while the round stays open; a flip back to code is free, and a comp that already landed stays as the critique reference. The toggle may only be offered when image generation exists: a harness with no image tool and no API key never sets toggle: true, so the choice never renders where comps cannot be made, and code-led simply rides as the untoggleable value. followup: true keeps the table open after a pick for a second round via --update; send the next payload immediately, the page is waiting on it.');
|
||||
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register <value> for the next round; offer the registers on direction rounds, and never pre-select one. buildPath rides the payload as { "value": "comp"|"code", "toggle": true }: the value is the recorded default (.impeccable/config.json buildPath, or .impeccable/config.local.json where one machine differs) and the toggle renders a footer switch whose flip binds that session only; the ANSWER then carries buildPath plus buildPathFlipped. On a code-led round each card still declares its comp path as a flip reserve: wireframes render, and a flip to comp makes --wait return once with BUILD PATH FLIPPED so you generate the comps into the declared slots while the round stays open; a flip back to code is free, and a comp that already landed stays as the critique reference. The toggle may only be offered when image generation exists: a harness with no image tool and no API key never sets toggle: true, so the choice never renders where comps cannot be made, and code-led simply rides as the untoggleable value. followup: true keeps the table open after a pick for a second round via --update; send the next payload immediately, the page is waiting on it.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user