Restore dice assignment, fusion, and the commitment counterweights

The ship40 concept pipeline had reversed the proven a-series mechanisms:
the seed's roll decayed into a shortlist nomination that taste functions
(model ranking, candidate floor, simulated user) then argmaxed into the
safest card; the costume check returned as the Translation veto and
carrier-removal test; and the 07-15 rewrite deleted the calibration,
reflex-font lanes, color strategies, and commit-every-atom language that
had held off the cream-editorial default since the alpha era. Five of six
frozen craft directions converged on the same warm-paper family and both
builders obeyed them.

This lands the repair on top of the in-progress simplification:

- new-work.md: the script assigns the build index again on both scopes;
  catalog challengers are fused (challenger supplies form and grammar,
  product supplies every fact, clarity wins conflicts) and weighed on the
  two proven axes only; attended runs present one fully committed
  direction with re-roll and an optional steer instead of a ranked
  lineup; the color-strategy picker, reflex-face list, saturated-look
  calibration, first-viewport thesis and memory test, commit-every-atom,
  scroll pacing, and prove-don't-claim return; the direction contract
  returns as five lean blocks audited by the separate-agent finish.
- concept-seed.mjs: PROMOTED INDEX becomes ASSIGNED INDEX with
  build-assignment semantics; self re-roll only on named factual grounds.
- craft-floor.md: hook-active sessions act on findings instead of
  re-auditing; the Refuse list is framed as category defaults the brief
  can earn; a closing commitment line keeps a ban list from being the
  last word before code.
- codex.md / shape.md: contract references restored for flow coherence.

