Move the slop defects back into the craft floor

The detector-blind slop review existed because the AI-tell rules had been
stripped out of SKILL.md and nothing carried them. The floor is a better
home: it loads after concept ideation and immediately before editing UI,
which is the placement that made stripping them necessary in the first
place. Models tread lightly when a ban list is present during ideation;
by the time the floor loads, the direction is already committed.

- Rename build-floor.md to craft-floor.md and restore the absolute bans
  (side-stripes, gradient text, glassmorphism, hero-metric, identical card
  grids, eyebrow-on-every-section, numbered markers, text overflow), the
  codex and gemini defect lists, and the reflexes no scanner catches.
  Rule ids match the ones the ablation catalog already knows.
- Delete lib/slop-review.mjs and both injections. The Stop hook is now
  purely a mechanical pass and stays silent with nothing to report.
- context.mjs replaces AI_SLOP_REVIEW_REQUIRED with the narrower
  MANUAL_DETECTOR_REQUIRED, emitted only when a session has no hook at
  all. A per-edit hook already covers the mechanical gap, and the floor
  covers the judgment one either way.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-21 02:04:36 -07:00
co-authored by Claude
parent d7d10277d1
commit 153b416f2e
9 changed files with 87 additions and 118 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ Core principles:
1. Run `node {{scripts_path}}/context.mjs` once per session (if the runtime shows this skill's loaded base directory, run `node <skill-base-dir>/scripts/context.mjs`; keep cwd at the user's project). Pass a named source file or route as `--target <path>`. It loads PRODUCT.md, DESIGN.md, the matching surface brief, and native-platform guidance when applicable; follow its directives and do not rerun it. <!-- rule:skill-setup-context -->
2. Before acting, load the one playbook that owns the request: the Commands table's reference for an explicit or clearly implied sub-command, or [reference/new-work.md](reference/new-work.md) for a new surface or replacement visual world. Then inspect the target and at least one representative source of incumbent visual truth (tokens, theme, CSS, component, or asset) before editing. <!-- rule:skill-setup-command-ref --> <!-- rule:skill-setup-read-project -->
3. After analysis and direction are resolved, load [reference/build-floor.md](reference/build-floor.md) immediately before editing UI. Do not load it for planning-only work. <!-- rule:skill-build-floor-load -->
3. After analysis and direction are resolved, load [reference/craft-floor.md](reference/craft-floor.md) immediately before editing UI. It carries the quality floor, the absolute bans, and the reflexes no detector catches. Do not load it for planning-only work. <!-- rule:skill-craft-floor-load -->
## How to design
-24
View File
@@ -1,24 +0,0 @@
# Build floor
Apply this only after analysis and direction are settled. Build without announcing the checklist. <!-- rule:skill-craft-floor -->
- **Contrast:** body and placeholder text ≥4.5:1; large text ≥3:1. On colored surfaces, tint secondary text from that hue or the foreground instead of using gray. <!-- rule:skill-color-verify-contrast -->
- **Depth:** shadows describe light with offset and soft blur; zero-offset colored halos are decoration. <!-- rule:skill-color-no-glow-halo -->
- **Spacing:** tight groups, generous separation, no cramped containers; space above a heading exceeds space below. Verify computed values. <!-- rule:skill-layout-spacing-rhythm -->
- **Type:** body measure 6575ch; display max 6rem and tracking floor -0.04em; balance headings; use clear scale/weight contrast; test overflow at every breakpoint. <!-- rule:skill-typo-floor -->
- **Motion:** author one coherent moment instead of scattered effects. Use exponential ease-out and an already-visible default. Premium motion may add focus, depth, masks, light, or material change through blur/filter, backdrop-filter, clip-path/masks, or shadow when smooth; do not rely on transform/opacity alone. <!-- rule:skill-motion-floor --> <!-- rule:skill-motion-materials-palette -->
- **Shipping:** real content, working controls, responsive composition, keyboard focus, and the states users hit: hover, disabled, loading, error, and empty. <!-- rule:skill-floor-shipping -->
- **Copy:** use the product's language; controls name their action, errors name the problem and recovery. <!-- rule:skill-copy-design-material -->
- **Coverage:** every brief requirement must exist and be findable within seconds. <!-- rule:skill-floor-brief-coverage -->
Before finishing changed UI, follow the quality guidance supplied by `context.mjs` and hooks. Context requests a manual scan only when no automatic detector is active; never add a second detector pass. <!-- rule:skill-llm-only-slop-check --> <!-- rule:skill-detector-finish-mode -->
<codex>
- Display tracking stops at -0.04em; -0.02 to -0.03em is usually enough. <!-- rule:skill-typo-codex-tracking-repeat -->
- Declare elevation once: border or shadow, not both as decoration. Keep container radii modest; reserve pills for small controls. <!-- rule:skill-codex-elevation-radius -->
- Use real illustration or none. Treat backgrounds as surfaces and add texture only from the subject's world. Claims, evidence, and configuration come from supplied truth; label illustrative behavior and unresolved values honestly. <!-- rule:skill-codex-material-honesty -->
</codex>
<gemini>
Never animate `<img>` elements on hover, directly or through a parent. Give the card itself feedback instead. <!-- rule:skill-interaction-gemini-no-image-hover -->
</gemini>
+51
View File
@@ -0,0 +1,51 @@
# Craft floor
Apply this only after analysis and direction are settled. Build without announcing the checklist. A pinned brief or the committed visual world beats every line below; your own reflex does not. <!-- rule:skill-craft-floor -->
## Floor
- **Contrast:** body and placeholder text ≥4.5:1; large text ≥3:1. On colored surfaces, tint secondary text from that hue or the foreground instead of using gray. <!-- rule:skill-color-verify-contrast -->
- **Depth:** shadows describe light with offset and soft blur; zero-offset colored halos are decoration. <!-- rule:skill-color-no-glow-halo -->
- **Spacing:** tight groups, generous separation, no cramped containers; space above a heading exceeds space below. Verify computed values. <!-- rule:skill-layout-spacing-rhythm -->
- **Type:** body measure 6575ch; display max 6rem and tracking floor -0.04em; balance headings; use clear scale/weight contrast; test overflow at every breakpoint. <!-- rule:skill-typo-floor -->
- **Motion:** author one coherent moment instead of scattered effects. Use exponential ease-out and an already-visible default. Premium motion may add focus, depth, masks, light, or material change through blur/filter, backdrop-filter, clip-path/masks, or shadow when smooth; do not rely on transform/opacity alone. <!-- rule:skill-motion-floor --> <!-- rule:skill-motion-materials-palette -->
- **Shipping:** real content, working controls, responsive composition, keyboard focus, and the states users hit: hover, disabled, loading, error, and empty. <!-- rule:skill-floor-shipping -->
- **Copy:** use the product's language; controls name their action, errors name the problem and recovery. <!-- rule:skill-copy-design-material -->
- **Coverage:** every brief requirement must exist and be findable within seconds. <!-- rule:skill-floor-brief-coverage -->
## Absolute bans
Match and refuse. If you are about to write one of these, rewrite the element with different structure.
- **Side-stripe borders.** `border-left` or `border-right` above 1px as a colored accent on cards, list items, callouts, or alerts. Never intentional. Use full borders, background tints, leading numbers or icons, or nothing. <!-- rule:skill-ban-side-stripe-borders -->
- **Gradient text.** `background-clip: text` over a gradient. Decorative, never meaningful. One solid color; emphasis through weight or size. <!-- rule:skill-ban-gradient-text -->
- **Glassmorphism as default.** Blur and glass panels used as decoration. Rare and purposeful, or nothing. <!-- rule:skill-ban-glassmorphism-default -->
- **The hero-metric template.** Big number, small label, supporting stats, accent treatment. <!-- rule:skill-ban-hero-metric -->
- **Identical card grids.** Same-sized cards carrying icon plus heading plus text, repeated as the page structure. Cards are the lazy answer; nested cards are always wrong. <!-- rule:skill-ban-identical-card-grids --> <!-- rule:skill-layout-cards-lazy -->
- **A tiny tracked uppercase eyebrow above every section.** Small all-caps with wide tracking ("ABOUT", "PROCESS", "PRICING") shows up on most generations regardless of brief, which is what makes it a tell. One named kicker as a deliberate system is voice; an eyebrow on every section is AI grammar. <!-- rule:skill-ban-eyebrow-on-every-section -->
- **Numbered section markers as scaffolding (01 / 02 / 03).** The eyebrow trope one tier deeper. Numbers earn their place when the section really is a sequence and the order carries information the reader needs. <!-- rule:skill-ban-numbered-section-markers -->
- **Text that overflows its container.** Long heading words plus large clamp scales plus narrow grids overflow on tablet and mobile. Test the real copy at every breakpoint, then reduce the clamp max or rewrite the line. The viewport is part of the design. <!-- rule:skill-ban-text-overflow -->
## Detector-blind reflexes
No scanner catches these, and they are where an otherwise competent build still reads as generated. Inspect the rendered result for them.
- Monospace used to signal "technical" or "developer" rather than to show code, data, or measurement. <!-- rule:skill-reflex-mono-as-technical -->
- Light or dark chosen by category habit instead of the real use scene: who uses this, where, under what ambient light. <!-- rule:skill-reflex-theme-by-habit -->
- Decorative sparklines, meaningless progress rings, and rounded rectangles with soft shadows standing in for content. <!-- rule:skill-reflex-decorative-chrome -->
- A modal reached for when the task needs neither interruption nor protected focus. <!-- rule:skill-reflex-modal-by-reflex -->
- One uniform entrance animation applied to every section. Staggering items within a single list is legitimate; the reflex is the identical reveal, not motion itself. <!-- rule:skill-motion-no-section-fade -->
Run the mechanical detector once over the changed web UI, and only when this session has no automatic hook: `node {{scripts_path}}/detect.mjs --json <changed targets>`. Never add a second detector pass, and never run it during concept selection. <!-- rule:skill-detector-finish-mode -->
<codex>
- Display tracking stops at -0.04em; -0.02 to -0.03em is usually enough. <!-- rule:skill-typo-codex-tracking-repeat -->
- Declare elevation once: border or shadow, not both as decoration. A 1px border paired with a soft shadow past 16px blur is the ghost-card tell. Keep container radii modest, 1216px on cards; reserve pills for small controls. <!-- rule:skill-codex-elevation-radius --> <!-- rule:skill-ban-codex-ghost-card --> <!-- rule:skill-ban-codex-over-round -->
- Use real illustration or none. Hand-drawn SVG scenes, `loose-sketch` / `doodle` class names, and `feTurbulence` paper-grain filters read as amateurish rather than whimsical. <!-- rule:skill-ban-codex-sketchy-svg -->
- Treat backgrounds as surfaces and add texture only from the subject's world. `repeating-linear-gradient` stripes and two-axis CSS grid overlays are decoration unless the surface really is a canvas, map, blueprint, or measurement tool. <!-- rule:skill-ban-codex-stripes --> <!-- rule:skill-ban-codex-grid-backgrounds -->
- Claims, evidence, and configuration come from supplied truth; label illustrative behavior and unresolved values honestly. Naming a concept and then layering an ironic modifier is not a claim. <!-- rule:skill-codex-material-honesty --> <!-- rule:skill-ban-codex-x-theater -->
</codex>
<gemini>
Never animate `<img>` elements on hover, directly or through a parent. That includes Tailwind's `.group:hover .group-hover\:scale` / `:rotate` / `:translate` reaching a child image. The image is not an action target, so the motion carries nothing. Give the card itself feedback instead. <!-- rule:skill-interaction-gemini-no-image-hover -->
</gemini>
+3 -1
View File
@@ -4,7 +4,9 @@ Manage the **design detector hook** for the current project.
The hook runs the impeccable design detector on direct file edits to design-relevant files (`.tsx`, `.jsx`, `.html`, `.vue`, `.svelte`, `.astro`, `.css`, `.scss`, `.sass`, `.less`, `.ts`, `.js`). Claude Code, Codex, and GitHub Copilot use a post-tool-use hook and push a short system reminder into the agent's context after the edit; findings get a correction prompt, pending issues get a re-nudge, and clean UI-ish files get a short ack unless quiet mode is on (`hook.quiet` in config). Plain `.ts` and `.js` files are still scanned, but stay quiet unless the detector finds something. Cursor uses `preToolUse` to block bad proposed writes before they land and stays silent when it allows a clean write.
The detector rules run in two tiers. The per-edit hook surfaces only the immediate tier: mechanical, unambiguous problems worth interrupting an edit for, such as broken images, overflowing or clipped content, contrast and legibility failures, gradient text, glow shadows, and design-system drift. Everything else (copy cadence, palette and typography taste, layout rhythm) is deferred to a deep pass on the `Stop` hook event, which runs the full rule set over every UI file touched in the session and surfaces the remaining findings once, deduplicated against what the per-edit pass already reported. That Stop message also asks for one authored review of detector-blind model reflexes. A session that touched no UI files stops silently. Set `hook.perEditRules` to `"all"` in `.impeccable/config.json` to restore the full rule set on every edit. The Stop deep pass is wired for Claude Code and Codex, which both dispatch a native `Stop` hook event. Cursor does not get one (its stop hook is not consistently dispatched; the pre-write gate covers it), and GitHub Copilot's stop-style events do not feed context back to the model, so they keep the full detector per edit while `context.mjs` supplies the detector-blind review. When no automatic hook is active, `context.mjs` instead supplies that review plus one manual detector command.
The detector rules run in two tiers. The per-edit hook surfaces only the immediate tier: mechanical, unambiguous problems worth interrupting an edit for, such as broken images, overflowing or clipped content, contrast and legibility failures, gradient text, glow shadows, and design-system drift. Everything else (copy cadence, palette and typography taste, layout rhythm) is deferred to a deep pass on the `Stop` hook event, which runs the full rule set over every UI file touched in the session and surfaces the remaining findings once, deduplicated against what the per-edit pass already reported. A session with nothing left to report stops silently. Set `hook.perEditRules` to `"all"` in `.impeccable/config.json` to restore the full rule set on every edit. The Stop deep pass is wired for Claude Code and Codex, which both dispatch a native `Stop` hook event. Cursor does not get one (its stop hook is not consistently dispatched; the pre-write gate covers it), and GitHub Copilot's stop-style events do not feed context back to the model, so they keep the full detector per edit.
Every hook is a mechanical pass. The reflexes no scanner catches live in [craft-floor.md](craft-floor.md), which the skill loads before it edits UI, so they apply whether or not a hook is wired. A session with no automatic hook gets one `MANUAL_DETECTOR_REQUIRED` directive from `context.mjs` asking for a single detector run at the end.
This command toggles the hook **per project** by editing `.impeccable/config.json` (the unified Impeccable config; hook runtime settings live under its `hook` key, and shared detector ignores live under `detector`). Per-developer overrides, including the install consent decision (`hook.consent`) the CLI records, live in the gitignored `.impeccable/config.local.json`. Set `hook.enabled: false` to turn the hook off, `hook.quiet: true` to silence the clean/pending acks, or `hook.auditLog` to a file path for an NDJSON log. The legacy `IMPECCABLE_HOOK_DISABLED`, `IMPECCABLE_HOOK_QUIET`, and `IMPECCABLE_HOOK_LOG` env vars are still honored and override these config values when set.
+15 -12
View File
@@ -32,7 +32,6 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseTargetOptions } from './lib/target-args.mjs';
import { IMPECCABLE_COMMAND, IMPECCABLE_PROVIDER_ID } from './lib/provider.mjs';
import { renderLlmOnlySlopReview } from './lib/slop-review.mjs';
import { resolveSurfaceBrief } from './lib/surface-briefs.mjs';
const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md'];
@@ -1127,7 +1126,7 @@ async function cli() {
];
appendSurfaceBriefContext(parts, ctx);
parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists }));
appendHookFallback(parts, ctx);
appendDetectorFallback(parts, ctx);
if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) {
parts.push(buildMissingTargetDirective());
}
@@ -1141,7 +1140,7 @@ async function cli() {
}
appendSurfaceBriefContext(parts, ctx);
parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists }));
appendHookFallback(parts, ctx);
appendDetectorFallback(parts, ctx);
if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) {
parts.push(buildMissingTargetDirective());
}
@@ -1244,15 +1243,19 @@ function automaticHookMode(ctx) {
return 'none';
}
function appendHookFallback(parts, ctx) {
const hookMode = automaticHookMode(ctx);
if (hookMode === 'stop') return;
const native = ctx.platform === 'ios' || ctx.platform === 'android' || ctx.platform === 'adaptive';
parts.push(renderLlmOnlySlopReview({
automaticDetector: hookMode === 'per-edit',
manualDetector: hookMode === 'none' && !native,
scriptsPath: path.dirname(fileURLToPath(import.meta.url)),
}));
// reference/craft-floor.md carries the detector-blind reflexes on every build,
// so the only gap left here is the mechanical pass. A hook covers it, per-edit
// or Stop; a session without one has to run the detector by hand. The detector
// reads HTML and CSS, so native projects get nothing.
function appendDetectorFallback(parts, ctx) {
if (automaticHookMode(ctx) !== 'none') return;
if (ctx.platform === 'ios' || ctx.platform === 'android' || ctx.platform === 'adaptive') return;
const scriptsPath = path.dirname(fileURLToPath(import.meta.url));
parts.push([
'MANUAL_DETECTOR_REQUIRED: No automatic Impeccable design hook is active this session.',
`Once the changed web UI is finished, run the mechanical detector over it: \`node ${scriptsPath}/detect.mjs --json <changed targets>\`.`,
'Run it once, and not earlier during concept selection.',
].join(' '));
}
function buildResolvedContextDirective(ctx, options, { targetExists = null } = {}) {
+2 -26
View File
@@ -46,7 +46,6 @@ import path from 'node:path';
import { pathToFileURL, fileURLToPath } from 'node:url';
import { extractPlatform, loadContext } from './context.mjs';
import { IMPECCABLE_COMMAND } from './lib/provider.mjs';
import { renderStopSlopReview } from './lib/slop-review.mjs';
// `detector.extensions` (issue #316) is shared with Live's source search, which
// needs the same answer for `.heex` / `.blade.php` when it hunts for session
// markers. lib/template-extensions.mjs owns the shape; re-exported here because
@@ -2054,22 +2053,9 @@ export async function runStopHook({ stdinJson, env = {}, cwd = process.cwd(), no
return result({ skipped: 'native-platform', platform, durationMs: Date.now() - started });
}
const session = ensureSession(cache, sessionId);
const needsSlopReview = session.llmSlopReviewed !== true;
const det = detector || await loadDetector();
if (!det || typeof det.detectText !== 'function') {
if (!needsSlopReview) return result({ skipped: 'detector-missing', durationMs: Date.now() - started });
session.llmSlopReviewed = true;
session.updatedAt = Date.now();
persistCache(projectCwd, cache);
const text = renderStopSlopReview();
return {
exitCode: 0,
stdout: payload(text, 'Stop', harness),
emission: { kind: 'stop-llm-slop-review', llmSlopReview: true },
audit: { ...audit, emitted: true, detectorMissing: true, llmSlopReview: true, chars: text.length, durationMs: Date.now() - started },
};
return result({ skipped: 'detector-missing', durationMs: Date.now() - started });
}
const scanOptions = designSystemOptions(config, det, projectCwd);
@@ -2130,15 +2116,10 @@ export async function runStopHook({ stdinJson, env = {}, cwd = process.cwd(), no
}
audit.scannedFiles = scanned;
if (freshGroups.length === 0 && contractEntries.length === 0 && !needsSlopReview) {
if (freshGroups.length === 0 && contractEntries.length === 0) {
return result({ emitted: false, skipped: 'stop-clean', durationMs: Date.now() - started });
}
if (needsSlopReview) {
session.llmSlopReviewed = true;
session.updatedAt = Date.now();
}
// Fresh findings and first-time contract audits earn the cache write;
// both mark this batch as surfaced so the next Stop fire is silent
// unless new issues appear.
@@ -2153,9 +2134,6 @@ export async function runStopHook({ stdinJson, env = {}, cwd = process.cwd(), no
if (contractEntries.length > 0) {
parts.push(renderContractAudit(contractEntries, { cwd: projectCwd }));
}
if (needsSlopReview) {
parts.push(renderStopSlopReview());
}
const text = appendDesignSystemNote(parts.join('\n\n'), scanOptions);
return {
exitCode: 0,
@@ -2166,7 +2144,6 @@ export async function runStopHook({ stdinJson, env = {}, cwd = process.cwd(), no
...(contractEntries.length > 0
? { contractFiles: contractEntries.map((entry) => entry.filePath) }
: {}),
...(needsSlopReview ? { llmSlopReview: true } : {}),
},
audit: {
...audit,
@@ -2174,7 +2151,6 @@ export async function runStopHook({ stdinJson, env = {}, cwd = process.cwd(), no
freshFiles: freshGroups.length,
freshFindings: freshGroups.reduce((sum, group) => sum + group.findings.length, 0),
...(contractEntries.length > 0 ? { contractAudits: contractEntries.length } : {}),
...(needsSlopReview ? { llmSlopReview: true } : {}),
chars: text.length,
durationMs: Date.now() - started,
},
-30
View File
@@ -1,30 +0,0 @@
export const LLM_ONLY_SLOP_ITEMS = Object.freeze([
'Monospace used merely to signal “technical” or “developer.”',
'Light or dark mode chosen by category habit rather than the actual use scene.',
'Everything wrapped in cards, or identical icon-heading-text cards repeated as the page structure.',
'Hero-metric scaffolds: one oversized number, a small label, supporting stats, and an accent treatment.',
'Decorative glassmorphism, meaningless sparklines, or generic rounded rectangles with drop shadows.',
'A modal chosen by reflex when the task does not require interruption or protected focus.',
]);
export function renderLlmOnlySlopReview({ manualDetector = false, automaticDetector = false, scriptsPath = null } = {}) {
const lines = [
automaticDetector
? 'AI_SLOP_REVIEW_REQUIRED: The automatic detector covers mechanical rules, but this harness has no reliable late review. Before finishing changed UI, inspect the authored result for detector-blind model reflexes:'
: 'AI_SLOP_REVIEW_REQUIRED: The automatic Impeccable design hook is not available for this session. Before finishing changed UI, inspect the authored result for detector-blind model reflexes:',
...LLM_ONLY_SLOP_ITEMS.map((item) => `- ${item}`),
'Fix reflexes, not intentional choices required by the brief or established visual authority.',
];
if (manualDetector && scriptsPath) {
lines.push(`Then run the mechanical detector once over the changed web UI: \`node ${scriptsPath}/detect.mjs --json <changed targets>\`. Do not run it earlier during concept selection.`);
}
return lines.join('\n');
}
export function renderStopSlopReview() {
return [
'[impeccable@1] Detector-blind AI-slop review. The mechanical pass is complete; now inspect the rendered result for model reflexes it cannot reliably detect:',
...LLM_ONLY_SLOP_ITEMS.map((item) => `- ${item}`),
'Fix reflexes, not intentional choices required by the brief or established visual authority. Do not rerun the detector; this is the authored judgment layer.',
].join('\n');
}
+9 -11
View File
@@ -1035,17 +1035,16 @@ describe('context.mjs CLI', () => {
// routes to new-work rather than back through product init.
assert.match(res.stdout, /\n---\n\n/);
assert.match(res.stdout, /WORLD_DISCOVERY_REQUIRED: PRODUCT\.md exists but no DESIGN\.md/);
assert.match(res.stdout, /AI_SLOP_REVIEW_REQUIRED:/);
assert.match(res.stdout, /Monospace used merely/);
assert.match(res.stdout, /MANUAL_DETECTOR_REQUIRED:/);
assert.match(res.stdout, /detect\.mjs --json <changed targets>/);
});
it('keeps LLM-only fallback guidance out of early context when the current provider hook is active', () => {
it('keeps the manual-detector directive out of early context when the current provider hook is active', () => {
const scripts = path.join(scratch, 'bundle', 'skills', 'impeccable', 'scripts');
const lib = path.join(scripts, 'lib');
fs.mkdirSync(lib, { recursive: true });
fs.copyFileSync(SCRIPT_PATH, path.join(scripts, 'context.mjs'));
for (const helper of ['target-args.mjs', 'surface-briefs.mjs', 'target-slug.mjs', 'slop-review.mjs']) {
for (const helper of ['target-args.mjs', 'surface-briefs.mjs', 'target-slug.mjs']) {
fs.copyFileSync(path.join(path.dirname(SCRIPT_PATH), 'lib', helper), path.join(lib, helper));
}
const provider = fs.readFileSync(path.join(path.dirname(SCRIPT_PATH), 'lib', 'provider.mjs'), 'utf8')
@@ -1065,7 +1064,7 @@ describe('context.mjs CLI', () => {
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' },
});
assert.equal(res.status, 0, res.stderr);
assert.doesNotMatch(res.stdout, /AI_SLOP_REVIEW_REQUIRED:/);
assert.doesNotMatch(res.stdout, /MANUAL_DETECTOR_REQUIRED:/);
fs.mkdirSync(path.join(project, '.impeccable'), { recursive: true });
fs.writeFileSync(path.join(project, '.impeccable', 'config.json'), JSON.stringify({ hook: { enabled: false } }));
@@ -1075,16 +1074,16 @@ describe('context.mjs CLI', () => {
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' },
});
assert.equal(disabled.status, 0, disabled.stderr);
assert.match(disabled.stdout, /AI_SLOP_REVIEW_REQUIRED:/);
assert.match(disabled.stdout, /MANUAL_DETECTOR_REQUIRED:/);
assert.match(disabled.stdout, /detect\.mjs --json <changed targets>/);
});
it('injects only detector-blind guidance when a per-edit-only hook is active', () => {
it('adds no detector directive when a per-edit-only hook is active', () => {
const scripts = path.join(scratch, 'bundle', 'skills', 'impeccable', 'scripts');
const lib = path.join(scripts, 'lib');
fs.mkdirSync(lib, { recursive: true });
fs.copyFileSync(SCRIPT_PATH, path.join(scripts, 'context.mjs'));
for (const helper of ['target-args.mjs', 'surface-briefs.mjs', 'target-slug.mjs', 'slop-review.mjs']) {
for (const helper of ['target-args.mjs', 'surface-briefs.mjs', 'target-slug.mjs']) {
fs.copyFileSync(path.join(path.dirname(SCRIPT_PATH), 'lib', helper), path.join(lib, helper));
}
const provider = fs.readFileSync(path.join(path.dirname(SCRIPT_PATH), 'lib', 'provider.mjs'), 'utf8')
@@ -1104,8 +1103,7 @@ describe('context.mjs CLI', () => {
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' },
});
assert.equal(res.status, 0, res.stderr);
assert.match(res.stdout, /AI_SLOP_REVIEW_REQUIRED:/);
assert.match(res.stdout, /automatic detector covers mechanical rules/);
assert.doesNotMatch(res.stdout, /MANUAL_DETECTOR_REQUIRED:/);
assert.doesNotMatch(res.stdout, /detect\.mjs --json <changed targets>/);
});
@@ -1354,7 +1352,7 @@ describe('context.mjs update check', () => {
const providerSrc = path.join(path.dirname(SCRIPT_PATH), 'lib', 'provider.mjs');
const providerDest = path.join(path.dirname(skillScript), 'lib', 'provider.mjs');
fs.copyFileSync(providerSrc, providerDest);
for (const helper of ['surface-briefs.mjs', 'target-slug.mjs', 'slop-review.mjs']) {
for (const helper of ['surface-briefs.mjs', 'target-slug.mjs']) {
fs.copyFileSync(
path.join(path.dirname(SCRIPT_PATH), 'lib', helper),
path.join(path.dirname(skillScript), 'lib', helper),
+6 -13
View File
@@ -3281,8 +3281,6 @@ describe('runStopHook()', () => {
assert.match(out.hookSpecificOutput.additionalContext, /em-dash-overuse/);
assert.match(out.hookSpecificOutput.additionalContext, /side-tab/);
assert.doesNotMatch(out.hookSpecificOutput.additionalContext, /dark-glow/);
assert.match(out.hookSpecificOutput.additionalContext, /Detector-blind AI-slop review/);
assert.match(out.hookSpecificOutput.additionalContext, /Monospace used merely/);
assert.equal(stop.emission.kind, 'stop-deep-pass');
});
@@ -3307,7 +3305,7 @@ describe('runStopHook()', () => {
assert.equal(second.audit.skipped, 'stop-clean');
});
it('respects detector.ignoreRules while still delivering the one-time LLM-only review', async () => {
it('stays silent when detector.ignoreRules filters away every touched finding', async () => {
const sid = 'stop-ignored';
fs.mkdirSync(path.join(cwd, '.impeccable'), { recursive: true });
fs.writeFileSync(getConfigPath(cwd), JSON.stringify({
@@ -3318,12 +3316,8 @@ describe('runStopHook()', () => {
await runHook({ stdinJson: JSON.stringify(editEvent(file, sid)), env: {}, cwd, detector: det });
const stop = await runStopHook({ stdinJson: JSON.stringify(stopEvent(sid)), env: {}, cwd, detector: det });
const text = JSON.parse(stop.stdout).hookSpecificOutput.additionalContext;
assert.doesNotMatch(text, /em-dash-overuse/);
assert.match(text, /Detector-blind AI-slop review/);
const second = await runStopHook({ stdinJson: JSON.stringify(stopEvent(sid)), env: {}, cwd, detector: det });
assert.equal(second.stdout, '');
assert.equal(second.audit.skipped, 'stop-clean');
assert.equal(stop.stdout, '');
assert.equal(stop.audit.skipped, 'stop-clean');
});
it('honors kill switches and the re-entrancy guard', async () => {
@@ -3586,15 +3580,14 @@ describe('runStopHook() — direction-contract audit', () => {
assert.doesNotMatch(text, /Direction-contract audit/);
});
it('does not audit a malformed contract but still emits the one-time LLM-only review', async () => {
it('stays silent on a malformed contract with nothing else to report', async () => {
const sid = 'contract-malformed';
const file = write('index.html', '<!-- DIRECTION CONTRACT never closed\n<html><body></body></html>');
await touch(file, sid, fakeDetector([]));
const stop = await runStopHook({ stdinJson: JSON.stringify(stopEvent(sid)), env: {}, cwd, detector: fakeDetector([]) });
const text = JSON.parse(stop.stdout).hookSpecificOutput.additionalContext;
assert.doesNotMatch(text, /Direction-contract audit/);
assert.match(text, /Detector-blind AI-slop review/);
assert.equal(stop.stdout, '');
assert.equal(stop.audit.skipped, 'stop-clean');
});
it('truncates an oversized contract in the emitted message', async () => {