Adopts the concurrent session's ceremony cuts, softened challenger
instruction, seed SOURCE IDs and --candidate-count, detector-ownership
fix, and the removal of the hook-side contract audit (the audit now
belongs to the separate reviewer at finish).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-21 10:43:44 -07:00
co-authored by Claude Fable 5
parent a9540b5fe3
commit 36e3c05ca7
11 changed files with 165 additions and 629 deletions
+75 -67
View File
@@ -1,10 +1,10 @@
#!/usr/bin/env node
/**
* External concept seed: the dice half of new-work's coupled-direction and
* External concept seed: the dice half of new-work's complete-direction and
* established-world surface procedures.
*
* The model derives a grounded shortlist of candidate FORMS from the
* audience's world and the subject's cultural home (see
* Before this script runs, the model retrieves cultural material and derives
* a grounded shortlist of complete candidate directions from it (see
* reference/new-work.md). Left alone, it then always builds its #1 —
* and a single model's resonance ranking is deterministic, so every run
* in a category ships the same one or two concepts. Measured: 30/35
@@ -13,19 +13,23 @@
*
* This script rolls them from outside, the same trick that made the
* palette seed work:
* - PROMOTED INDEX: which entry of the model's own resonance-ordered
* shortlist must be taken seriously beside its favorites. The dice never
* choose an ungrounded ingredient; they only refuse the argmax rut.
* - ASSIGNED INDEX: which entry of the model's own resonance-ordered
* shortlist gets built. The assignment is the dice: it never chooses an
* ungrounded ingredient, it only refuses the argmax rut. Attended runs
* present the assigned direction and offer re-roll instead of a ranked
* lineup, because a lineup hands selection back to a taste function
* (model or user) and taste functions pick the safest card.
* - CHALLENGERS (6): outside forms from concept-ingredients.json, two from
* each challenger tier (graphic system, instrument language, atmosphere
* world), weighed
* against the derived candidates on audience identification, product
* clarity, system leverage, and use of the medium. They win only when they beat the
* grounded list; measured behavior is that they lose to strong cultural
* material and win over thin categories, which is the intended shape.
* world), fused with the product first (challenger supplies form and
* system grammar, product supplies every fact, clarity wins conflicts),
* then weighed against the derived candidates on audience identification
* and product clarity. They win only when they beat the grounded list;
* measured behavior is that they lose to strong cultural material and
* win over thin categories, which is the intended shape.
* - RE-ROLL (--reroll <n>): round n of the same base key. The script
* recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a
* fresh promoted index, challengers, and staging. One base key therefore
* fresh assigned index, challengers, and staging. One base key therefore
* reproduces the entire chain of rounds.
* - RATINGS: the reviewer's approval ratings weight the challenger draw
* (3-star doubles the odds, 1-star sits out); the approved pool itself
@@ -34,6 +38,7 @@
* Usage:
* node scripts/concept-seed.mjs --scope direction --mode persuade
* node scripts/concept-seed.mjs --scope surface --mode operate --from <key>
* node scripts/concept-seed.mjs --scope direction --candidate-count 6
* node scripts/concept-seed.mjs --scope direction --mode persuade --from <key> --reroll 1
* node scripts/concept-seed.mjs --chosen <challenger-id> --from <key> --scope direction
*
@@ -43,7 +48,7 @@
*
* Challenger data resolves in order: a local catalog directory (the private
* service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll
* API at impeccable.style, then a degraded promotion-only seed when both are
* API at impeccable.style, then a degraded assignment-only seed when both are
* unavailable. --chosen sends the anonymous choice ping for API-dealt rolls;
* DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it.
*
@@ -70,7 +75,7 @@ const here = dirname(fileURLToPath(import.meta.url));
// Data resolution order: a local catalog (the private service repo, evals, and
// tests point IMPECCABLE_CATALOG_DIR at one), then the roll API, then a
// degraded promotion-only seed. The full catalog does not ship with the skill.
// degraded assignment-only seed. The full catalog does not ship with the skill.
const CATALOG_DIR = process.env.IMPECCABLE_CATALOG_DIR || here;
const API_BASE = (process.env.IMPECCABLE_API_URL || 'https://impeccable.style/api').replace(/\/$/, '');
const API_TIMEOUT_MS = Number(process.env.IMPECCABLE_API_TIMEOUT || 4000);
@@ -157,6 +162,7 @@ export async function pingChosen({ chosenId, key, scope, mode }) {
export function renderChallenger(concept, index) {
const system = concept.system.map(rule => ` - ${rule}`).join('\n');
return ` ${index + 1}. ${concept.form}
SOURCE ID: ${concept.id}
CREATIVE SPARK: ${concept.spark}
SYSTEM GRAMMAR:
${system}
@@ -166,6 +172,7 @@ ${system}
export function renderStaging(composition, index = null) {
const grammar = composition.grammar.map(rule => ` - ${rule}`).join('\n');
return ` ${index == null ? '' : `${index + 1}. `}${composition.form}
SOURCE ID: ${composition.id}
SPARK: ${composition.spark}
STAGING GRAMMAR:
${grammar}
@@ -309,6 +316,7 @@ export function renderConceptSeed({
key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'),
reroll = 0,
mode = null,
candidateCount = 7,
catalogDir = CATALOG_DIR,
_resolvedData = undefined,
} = {}) {
@@ -321,15 +329,18 @@ export function renderConceptSeed({
if (mode !== null && !SEED_MODES.has(mode)) {
throw new Error('concept-seed: --mode must be persuade, operate, read, or experience');
}
if (!Number.isInteger(candidateCount) || candidateCount < 5 || candidateCount > 7) {
throw new Error('concept-seed: --candidate-count must be an integer from 5 to 7');
}
const unit = (salt) => {
const h = crypto.createHash('sha256').update(`${scope}:${salt}:${key}`).digest();
return h.readUInt32BE(0) / 0xffffffff;
};
const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`;
const buildIndex = 3 + Math.floor(unit(indexSalt) * 5); // 3..7
const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount
// Local catalog first (private repo, evals, tests), then the roll API,
// then a degraded promotion-only seed. The promoted index is pure local
// then a degraded assignment-only seed. The assigned index is pure local
// math, so even a fully offline run keeps the anti-argmax mechanism.
let data = _resolvedData ?? null;
if (_resolvedData === undefined) {
@@ -357,6 +368,7 @@ export function renderConceptSeed({
key,
reroll,
mode,
candidateCount,
catalogDir,
_resolvedData: roll ? {
source: 'api',
@@ -371,68 +383,62 @@ export function renderConceptSeed({
}
const promotedInstruction = scope === 'direction'
? `After ordering the grounded coupled directions by product fit, promote
candidate ${buildIndex} into the serious shortlist. Each candidate must join
a durable visual system to a concrete expression for the requested first
surface; select or revise that pair as one decision. It must survive the
current task plus navigation, quiet and dense content, interaction and state,
and a substantially different future surface.`
? `After ordering the grounded directions by resonance, build candidate
${buildIndex}. The assignment is the roll, not a suggestion: your top-ranked
direction is what every run would ship, so the script decides which grounded
direction gets built. Each direction joins a durable visual system to a
concrete expression for the requested first surface, decided as one. It must
survive the current task plus navigation, quiet and dense content,
interaction and state, and a substantially different future surface. In an
attended run, present the assigned direction fully committed and offer
re-roll; never present a ranked lineup to choose from. Re-roll yourself only
on named factual grounds, when the assignment cannot carry the product's
truth or task; taste is never grounds.`
: `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, product clarity,
system leverage, and use of the medium.`;
build candidate ${buildIndex}. The assignment is the roll, not a suggestion.
In an attended run, present the assigned structure and offer re-roll; never
present a ranked lineup to choose from. Re-roll yourself only when the
assignment fails audience identification or product clarity on named
factual grounds.`;
const challengerInstruction = scope === 'direction'
? `Translate each challenger's organizing logic into reusable identity grammar
and a strong first-surface structure before judging it. Noticeable form is
allowed when the product stays clear. Compare audience identification,
product clarity, system leverage, and use of the medium.`
: `A challenger wins only when it beats the grounded list on audience
identification, product clarity, system leverage, and use of the medium. It may change task topology or
? `Fuse each challenger before judging it: the challenger supplies the form
and its system grammar, the product supplies every fact, and clarity wins
conflicts. Weigh the fused result against the assigned direction on exactly
two axes, audience identification and product clarity. Losing to strong
grounded material is a valid outcome; beating a thin or tool-monoculture
list is the point. A fused challenger that wins both axes becomes the build.`
: `A challenger wins only when its fused result beats the grounded list on
audience identification and product clarity. It may change task topology or
interaction, but never the committed visual identity.`;
const authorityInstruction = scope === 'direction'
? `PRODUCT.md and explicit incumbent brand commitments constrain every coupled
direction. The seed never chooses exact colors, fonts, tokens, or a user
preference, and it never permits the world and first surface to be selected
independently.`
? `PRODUCT.md and explicit incumbent brand commitments constrain every direction.
The seed never chooses exact colors, fonts, tokens, or a user preference, and
it never permits the world and first surface to be selected independently.`
: `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.`;
const richnessInstruction = `The CREATIVE SPARK is a visual world, artifact, or graphic tradition people
would genuinely choose to enter, study, or explore, and whose palette,
materials, type voice, and component grammar a designer could sketch on
sight, not decorative art direction. The challengers are drawn two from each
translation tier: graphic systems that map to interface almost directly,
instrument or display languages that carry interaction physics, and material
or performed worlds that need the largest translation step; judge
each in its own register and pay that translation cost honestly. Translate
its scale, material, spatial or compositional law, tension, rhythm, and
memorable human experience into product structure. Inherit a movement's or
artifact's rules, never just its name: grid, geometry, ornament logic,
material behavior, and information structure become the interface's. Preserve
the spark's imaginative distance: do not collapse a galaxy into a mission
dashboard, a forest into a taxonomy app, or a performance into a control
console. Translation is complete only when the source name and physical prop
can disappear while a product-native relationship, state change, or proof
remains. A carrier survives only when product evidence makes it functional;
never name a candidate after the prompt merely to preserve the spark. Use
Three.js, generative motion, film language, typography, craft,
or another ambitious medium when it materially strengthens the task; keep
semantic structure and graceful fallbacks fully capable.`;
const richnessInstruction = `The CREATIVE SPARK is a complete visual system, not a theme or decorative
reference. Translate every supplied system rule into the product: palette and
material, type and composition, topology, controls and states, and adaptation.
Keep the source's visible character, scale, rhythm, and interaction instead of
reducing vivid grammar to generic nouns. When the source is already a credible
interface language, commit to it across navigation, content, controls, and
states. Otherwise keep a literal carrier only when it becomes functional.
Ambitious motion, spatial media, or interaction is welcome when it strengthens
the product without weakening semantics, performance, or fallback behavior.`;
if (!data) {
return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''})
PROMOTED INDEX: ${buildIndex}
return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount})
ASSIGNED INDEX: ${buildIndex}
${promotedInstruction}
The promotion exists to refuse the model's ranking rut, not to outrank the
user or the brief. Never expose promotion metadata in choice labels or order.
The assignment exists to refuse the model's ranking rut, never to outrank
the user or the brief. Never expose assignment metadata in user-facing labels.
No challengers this run: the roll service was unreachable and no local
catalog exists. Proceed with the grounded candidates alone; the promotion
catalog exists. Proceed with the grounded candidates alone; the assignment
above still applies at full strength.
${authorityInstruction}
A user- or brief-pinned decision beats the roll, always.
@@ -461,11 +467,11 @@ habitual composition, but keep only structures that strengthen this product.\n`
after resolution. The ping is anonymous (chosen id only) and is skipped
automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n`
: '';
return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} to reproduce this roll against this catalog revision)
${rerollBlock}PROMOTED INDEX: ${buildIndex}
return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision)
${rerollBlock}ASSIGNED INDEX: ${buildIndex}
${promotedInstruction}
The promotion exists to refuse the model's ranking rut, not to outrank the
user or the brief. Never expose promotion metadata in choice labels or order.
The assignment exists to refuse the model's ranking rut, never to outrank
the user or the brief. Never expose assignment metadata in user-facing labels.
CHALLENGERS:
${data.challengers.map(renderChallenger).join('\n')}
${stagingBlock}${challengerInstruction}
@@ -481,6 +487,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
const scopeIdx = args.indexOf('--scope');
const rerollIdx = args.indexOf('--reroll');
const modeIdx = args.indexOf('--mode');
const candidateCountIdx = args.indexOf('--candidate-count');
const chosenIdx = args.indexOf('--chosen');
try {
if (chosenIdx !== -1) {
@@ -500,6 +507,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
: (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')),
reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0,
mode: modeIdx !== -1 ? args[modeIdx + 1] : null,
candidateCount: candidateCountIdx !== -1 ? Number(args[candidateCountIdx + 1]) : 7,
}));
}
} catch (error) {
+4 -133
View File
@@ -26,7 +26,6 @@
* loadDetector() -> Promise<{ detectText, detectHtml }>
* matchesAnyGlob(filePath, globs)
* normalizeScanTargets(primaryTargets, projectCwd)
* extractDirectionContract(content) / renderContractAudit(entries, opts)
* runHook(deps) -> { exitCode, stdout, audit, reason? }
* runStopHook(deps) -> { exitCode, stdout, audit, emission? }
*
@@ -1888,100 +1887,6 @@ export async function runHook({ stdinJson, env = {}, cwd = process.cwd(), now =
}
}
// ── Direction-contract audit ─────────────────────────────────────────────
// The skill's decide-then-build step opens the built HTML artifact with a
// DIRECTION CONTRACT comment (THESIS / OWN-WORLD / STORY / FIRST VIEWPORT /
// BAR-RAISER / FORM blocks). At Stop time the deep pass extracts that
// comment and feeds it back so the model audits the render against its own
// promises. Proven in the eval harness: sample contracts promised radical
// compositions and the build shipped the standard template anyway, because
// nothing ever judged the build against the contract. Zero extra API calls:
// this is a message, not a judge. It fires at most once per file per session
// (a `contractAudited` flag on the session cache entry, the same state the
// deep pass uses for finding dedupe) and rides the Stop pass's existing
// emission rather than adding another block round.
// How far into the file to look for the leading comment. A contract lives at
// the very top of the artifact; anything deeper is not the contract.
export const CONTRACT_HEAD_CHARS = 6000;
// The contract/concept marker must appear early in the comment body, so a
// license header or unrelated note does not get mistaken for a contract.
export const CONTRACT_MARKER_CHARS = 200;
// Cap the extracted contract so a rambling comment cannot blow up the
// Stop message.
export const CONTRACT_MAX_CHARS = 1800;
// Cap contract sections per Stop emission so many touched artifacts cannot
// stack an unbounded message.
export const CONTRACT_AUDIT_MAX_FILES = 3;
export const CONTRACT_EXTS = new Set(['.html', '.htm', '.astro', '.svelte', '.vue', '.jsx', '.tsx']);
export const CONTRACT_REQUIRED_FIELDS = ['THESIS', 'OWN-WORLD', 'STORY', 'FIRST VIEWPORT', 'BAR-RAISER', 'FORM'];
/**
* Extract the artifact's own direction-contract comment from the head of an
* HTML or component file. Supports HTML-family comments and JSX block
* comments so the contract works in the Astro/Svelte/Vue/React scaffolds the
* skill actually builds. Returns the trimmed, length-capped body, or null
* when the file carries no valid contract block.
*/
export function extractDirectionContract(content) {
if (typeof content !== 'string' || !content) return null;
const head = content.slice(0, CONTRACT_HEAD_CHARS);
const candidates = [];
for (const pattern of [/<!--([\s\S]*?)-->/g, /\{\/\*([\s\S]*?)\*\/\}/g]) {
for (const match of head.matchAll(pattern)) {
const index = match.index ?? 0;
const linePrefix = head.slice(head.lastIndexOf('\n', index - 1) + 1, index).trim();
if (linePrefix.startsWith('//')) continue;
candidates.push({ index, body: match[1].trim() });
}
}
candidates.sort((a, b) => a.index - b.index);
for (const candidate of candidates) {
if (!candidate.body || !/direction\s+contract|concept\s+contract/i.test(candidate.body.slice(0, CONTRACT_MARKER_CHARS))) continue;
return candidate.body.slice(0, CONTRACT_MAX_CHARS);
}
return null;
}
export function missingDirectionContractFields(contract) {
const body = typeof contract === 'string' ? contract : '';
return CONTRACT_REQUIRED_FIELDS.filter((field) => {
const label = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '\\s+');
return !new RegExp(`\\b${label}\\s*:`, 'i').test(body);
});
}
/**
* Render the contract-audit section of the Stop message. `entries` is
* [{ filePath, contract }]; at most CONTRACT_AUDIT_MAX_FILES are shown.
* The two failure shapes named here are the ones observed in practice:
* a promise the pixels do not deliver, and a contract whose own plan is
* the standard template wearing the concept's nouns.
*/
export function renderContractAudit(entries, opts = {}) {
if (!Array.isArray(entries) || entries.length === 0) return '';
const cwd = opts.cwd || process.cwd();
const shown = entries.slice(0, CONTRACT_AUDIT_MAX_FILES);
const blocks = shown.map(({ filePath, contract }) => {
const display = relativize(filePath, cwd);
const missing = missingDirectionContractFields(contract);
const integrity = missing.length > 0
? `\n\nContract integrity defect: missing ${missing.join(', ')}. Repair the contract and the implementation together.`
: '';
return `${display} opens with this direction contract, written when the direction was decided:\n\n${contract}${integrity}`;
});
return [
`${ENVELOPE_PREFIX} Direction-contract audit. Before finishing, audit the rendered page against the contract it opens with, promise by promise.`,
'',
blocks.join('\n\n'),
'',
'Two failure shapes to check honestly:',
'1. A promise that is not in the pixels: the contract describes a composition or structure the built page does not deliver, because the build fell back to a standard arrangement, possibly one the contract explicitly rejects. Rebuild that part until the render matches the promise.',
"2. A contract whose own section plan is the standard template wearing the concept's nouns: if a neighboring product could ship the same sequence of sections under different labels, revise the plan and the page together.",
'State each promise and whether the render delivers it, and fix every gap before finishing.',
].join('\n');
}
// Cap on files the Stop deep pass will scan. The touched-file list is
// session-scoped and already capped per edit, but a very long session could
// accumulate more than the 30s hook timeout comfortably covers.
@@ -1990,10 +1895,7 @@ export const STOP_MAX_FILES = 20;
/**
* Run the Stop-event deep pass: the FULL detector rule set over every UI
* file touched this session, surfaced once, deduped against everything the
* per-edit hook already reported. Touched HTML artifacts that open with a
* direction-contract comment additionally get a one-time contract-audit
* section appended after the detector findings (see extractDirectionContract
* above). Same result contract as runHook():
* per-edit hook already reported. Same result contract as runHook():
* { exitCode, stdout, audit, emission? }
*
* Never throws; exits silent (and fast) when the session touched no UI
@@ -2060,7 +1962,6 @@ export async function runStopHook({ stdinJson, env = {}, cwd = process.cwd(), no
const scanOptions = designSystemOptions(config, det, projectCwd);
const freshGroups = [];
const contractEntries = [];
let scanned = 0;
for (const filePath of touched) {
if (scanned >= STOP_MAX_FILES) break;
@@ -2082,22 +1983,6 @@ export async function runStopHook({ stdinJson, env = {}, cwd = process.cwd(), no
? configuredExt.engine === 'html'
: (ext === '.html' || ext === '.htm');
// Direction-contract audit: HTML and component artifacts, at most once per file
// per session. The flag lives on the same session cache entry the
// finding dedupe uses, so a second Stop fire stays quiet about it.
const contractCapable = useHtmlEngine || CONTRACT_EXTS.has(ext);
if (contractCapable) {
const fileEntry = ensureFile(cache, sessionId, filePath);
if (!fileEntry.contractAudited) {
const contract = extractDirectionContract(content);
if (contract) {
fileEntry.contractAudited = true;
ensureSession(cache, sessionId).updatedAt = Date.now();
contractEntries.push({ filePath, contract });
}
}
}
if (useHtmlEngine && typeof det.detectHtml === 'function') {
try { findings = await det.detectHtml(filePath, scanOptions); } catch { findings = []; }
} else {
@@ -2116,41 +2001,27 @@ export async function runStopHook({ stdinJson, env = {}, cwd = process.cwd(), no
}
audit.scannedFiles = scanned;
if (freshGroups.length === 0 && contractEntries.length === 0) {
if (freshGroups.length === 0) {
return result({ emitted: false, skipped: 'stop-clean', durationMs: Date.now() - started });
}
// Fresh findings and first-time contract audits earn the cache write;
// both mark this batch as surfaced so the next Stop fire is silent
// Fresh findings earn the cache write so the next Stop fire is silent
// unless new issues appear.
persistCache(projectCwd, cache);
// Detector findings first, then the contract audit. Both ride the same
// single Stop emission: the audit never adds an extra block round.
const parts = [];
if (freshGroups.length > 0) {
parts.push(renderGroupedTemplate(freshGroups, config, { cwd: projectCwd }));
}
if (contractEntries.length > 0) {
parts.push(renderContractAudit(contractEntries, { cwd: projectCwd }));
}
const text = appendDesignSystemNote(parts.join('\n\n'), scanOptions);
const text = appendDesignSystemNote(renderGroupedTemplate(freshGroups, config, { cwd: projectCwd }), scanOptions);
return {
exitCode: 0,
stdout: payload(text, 'Stop', harness),
emission: {
kind: 'stop-deep-pass',
groups: freshGroups,
...(contractEntries.length > 0
? { contractFiles: contractEntries.map((entry) => entry.filePath) }
: {}),
},
audit: {
...audit,
emitted: true,
freshFiles: freshGroups.length,
freshFindings: freshGroups.reduce((sum, group) => sum + group.findings.length, 0),
...(contractEntries.length > 0 ? { contractAudits: contractEntries.length } : {}),
chars: text.length,
durationMs: Date.now() - started,
},
+1 -3
View File
@@ -10,9 +10,7 @@
* `hookSpecificOutput.additionalContext` when findings exist.
* - Stop: runs the FULL detector rule set over every UI file touched this
* session (the deep pass), deduped against what the per-edit pass already
* surfaced, and emits once via the Stop additionalContext channel. Touched
* HTML artifacts opening with a direction-contract comment get a one-time
* contract-audit section appended to the same emission.
* surfaced, and emits once via the Stop additionalContext channel.
*
* Contract: never break a turn. Always exit 0. Clean files emit a small ack
* unless quiet mode is enabled; a clean Stop pass is silent.