Compare commits

..
Author SHA1 Message Date
Abdul WahabandCursor c7b9deab21 Fix: regenerate browser detector and isolate Inter-before-system coverage
CI rebuilds the browser bundle and fails when it is stale. The shared fixture cannot uniquely prove Inter ahead of a system stack because that page already flags Inter.

AI-assisted (Cursor).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-31 20:07:21 +05:00
Abdul WahabandCursor cb670ceb23 Test: cover overused-font system stacks in the fixture matrix
Move the #671 regressions into tests/fixtures/antipatterns/overused-font.html so the static fixture suite owns the pass cases (system stacks) and the flag cases (named overused primaries).

AI-assisted (Cursor).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-31 01:57:34 +05:00
Abdul WahabandCursor 1828415f84 Fix: do not flag Roboto in system font stacks (#671)
Overused-font primary selection skipped platform UI faces, so a canonical system stack reported the Android fallback as the primary.

AI-assisted (Cursor).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-31 01:48:39 +05:00
285 changed files with 2504 additions and 9071 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ description: Use when the user wants to design, redesign, shape, critique, audit
version: 4.1.2 version: 4.1.2
--- ---
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft. This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
Core principles: Core principles:
- Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide).
@@ -1,6 +1,6 @@
### Purpose ### Purpose
Resolve one stable target, run two independent assessments, synthesize a design critique, persist a snapshot, and ask the user what to improve next. The chat response is the primary deliverable; the snapshot is an archive of that run. Resolve one stable target, run two independent assessments, synthesize a design critique, persist a snapshot, and ask the user what to improve next. The chat response is the primary deliverable; the snapshot is an archive/backlog for future commands.
### Hard Invariants ### Hard Invariants
@@ -97,7 +97,7 @@ Codex failure accounting: final Run Notes must include target slug, ignore list,
Synthesize both assessments into a single report. Do NOT simply concatenate. Weave the findings together, noting where the LLM review and detector agree, where the detector caught issues the LLM missed, and where detector findings are false positives. Synthesize both assessments into a single report. Do NOT simply concatenate. Weave the findings together, noting where the LLM review and detector agree, where the detector caught issues the LLM missed, and where detector findings are false positives.
The chat response is the primary user-facing deliverable. Present the full structured critique below in chat; do not replace it with a summary and a link. The persisted snapshot is an archive of that run. The chat response is the primary user-facing deliverable. Present the full structured critique below in chat; do not replace it with a summary and a link. The persisted snapshot is only an archive/backlog for later commands.
Codex final-answer note: `$impeccable critique` produces a report artifact, so the final chat response should intentionally exceed the usual concise close-out style. Do not title the final response "Critique Summary" unless the user explicitly asked for a summary. Codex final-answer note: `$impeccable critique` produces a report artifact, so the final chat response should intentionally exceed the usual concise close-out style. Do not title the final response "Critique Summary" unless the user explicitly asked for a summary.
@@ -219,7 +219,7 @@ Skip this step if the Setup slug was null (vague or root-level target).
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"max_score":<n>,"na_heuristics":"<comma-separated numbers, or empty>","p0_count":<n>,"p1_count":<n>}' \ IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"max_score":<n>,"na_heuristics":"<comma-separated numbers, or empty>","p0_count":<n>,"p1_count":<n>}' \
node .agents/skills/impeccable/scripts/critique-storage.mjs write "<resolved target>" <body-file> node .agents/skills/impeccable/scripts/critique-storage.mjs write "<resolved target>" <body-file>
``` ```
`max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. For a local file target, the helper also records an exact content fingerprint so polish can distinguish the assessed bytes from later edits without relying on Git state or timestamps. The helper prints the absolute path it wrote. Leave that file on disk. Polish closes it; this run does not. `max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. The helper prints the absolute path it wrote.
3. **Delete the temp body file** after the write attempt completes, whether the write succeeded or failed. If deletion fails, mention `temp-file cleanup failed: <reason>` briefly in the final output, but do not block the critique. 3. **Delete the temp body file** after the write attempt completes, whether the write succeeded or failed. If deletion fails, mention `temp-file cleanup failed: <reason>` briefly in the final output, but do not block the critique.
+1 -1
View File
@@ -32,7 +32,7 @@ The first argument is the action. Defaults to `status`.
| `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. | | `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. |
| `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. | | `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. |
| `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. | | `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. |
| `reset` | Delete the project config, dedup cache, and Cursor pending queue, and remove the hook's entries from every provider manifest `on` installs, the committed Copilot file included (a team-shared `settings.json` that `on` never writes is never touched). | | `reset` | Delete the project config, dedup cache, and Cursor pending queue. |
## Flow ## Flow
@@ -68,20 +68,16 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su
## 5. Record the decision ## 5. Record the decision
Before code, record the chosen direction as a development-only contract under `## Direction contract` in the relevant surface brief. A direction contract is durable route or artifact strategy, so create or update the brief even when no other surface strategy needs persistence. Keep the contract to six short blocks and 150 words at most. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The surface brief is the reminder later agents reload across edits and sessions: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
Never copy the direction contract into implementation source or any browser-delivered artifact. This includes HTML or framework comments, hidden DOM, `<template>` elements, `data-*` attributes, rendered JSX or TSX output, serialized props or state, React Server Component payloads, client bundles, metadata or JSON-LD, accessibility-only text, and files served beside the artifact. A compiler or optimizer removing development metadata is not a safety boundary. Reviewers and documenters receive the contract from the surface brief.
On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md.
Read the existing surface brief before updating it: If the work establishes durable strategy for a route or artifact, read its existing surface brief, then update it:
`node .agents/skills/impeccable/scripts/surface-brief.mjs read <primary-target>` `node .agents/skills/impeccable/scripts/surface-brief.mjs read <primary-target>`
`node .agents/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]` `node .agents/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]`
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it.
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior.
+2 -10
View File
@@ -29,10 +29,10 @@ Use the feature yourself at the surface's representative sizes: desktop and mobi
If a prior critique exists, use it as one input: If a prior critique exists, use it as one input:
```bash ```bash
node .agents/skills/impeccable/scripts/critique-storage.mjs latest "<resolved target>" --json node .agents/skills/impeccable/scripts/critique-storage.mjs latest "<resolved target>"
``` ```
Exit 0 returns JSON with the latest snapshot's `body` and an exact `snapshot_file` identity. Retain `snapshot_file` until the end of the pass. For a local file target, the helper compares the file's exact current content fingerprint with the fingerprint captured by critique. Unchanged staged, unstaged, or untracked content remains current; any byte change, deletion, or replacement with a non-file closes the backlog it identified while preserving its trend history and exits 2. A URL target has no local fingerprint and remains current until explicitly closed. When current, incorporate relevant P0/P1 findings from `body` and name the snapshot read. Exit 2 means none exists or the target changed. Perform an independent pass either way. Exit 0 returns the latest snapshot; incorporate relevant P0/P1 findings and name the snapshot read. Exit 2 means none exists. Perform an independent pass either way.
## 3. Triage ## 3. Triage
@@ -95,11 +95,3 @@ Walk the complete path again with mouse, keyboard, and touch where applicable. C
Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment.
Finish with a source diff: remove accidental churn, orphaned code, redundant values, and temporary artifacts. Ship only when the feature is functionally complete and consistently finished across the path. Finish with a source diff: remove accidental churn, orphaned code, redundant values, and temporary artifacts. Ship only when the feature is functionally complete and consistently finished across the path.
When this pass clears every Priority Issue it took from a snapshot, close that snapshot:
```bash
node .agents/skills/impeccable/scripts/critique-storage.mjs close "<resolved target>" "<snapshot_file returned by latest>"
```
This closes only the snapshot this pass actually processed; if a newer critique landed meanwhile, its backlog stays live. Do not close when no snapshot was read, when `snapshot_file` was not retained, or when Priority Issues remain.
@@ -8,7 +8,7 @@ Reason over the signals; there is no score to obey:
- `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system).
- `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `$impeccable critique <surface>` is a strong default. - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `$impeccable critique <surface>` is a strong default.
- `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared). - `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale.
- `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them.
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `detect.mjs` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code. - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `detect.mjs` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code.
- Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`. - Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`.
@@ -16,9 +16,8 @@
* CLI entry points (called from skill instructions): * CLI entry points (called from skill instructions):
* node critique-storage.mjs slug <resolved-target> * node critique-storage.mjs slug <resolved-target>
* node critique-storage.mjs write <slug> <snapshot-body-file> * node critique-storage.mjs write <slug> <snapshot-body-file>
* node critique-storage.mjs latest <slug> [--json] * node critique-storage.mjs latest <slug>
* node critique-storage.mjs trend <slug> [limit] * node critique-storage.mjs trend <slug> [limit]
* node critique-storage.mjs close <resolved-target> <snapshot-file>
* *
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain * Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
* markdown file; the model reads it directly with its file-read tool. This * markdown file; the model reads it directly with its file-read tool. This
@@ -28,7 +27,6 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { createHash } from 'node:crypto';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
import { getCritiqueDir } from './lib/impeccable-paths.mjs'; import { getCritiqueDir } from './lib/impeccable-paths.mjs';
import { slugFromTarget } from './lib/target-slug.mjs'; import { slugFromTarget } from './lib/target-slug.mjs';
@@ -52,45 +50,6 @@ export function nowFilenameStamp(date = new Date()) {
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z'); return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
} }
/**
* Return an exact content fingerprint for a local file target. URLs and
* non-files return null because their content is not available here.
*
* The fingerprint deliberately describes bytes, not Git state or mtimes:
* critique often assesses an uncommitted file, and a later polish run should
* inherit that backlog when the bytes are unchanged regardless of staging.
*/
function resolveLocalTargetPath(target, { cwd = process.cwd() } = {}) {
if (!target || /^https?:\/\//i.test(target)) return null;
return path.isAbsolute(target) ? path.resolve(target) : path.resolve(cwd, target);
}
function resolveTargetIdentity(target, { cwd = process.cwd() } = {}) {
if (!target || typeof target !== 'string') return null;
if (/^https?:\/\//i.test(target)) {
try {
const url = new URL(target);
const pathname = url.pathname.replace(/\/+$/, '') || '/';
return `url:${url.origin}${pathname}`;
} catch {
return null;
}
}
const filePath = resolveLocalTargetPath(target, { cwd });
return filePath ? `file:${filePath}` : null;
}
export function fingerprintTarget(target, { cwd = process.cwd() } = {}) {
const filePath = resolveLocalTargetPath(target, { cwd });
if (!filePath) return null;
try {
if (!fs.statSync(filePath).isFile()) return null;
return `sha256:${createHash('sha256').update(fs.readFileSync(filePath)).digest('hex')}`;
} catch {
return null;
}
}
/** /**
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter * Write a snapshot for `slug`. `meta` carries the small structured frontmatter
* keys read back by readTrend(). `body` is the human-readable critique * keys read back by readTrend(). `body` is the human-readable critique
@@ -103,27 +62,14 @@ export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new
const dir = getCritiqueDir(cwd); const dir = getCritiqueDir(cwd);
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
const timestamp = nowFilenameStamp(now); const timestamp = nowFilenameStamp(now);
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
// Spread `meta` first so internally computed `timestamp` and `slug` // Spread `meta` first so internally computed `timestamp` and `slug`
// always win. Otherwise a caller-supplied meta blob (parsed from the // always win. Otherwise a caller-supplied meta blob (parsed from the
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the // IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
// filename in disagreement with its frontmatter and corrupting trends. // filename in disagreement with its frontmatter and corrupting trends.
const front = serializeFrontmatter({ ...meta, timestamp, slug }); const front = serializeFrontmatter({ ...meta, timestamp, slug });
const contents = `${front}\n${body.trim()}\n`; fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
return filePath;
// A second critique can finish in the same UTC second. Use exclusive
// creation and a fixed-width suffix so concurrent writers cannot replace
// history and lexical ordering still keeps collision entries newest.
for (let collision = 0; collision <= 9999; collision += 1) {
const suffix = collision === 0 ? '' : `~${String(collision).padStart(4, '0')}`;
const filePath = path.join(dir, `${timestamp}${suffix}__${slug}.md`);
try {
fs.writeFileSync(filePath, contents, { encoding: 'utf-8', flag: 'wx' });
return filePath;
} catch (error) {
if (error?.code !== 'EEXIST') throw error;
}
}
throw new Error(`Too many critique snapshots for ${slug} at ${timestamp}`);
} }
function serializeFrontmatter(obj) { function serializeFrontmatter(obj) {
@@ -152,8 +98,6 @@ function parseFrontmatter(text) {
try { value = JSON.parse(value); } catch { /* leave as-is */ } try { value = JSON.parse(value); } catch { /* leave as-is */ }
} else if (/^-?\d+$/.test(value)) { } else if (/^-?\d+$/.test(value)) {
value = Number(value); value = Number(value);
} else if (value === 'true' || value === 'false') {
value = value === 'true';
} }
out[key] = value; out[key] = value;
} }
@@ -163,7 +107,7 @@ function parseFrontmatter(text) {
/** /**
* Return snapshot files matching `suffix`, sorted oldest → newest. * Return snapshot files matching `suffix`, sorted oldest → newest.
*/ */
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z(?:~\d{4})?__.+\.md$/; const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
function listSnapshots(suffix, cwd) { function listSnapshots(suffix, cwd) {
const dir = getCritiqueDir(cwd); const dir = getCritiqueDir(cwd);
@@ -174,105 +118,24 @@ function listSnapshots(suffix, cwd) {
.map((f) => path.join(dir, f)); .map((f) => path.join(dir, f));
} }
function readSnapshot(filePath) { function readLatestSnapshotMatching(suffix, cwd) {
const filePath = listSnapshots(suffix, cwd).at(-1);
if (!filePath) return null; if (!filePath) return null;
const body = fs.readFileSync(filePath, 'utf-8'); const body = fs.readFileSync(filePath, 'utf-8');
return { path: filePath, body, meta: parseFrontmatter(body) }; return { path: filePath, body, meta: parseFrontmatter(body) };
} }
function snapshotTargetIdentity(snapshot) {
const targetPath = snapshot?.meta.target_path;
return snapshot?.meta.target_identity
|| (targetPath ? `file:${targetPath}` : null);
}
function readNewestSnapshot(slug, { cwd = process.cwd() } = {}) {
return readSnapshot(listSnapshots(`__${slug}.md`, cwd).at(-1));
}
function readNewestSnapshotForIdentity(
slug,
targetIdentity,
{ cwd = process.cwd() } = {},
) {
const matches = listSnapshots(`__${slug}.md`, cwd)
.map(readSnapshot)
.filter((snapshot) => snapshotTargetIdentity(snapshot) === targetIdentity);
return matches.at(-1) || null;
}
/** /**
* Return the most recent snapshot for `slug`, or null. Polish reads this * Return the most recent snapshot for `slug`, or null. Polish reads this
* to find its fix backlog when the slug matches. * to find its fix backlog when the slug matches.
*/ */
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
const latest = readNewestSnapshot(slug, { cwd }); return readLatestSnapshotMatching(`__${slug}.md`, cwd);
return latest?.meta.closed === true ? null : latest;
}
/**
* Mark one exact snapshot closed without deleting the score history consumed
* by `trend`. Exact identity matters: a newer critique may land after polish
* reads its backlog, and that newer snapshot must remain live. `snapshotFile`
* may be the absolute path returned by readLatestSnapshot() or the basename
* emitted by `latest --json`. Returns the path marked closed, or null.
*/
export function closeSnapshot(snapshotFile, { cwd = process.cwd() } = {}) {
if (!snapshotFile || typeof snapshotFile !== 'string') return null;
const dir = path.resolve(getCritiqueDir(cwd));
const snapshotPath = path.isAbsolute(snapshotFile)
? path.resolve(snapshotFile)
: path.resolve(dir, snapshotFile);
const filename = path.basename(snapshotPath);
if (
path.dirname(snapshotPath) !== dir
|| !SNAPSHOT_FILENAME.test(filename)
) return null;
let snapshot;
try {
if (!fs.lstatSync(snapshotPath).isFile()) return null;
snapshot = readSnapshot(snapshotPath);
} catch {
return null;
}
if (!snapshot || snapshot.meta.closed === true) return null;
const closedBody = snapshot.body.replace(
/^(---\r?\n[\s\S]*?)(\r?\n---)/,
'$1\nclosed: true$2',
);
if (closedBody === snapshot.body) {
throw new Error(`Cannot close snapshot without frontmatter: ${snapshot.path}`);
}
fs.writeFileSync(snapshot.path, closedBody, 'utf-8');
return snapshot.path;
} }
/** Return the most recent snapshot across all targets, or null. */ /** Return the most recent snapshot across all targets, or null. */
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
const snapshots = listSnapshots('.md', cwd).map(readSnapshot); return readLatestSnapshotMatching('.md', cwd);
const identifiedSlugs = new Set(
snapshots
.filter((snapshot) => snapshotTargetIdentity(snapshot))
.map((snapshot) => snapshot.meta.slug),
);
const latestByTarget = new Map();
for (const snapshot of snapshots) {
if (!snapshot?.meta.slug) continue;
// Slugs are lossy: distinct targets such as foo/bar and foo-bar can share
// one. Keep each known identity's latest open/closed state independent so
// closing one target cannot hide another target's live backlog. Once a
// slug has any identity-aware snapshot, its older legacy records are no
// longer independently routable and must not resurface as zombie work.
const targetIdentity = snapshotTargetIdentity(snapshot);
if (!targetIdentity && identifiedSlugs.has(snapshot.meta.slug)) continue;
const streamKey = targetIdentity || `slug:${snapshot.meta.slug}`;
latestByTarget.set(streamKey, snapshot);
}
return [...latestByTarget.values()]
.filter((snapshot) => snapshot.meta.closed !== true)
.sort((a, b) => a.path.localeCompare(b.path))
.at(-1) || null;
} }
/** /**
@@ -290,13 +153,9 @@ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
// Accept either a ready slug or a concrete target (path/URL) everywhere, so // Accept either a ready slug or a concrete target (path/URL) everywhere, so
// callers never have to run the slug step separately. Anything containing a // callers never have to run the slug step separately. Anything containing a
// path or URL marker is resolved through slugFromTarget. // path or URL marker is resolved through slugFromTarget.
function isReadySlug(value) {
return /^[a-z0-9-]+$/.test(value || '') && !value.includes('/');
}
function coerceSlug(value) { function coerceSlug(value) {
if (!value) return null; if (!value) return null;
if (isReadySlug(value)) return value; if (/^[a-z0-9-]+$/.test(value) && !value.includes('/')) return value;
return slugFromTarget(value); return slugFromTarget(value);
} }
@@ -322,124 +181,14 @@ function main(argv) {
if (metaArg) { if (metaArg) {
try { meta = JSON.parse(metaArg); } catch { /* ignore */ } try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
} }
// The helper, not caller-provided metadata, owns the target fingerprint.
// This makes the snapshot describe the exact file bytes critique saw.
delete meta.target_fingerprint;
delete meta.target_path;
delete meta.target_identity;
const targetIdentity = resolveTargetIdentity(slugArg);
if (targetIdentity) meta.target_identity = targetIdentity;
const targetFingerprint = fingerprintTarget(slugArg);
if (targetFingerprint) {
meta.target_fingerprint = targetFingerprint;
meta.target_path = resolveLocalTargetPath(slugArg);
}
const out = writeSnapshot({ slug, meta, body: raw }); const out = writeSnapshot({ slug, meta, body: raw });
process.stdout.write(`${out}\n`); process.stdout.write(`${out}\n`);
return; return;
} }
case 'latest': { case 'latest': {
const target = args[0]; const latest = readLatestSnapshot(coerceSlug(args[0]));
const format = args[1]; if (!latest) { process.exit(2); }
const slug = coerceSlug(target); process.stdout.write(latest.body);
if (!slug || (format && format !== '--json')) {
process.stderr.write('usage: latest <slug-or-target> [--json]\n');
process.exit(1);
}
const targetFingerprint = fingerprintTarget(target);
const targetPath = resolveLocalTargetPath(target);
const targetIdentity = resolveTargetIdentity(target);
const readySlug = isReadySlug(target);
const newestForSlug = readNewestSnapshot(slug);
if (!newestForSlug) { process.exit(2); }
// Concrete targets select the newest snapshot for their exact identity,
// not merely the newest filename for a lossy slug. This keeps distinct
// targets such as foo/bar and foo-bar from hiding each other's backlog.
const exactSnapshot = readNewestSnapshotForIdentity(slug, targetIdentity);
let latest = exactSnapshot;
if (!latest && !readySlug) {
// Legacy snapshots have no identity. Preserve their old explicit
// path/URL behavior only when no known target identity was selected.
latest = readNewestSnapshotForIdentity(slug, null);
}
if (!latest) latest = newestForSlug;
if (latest.meta.closed === true) { process.exit(2); }
const recordedTargetPath = latest.meta.target_path;
const recordedTargetIdentity = snapshotTargetIdentity(latest);
const matchingIdentity = recordedTargetIdentity === targetIdentity;
// Bare slugs remain a supported lookup mode, including for URL
// snapshots. But when a same-named local file exists, the request is
// ambiguous unless that exact file owns the snapshot identity.
if (readySlug && !recordedTargetIdentity) {
process.stderr.write(
'ambiguous legacy snapshot target; use an explicit ./path or full URL\n',
);
process.exit(2);
}
if (readySlug && targetPath && fs.existsSync(targetPath) && !matchingIdentity) {
process.stderr.write(
'ambiguous snapshot slug; use an explicit ./path or remove the local name collision\n',
);
process.exit(2);
}
const concreteTarget = !readySlug || matchingIdentity;
if (concreteTarget && recordedTargetIdentity && !matchingIdentity) {
process.exit(2);
}
const concreteLocalTarget = concreteTarget && targetPath;
if (concreteLocalTarget && latest.meta.target_fingerprint !== targetFingerprint) {
closeSnapshot(latest.path);
process.exit(2);
}
if (format === '--json') {
process.stdout.write(JSON.stringify({
snapshot_file: path.basename(latest.path),
body: latest.body,
}, null, 2) + '\n');
} else {
process.stdout.write(latest.body);
}
return;
}
case 'close': {
const [slugArg, snapshotFile, ...extra] = args;
const slug = coerceSlug(slugArg);
if (!slug || !snapshotFile || extra.length > 0) {
process.stderr.write('usage: close <resolved-target> <snapshot-file>\n');
process.exit(1);
}
if (
path.basename(snapshotFile) !== snapshotFile
|| !SNAPSHOT_FILENAME.test(snapshotFile)
|| !snapshotFile.endsWith(`__${slug}.md`)
) process.exit(2);
// A slug and filename are not enough to prove ownership because two
// distinct targets can normalize to the same slug. Modern snapshots
// carry a canonical identity, so require the supplied resolved target
// to match it before allowing the exact snapshot to be closed. Legacy
// snapshots without identity retain their historical close behavior.
const snapshotPath = path.join(getCritiqueDir(process.cwd()), snapshotFile);
let snapshot;
try {
if (!fs.lstatSync(snapshotPath).isFile()) process.exit(2);
snapshot = readSnapshot(snapshotPath);
} catch {
process.exit(2);
}
const recordedTargetIdentity = snapshotTargetIdentity(snapshot);
if (
recordedTargetIdentity
&& recordedTargetIdentity !== resolveTargetIdentity(slugArg)
) process.exit(2);
const closed = closeSnapshot(snapshotFile);
if (!closed) { process.exit(2); }
process.stdout.write(`${closed}\n`);
return; return;
} }
case 'trend': { case 'trend': {
@@ -448,7 +197,7 @@ function main(argv) {
return; return;
} }
default: default:
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend|close> [args]\n'); process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
process.exit(1); process.exit(1);
} }
} }
@@ -70,27 +70,13 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
} }
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([ const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS, 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui', '-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]); ]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and // WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1605,7 +1591,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) { function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false }; if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = primaryFontFace(fontFamily, GENERIC_FONTS); const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
if (!primary) return { primary: null, isSerif: false }; if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5204,7 +5190,8 @@ function checkTypography() {
const style = getComputedStyle(el); const style = getComputedStyle(el);
const ff = style.fontFamily; const ff = style.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++; totalTextElements++;
@@ -5449,7 +5436,8 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue; if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily; const ff = rule.style?.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (primary) { if (primary) {
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5468,10 +5456,11 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi; const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm; let fm;
while ((fm = ffRe.exec(html)) !== null) { while ((fm = ffRe.exec(html)) !== null) {
const primary = primaryFontFace(fm[1]); for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (primary) { if (f && !GENERIC_FONTS.has(f)) {
fonts.add(primary); fonts.add(f);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
} }
} }
@@ -1,7 +1,7 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { import {
checkSourceDesignSystem, checkSourceDesignSystem,
collectStaticDesignSystemFindings, collectStaticDesignSystemFindings,
@@ -51,7 +51,9 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue; if (!hasText) continue;
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,7 +9,6 @@ import {
WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX, WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain, isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs'; } from '../shared/constants.mjs';
import { import {
CSS_NAMED_COLORS, CSS_NAMED_COLORS,
@@ -332,7 +331,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) { function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false }; if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = primaryFontFace(fontFamily, GENERIC_FONTS); const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
if (!primary) return { primary: null, isSerif: false }; if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3931,7 +3930,8 @@ function checkTypography() {
const style = getComputedStyle(el); const style = getComputedStyle(el);
const ff = style.fontFamily; const ff = style.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++; totalTextElements++;
@@ -4176,7 +4176,8 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue; if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily; const ff = rule.style?.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (primary) { if (primary) {
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4195,10 +4196,11 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi; const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm; let fm;
while ((fm = ffRe.exec(html)) !== null) { while ((fm = ffRe.exec(html)) !== null) {
const primary = primaryFontFace(fm[1]); for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (primary) { if (f && !GENERIC_FONTS.has(f)) {
fonts.add(primary); fonts.add(f);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
} }
} }
@@ -56,27 +56,13 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
} }
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([ const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS, 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui', '-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]); ]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and // WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -118,7 +104,6 @@ export {
BRAND_FONT_DOMAINS, BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain, isBrandFontOnOwnDomain,
GENERIC_FONTS, GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX, WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR, EM_DASH_FLOOR,
@@ -21,24 +21,22 @@ import zlib from 'node:zlib';
const KEYWORD = 'impeccable:prompt'; const KEYWORD = 'impeccable:prompt';
const args = process.argv.slice(2); const args = process.argv.slice(2);
const file = args.find(a => !a.startsWith('--')); const file = args.find(a => !a.startsWith('--'));
const readMode = args.includes('--read');
const scanMode = args.includes('--scan');
const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; }; const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; };
function imageType(buffer) { function promptOf(imagePath) {
if (buffer.length > 8 && buffer.readUInt32BE(0) === 0x89504e47) return 'png'; const b = fs.readFileSync(imagePath);
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8) return 'jpeg'; let prompt = null;
return null; if (b.length > 8 && b.readUInt32BE(0) === 0x89504e47) prompt = readPngText(b);
} else if (b.length > 3 && b[0] === 0xff && b[1] === 0xd8) prompt = readJpegCom(b);
function readPrompt(imagePath, buffer = fs.readFileSync(imagePath)) {
const type = imageType(buffer);
let prompt = type === 'png' ? parsePng(buffer).prompt : type === 'jpeg' ? readJpegCom(buffer) : null;
if (prompt == null && fs.existsSync(`${imagePath}.json`)) { if (prompt == null && fs.existsSync(`${imagePath}.json`)) {
try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ } try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ }
} }
return prompt; return prompt;
} }
if (args.includes('--scan')) { if (scanMode) {
const targets = args.filter(a => !a.startsWith('--')); const targets = args.filter(a => !a.startsWith('--'));
if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); } if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); }
const RASTER = /\.(png|jpe?g|webp)$/i; const RASTER = /\.(png|jpe?g|webp)$/i;
@@ -61,7 +59,7 @@ if (args.includes('--scan')) {
} }
let missing = 0; let missing = 0;
for (const raster of rasters) { for (const raster of rasters) {
if (readPrompt(raster) == null) { console.log(`MISSING: ${raster}`); missing++; } if (promptOf(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
} }
console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`); console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`);
process.exit(missing > 0 ? 3 : 0); process.exit(missing > 0 ? 3 : 0);
@@ -70,7 +68,8 @@ if (args.includes('--scan')) {
if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); }
const buf = fs.readFileSync(file); const buf = fs.readFileSync(file);
const type = imageType(buf); const isPng = buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47;
const isJpeg = buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8;
const crcTable = (() => { const crcTable = (() => {
const t = new Uint32Array(256); const t = new Uint32Array(256);
@@ -88,26 +87,22 @@ function pngChunk(type, data) {
return out; return out;
} }
function parsePng(buffer) { function readPngText(b) {
const chunks = []; let off = 8;
let prompt = null; while (off + 12 <= b.length) {
let offset = 8; const len = b.readUInt32BE(off);
while (offset + 12 <= buffer.length) { const type = b.toString('ascii', off + 4, off + 8);
const length = buffer.readUInt32BE(offset); if (type === 'tEXt' || type === 'zTXt') {
const type = buffer.toString('ascii', offset + 4, offset + 8); const data = b.subarray(off + 8, off + 8 + len);
const data = buffer.subarray(offset + 8, offset + 8 + length); const nul = data.indexOf(0);
const nul = data.indexOf(0); if (nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD) {
const promptChunk = (type === 'tEXt' || type === 'zTXt') if (type === 'tEXt') return data.toString('utf8', nul + 1);
&& nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD; return zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
if (prompt == null && promptChunk) { }
prompt = type === 'tEXt'
? data.toString('utf8', nul + 1)
: zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
} }
chunks.push({ offset, type, promptChunk, bytes: buffer.subarray(offset, offset + 12 + length) }); off += 12 + len;
offset += 12 + length;
} }
return { chunks, prompt }; return null;
} }
function readJpegCom(b) { function readJpegCom(b) {
@@ -126,34 +121,48 @@ function readJpegCom(b) {
} }
const sidecar = `${file}.json`; const sidecar = `${file}.json`;
if (args.includes('--read')) { if (readMode) {
const prompt = readPrompt(file, buf); let prompt = null;
if (isPng) prompt = readPngText(buf);
else if (isJpeg) prompt = readJpegCom(buf);
if (prompt == null && fs.existsSync(sidecar)) {
try { prompt = JSON.parse(fs.readFileSync(sidecar, 'utf8')).prompt ?? null; } catch { /* fall through */ }
}
if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); } if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); }
console.log(prompt); console.log(prompt);
process.exit(0); process.exit(0);
} }
const promptFile = argOf('--prompt-file'); const prompt = argOf('--prompt') ?? (argOf('--prompt-file') ? fs.readFileSync(argOf('--prompt-file'), 'utf8') : null);
const prompt = argOf('--prompt') ?? (promptFile ? fs.readFileSync(promptFile, 'utf8') : null);
if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); } if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); }
if (type === 'png') { if (isPng) {
// Insert (or replace) our tEXt chunk immediately before IEND. // Insert (or replace) our tEXt chunk immediately before IEND.
const { chunks, prompt: existingPrompt } = parsePng(buf); const iend = buf.indexOf(Buffer.from('IEND', 'ascii')) - 4;
const iend = chunks.find((chunk) => chunk.type === 'IEND')?.offset ?? -1;
if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); } if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); }
// Drop any existing chunk with our keyword to keep embedding idempotent. // Drop any existing chunk with our keyword to keep embedding idempotent.
const replacing = existingPrompt != null; let body = buf.subarray(8, iend);
const body = replacing const existing = readPngText(buf);
? Buffer.concat(chunks if (existing != null) {
.filter((chunk) => chunk.offset < iend && !chunk.promptChunk) const parts = [];
.map((chunk) => chunk.bytes)) let off = 8;
: buf.subarray(8, iend); while (off + 12 <= buf.length && off < iend + 12) {
const promptChunk = pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])); const len = buf.readUInt32BE(off);
const end = replacing ? pngChunk('IEND', Buffer.alloc(0)) : buf.subarray(iend); const type = buf.toString('ascii', off + 4, off + 8);
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, promptChunk, end])); const chunk = buf.subarray(off, off + 12 + len);
const data = buf.subarray(off + 8, off + 8 + len);
const nul = data.indexOf(0);
const ours = (type === 'tEXt' || type === 'zTXt') && nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
if (!ours && type !== 'IEND') parts.push(chunk);
off += 12 + len;
}
body = Buffer.concat(parts).subarray(8 * 0); // parts exclude signature
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), pngChunk('IEND', Buffer.alloc(0))]));
} else {
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, iend), pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), buf.subarray(iend)]));
}
console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`); console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`);
} else if (type === 'jpeg') { } else if (isJpeg) {
const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8'); const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8');
if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); } if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); }
const com = Buffer.alloc(4 + seg.length); const com = Buffer.alloc(4 + seg.length);
@@ -431,15 +431,12 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64'));
// The prompt travels with the asset: embedded in the file itself (EXIF-class // The prompt travels with the asset: embedded in the file itself (EXIF-class
// metadata via embed-prompt.mjs) so intent survives copies across harnesses, // metadata via embed-prompt.mjs) so intent survives copies across harnesses,
// plus a sidecar for anything that indexes rather than opens the image. // plus a sidecar for anything that indexes rather than opens the image.
let embedded = false;
try { try {
const { spawnSync } = await import('node:child_process'); const { spawnSync } = await import('node:child_process');
const result = spawnSync(process.execPath, [fileURLToPath(new URL('./embed-prompt.mjs', import.meta.url)), out, '--prompt', prompt], { stdio: 'ignore' }); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' });
embedded = !result.error && result.status === 0;
if (!embedded) console.warn('generate-image: failed to embed prompt in the image');
fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2));
} catch { /* embedding is best-effort */ } } catch { /* embedding is best-effort */ }
console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); ${embedded ? 'prompt embedded + sidecar' : 'sidecar'} at ${out}.json`); console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`);
if (plateCtx && plateCtx.chroma) { if (plateCtx && plateCtx.chroma) {
const frac = await keyChroma(out, plateCtx.chroma); const frac = await keyChroma(out, plateCtx.chroma);
console.log(`PLATE-CHROMA keyed ${(frac * 100).toFixed(0)}% of pixels to alpha (${plateCtx.chroma}); place with a plain <img> over the page's own ground, no background on the plate. If the keyed fraction is under 20% the generator ignored the key: regenerate with --no-chroma and use mix-blend-mode: multiply instead.`); console.log(`PLATE-CHROMA keyed ${(frac * 100).toFixed(0)}% of pixels to alpha (${plateCtx.chroma}); place with a plain <img> over the page's own ground, no background on the plate. If the keyed fraction is under 20% the generator ignored the key: regenerate with --no-chroma and use mix-blend-mode: multiply instead.`);
@@ -770,22 +770,9 @@ function reset(cwd) {
} }
} catch { /* ignore */ } } catch { /* ignore */ }
} }
// `on` writes three things: config, consent, and hook entries in the return removed.length
// provider manifests. Reset must undo all three (issue #512): a leftover ? `Reset design hook config and cache (removed: ${removed.join(', ')}).`
// manifest entry kept invoking the hook after the config that said "off" : 'No hook config or cache to remove. Already at defaults.';
// was deleted. Local destRel only, since `on` never writes the team-shared
// sharedDestRel. No skill-folder gate: a reset mid-uninstall (skill files
// gone, manifest still wired) is the case that most needs the prune.
const pruned = [];
for (const target of HOOK_MANIFEST_TARGETS) {
try {
if (pruneImpeccableHookFromManifest(path.join(cwd, target.destRel))) pruned.push(target.provider);
} catch { /* ignore */ }
}
const parts = [];
if (removed.length) parts.push(`Reset design hook config and cache (removed: ${removed.join(', ')}).`);
if (pruned.length) parts.push(`Removed hook entries from: ${pruned.join(', ')}.`);
return parts.length ? parts.join(' ') : 'No hook config or cache to remove. Already at defaults.';
} }
function main() { function main() {
@@ -329,37 +329,47 @@ function stripBold(s) {
function extractNamedRules(lines) { function extractNamedRules(lines) {
const rules = []; const rules = [];
const seen = new Set(); const seen = new Set();
const addRule = (name, body, { allowDuplicate = false } = {}) => {
const key = name.toLowerCase();
if (!allowDuplicate && seen.has(key)) return;
seen.add(key);
rules.push({ name, body });
};
// Style A (Impeccable): "**The X Rule.** body body body" — can span lines. // Style A (Impeccable): "**The X Rule.** body body body" — can span lines.
const joined = lines.join('\n'); const joined = lines.join('\n');
const inlineMatches = [...joined.matchAll(/\*\*(The [^*]+?Rule)\.\*\*/g)]; const inlineStart = /\*\*(The [^*]+?Rule)\.\*\*/g;
const inlineMatches = [];
let m;
while ((m = inlineStart.exec(joined)) !== null) {
inlineMatches.push({ name: m[1], start: m.index, end: inlineStart.lastIndex });
}
for (let i = 0; i < inlineMatches.length; i++) { for (let i = 0; i < inlineMatches.length; i++) {
const match = inlineMatches[i]; const mm = inlineMatches[i];
const bodyEnd = inlineMatches[i + 1]?.index ?? joined.length; const bodyEnd = i + 1 < inlineMatches.length ? inlineMatches[i + 1].start : joined.length;
const body = joined const body = joined
.slice(match.index + match[0].length, bodyEnd) .slice(mm.end, bodyEnd)
.replace(/\n##[^\n]*$/s, '') .replace(/\n##[^\n]*$/s, '')
.replace(/\n###[^\n]*$/s, '') .replace(/\n###[^\n]*$/s, '')
.trim(); .trim();
// Preserve the inline format's historical behavior: repeated inline rules const name = stripBold(mm.name).trim();
// remain visible, while the later heading and bullet formats dedupe. seen.add(name.toLowerCase());
addRule(stripBold(match[1]).trim(), stripBold(body), { allowDuplicate: true }); rules.push({ name, body: stripBold(body) });
} }
// Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the // Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the
// bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle. // bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle.
for (const subsection of splitSubsections(lines).slice(1)) { for (let i = 0; i < lines.length; i++) {
const headerName = stripBold(subsection.name).replace(/["“”]/g, '').trim(); const h3 = lines[i].match(/^###\s+(.+?)\s*$/);
if (!h3) continue;
const headerName = stripBold(h3[1]).replace(/["“”]/g, '').trim();
if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue; if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue;
if (seen.has(headerName.toLowerCase())) continue;
const body = stripBold(subsection.lines.join('\n').replace(/\n+/g, ' ')).trim(); const bodyLines = [];
if (body) addRule(headerName, body); for (let j = i + 1; j < lines.length; j++) {
if (/^##\s|^###\s/.test(lines[j])) break;
bodyLines.push(lines[j]);
}
const body = stripBold(bodyLines.join('\n').replace(/\n+/g, ' ')).trim();
if (body) {
seen.add(headerName.toLowerCase());
rules.push({ name: headerName, body });
}
} }
// Style C (Stitch bullet form): "* **The Layering Principle:** body" // Style C (Stitch bullet form): "* **The Layering Principle:** body"
@@ -369,7 +379,9 @@ function extractNamedRules(lines) {
if (!mm) continue; if (!mm) continue;
const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim(); const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim();
if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue; if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue;
addRule(nameRaw, stripBold(mm[2]).trim()); if (seen.has(nameRaw.toLowerCase())) continue;
seen.add(nameRaw.toLowerCase());
rules.push({ name: nameRaw, body: stripBold(mm[2]).trim() });
} }
return rules; return rules;
+1 -1
View File
@@ -10,7 +10,7 @@ allowed-tools:
- Bash(node .claude/skills/impeccable/scripts/*) - Bash(node .claude/skills/impeccable/scripts/*)
--- ---
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft. This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
Core principles: Core principles:
- Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide).
@@ -1,6 +1,6 @@
### Purpose ### Purpose
Resolve one stable target, run two independent assessments, synthesize a design critique, persist a snapshot, and ask the user what to improve next. The chat response is the primary deliverable; the snapshot is an archive of that run. Resolve one stable target, run two independent assessments, synthesize a design critique, persist a snapshot, and ask the user what to improve next. The chat response is the primary deliverable; the snapshot is an archive/backlog for future commands.
### Hard Invariants ### Hard Invariants
@@ -84,7 +84,7 @@ After Assessment B returns usable CLI findings, reuse them. Do not rerun `detect
Synthesize both assessments into a single report. Do NOT simply concatenate. Weave the findings together, noting where the LLM review and detector agree, where the detector caught issues the LLM missed, and where detector findings are false positives. Synthesize both assessments into a single report. Do NOT simply concatenate. Weave the findings together, noting where the LLM review and detector agree, where the detector caught issues the LLM missed, and where detector findings are false positives.
The chat response is the primary user-facing deliverable. Present the full structured critique below in chat; do not replace it with a summary and a link. The persisted snapshot is an archive of that run. The chat response is the primary user-facing deliverable. Present the full structured critique below in chat; do not replace it with a summary and a link. The persisted snapshot is only an archive/backlog for later commands.
Structure your feedback as a design director would: Structure your feedback as a design director would:
@@ -197,7 +197,7 @@ Skip this step if the Setup slug was null (vague or root-level target).
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"max_score":<n>,"na_heuristics":"<comma-separated numbers, or empty>","p0_count":<n>,"p1_count":<n>}' \ IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"max_score":<n>,"na_heuristics":"<comma-separated numbers, or empty>","p0_count":<n>,"p1_count":<n>}' \
node .claude/skills/impeccable/scripts/critique-storage.mjs write "<resolved target>" <body-file> node .claude/skills/impeccable/scripts/critique-storage.mjs write "<resolved target>" <body-file>
``` ```
`max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. For a local file target, the helper also records an exact content fingerprint so polish can distinguish the assessed bytes from later edits without relying on Git state or timestamps. The helper prints the absolute path it wrote. Leave that file on disk. Polish closes it; this run does not. `max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. The helper prints the absolute path it wrote.
3. **Delete the temp body file** after the write attempt completes, whether the write succeeded or failed. If deletion fails, mention `temp-file cleanup failed: <reason>` briefly in the final output, but do not block the critique. 3. **Delete the temp body file** after the write attempt completes, whether the write succeeded or failed. If deletion fails, mention `temp-file cleanup failed: <reason>` briefly in the final output, but do not block the critique.
+1 -1
View File
@@ -32,7 +32,7 @@ The first argument is the action. Defaults to `status`.
| `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. | | `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. |
| `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. | | `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. |
| `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. | | `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. |
| `reset` | Delete the project config, dedup cache, and Cursor pending queue, and remove the hook's entries from every provider manifest `on` installs, the committed Copilot file included (a team-shared `settings.json` that `on` never writes is never touched). | | `reset` | Delete the project config, dedup cache, and Cursor pending queue. |
## Flow ## Flow
@@ -70,20 +70,16 @@ Your measured rendition prior: warm, bookish, family, and child-facing subjects
## 5. Record the decision ## 5. Record the decision
Before code, record the chosen direction as a development-only contract under `## Direction contract` in the relevant surface brief. A direction contract is durable route or artifact strategy, so create or update the brief even when no other surface strategy needs persistence. Keep the contract to six short blocks and 150 words at most. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The surface brief is the reminder later agents reload across edits and sessions: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
Never copy the direction contract into implementation source or any browser-delivered artifact. This includes HTML or framework comments, hidden DOM, `<template>` elements, `data-*` attributes, rendered JSX or TSX output, serialized props or state, React Server Component payloads, client bundles, metadata or JSON-LD, accessibility-only text, and files served beside the artifact. A compiler or optimizer removing development metadata is not a safety boundary. Reviewers and documenters receive the contract from the surface brief.
On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md.
Read the existing surface brief before updating it: If the work establishes durable strategy for a route or artifact, read its existing surface brief, then update it:
`node .claude/skills/impeccable/scripts/surface-brief.mjs read <primary-target>` `node .claude/skills/impeccable/scripts/surface-brief.mjs read <primary-target>`
`node .claude/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]` `node .claude/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]`
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it.
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior.
+2 -10
View File
@@ -29,10 +29,10 @@ Use the feature yourself at the surface's representative sizes: desktop and mobi
If a prior critique exists, use it as one input: If a prior critique exists, use it as one input:
```bash ```bash
node .claude/skills/impeccable/scripts/critique-storage.mjs latest "<resolved target>" --json node .claude/skills/impeccable/scripts/critique-storage.mjs latest "<resolved target>"
``` ```
Exit 0 returns JSON with the latest snapshot's `body` and an exact `snapshot_file` identity. Retain `snapshot_file` until the end of the pass. For a local file target, the helper compares the file's exact current content fingerprint with the fingerprint captured by critique. Unchanged staged, unstaged, or untracked content remains current; any byte change, deletion, or replacement with a non-file closes the backlog it identified while preserving its trend history and exits 2. A URL target has no local fingerprint and remains current until explicitly closed. When current, incorporate relevant P0/P1 findings from `body` and name the snapshot read. Exit 2 means none exists or the target changed. Perform an independent pass either way. Exit 0 returns the latest snapshot; incorporate relevant P0/P1 findings and name the snapshot read. Exit 2 means none exists. Perform an independent pass either way.
## 3. Triage ## 3. Triage
@@ -95,11 +95,3 @@ Walk the complete path again with mouse, keyboard, and touch where applicable. C
Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment.
Finish with a source diff: remove accidental churn, orphaned code, redundant values, and temporary artifacts. Ship only when the feature is functionally complete and consistently finished across the path. Finish with a source diff: remove accidental churn, orphaned code, redundant values, and temporary artifacts. Ship only when the feature is functionally complete and consistently finished across the path.
When this pass clears every Priority Issue it took from a snapshot, close that snapshot:
```bash
node .claude/skills/impeccable/scripts/critique-storage.mjs close "<resolved target>" "<snapshot_file returned by latest>"
```
This closes only the snapshot this pass actually processed; if a newer critique landed meanwhile, its backlog stays live. Do not close when no snapshot was read, when `snapshot_file` was not retained, or when Priority Issues remain.
@@ -8,7 +8,7 @@ Reason over the signals; there is no score to obey:
- `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system).
- `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default. - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default.
- `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared). - `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale.
- `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them.
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `detect.mjs` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code. - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `detect.mjs` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code.
- Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`. - Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`.
@@ -16,9 +16,8 @@
* CLI entry points (called from skill instructions): * CLI entry points (called from skill instructions):
* node critique-storage.mjs slug <resolved-target> * node critique-storage.mjs slug <resolved-target>
* node critique-storage.mjs write <slug> <snapshot-body-file> * node critique-storage.mjs write <slug> <snapshot-body-file>
* node critique-storage.mjs latest <slug> [--json] * node critique-storage.mjs latest <slug>
* node critique-storage.mjs trend <slug> [limit] * node critique-storage.mjs trend <slug> [limit]
* node critique-storage.mjs close <resolved-target> <snapshot-file>
* *
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain * Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
* markdown file; the model reads it directly with its file-read tool. This * markdown file; the model reads it directly with its file-read tool. This
@@ -28,7 +27,6 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { createHash } from 'node:crypto';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
import { getCritiqueDir } from './lib/impeccable-paths.mjs'; import { getCritiqueDir } from './lib/impeccable-paths.mjs';
import { slugFromTarget } from './lib/target-slug.mjs'; import { slugFromTarget } from './lib/target-slug.mjs';
@@ -52,45 +50,6 @@ export function nowFilenameStamp(date = new Date()) {
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z'); return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
} }
/**
* Return an exact content fingerprint for a local file target. URLs and
* non-files return null because their content is not available here.
*
* The fingerprint deliberately describes bytes, not Git state or mtimes:
* critique often assesses an uncommitted file, and a later polish run should
* inherit that backlog when the bytes are unchanged regardless of staging.
*/
function resolveLocalTargetPath(target, { cwd = process.cwd() } = {}) {
if (!target || /^https?:\/\//i.test(target)) return null;
return path.isAbsolute(target) ? path.resolve(target) : path.resolve(cwd, target);
}
function resolveTargetIdentity(target, { cwd = process.cwd() } = {}) {
if (!target || typeof target !== 'string') return null;
if (/^https?:\/\//i.test(target)) {
try {
const url = new URL(target);
const pathname = url.pathname.replace(/\/+$/, '') || '/';
return `url:${url.origin}${pathname}`;
} catch {
return null;
}
}
const filePath = resolveLocalTargetPath(target, { cwd });
return filePath ? `file:${filePath}` : null;
}
export function fingerprintTarget(target, { cwd = process.cwd() } = {}) {
const filePath = resolveLocalTargetPath(target, { cwd });
if (!filePath) return null;
try {
if (!fs.statSync(filePath).isFile()) return null;
return `sha256:${createHash('sha256').update(fs.readFileSync(filePath)).digest('hex')}`;
} catch {
return null;
}
}
/** /**
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter * Write a snapshot for `slug`. `meta` carries the small structured frontmatter
* keys read back by readTrend(). `body` is the human-readable critique * keys read back by readTrend(). `body` is the human-readable critique
@@ -103,27 +62,14 @@ export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new
const dir = getCritiqueDir(cwd); const dir = getCritiqueDir(cwd);
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
const timestamp = nowFilenameStamp(now); const timestamp = nowFilenameStamp(now);
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
// Spread `meta` first so internally computed `timestamp` and `slug` // Spread `meta` first so internally computed `timestamp` and `slug`
// always win. Otherwise a caller-supplied meta blob (parsed from the // always win. Otherwise a caller-supplied meta blob (parsed from the
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the // IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
// filename in disagreement with its frontmatter and corrupting trends. // filename in disagreement with its frontmatter and corrupting trends.
const front = serializeFrontmatter({ ...meta, timestamp, slug }); const front = serializeFrontmatter({ ...meta, timestamp, slug });
const contents = `${front}\n${body.trim()}\n`; fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
return filePath;
// A second critique can finish in the same UTC second. Use exclusive
// creation and a fixed-width suffix so concurrent writers cannot replace
// history and lexical ordering still keeps collision entries newest.
for (let collision = 0; collision <= 9999; collision += 1) {
const suffix = collision === 0 ? '' : `~${String(collision).padStart(4, '0')}`;
const filePath = path.join(dir, `${timestamp}${suffix}__${slug}.md`);
try {
fs.writeFileSync(filePath, contents, { encoding: 'utf-8', flag: 'wx' });
return filePath;
} catch (error) {
if (error?.code !== 'EEXIST') throw error;
}
}
throw new Error(`Too many critique snapshots for ${slug} at ${timestamp}`);
} }
function serializeFrontmatter(obj) { function serializeFrontmatter(obj) {
@@ -152,8 +98,6 @@ function parseFrontmatter(text) {
try { value = JSON.parse(value); } catch { /* leave as-is */ } try { value = JSON.parse(value); } catch { /* leave as-is */ }
} else if (/^-?\d+$/.test(value)) { } else if (/^-?\d+$/.test(value)) {
value = Number(value); value = Number(value);
} else if (value === 'true' || value === 'false') {
value = value === 'true';
} }
out[key] = value; out[key] = value;
} }
@@ -163,7 +107,7 @@ function parseFrontmatter(text) {
/** /**
* Return snapshot files matching `suffix`, sorted oldest → newest. * Return snapshot files matching `suffix`, sorted oldest → newest.
*/ */
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z(?:~\d{4})?__.+\.md$/; const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
function listSnapshots(suffix, cwd) { function listSnapshots(suffix, cwd) {
const dir = getCritiqueDir(cwd); const dir = getCritiqueDir(cwd);
@@ -174,105 +118,24 @@ function listSnapshots(suffix, cwd) {
.map((f) => path.join(dir, f)); .map((f) => path.join(dir, f));
} }
function readSnapshot(filePath) { function readLatestSnapshotMatching(suffix, cwd) {
const filePath = listSnapshots(suffix, cwd).at(-1);
if (!filePath) return null; if (!filePath) return null;
const body = fs.readFileSync(filePath, 'utf-8'); const body = fs.readFileSync(filePath, 'utf-8');
return { path: filePath, body, meta: parseFrontmatter(body) }; return { path: filePath, body, meta: parseFrontmatter(body) };
} }
function snapshotTargetIdentity(snapshot) {
const targetPath = snapshot?.meta.target_path;
return snapshot?.meta.target_identity
|| (targetPath ? `file:${targetPath}` : null);
}
function readNewestSnapshot(slug, { cwd = process.cwd() } = {}) {
return readSnapshot(listSnapshots(`__${slug}.md`, cwd).at(-1));
}
function readNewestSnapshotForIdentity(
slug,
targetIdentity,
{ cwd = process.cwd() } = {},
) {
const matches = listSnapshots(`__${slug}.md`, cwd)
.map(readSnapshot)
.filter((snapshot) => snapshotTargetIdentity(snapshot) === targetIdentity);
return matches.at(-1) || null;
}
/** /**
* Return the most recent snapshot for `slug`, or null. Polish reads this * Return the most recent snapshot for `slug`, or null. Polish reads this
* to find its fix backlog when the slug matches. * to find its fix backlog when the slug matches.
*/ */
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
const latest = readNewestSnapshot(slug, { cwd }); return readLatestSnapshotMatching(`__${slug}.md`, cwd);
return latest?.meta.closed === true ? null : latest;
}
/**
* Mark one exact snapshot closed without deleting the score history consumed
* by `trend`. Exact identity matters: a newer critique may land after polish
* reads its backlog, and that newer snapshot must remain live. `snapshotFile`
* may be the absolute path returned by readLatestSnapshot() or the basename
* emitted by `latest --json`. Returns the path marked closed, or null.
*/
export function closeSnapshot(snapshotFile, { cwd = process.cwd() } = {}) {
if (!snapshotFile || typeof snapshotFile !== 'string') return null;
const dir = path.resolve(getCritiqueDir(cwd));
const snapshotPath = path.isAbsolute(snapshotFile)
? path.resolve(snapshotFile)
: path.resolve(dir, snapshotFile);
const filename = path.basename(snapshotPath);
if (
path.dirname(snapshotPath) !== dir
|| !SNAPSHOT_FILENAME.test(filename)
) return null;
let snapshot;
try {
if (!fs.lstatSync(snapshotPath).isFile()) return null;
snapshot = readSnapshot(snapshotPath);
} catch {
return null;
}
if (!snapshot || snapshot.meta.closed === true) return null;
const closedBody = snapshot.body.replace(
/^(---\r?\n[\s\S]*?)(\r?\n---)/,
'$1\nclosed: true$2',
);
if (closedBody === snapshot.body) {
throw new Error(`Cannot close snapshot without frontmatter: ${snapshot.path}`);
}
fs.writeFileSync(snapshot.path, closedBody, 'utf-8');
return snapshot.path;
} }
/** Return the most recent snapshot across all targets, or null. */ /** Return the most recent snapshot across all targets, or null. */
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
const snapshots = listSnapshots('.md', cwd).map(readSnapshot); return readLatestSnapshotMatching('.md', cwd);
const identifiedSlugs = new Set(
snapshots
.filter((snapshot) => snapshotTargetIdentity(snapshot))
.map((snapshot) => snapshot.meta.slug),
);
const latestByTarget = new Map();
for (const snapshot of snapshots) {
if (!snapshot?.meta.slug) continue;
// Slugs are lossy: distinct targets such as foo/bar and foo-bar can share
// one. Keep each known identity's latest open/closed state independent so
// closing one target cannot hide another target's live backlog. Once a
// slug has any identity-aware snapshot, its older legacy records are no
// longer independently routable and must not resurface as zombie work.
const targetIdentity = snapshotTargetIdentity(snapshot);
if (!targetIdentity && identifiedSlugs.has(snapshot.meta.slug)) continue;
const streamKey = targetIdentity || `slug:${snapshot.meta.slug}`;
latestByTarget.set(streamKey, snapshot);
}
return [...latestByTarget.values()]
.filter((snapshot) => snapshot.meta.closed !== true)
.sort((a, b) => a.path.localeCompare(b.path))
.at(-1) || null;
} }
/** /**
@@ -290,13 +153,9 @@ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
// Accept either a ready slug or a concrete target (path/URL) everywhere, so // Accept either a ready slug or a concrete target (path/URL) everywhere, so
// callers never have to run the slug step separately. Anything containing a // callers never have to run the slug step separately. Anything containing a
// path or URL marker is resolved through slugFromTarget. // path or URL marker is resolved through slugFromTarget.
function isReadySlug(value) {
return /^[a-z0-9-]+$/.test(value || '') && !value.includes('/');
}
function coerceSlug(value) { function coerceSlug(value) {
if (!value) return null; if (!value) return null;
if (isReadySlug(value)) return value; if (/^[a-z0-9-]+$/.test(value) && !value.includes('/')) return value;
return slugFromTarget(value); return slugFromTarget(value);
} }
@@ -322,124 +181,14 @@ function main(argv) {
if (metaArg) { if (metaArg) {
try { meta = JSON.parse(metaArg); } catch { /* ignore */ } try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
} }
// The helper, not caller-provided metadata, owns the target fingerprint.
// This makes the snapshot describe the exact file bytes critique saw.
delete meta.target_fingerprint;
delete meta.target_path;
delete meta.target_identity;
const targetIdentity = resolveTargetIdentity(slugArg);
if (targetIdentity) meta.target_identity = targetIdentity;
const targetFingerprint = fingerprintTarget(slugArg);
if (targetFingerprint) {
meta.target_fingerprint = targetFingerprint;
meta.target_path = resolveLocalTargetPath(slugArg);
}
const out = writeSnapshot({ slug, meta, body: raw }); const out = writeSnapshot({ slug, meta, body: raw });
process.stdout.write(`${out}\n`); process.stdout.write(`${out}\n`);
return; return;
} }
case 'latest': { case 'latest': {
const target = args[0]; const latest = readLatestSnapshot(coerceSlug(args[0]));
const format = args[1]; if (!latest) { process.exit(2); }
const slug = coerceSlug(target); process.stdout.write(latest.body);
if (!slug || (format && format !== '--json')) {
process.stderr.write('usage: latest <slug-or-target> [--json]\n');
process.exit(1);
}
const targetFingerprint = fingerprintTarget(target);
const targetPath = resolveLocalTargetPath(target);
const targetIdentity = resolveTargetIdentity(target);
const readySlug = isReadySlug(target);
const newestForSlug = readNewestSnapshot(slug);
if (!newestForSlug) { process.exit(2); }
// Concrete targets select the newest snapshot for their exact identity,
// not merely the newest filename for a lossy slug. This keeps distinct
// targets such as foo/bar and foo-bar from hiding each other's backlog.
const exactSnapshot = readNewestSnapshotForIdentity(slug, targetIdentity);
let latest = exactSnapshot;
if (!latest && !readySlug) {
// Legacy snapshots have no identity. Preserve their old explicit
// path/URL behavior only when no known target identity was selected.
latest = readNewestSnapshotForIdentity(slug, null);
}
if (!latest) latest = newestForSlug;
if (latest.meta.closed === true) { process.exit(2); }
const recordedTargetPath = latest.meta.target_path;
const recordedTargetIdentity = snapshotTargetIdentity(latest);
const matchingIdentity = recordedTargetIdentity === targetIdentity;
// Bare slugs remain a supported lookup mode, including for URL
// snapshots. But when a same-named local file exists, the request is
// ambiguous unless that exact file owns the snapshot identity.
if (readySlug && !recordedTargetIdentity) {
process.stderr.write(
'ambiguous legacy snapshot target; use an explicit ./path or full URL\n',
);
process.exit(2);
}
if (readySlug && targetPath && fs.existsSync(targetPath) && !matchingIdentity) {
process.stderr.write(
'ambiguous snapshot slug; use an explicit ./path or remove the local name collision\n',
);
process.exit(2);
}
const concreteTarget = !readySlug || matchingIdentity;
if (concreteTarget && recordedTargetIdentity && !matchingIdentity) {
process.exit(2);
}
const concreteLocalTarget = concreteTarget && targetPath;
if (concreteLocalTarget && latest.meta.target_fingerprint !== targetFingerprint) {
closeSnapshot(latest.path);
process.exit(2);
}
if (format === '--json') {
process.stdout.write(JSON.stringify({
snapshot_file: path.basename(latest.path),
body: latest.body,
}, null, 2) + '\n');
} else {
process.stdout.write(latest.body);
}
return;
}
case 'close': {
const [slugArg, snapshotFile, ...extra] = args;
const slug = coerceSlug(slugArg);
if (!slug || !snapshotFile || extra.length > 0) {
process.stderr.write('usage: close <resolved-target> <snapshot-file>\n');
process.exit(1);
}
if (
path.basename(snapshotFile) !== snapshotFile
|| !SNAPSHOT_FILENAME.test(snapshotFile)
|| !snapshotFile.endsWith(`__${slug}.md`)
) process.exit(2);
// A slug and filename are not enough to prove ownership because two
// distinct targets can normalize to the same slug. Modern snapshots
// carry a canonical identity, so require the supplied resolved target
// to match it before allowing the exact snapshot to be closed. Legacy
// snapshots without identity retain their historical close behavior.
const snapshotPath = path.join(getCritiqueDir(process.cwd()), snapshotFile);
let snapshot;
try {
if (!fs.lstatSync(snapshotPath).isFile()) process.exit(2);
snapshot = readSnapshot(snapshotPath);
} catch {
process.exit(2);
}
const recordedTargetIdentity = snapshotTargetIdentity(snapshot);
if (
recordedTargetIdentity
&& recordedTargetIdentity !== resolveTargetIdentity(slugArg)
) process.exit(2);
const closed = closeSnapshot(snapshotFile);
if (!closed) { process.exit(2); }
process.stdout.write(`${closed}\n`);
return; return;
} }
case 'trend': { case 'trend': {
@@ -448,7 +197,7 @@ function main(argv) {
return; return;
} }
default: default:
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend|close> [args]\n'); process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
process.exit(1); process.exit(1);
} }
} }
@@ -70,27 +70,13 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
} }
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([ const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS, 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui', '-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]); ]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and // WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1605,7 +1591,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) { function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false }; if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = primaryFontFace(fontFamily, GENERIC_FONTS); const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
if (!primary) return { primary: null, isSerif: false }; if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5204,7 +5190,8 @@ function checkTypography() {
const style = getComputedStyle(el); const style = getComputedStyle(el);
const ff = style.fontFamily; const ff = style.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++; totalTextElements++;
@@ -5449,7 +5436,8 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue; if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily; const ff = rule.style?.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (primary) { if (primary) {
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5468,10 +5456,11 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi; const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm; let fm;
while ((fm = ffRe.exec(html)) !== null) { while ((fm = ffRe.exec(html)) !== null) {
const primary = primaryFontFace(fm[1]); for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (primary) { if (f && !GENERIC_FONTS.has(f)) {
fonts.add(primary); fonts.add(f);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
} }
} }
@@ -1,7 +1,7 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { import {
checkSourceDesignSystem, checkSourceDesignSystem,
collectStaticDesignSystemFindings, collectStaticDesignSystemFindings,
@@ -51,7 +51,9 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue; if (!hasText) continue;
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,7 +9,6 @@ import {
WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX, WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain, isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs'; } from '../shared/constants.mjs';
import { import {
CSS_NAMED_COLORS, CSS_NAMED_COLORS,
@@ -332,7 +331,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) { function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false }; if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = primaryFontFace(fontFamily, GENERIC_FONTS); const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
if (!primary) return { primary: null, isSerif: false }; if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3931,7 +3930,8 @@ function checkTypography() {
const style = getComputedStyle(el); const style = getComputedStyle(el);
const ff = style.fontFamily; const ff = style.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++; totalTextElements++;
@@ -4176,7 +4176,8 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue; if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily; const ff = rule.style?.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (primary) { if (primary) {
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4195,10 +4196,11 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi; const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm; let fm;
while ((fm = ffRe.exec(html)) !== null) { while ((fm = ffRe.exec(html)) !== null) {
const primary = primaryFontFace(fm[1]); for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (primary) { if (f && !GENERIC_FONTS.has(f)) {
fonts.add(primary); fonts.add(f);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
} }
} }
@@ -56,27 +56,13 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
} }
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([ const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS, 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui', '-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]); ]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and // WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -118,7 +104,6 @@ export {
BRAND_FONT_DOMAINS, BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain, isBrandFontOnOwnDomain,
GENERIC_FONTS, GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX, WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR, EM_DASH_FLOOR,
@@ -21,24 +21,22 @@ import zlib from 'node:zlib';
const KEYWORD = 'impeccable:prompt'; const KEYWORD = 'impeccable:prompt';
const args = process.argv.slice(2); const args = process.argv.slice(2);
const file = args.find(a => !a.startsWith('--')); const file = args.find(a => !a.startsWith('--'));
const readMode = args.includes('--read');
const scanMode = args.includes('--scan');
const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; }; const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; };
function imageType(buffer) { function promptOf(imagePath) {
if (buffer.length > 8 && buffer.readUInt32BE(0) === 0x89504e47) return 'png'; const b = fs.readFileSync(imagePath);
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8) return 'jpeg'; let prompt = null;
return null; if (b.length > 8 && b.readUInt32BE(0) === 0x89504e47) prompt = readPngText(b);
} else if (b.length > 3 && b[0] === 0xff && b[1] === 0xd8) prompt = readJpegCom(b);
function readPrompt(imagePath, buffer = fs.readFileSync(imagePath)) {
const type = imageType(buffer);
let prompt = type === 'png' ? parsePng(buffer).prompt : type === 'jpeg' ? readJpegCom(buffer) : null;
if (prompt == null && fs.existsSync(`${imagePath}.json`)) { if (prompt == null && fs.existsSync(`${imagePath}.json`)) {
try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ } try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ }
} }
return prompt; return prompt;
} }
if (args.includes('--scan')) { if (scanMode) {
const targets = args.filter(a => !a.startsWith('--')); const targets = args.filter(a => !a.startsWith('--'));
if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); } if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); }
const RASTER = /\.(png|jpe?g|webp)$/i; const RASTER = /\.(png|jpe?g|webp)$/i;
@@ -61,7 +59,7 @@ if (args.includes('--scan')) {
} }
let missing = 0; let missing = 0;
for (const raster of rasters) { for (const raster of rasters) {
if (readPrompt(raster) == null) { console.log(`MISSING: ${raster}`); missing++; } if (promptOf(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
} }
console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`); console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`);
process.exit(missing > 0 ? 3 : 0); process.exit(missing > 0 ? 3 : 0);
@@ -70,7 +68,8 @@ if (args.includes('--scan')) {
if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); }
const buf = fs.readFileSync(file); const buf = fs.readFileSync(file);
const type = imageType(buf); const isPng = buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47;
const isJpeg = buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8;
const crcTable = (() => { const crcTable = (() => {
const t = new Uint32Array(256); const t = new Uint32Array(256);
@@ -88,26 +87,22 @@ function pngChunk(type, data) {
return out; return out;
} }
function parsePng(buffer) { function readPngText(b) {
const chunks = []; let off = 8;
let prompt = null; while (off + 12 <= b.length) {
let offset = 8; const len = b.readUInt32BE(off);
while (offset + 12 <= buffer.length) { const type = b.toString('ascii', off + 4, off + 8);
const length = buffer.readUInt32BE(offset); if (type === 'tEXt' || type === 'zTXt') {
const type = buffer.toString('ascii', offset + 4, offset + 8); const data = b.subarray(off + 8, off + 8 + len);
const data = buffer.subarray(offset + 8, offset + 8 + length); const nul = data.indexOf(0);
const nul = data.indexOf(0); if (nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD) {
const promptChunk = (type === 'tEXt' || type === 'zTXt') if (type === 'tEXt') return data.toString('utf8', nul + 1);
&& nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD; return zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
if (prompt == null && promptChunk) { }
prompt = type === 'tEXt'
? data.toString('utf8', nul + 1)
: zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
} }
chunks.push({ offset, type, promptChunk, bytes: buffer.subarray(offset, offset + 12 + length) }); off += 12 + len;
offset += 12 + length;
} }
return { chunks, prompt }; return null;
} }
function readJpegCom(b) { function readJpegCom(b) {
@@ -126,34 +121,48 @@ function readJpegCom(b) {
} }
const sidecar = `${file}.json`; const sidecar = `${file}.json`;
if (args.includes('--read')) { if (readMode) {
const prompt = readPrompt(file, buf); let prompt = null;
if (isPng) prompt = readPngText(buf);
else if (isJpeg) prompt = readJpegCom(buf);
if (prompt == null && fs.existsSync(sidecar)) {
try { prompt = JSON.parse(fs.readFileSync(sidecar, 'utf8')).prompt ?? null; } catch { /* fall through */ }
}
if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); } if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); }
console.log(prompt); console.log(prompt);
process.exit(0); process.exit(0);
} }
const promptFile = argOf('--prompt-file'); const prompt = argOf('--prompt') ?? (argOf('--prompt-file') ? fs.readFileSync(argOf('--prompt-file'), 'utf8') : null);
const prompt = argOf('--prompt') ?? (promptFile ? fs.readFileSync(promptFile, 'utf8') : null);
if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); } if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); }
if (type === 'png') { if (isPng) {
// Insert (or replace) our tEXt chunk immediately before IEND. // Insert (or replace) our tEXt chunk immediately before IEND.
const { chunks, prompt: existingPrompt } = parsePng(buf); const iend = buf.indexOf(Buffer.from('IEND', 'ascii')) - 4;
const iend = chunks.find((chunk) => chunk.type === 'IEND')?.offset ?? -1;
if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); } if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); }
// Drop any existing chunk with our keyword to keep embedding idempotent. // Drop any existing chunk with our keyword to keep embedding idempotent.
const replacing = existingPrompt != null; let body = buf.subarray(8, iend);
const body = replacing const existing = readPngText(buf);
? Buffer.concat(chunks if (existing != null) {
.filter((chunk) => chunk.offset < iend && !chunk.promptChunk) const parts = [];
.map((chunk) => chunk.bytes)) let off = 8;
: buf.subarray(8, iend); while (off + 12 <= buf.length && off < iend + 12) {
const promptChunk = pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])); const len = buf.readUInt32BE(off);
const end = replacing ? pngChunk('IEND', Buffer.alloc(0)) : buf.subarray(iend); const type = buf.toString('ascii', off + 4, off + 8);
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, promptChunk, end])); const chunk = buf.subarray(off, off + 12 + len);
const data = buf.subarray(off + 8, off + 8 + len);
const nul = data.indexOf(0);
const ours = (type === 'tEXt' || type === 'zTXt') && nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
if (!ours && type !== 'IEND') parts.push(chunk);
off += 12 + len;
}
body = Buffer.concat(parts).subarray(8 * 0); // parts exclude signature
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), pngChunk('IEND', Buffer.alloc(0))]));
} else {
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, iend), pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), buf.subarray(iend)]));
}
console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`); console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`);
} else if (type === 'jpeg') { } else if (isJpeg) {
const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8'); const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8');
if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); } if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); }
const com = Buffer.alloc(4 + seg.length); const com = Buffer.alloc(4 + seg.length);
@@ -431,15 +431,12 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64'));
// The prompt travels with the asset: embedded in the file itself (EXIF-class // The prompt travels with the asset: embedded in the file itself (EXIF-class
// metadata via embed-prompt.mjs) so intent survives copies across harnesses, // metadata via embed-prompt.mjs) so intent survives copies across harnesses,
// plus a sidecar for anything that indexes rather than opens the image. // plus a sidecar for anything that indexes rather than opens the image.
let embedded = false;
try { try {
const { spawnSync } = await import('node:child_process'); const { spawnSync } = await import('node:child_process');
const result = spawnSync(process.execPath, [fileURLToPath(new URL('./embed-prompt.mjs', import.meta.url)), out, '--prompt', prompt], { stdio: 'ignore' }); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' });
embedded = !result.error && result.status === 0;
if (!embedded) console.warn('generate-image: failed to embed prompt in the image');
fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2));
} catch { /* embedding is best-effort */ } } catch { /* embedding is best-effort */ }
console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); ${embedded ? 'prompt embedded + sidecar' : 'sidecar'} at ${out}.json`); console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`);
if (plateCtx && plateCtx.chroma) { if (plateCtx && plateCtx.chroma) {
const frac = await keyChroma(out, plateCtx.chroma); const frac = await keyChroma(out, plateCtx.chroma);
console.log(`PLATE-CHROMA keyed ${(frac * 100).toFixed(0)}% of pixels to alpha (${plateCtx.chroma}); place with a plain <img> over the page's own ground, no background on the plate. If the keyed fraction is under 20% the generator ignored the key: regenerate with --no-chroma and use mix-blend-mode: multiply instead.`); console.log(`PLATE-CHROMA keyed ${(frac * 100).toFixed(0)}% of pixels to alpha (${plateCtx.chroma}); place with a plain <img> over the page's own ground, no background on the plate. If the keyed fraction is under 20% the generator ignored the key: regenerate with --no-chroma and use mix-blend-mode: multiply instead.`);
@@ -770,22 +770,9 @@ function reset(cwd) {
} }
} catch { /* ignore */ } } catch { /* ignore */ }
} }
// `on` writes three things: config, consent, and hook entries in the return removed.length
// provider manifests. Reset must undo all three (issue #512): a leftover ? `Reset design hook config and cache (removed: ${removed.join(', ')}).`
// manifest entry kept invoking the hook after the config that said "off" : 'No hook config or cache to remove. Already at defaults.';
// was deleted. Local destRel only, since `on` never writes the team-shared
// sharedDestRel. No skill-folder gate: a reset mid-uninstall (skill files
// gone, manifest still wired) is the case that most needs the prune.
const pruned = [];
for (const target of HOOK_MANIFEST_TARGETS) {
try {
if (pruneImpeccableHookFromManifest(path.join(cwd, target.destRel))) pruned.push(target.provider);
} catch { /* ignore */ }
}
const parts = [];
if (removed.length) parts.push(`Reset design hook config and cache (removed: ${removed.join(', ')}).`);
if (pruned.length) parts.push(`Removed hook entries from: ${pruned.join(', ')}.`);
return parts.length ? parts.join(' ') : 'No hook config or cache to remove. Already at defaults.';
} }
function main() { function main() {
@@ -329,37 +329,47 @@ function stripBold(s) {
function extractNamedRules(lines) { function extractNamedRules(lines) {
const rules = []; const rules = [];
const seen = new Set(); const seen = new Set();
const addRule = (name, body, { allowDuplicate = false } = {}) => {
const key = name.toLowerCase();
if (!allowDuplicate && seen.has(key)) return;
seen.add(key);
rules.push({ name, body });
};
// Style A (Impeccable): "**The X Rule.** body body body" — can span lines. // Style A (Impeccable): "**The X Rule.** body body body" — can span lines.
const joined = lines.join('\n'); const joined = lines.join('\n');
const inlineMatches = [...joined.matchAll(/\*\*(The [^*]+?Rule)\.\*\*/g)]; const inlineStart = /\*\*(The [^*]+?Rule)\.\*\*/g;
const inlineMatches = [];
let m;
while ((m = inlineStart.exec(joined)) !== null) {
inlineMatches.push({ name: m[1], start: m.index, end: inlineStart.lastIndex });
}
for (let i = 0; i < inlineMatches.length; i++) { for (let i = 0; i < inlineMatches.length; i++) {
const match = inlineMatches[i]; const mm = inlineMatches[i];
const bodyEnd = inlineMatches[i + 1]?.index ?? joined.length; const bodyEnd = i + 1 < inlineMatches.length ? inlineMatches[i + 1].start : joined.length;
const body = joined const body = joined
.slice(match.index + match[0].length, bodyEnd) .slice(mm.end, bodyEnd)
.replace(/\n##[^\n]*$/s, '') .replace(/\n##[^\n]*$/s, '')
.replace(/\n###[^\n]*$/s, '') .replace(/\n###[^\n]*$/s, '')
.trim(); .trim();
// Preserve the inline format's historical behavior: repeated inline rules const name = stripBold(mm.name).trim();
// remain visible, while the later heading and bullet formats dedupe. seen.add(name.toLowerCase());
addRule(stripBold(match[1]).trim(), stripBold(body), { allowDuplicate: true }); rules.push({ name, body: stripBold(body) });
} }
// Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the // Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the
// bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle. // bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle.
for (const subsection of splitSubsections(lines).slice(1)) { for (let i = 0; i < lines.length; i++) {
const headerName = stripBold(subsection.name).replace(/["“”]/g, '').trim(); const h3 = lines[i].match(/^###\s+(.+?)\s*$/);
if (!h3) continue;
const headerName = stripBold(h3[1]).replace(/["“”]/g, '').trim();
if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue; if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue;
if (seen.has(headerName.toLowerCase())) continue;
const body = stripBold(subsection.lines.join('\n').replace(/\n+/g, ' ')).trim(); const bodyLines = [];
if (body) addRule(headerName, body); for (let j = i + 1; j < lines.length; j++) {
if (/^##\s|^###\s/.test(lines[j])) break;
bodyLines.push(lines[j]);
}
const body = stripBold(bodyLines.join('\n').replace(/\n+/g, ' ')).trim();
if (body) {
seen.add(headerName.toLowerCase());
rules.push({ name: headerName, body });
}
} }
// Style C (Stitch bullet form): "* **The Layering Principle:** body" // Style C (Stitch bullet form): "* **The Layering Principle:** body"
@@ -369,7 +379,9 @@ function extractNamedRules(lines) {
if (!mm) continue; if (!mm) continue;
const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim(); const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim();
if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue; if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue;
addRule(nameRaw, stripBold(mm[2]).trim()); if (seen.has(nameRaw.toLowerCase())) continue;
seen.add(nameRaw.toLowerCase());
rules.push({ name: nameRaw, body: stripBold(mm[2]).trim() });
} }
return rules; return rules;
+1 -1
View File
@@ -5,7 +5,7 @@ version: 4.1.2
license: Apache 2.0 license: Apache 2.0
--- ---
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft. This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
Core principles: Core principles:
- Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide).
@@ -1,6 +1,6 @@
### Purpose ### Purpose
Resolve one stable target, run two independent assessments, synthesize a design critique, persist a snapshot, and ask the user what to improve next. The chat response is the primary deliverable; the snapshot is an archive of that run. Resolve one stable target, run two independent assessments, synthesize a design critique, persist a snapshot, and ask the user what to improve next. The chat response is the primary deliverable; the snapshot is an archive/backlog for future commands.
### Hard Invariants ### Hard Invariants
@@ -84,7 +84,7 @@ After Assessment B returns usable CLI findings, reuse them. Do not rerun `detect
Synthesize both assessments into a single report. Do NOT simply concatenate. Weave the findings together, noting where the LLM review and detector agree, where the detector caught issues the LLM missed, and where detector findings are false positives. Synthesize both assessments into a single report. Do NOT simply concatenate. Weave the findings together, noting where the LLM review and detector agree, where the detector caught issues the LLM missed, and where detector findings are false positives.
The chat response is the primary user-facing deliverable. Present the full structured critique below in chat; do not replace it with a summary and a link. The persisted snapshot is an archive of that run. The chat response is the primary user-facing deliverable. Present the full structured critique below in chat; do not replace it with a summary and a link. The persisted snapshot is only an archive/backlog for later commands.
Structure your feedback as a design director would: Structure your feedback as a design director would:
@@ -197,7 +197,7 @@ Skip this step if the Setup slug was null (vague or root-level target).
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"max_score":<n>,"na_heuristics":"<comma-separated numbers, or empty>","p0_count":<n>,"p1_count":<n>}' \ IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"max_score":<n>,"na_heuristics":"<comma-separated numbers, or empty>","p0_count":<n>,"p1_count":<n>}' \
node .cursor/skills/impeccable/scripts/critique-storage.mjs write "<resolved target>" <body-file> node .cursor/skills/impeccable/scripts/critique-storage.mjs write "<resolved target>" <body-file>
``` ```
`max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. For a local file target, the helper also records an exact content fingerprint so polish can distinguish the assessed bytes from later edits without relying on Git state or timestamps. The helper prints the absolute path it wrote. Leave that file on disk. Polish closes it; this run does not. `max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. The helper prints the absolute path it wrote.
3. **Delete the temp body file** after the write attempt completes, whether the write succeeded or failed. If deletion fails, mention `temp-file cleanup failed: <reason>` briefly in the final output, but do not block the critique. 3. **Delete the temp body file** after the write attempt completes, whether the write succeeded or failed. If deletion fails, mention `temp-file cleanup failed: <reason>` briefly in the final output, but do not block the critique.
+1 -1
View File
@@ -32,7 +32,7 @@ The first argument is the action. Defaults to `status`.
| `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. | | `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. |
| `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. | | `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. |
| `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. | | `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. |
| `reset` | Delete the project config, dedup cache, and Cursor pending queue, and remove the hook's entries from every provider manifest `on` installs, the committed Copilot file included (a team-shared `settings.json` that `on` never writes is never touched). | | `reset` | Delete the project config, dedup cache, and Cursor pending queue. |
## Flow ## Flow
@@ -68,20 +68,16 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su
## 5. Record the decision ## 5. Record the decision
Before code, record the chosen direction as a development-only contract under `## Direction contract` in the relevant surface brief. A direction contract is durable route or artifact strategy, so create or update the brief even when no other surface strategy needs persistence. Keep the contract to six short blocks and 150 words at most. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The surface brief is the reminder later agents reload across edits and sessions: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
Never copy the direction contract into implementation source or any browser-delivered artifact. This includes HTML or framework comments, hidden DOM, `<template>` elements, `data-*` attributes, rendered JSX or TSX output, serialized props or state, React Server Component payloads, client bundles, metadata or JSON-LD, accessibility-only text, and files served beside the artifact. A compiler or optimizer removing development metadata is not a safety boundary. Reviewers and documenters receive the contract from the surface brief.
On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md.
Read the existing surface brief before updating it: If the work establishes durable strategy for a route or artifact, read its existing surface brief, then update it:
`node .cursor/skills/impeccable/scripts/surface-brief.mjs read <primary-target>` `node .cursor/skills/impeccable/scripts/surface-brief.mjs read <primary-target>`
`node .cursor/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]` `node .cursor/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]`
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it.
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior.
+2 -10
View File
@@ -29,10 +29,10 @@ Use the feature yourself at the surface's representative sizes: desktop and mobi
If a prior critique exists, use it as one input: If a prior critique exists, use it as one input:
```bash ```bash
node .cursor/skills/impeccable/scripts/critique-storage.mjs latest "<resolved target>" --json node .cursor/skills/impeccable/scripts/critique-storage.mjs latest "<resolved target>"
``` ```
Exit 0 returns JSON with the latest snapshot's `body` and an exact `snapshot_file` identity. Retain `snapshot_file` until the end of the pass. For a local file target, the helper compares the file's exact current content fingerprint with the fingerprint captured by critique. Unchanged staged, unstaged, or untracked content remains current; any byte change, deletion, or replacement with a non-file closes the backlog it identified while preserving its trend history and exits 2. A URL target has no local fingerprint and remains current until explicitly closed. When current, incorporate relevant P0/P1 findings from `body` and name the snapshot read. Exit 2 means none exists or the target changed. Perform an independent pass either way. Exit 0 returns the latest snapshot; incorporate relevant P0/P1 findings and name the snapshot read. Exit 2 means none exists. Perform an independent pass either way.
## 3. Triage ## 3. Triage
@@ -95,11 +95,3 @@ Walk the complete path again with mouse, keyboard, and touch where applicable. C
Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment.
Finish with a source diff: remove accidental churn, orphaned code, redundant values, and temporary artifacts. Ship only when the feature is functionally complete and consistently finished across the path. Finish with a source diff: remove accidental churn, orphaned code, redundant values, and temporary artifacts. Ship only when the feature is functionally complete and consistently finished across the path.
When this pass clears every Priority Issue it took from a snapshot, close that snapshot:
```bash
node .cursor/skills/impeccable/scripts/critique-storage.mjs close "<resolved target>" "<snapshot_file returned by latest>"
```
This closes only the snapshot this pass actually processed; if a newer critique landed meanwhile, its backlog stays live. Do not close when no snapshot was read, when `snapshot_file` was not retained, or when Priority Issues remain.
@@ -8,7 +8,7 @@ Reason over the signals; there is no score to obey:
- `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system).
- `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default. - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default.
- `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared). - `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale.
- `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them.
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `detect.mjs` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code. - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `detect.mjs` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code.
- Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`. - Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`.
@@ -16,9 +16,8 @@
* CLI entry points (called from skill instructions): * CLI entry points (called from skill instructions):
* node critique-storage.mjs slug <resolved-target> * node critique-storage.mjs slug <resolved-target>
* node critique-storage.mjs write <slug> <snapshot-body-file> * node critique-storage.mjs write <slug> <snapshot-body-file>
* node critique-storage.mjs latest <slug> [--json] * node critique-storage.mjs latest <slug>
* node critique-storage.mjs trend <slug> [limit] * node critique-storage.mjs trend <slug> [limit]
* node critique-storage.mjs close <resolved-target> <snapshot-file>
* *
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain * Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
* markdown file; the model reads it directly with its file-read tool. This * markdown file; the model reads it directly with its file-read tool. This
@@ -28,7 +27,6 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { createHash } from 'node:crypto';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
import { getCritiqueDir } from './lib/impeccable-paths.mjs'; import { getCritiqueDir } from './lib/impeccable-paths.mjs';
import { slugFromTarget } from './lib/target-slug.mjs'; import { slugFromTarget } from './lib/target-slug.mjs';
@@ -52,45 +50,6 @@ export function nowFilenameStamp(date = new Date()) {
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z'); return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
} }
/**
* Return an exact content fingerprint for a local file target. URLs and
* non-files return null because their content is not available here.
*
* The fingerprint deliberately describes bytes, not Git state or mtimes:
* critique often assesses an uncommitted file, and a later polish run should
* inherit that backlog when the bytes are unchanged regardless of staging.
*/
function resolveLocalTargetPath(target, { cwd = process.cwd() } = {}) {
if (!target || /^https?:\/\//i.test(target)) return null;
return path.isAbsolute(target) ? path.resolve(target) : path.resolve(cwd, target);
}
function resolveTargetIdentity(target, { cwd = process.cwd() } = {}) {
if (!target || typeof target !== 'string') return null;
if (/^https?:\/\//i.test(target)) {
try {
const url = new URL(target);
const pathname = url.pathname.replace(/\/+$/, '') || '/';
return `url:${url.origin}${pathname}`;
} catch {
return null;
}
}
const filePath = resolveLocalTargetPath(target, { cwd });
return filePath ? `file:${filePath}` : null;
}
export function fingerprintTarget(target, { cwd = process.cwd() } = {}) {
const filePath = resolveLocalTargetPath(target, { cwd });
if (!filePath) return null;
try {
if (!fs.statSync(filePath).isFile()) return null;
return `sha256:${createHash('sha256').update(fs.readFileSync(filePath)).digest('hex')}`;
} catch {
return null;
}
}
/** /**
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter * Write a snapshot for `slug`. `meta` carries the small structured frontmatter
* keys read back by readTrend(). `body` is the human-readable critique * keys read back by readTrend(). `body` is the human-readable critique
@@ -103,27 +62,14 @@ export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new
const dir = getCritiqueDir(cwd); const dir = getCritiqueDir(cwd);
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
const timestamp = nowFilenameStamp(now); const timestamp = nowFilenameStamp(now);
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
// Spread `meta` first so internally computed `timestamp` and `slug` // Spread `meta` first so internally computed `timestamp` and `slug`
// always win. Otherwise a caller-supplied meta blob (parsed from the // always win. Otherwise a caller-supplied meta blob (parsed from the
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the // IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
// filename in disagreement with its frontmatter and corrupting trends. // filename in disagreement with its frontmatter and corrupting trends.
const front = serializeFrontmatter({ ...meta, timestamp, slug }); const front = serializeFrontmatter({ ...meta, timestamp, slug });
const contents = `${front}\n${body.trim()}\n`; fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
return filePath;
// A second critique can finish in the same UTC second. Use exclusive
// creation and a fixed-width suffix so concurrent writers cannot replace
// history and lexical ordering still keeps collision entries newest.
for (let collision = 0; collision <= 9999; collision += 1) {
const suffix = collision === 0 ? '' : `~${String(collision).padStart(4, '0')}`;
const filePath = path.join(dir, `${timestamp}${suffix}__${slug}.md`);
try {
fs.writeFileSync(filePath, contents, { encoding: 'utf-8', flag: 'wx' });
return filePath;
} catch (error) {
if (error?.code !== 'EEXIST') throw error;
}
}
throw new Error(`Too many critique snapshots for ${slug} at ${timestamp}`);
} }
function serializeFrontmatter(obj) { function serializeFrontmatter(obj) {
@@ -152,8 +98,6 @@ function parseFrontmatter(text) {
try { value = JSON.parse(value); } catch { /* leave as-is */ } try { value = JSON.parse(value); } catch { /* leave as-is */ }
} else if (/^-?\d+$/.test(value)) { } else if (/^-?\d+$/.test(value)) {
value = Number(value); value = Number(value);
} else if (value === 'true' || value === 'false') {
value = value === 'true';
} }
out[key] = value; out[key] = value;
} }
@@ -163,7 +107,7 @@ function parseFrontmatter(text) {
/** /**
* Return snapshot files matching `suffix`, sorted oldest newest. * Return snapshot files matching `suffix`, sorted oldest newest.
*/ */
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z(?:~\d{4})?__.+\.md$/; const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
function listSnapshots(suffix, cwd) { function listSnapshots(suffix, cwd) {
const dir = getCritiqueDir(cwd); const dir = getCritiqueDir(cwd);
@@ -174,105 +118,24 @@ function listSnapshots(suffix, cwd) {
.map((f) => path.join(dir, f)); .map((f) => path.join(dir, f));
} }
function readSnapshot(filePath) { function readLatestSnapshotMatching(suffix, cwd) {
const filePath = listSnapshots(suffix, cwd).at(-1);
if (!filePath) return null; if (!filePath) return null;
const body = fs.readFileSync(filePath, 'utf-8'); const body = fs.readFileSync(filePath, 'utf-8');
return { path: filePath, body, meta: parseFrontmatter(body) }; return { path: filePath, body, meta: parseFrontmatter(body) };
} }
function snapshotTargetIdentity(snapshot) {
const targetPath = snapshot?.meta.target_path;
return snapshot?.meta.target_identity
|| (targetPath ? `file:${targetPath}` : null);
}
function readNewestSnapshot(slug, { cwd = process.cwd() } = {}) {
return readSnapshot(listSnapshots(`__${slug}.md`, cwd).at(-1));
}
function readNewestSnapshotForIdentity(
slug,
targetIdentity,
{ cwd = process.cwd() } = {},
) {
const matches = listSnapshots(`__${slug}.md`, cwd)
.map(readSnapshot)
.filter((snapshot) => snapshotTargetIdentity(snapshot) === targetIdentity);
return matches.at(-1) || null;
}
/** /**
* Return the most recent snapshot for `slug`, or null. Polish reads this * Return the most recent snapshot for `slug`, or null. Polish reads this
* to find its fix backlog when the slug matches. * to find its fix backlog when the slug matches.
*/ */
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
const latest = readNewestSnapshot(slug, { cwd }); return readLatestSnapshotMatching(`__${slug}.md`, cwd);
return latest?.meta.closed === true ? null : latest;
}
/**
* Mark one exact snapshot closed without deleting the score history consumed
* by `trend`. Exact identity matters: a newer critique may land after polish
* reads its backlog, and that newer snapshot must remain live. `snapshotFile`
* may be the absolute path returned by readLatestSnapshot() or the basename
* emitted by `latest --json`. Returns the path marked closed, or null.
*/
export function closeSnapshot(snapshotFile, { cwd = process.cwd() } = {}) {
if (!snapshotFile || typeof snapshotFile !== 'string') return null;
const dir = path.resolve(getCritiqueDir(cwd));
const snapshotPath = path.isAbsolute(snapshotFile)
? path.resolve(snapshotFile)
: path.resolve(dir, snapshotFile);
const filename = path.basename(snapshotPath);
if (
path.dirname(snapshotPath) !== dir
|| !SNAPSHOT_FILENAME.test(filename)
) return null;
let snapshot;
try {
if (!fs.lstatSync(snapshotPath).isFile()) return null;
snapshot = readSnapshot(snapshotPath);
} catch {
return null;
}
if (!snapshot || snapshot.meta.closed === true) return null;
const closedBody = snapshot.body.replace(
/^(---\r?\n[\s\S]*?)(\r?\n---)/,
'$1\nclosed: true$2',
);
if (closedBody === snapshot.body) {
throw new Error(`Cannot close snapshot without frontmatter: ${snapshot.path}`);
}
fs.writeFileSync(snapshot.path, closedBody, 'utf-8');
return snapshot.path;
} }
/** Return the most recent snapshot across all targets, or null. */ /** Return the most recent snapshot across all targets, or null. */
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
const snapshots = listSnapshots('.md', cwd).map(readSnapshot); return readLatestSnapshotMatching('.md', cwd);
const identifiedSlugs = new Set(
snapshots
.filter((snapshot) => snapshotTargetIdentity(snapshot))
.map((snapshot) => snapshot.meta.slug),
);
const latestByTarget = new Map();
for (const snapshot of snapshots) {
if (!snapshot?.meta.slug) continue;
// Slugs are lossy: distinct targets such as foo/bar and foo-bar can share
// one. Keep each known identity's latest open/closed state independent so
// closing one target cannot hide another target's live backlog. Once a
// slug has any identity-aware snapshot, its older legacy records are no
// longer independently routable and must not resurface as zombie work.
const targetIdentity = snapshotTargetIdentity(snapshot);
if (!targetIdentity && identifiedSlugs.has(snapshot.meta.slug)) continue;
const streamKey = targetIdentity || `slug:${snapshot.meta.slug}`;
latestByTarget.set(streamKey, snapshot);
}
return [...latestByTarget.values()]
.filter((snapshot) => snapshot.meta.closed !== true)
.sort((a, b) => a.path.localeCompare(b.path))
.at(-1) || null;
} }
/** /**
@@ -290,13 +153,9 @@ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
// Accept either a ready slug or a concrete target (path/URL) everywhere, so // Accept either a ready slug or a concrete target (path/URL) everywhere, so
// callers never have to run the slug step separately. Anything containing a // callers never have to run the slug step separately. Anything containing a
// path or URL marker is resolved through slugFromTarget. // path or URL marker is resolved through slugFromTarget.
function isReadySlug(value) {
return /^[a-z0-9-]+$/.test(value || '') && !value.includes('/');
}
function coerceSlug(value) { function coerceSlug(value) {
if (!value) return null; if (!value) return null;
if (isReadySlug(value)) return value; if (/^[a-z0-9-]+$/.test(value) && !value.includes('/')) return value;
return slugFromTarget(value); return slugFromTarget(value);
} }
@@ -322,124 +181,14 @@ function main(argv) {
if (metaArg) { if (metaArg) {
try { meta = JSON.parse(metaArg); } catch { /* ignore */ } try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
} }
// The helper, not caller-provided metadata, owns the target fingerprint.
// This makes the snapshot describe the exact file bytes critique saw.
delete meta.target_fingerprint;
delete meta.target_path;
delete meta.target_identity;
const targetIdentity = resolveTargetIdentity(slugArg);
if (targetIdentity) meta.target_identity = targetIdentity;
const targetFingerprint = fingerprintTarget(slugArg);
if (targetFingerprint) {
meta.target_fingerprint = targetFingerprint;
meta.target_path = resolveLocalTargetPath(slugArg);
}
const out = writeSnapshot({ slug, meta, body: raw }); const out = writeSnapshot({ slug, meta, body: raw });
process.stdout.write(`${out}\n`); process.stdout.write(`${out}\n`);
return; return;
} }
case 'latest': { case 'latest': {
const target = args[0]; const latest = readLatestSnapshot(coerceSlug(args[0]));
const format = args[1]; if (!latest) { process.exit(2); }
const slug = coerceSlug(target); process.stdout.write(latest.body);
if (!slug || (format && format !== '--json')) {
process.stderr.write('usage: latest <slug-or-target> [--json]\n');
process.exit(1);
}
const targetFingerprint = fingerprintTarget(target);
const targetPath = resolveLocalTargetPath(target);
const targetIdentity = resolveTargetIdentity(target);
const readySlug = isReadySlug(target);
const newestForSlug = readNewestSnapshot(slug);
if (!newestForSlug) { process.exit(2); }
// Concrete targets select the newest snapshot for their exact identity,
// not merely the newest filename for a lossy slug. This keeps distinct
// targets such as foo/bar and foo-bar from hiding each other's backlog.
const exactSnapshot = readNewestSnapshotForIdentity(slug, targetIdentity);
let latest = exactSnapshot;
if (!latest && !readySlug) {
// Legacy snapshots have no identity. Preserve their old explicit
// path/URL behavior only when no known target identity was selected.
latest = readNewestSnapshotForIdentity(slug, null);
}
if (!latest) latest = newestForSlug;
if (latest.meta.closed === true) { process.exit(2); }
const recordedTargetPath = latest.meta.target_path;
const recordedTargetIdentity = snapshotTargetIdentity(latest);
const matchingIdentity = recordedTargetIdentity === targetIdentity;
// Bare slugs remain a supported lookup mode, including for URL
// snapshots. But when a same-named local file exists, the request is
// ambiguous unless that exact file owns the snapshot identity.
if (readySlug && !recordedTargetIdentity) {
process.stderr.write(
'ambiguous legacy snapshot target; use an explicit ./path or full URL\n',
);
process.exit(2);
}
if (readySlug && targetPath && fs.existsSync(targetPath) && !matchingIdentity) {
process.stderr.write(
'ambiguous snapshot slug; use an explicit ./path or remove the local name collision\n',
);
process.exit(2);
}
const concreteTarget = !readySlug || matchingIdentity;
if (concreteTarget && recordedTargetIdentity && !matchingIdentity) {
process.exit(2);
}
const concreteLocalTarget = concreteTarget && targetPath;
if (concreteLocalTarget && latest.meta.target_fingerprint !== targetFingerprint) {
closeSnapshot(latest.path);
process.exit(2);
}
if (format === '--json') {
process.stdout.write(JSON.stringify({
snapshot_file: path.basename(latest.path),
body: latest.body,
}, null, 2) + '\n');
} else {
process.stdout.write(latest.body);
}
return;
}
case 'close': {
const [slugArg, snapshotFile, ...extra] = args;
const slug = coerceSlug(slugArg);
if (!slug || !snapshotFile || extra.length > 0) {
process.stderr.write('usage: close <resolved-target> <snapshot-file>\n');
process.exit(1);
}
if (
path.basename(snapshotFile) !== snapshotFile
|| !SNAPSHOT_FILENAME.test(snapshotFile)
|| !snapshotFile.endsWith(`__${slug}.md`)
) process.exit(2);
// A slug and filename are not enough to prove ownership because two
// distinct targets can normalize to the same slug. Modern snapshots
// carry a canonical identity, so require the supplied resolved target
// to match it before allowing the exact snapshot to be closed. Legacy
// snapshots without identity retain their historical close behavior.
const snapshotPath = path.join(getCritiqueDir(process.cwd()), snapshotFile);
let snapshot;
try {
if (!fs.lstatSync(snapshotPath).isFile()) process.exit(2);
snapshot = readSnapshot(snapshotPath);
} catch {
process.exit(2);
}
const recordedTargetIdentity = snapshotTargetIdentity(snapshot);
if (
recordedTargetIdentity
&& recordedTargetIdentity !== resolveTargetIdentity(slugArg)
) process.exit(2);
const closed = closeSnapshot(snapshotFile);
if (!closed) { process.exit(2); }
process.stdout.write(`${closed}\n`);
return; return;
} }
case 'trend': { case 'trend': {
@@ -448,7 +197,7 @@ function main(argv) {
return; return;
} }
default: default:
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend|close> [args]\n'); process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
process.exit(1); process.exit(1);
} }
} }
@@ -70,27 +70,13 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
} }
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([ const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS, 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui', '-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]); ]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and // WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1605,7 +1591,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) { function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false }; if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = primaryFontFace(fontFamily, GENERIC_FONTS); const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
if (!primary) return { primary: null, isSerif: false }; if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5204,7 +5190,8 @@ function checkTypography() {
const style = getComputedStyle(el); const style = getComputedStyle(el);
const ff = style.fontFamily; const ff = style.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++; totalTextElements++;
@@ -5449,7 +5436,8 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue; if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily; const ff = rule.style?.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (primary) { if (primary) {
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5468,10 +5456,11 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi; const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm; let fm;
while ((fm = ffRe.exec(html)) !== null) { while ((fm = ffRe.exec(html)) !== null) {
const primary = primaryFontFace(fm[1]); for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (primary) { if (f && !GENERIC_FONTS.has(f)) {
fonts.add(primary); fonts.add(f);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
} }
} }
@@ -1,7 +1,7 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { import {
checkSourceDesignSystem, checkSourceDesignSystem,
collectStaticDesignSystemFindings, collectStaticDesignSystemFindings,
@@ -51,7 +51,9 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue; if (!hasText) continue;
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,7 +9,6 @@ import {
WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX, WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain, isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs'; } from '../shared/constants.mjs';
import { import {
CSS_NAMED_COLORS, CSS_NAMED_COLORS,
@@ -332,7 +331,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) { function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false }; if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = primaryFontFace(fontFamily, GENERIC_FONTS); const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
if (!primary) return { primary: null, isSerif: false }; if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3931,7 +3930,8 @@ function checkTypography() {
const style = getComputedStyle(el); const style = getComputedStyle(el);
const ff = style.fontFamily; const ff = style.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++; totalTextElements++;
@@ -4176,7 +4176,8 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue; if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily; const ff = rule.style?.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (primary) { if (primary) {
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4195,10 +4196,11 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi; const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm; let fm;
while ((fm = ffRe.exec(html)) !== null) { while ((fm = ffRe.exec(html)) !== null) {
const primary = primaryFontFace(fm[1]); for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (primary) { if (f && !GENERIC_FONTS.has(f)) {
fonts.add(primary); fonts.add(f);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
} }
} }
@@ -56,27 +56,13 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
} }
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([ const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS, 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui', '-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]); ]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and // WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -118,7 +104,6 @@ export {
BRAND_FONT_DOMAINS, BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain, isBrandFontOnOwnDomain,
GENERIC_FONTS, GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX, WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR, EM_DASH_FLOOR,
@@ -21,24 +21,22 @@ import zlib from 'node:zlib';
const KEYWORD = 'impeccable:prompt'; const KEYWORD = 'impeccable:prompt';
const args = process.argv.slice(2); const args = process.argv.slice(2);
const file = args.find(a => !a.startsWith('--')); const file = args.find(a => !a.startsWith('--'));
const readMode = args.includes('--read');
const scanMode = args.includes('--scan');
const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; }; const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; };
function imageType(buffer) { function promptOf(imagePath) {
if (buffer.length > 8 && buffer.readUInt32BE(0) === 0x89504e47) return 'png'; const b = fs.readFileSync(imagePath);
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8) return 'jpeg'; let prompt = null;
return null; if (b.length > 8 && b.readUInt32BE(0) === 0x89504e47) prompt = readPngText(b);
} else if (b.length > 3 && b[0] === 0xff && b[1] === 0xd8) prompt = readJpegCom(b);
function readPrompt(imagePath, buffer = fs.readFileSync(imagePath)) {
const type = imageType(buffer);
let prompt = type === 'png' ? parsePng(buffer).prompt : type === 'jpeg' ? readJpegCom(buffer) : null;
if (prompt == null && fs.existsSync(`${imagePath}.json`)) { if (prompt == null && fs.existsSync(`${imagePath}.json`)) {
try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ } try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ }
} }
return prompt; return prompt;
} }
if (args.includes('--scan')) { if (scanMode) {
const targets = args.filter(a => !a.startsWith('--')); const targets = args.filter(a => !a.startsWith('--'));
if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); } if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); }
const RASTER = /\.(png|jpe?g|webp)$/i; const RASTER = /\.(png|jpe?g|webp)$/i;
@@ -61,7 +59,7 @@ if (args.includes('--scan')) {
} }
let missing = 0; let missing = 0;
for (const raster of rasters) { for (const raster of rasters) {
if (readPrompt(raster) == null) { console.log(`MISSING: ${raster}`); missing++; } if (promptOf(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
} }
console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`); console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`);
process.exit(missing > 0 ? 3 : 0); process.exit(missing > 0 ? 3 : 0);
@@ -70,7 +68,8 @@ if (args.includes('--scan')) {
if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); }
const buf = fs.readFileSync(file); const buf = fs.readFileSync(file);
const type = imageType(buf); const isPng = buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47;
const isJpeg = buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8;
const crcTable = (() => { const crcTable = (() => {
const t = new Uint32Array(256); const t = new Uint32Array(256);
@@ -88,26 +87,22 @@ function pngChunk(type, data) {
return out; return out;
} }
function parsePng(buffer) { function readPngText(b) {
const chunks = []; let off = 8;
let prompt = null; while (off + 12 <= b.length) {
let offset = 8; const len = b.readUInt32BE(off);
while (offset + 12 <= buffer.length) { const type = b.toString('ascii', off + 4, off + 8);
const length = buffer.readUInt32BE(offset); if (type === 'tEXt' || type === 'zTXt') {
const type = buffer.toString('ascii', offset + 4, offset + 8); const data = b.subarray(off + 8, off + 8 + len);
const data = buffer.subarray(offset + 8, offset + 8 + length); const nul = data.indexOf(0);
const nul = data.indexOf(0); if (nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD) {
const promptChunk = (type === 'tEXt' || type === 'zTXt') if (type === 'tEXt') return data.toString('utf8', nul + 1);
&& nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD; return zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
if (prompt == null && promptChunk) { }
prompt = type === 'tEXt'
? data.toString('utf8', nul + 1)
: zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
} }
chunks.push({ offset, type, promptChunk, bytes: buffer.subarray(offset, offset + 12 + length) }); off += 12 + len;
offset += 12 + length;
} }
return { chunks, prompt }; return null;
} }
function readJpegCom(b) { function readJpegCom(b) {
@@ -126,34 +121,48 @@ function readJpegCom(b) {
} }
const sidecar = `${file}.json`; const sidecar = `${file}.json`;
if (args.includes('--read')) { if (readMode) {
const prompt = readPrompt(file, buf); let prompt = null;
if (isPng) prompt = readPngText(buf);
else if (isJpeg) prompt = readJpegCom(buf);
if (prompt == null && fs.existsSync(sidecar)) {
try { prompt = JSON.parse(fs.readFileSync(sidecar, 'utf8')).prompt ?? null; } catch { /* fall through */ }
}
if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); } if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); }
console.log(prompt); console.log(prompt);
process.exit(0); process.exit(0);
} }
const promptFile = argOf('--prompt-file'); const prompt = argOf('--prompt') ?? (argOf('--prompt-file') ? fs.readFileSync(argOf('--prompt-file'), 'utf8') : null);
const prompt = argOf('--prompt') ?? (promptFile ? fs.readFileSync(promptFile, 'utf8') : null);
if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); } if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); }
if (type === 'png') { if (isPng) {
// Insert (or replace) our tEXt chunk immediately before IEND. // Insert (or replace) our tEXt chunk immediately before IEND.
const { chunks, prompt: existingPrompt } = parsePng(buf); const iend = buf.indexOf(Buffer.from('IEND', 'ascii')) - 4;
const iend = chunks.find((chunk) => chunk.type === 'IEND')?.offset ?? -1;
if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); } if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); }
// Drop any existing chunk with our keyword to keep embedding idempotent. // Drop any existing chunk with our keyword to keep embedding idempotent.
const replacing = existingPrompt != null; let body = buf.subarray(8, iend);
const body = replacing const existing = readPngText(buf);
? Buffer.concat(chunks if (existing != null) {
.filter((chunk) => chunk.offset < iend && !chunk.promptChunk) const parts = [];
.map((chunk) => chunk.bytes)) let off = 8;
: buf.subarray(8, iend); while (off + 12 <= buf.length && off < iend + 12) {
const promptChunk = pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])); const len = buf.readUInt32BE(off);
const end = replacing ? pngChunk('IEND', Buffer.alloc(0)) : buf.subarray(iend); const type = buf.toString('ascii', off + 4, off + 8);
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, promptChunk, end])); const chunk = buf.subarray(off, off + 12 + len);
const data = buf.subarray(off + 8, off + 8 + len);
const nul = data.indexOf(0);
const ours = (type === 'tEXt' || type === 'zTXt') && nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
if (!ours && type !== 'IEND') parts.push(chunk);
off += 12 + len;
}
body = Buffer.concat(parts).subarray(8 * 0); // parts exclude signature
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), pngChunk('IEND', Buffer.alloc(0))]));
} else {
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, iend), pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), buf.subarray(iend)]));
}
console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`); console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`);
} else if (type === 'jpeg') { } else if (isJpeg) {
const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8'); const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8');
if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); } if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); }
const com = Buffer.alloc(4 + seg.length); const com = Buffer.alloc(4 + seg.length);
@@ -431,15 +431,12 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64'));
// The prompt travels with the asset: embedded in the file itself (EXIF-class // The prompt travels with the asset: embedded in the file itself (EXIF-class
// metadata via embed-prompt.mjs) so intent survives copies across harnesses, // metadata via embed-prompt.mjs) so intent survives copies across harnesses,
// plus a sidecar for anything that indexes rather than opens the image. // plus a sidecar for anything that indexes rather than opens the image.
let embedded = false;
try { try {
const { spawnSync } = await import('node:child_process'); const { spawnSync } = await import('node:child_process');
const result = spawnSync(process.execPath, [fileURLToPath(new URL('./embed-prompt.mjs', import.meta.url)), out, '--prompt', prompt], { stdio: 'ignore' }); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' });
embedded = !result.error && result.status === 0;
if (!embedded) console.warn('generate-image: failed to embed prompt in the image');
fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2));
} catch { /* embedding is best-effort */ } } catch { /* embedding is best-effort */ }
console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); ${embedded ? 'prompt embedded + sidecar' : 'sidecar'} at ${out}.json`); console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`);
if (plateCtx && plateCtx.chroma) { if (plateCtx && plateCtx.chroma) {
const frac = await keyChroma(out, plateCtx.chroma); const frac = await keyChroma(out, plateCtx.chroma);
console.log(`PLATE-CHROMA keyed ${(frac * 100).toFixed(0)}% of pixels to alpha (${plateCtx.chroma}); place with a plain <img> over the page's own ground, no background on the plate. If the keyed fraction is under 20% the generator ignored the key: regenerate with --no-chroma and use mix-blend-mode: multiply instead.`); console.log(`PLATE-CHROMA keyed ${(frac * 100).toFixed(0)}% of pixels to alpha (${plateCtx.chroma}); place with a plain <img> over the page's own ground, no background on the plate. If the keyed fraction is under 20% the generator ignored the key: regenerate with --no-chroma and use mix-blend-mode: multiply instead.`);
@@ -770,22 +770,9 @@ function reset(cwd) {
} }
} catch { /* ignore */ } } catch { /* ignore */ }
} }
// `on` writes three things: config, consent, and hook entries in the return removed.length
// provider manifests. Reset must undo all three (issue #512): a leftover ? `Reset design hook config and cache (removed: ${removed.join(', ')}).`
// manifest entry kept invoking the hook after the config that said "off" : 'No hook config or cache to remove. Already at defaults.';
// was deleted. Local destRel only, since `on` never writes the team-shared
// sharedDestRel. No skill-folder gate: a reset mid-uninstall (skill files
// gone, manifest still wired) is the case that most needs the prune.
const pruned = [];
for (const target of HOOK_MANIFEST_TARGETS) {
try {
if (pruneImpeccableHookFromManifest(path.join(cwd, target.destRel))) pruned.push(target.provider);
} catch { /* ignore */ }
}
const parts = [];
if (removed.length) parts.push(`Reset design hook config and cache (removed: ${removed.join(', ')}).`);
if (pruned.length) parts.push(`Removed hook entries from: ${pruned.join(', ')}.`);
return parts.length ? parts.join(' ') : 'No hook config or cache to remove. Already at defaults.';
} }
function main() { function main() {
@@ -329,37 +329,47 @@ function stripBold(s) {
function extractNamedRules(lines) { function extractNamedRules(lines) {
const rules = []; const rules = [];
const seen = new Set(); const seen = new Set();
const addRule = (name, body, { allowDuplicate = false } = {}) => {
const key = name.toLowerCase();
if (!allowDuplicate && seen.has(key)) return;
seen.add(key);
rules.push({ name, body });
};
// Style A (Impeccable): "**The X Rule.** body body body" — can span lines. // Style A (Impeccable): "**The X Rule.** body body body" — can span lines.
const joined = lines.join('\n'); const joined = lines.join('\n');
const inlineMatches = [...joined.matchAll(/\*\*(The [^*]+?Rule)\.\*\*/g)]; const inlineStart = /\*\*(The [^*]+?Rule)\.\*\*/g;
const inlineMatches = [];
let m;
while ((m = inlineStart.exec(joined)) !== null) {
inlineMatches.push({ name: m[1], start: m.index, end: inlineStart.lastIndex });
}
for (let i = 0; i < inlineMatches.length; i++) { for (let i = 0; i < inlineMatches.length; i++) {
const match = inlineMatches[i]; const mm = inlineMatches[i];
const bodyEnd = inlineMatches[i + 1]?.index ?? joined.length; const bodyEnd = i + 1 < inlineMatches.length ? inlineMatches[i + 1].start : joined.length;
const body = joined const body = joined
.slice(match.index + match[0].length, bodyEnd) .slice(mm.end, bodyEnd)
.replace(/\n##[^\n]*$/s, '') .replace(/\n##[^\n]*$/s, '')
.replace(/\n###[^\n]*$/s, '') .replace(/\n###[^\n]*$/s, '')
.trim(); .trim();
// Preserve the inline format's historical behavior: repeated inline rules const name = stripBold(mm.name).trim();
// remain visible, while the later heading and bullet formats dedupe. seen.add(name.toLowerCase());
addRule(stripBold(match[1]).trim(), stripBold(body), { allowDuplicate: true }); rules.push({ name, body: stripBold(body) });
} }
// Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the // Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the
// bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle. // bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle.
for (const subsection of splitSubsections(lines).slice(1)) { for (let i = 0; i < lines.length; i++) {
const headerName = stripBold(subsection.name).replace(/["“”]/g, '').trim(); const h3 = lines[i].match(/^###\s+(.+?)\s*$/);
if (!h3) continue;
const headerName = stripBold(h3[1]).replace(/["“”]/g, '').trim();
if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue; if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue;
if (seen.has(headerName.toLowerCase())) continue;
const body = stripBold(subsection.lines.join('\n').replace(/\n+/g, ' ')).trim(); const bodyLines = [];
if (body) addRule(headerName, body); for (let j = i + 1; j < lines.length; j++) {
if (/^##\s|^###\s/.test(lines[j])) break;
bodyLines.push(lines[j]);
}
const body = stripBold(bodyLines.join('\n').replace(/\n+/g, ' ')).trim();
if (body) {
seen.add(headerName.toLowerCase());
rules.push({ name: headerName, body });
}
} }
// Style C (Stitch bullet form): "* **The Layering Principle:** body" // Style C (Stitch bullet form): "* **The Layering Principle:** body"
@@ -369,7 +379,9 @@ function extractNamedRules(lines) {
if (!mm) continue; if (!mm) continue;
const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim(); const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim();
if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue; if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue;
addRule(nameRaw, stripBold(mm[2]).trim()); if (seen.has(nameRaw.toLowerCase())) continue;
seen.add(nameRaw.toLowerCase());
rules.push({ name: nameRaw, body: stripBold(mm[2]).trim() });
} }
return rules; return rules;
+1 -1
View File
@@ -4,7 +4,7 @@ description: Use when the user wants to design, redesign, shape, critique, audit
version: 4.1.2 version: 4.1.2
--- ---
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft. This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
Core principles: Core principles:
- Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide).
@@ -1,6 +1,6 @@
### Purpose ### Purpose
Resolve one stable target, run two independent assessments, synthesize a design critique, persist a snapshot, and ask the user what to improve next. The chat response is the primary deliverable; the snapshot is an archive of that run. Resolve one stable target, run two independent assessments, synthesize a design critique, persist a snapshot, and ask the user what to improve next. The chat response is the primary deliverable; the snapshot is an archive/backlog for future commands.
### Hard Invariants ### Hard Invariants
@@ -84,7 +84,7 @@ After Assessment B returns usable CLI findings, reuse them. Do not rerun `detect
Synthesize both assessments into a single report. Do NOT simply concatenate. Weave the findings together, noting where the LLM review and detector agree, where the detector caught issues the LLM missed, and where detector findings are false positives. Synthesize both assessments into a single report. Do NOT simply concatenate. Weave the findings together, noting where the LLM review and detector agree, where the detector caught issues the LLM missed, and where detector findings are false positives.
The chat response is the primary user-facing deliverable. Present the full structured critique below in chat; do not replace it with a summary and a link. The persisted snapshot is an archive of that run. The chat response is the primary user-facing deliverable. Present the full structured critique below in chat; do not replace it with a summary and a link. The persisted snapshot is only an archive/backlog for later commands.
Structure your feedback as a design director would: Structure your feedback as a design director would:
@@ -197,7 +197,7 @@ Skip this step if the Setup slug was null (vague or root-level target).
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"max_score":<n>,"na_heuristics":"<comma-separated numbers, or empty>","p0_count":<n>,"p1_count":<n>}' \ IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"max_score":<n>,"na_heuristics":"<comma-separated numbers, or empty>","p0_count":<n>,"p1_count":<n>}' \
node .gemini/skills/impeccable/scripts/critique-storage.mjs write "<resolved target>" <body-file> node .gemini/skills/impeccable/scripts/critique-storage.mjs write "<resolved target>" <body-file>
``` ```
`max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. For a local file target, the helper also records an exact content fingerprint so polish can distinguish the assessed bytes from later edits without relying on Git state or timestamps. The helper prints the absolute path it wrote. Leave that file on disk. Polish closes it; this run does not. `max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. The helper prints the absolute path it wrote.
3. **Delete the temp body file** after the write attempt completes, whether the write succeeded or failed. If deletion fails, mention `temp-file cleanup failed: <reason>` briefly in the final output, but do not block the critique. 3. **Delete the temp body file** after the write attempt completes, whether the write succeeded or failed. If deletion fails, mention `temp-file cleanup failed: <reason>` briefly in the final output, but do not block the critique.
+1 -1
View File
@@ -32,7 +32,7 @@ The first argument is the action. Defaults to `status`.
| `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. | | `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. |
| `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. | | `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. |
| `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. | | `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. |
| `reset` | Delete the project config, dedup cache, and Cursor pending queue, and remove the hook's entries from every provider manifest `on` installs, the committed Copilot file included (a team-shared `settings.json` that `on` never writes is never touched). | | `reset` | Delete the project config, dedup cache, and Cursor pending queue. |
## Flow ## Flow
@@ -68,20 +68,16 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su
## 5. Record the decision ## 5. Record the decision
Before code, record the chosen direction as a development-only contract under `## Direction contract` in the relevant surface brief. A direction contract is durable route or artifact strategy, so create or update the brief even when no other surface strategy needs persistence. Keep the contract to six short blocks and 150 words at most. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The surface brief is the reminder later agents reload across edits and sessions: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
Never copy the direction contract into implementation source or any browser-delivered artifact. This includes HTML or framework comments, hidden DOM, `<template>` elements, `data-*` attributes, rendered JSX or TSX output, serialized props or state, React Server Component payloads, client bundles, metadata or JSON-LD, accessibility-only text, and files served beside the artifact. A compiler or optimizer removing development metadata is not a safety boundary. Reviewers and documenters receive the contract from the surface brief.
On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md.
Read the existing surface brief before updating it: If the work establishes durable strategy for a route or artifact, read its existing surface brief, then update it:
`node .gemini/skills/impeccable/scripts/surface-brief.mjs read <primary-target>` `node .gemini/skills/impeccable/scripts/surface-brief.mjs read <primary-target>`
`node .gemini/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]` `node .gemini/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]`
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it.
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior.
+2 -10
View File
@@ -29,10 +29,10 @@ Use the feature yourself at the surface's representative sizes: desktop and mobi
If a prior critique exists, use it as one input: If a prior critique exists, use it as one input:
```bash ```bash
node .gemini/skills/impeccable/scripts/critique-storage.mjs latest "<resolved target>" --json node .gemini/skills/impeccable/scripts/critique-storage.mjs latest "<resolved target>"
``` ```
Exit 0 returns JSON with the latest snapshot's `body` and an exact `snapshot_file` identity. Retain `snapshot_file` until the end of the pass. For a local file target, the helper compares the file's exact current content fingerprint with the fingerprint captured by critique. Unchanged staged, unstaged, or untracked content remains current; any byte change, deletion, or replacement with a non-file closes the backlog it identified while preserving its trend history and exits 2. A URL target has no local fingerprint and remains current until explicitly closed. When current, incorporate relevant P0/P1 findings from `body` and name the snapshot read. Exit 2 means none exists or the target changed. Perform an independent pass either way. Exit 0 returns the latest snapshot; incorporate relevant P0/P1 findings and name the snapshot read. Exit 2 means none exists. Perform an independent pass either way.
## 3. Triage ## 3. Triage
@@ -95,11 +95,3 @@ Walk the complete path again with mouse, keyboard, and touch where applicable. C
Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment.
Finish with a source diff: remove accidental churn, orphaned code, redundant values, and temporary artifacts. Ship only when the feature is functionally complete and consistently finished across the path. Finish with a source diff: remove accidental churn, orphaned code, redundant values, and temporary artifacts. Ship only when the feature is functionally complete and consistently finished across the path.
When this pass clears every Priority Issue it took from a snapshot, close that snapshot:
```bash
node .gemini/skills/impeccable/scripts/critique-storage.mjs close "<resolved target>" "<snapshot_file returned by latest>"
```
This closes only the snapshot this pass actually processed; if a newer critique landed meanwhile, its backlog stays live. Do not close when no snapshot was read, when `snapshot_file` was not retained, or when Priority Issues remain.
@@ -8,7 +8,7 @@ Reason over the signals; there is no score to obey:
- `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system).
- `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default. - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default.
- `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared). - `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale.
- `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them.
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `detect.mjs` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code. - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `detect.mjs` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code.
- Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`. - Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`.
@@ -16,9 +16,8 @@
* CLI entry points (called from skill instructions): * CLI entry points (called from skill instructions):
* node critique-storage.mjs slug <resolved-target> * node critique-storage.mjs slug <resolved-target>
* node critique-storage.mjs write <slug> <snapshot-body-file> * node critique-storage.mjs write <slug> <snapshot-body-file>
* node critique-storage.mjs latest <slug> [--json] * node critique-storage.mjs latest <slug>
* node critique-storage.mjs trend <slug> [limit] * node critique-storage.mjs trend <slug> [limit]
* node critique-storage.mjs close <resolved-target> <snapshot-file>
* *
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain * Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
* markdown file; the model reads it directly with its file-read tool. This * markdown file; the model reads it directly with its file-read tool. This
@@ -28,7 +27,6 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { createHash } from 'node:crypto';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
import { getCritiqueDir } from './lib/impeccable-paths.mjs'; import { getCritiqueDir } from './lib/impeccable-paths.mjs';
import { slugFromTarget } from './lib/target-slug.mjs'; import { slugFromTarget } from './lib/target-slug.mjs';
@@ -52,45 +50,6 @@ export function nowFilenameStamp(date = new Date()) {
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z'); return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
} }
/**
* Return an exact content fingerprint for a local file target. URLs and
* non-files return null because their content is not available here.
*
* The fingerprint deliberately describes bytes, not Git state or mtimes:
* critique often assesses an uncommitted file, and a later polish run should
* inherit that backlog when the bytes are unchanged regardless of staging.
*/
function resolveLocalTargetPath(target, { cwd = process.cwd() } = {}) {
if (!target || /^https?:\/\//i.test(target)) return null;
return path.isAbsolute(target) ? path.resolve(target) : path.resolve(cwd, target);
}
function resolveTargetIdentity(target, { cwd = process.cwd() } = {}) {
if (!target || typeof target !== 'string') return null;
if (/^https?:\/\//i.test(target)) {
try {
const url = new URL(target);
const pathname = url.pathname.replace(/\/+$/, '') || '/';
return `url:${url.origin}${pathname}`;
} catch {
return null;
}
}
const filePath = resolveLocalTargetPath(target, { cwd });
return filePath ? `file:${filePath}` : null;
}
export function fingerprintTarget(target, { cwd = process.cwd() } = {}) {
const filePath = resolveLocalTargetPath(target, { cwd });
if (!filePath) return null;
try {
if (!fs.statSync(filePath).isFile()) return null;
return `sha256:${createHash('sha256').update(fs.readFileSync(filePath)).digest('hex')}`;
} catch {
return null;
}
}
/** /**
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter * Write a snapshot for `slug`. `meta` carries the small structured frontmatter
* keys read back by readTrend(). `body` is the human-readable critique * keys read back by readTrend(). `body` is the human-readable critique
@@ -103,27 +62,14 @@ export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new
const dir = getCritiqueDir(cwd); const dir = getCritiqueDir(cwd);
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
const timestamp = nowFilenameStamp(now); const timestamp = nowFilenameStamp(now);
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
// Spread `meta` first so internally computed `timestamp` and `slug` // Spread `meta` first so internally computed `timestamp` and `slug`
// always win. Otherwise a caller-supplied meta blob (parsed from the // always win. Otherwise a caller-supplied meta blob (parsed from the
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the // IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
// filename in disagreement with its frontmatter and corrupting trends. // filename in disagreement with its frontmatter and corrupting trends.
const front = serializeFrontmatter({ ...meta, timestamp, slug }); const front = serializeFrontmatter({ ...meta, timestamp, slug });
const contents = `${front}\n${body.trim()}\n`; fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
return filePath;
// A second critique can finish in the same UTC second. Use exclusive
// creation and a fixed-width suffix so concurrent writers cannot replace
// history and lexical ordering still keeps collision entries newest.
for (let collision = 0; collision <= 9999; collision += 1) {
const suffix = collision === 0 ? '' : `~${String(collision).padStart(4, '0')}`;
const filePath = path.join(dir, `${timestamp}${suffix}__${slug}.md`);
try {
fs.writeFileSync(filePath, contents, { encoding: 'utf-8', flag: 'wx' });
return filePath;
} catch (error) {
if (error?.code !== 'EEXIST') throw error;
}
}
throw new Error(`Too many critique snapshots for ${slug} at ${timestamp}`);
} }
function serializeFrontmatter(obj) { function serializeFrontmatter(obj) {
@@ -152,8 +98,6 @@ function parseFrontmatter(text) {
try { value = JSON.parse(value); } catch { /* leave as-is */ } try { value = JSON.parse(value); } catch { /* leave as-is */ }
} else if (/^-?\d+$/.test(value)) { } else if (/^-?\d+$/.test(value)) {
value = Number(value); value = Number(value);
} else if (value === 'true' || value === 'false') {
value = value === 'true';
} }
out[key] = value; out[key] = value;
} }
@@ -163,7 +107,7 @@ function parseFrontmatter(text) {
/** /**
* Return snapshot files matching `suffix`, sorted oldest newest. * Return snapshot files matching `suffix`, sorted oldest newest.
*/ */
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z(?:~\d{4})?__.+\.md$/; const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
function listSnapshots(suffix, cwd) { function listSnapshots(suffix, cwd) {
const dir = getCritiqueDir(cwd); const dir = getCritiqueDir(cwd);
@@ -174,105 +118,24 @@ function listSnapshots(suffix, cwd) {
.map((f) => path.join(dir, f)); .map((f) => path.join(dir, f));
} }
function readSnapshot(filePath) { function readLatestSnapshotMatching(suffix, cwd) {
const filePath = listSnapshots(suffix, cwd).at(-1);
if (!filePath) return null; if (!filePath) return null;
const body = fs.readFileSync(filePath, 'utf-8'); const body = fs.readFileSync(filePath, 'utf-8');
return { path: filePath, body, meta: parseFrontmatter(body) }; return { path: filePath, body, meta: parseFrontmatter(body) };
} }
function snapshotTargetIdentity(snapshot) {
const targetPath = snapshot?.meta.target_path;
return snapshot?.meta.target_identity
|| (targetPath ? `file:${targetPath}` : null);
}
function readNewestSnapshot(slug, { cwd = process.cwd() } = {}) {
return readSnapshot(listSnapshots(`__${slug}.md`, cwd).at(-1));
}
function readNewestSnapshotForIdentity(
slug,
targetIdentity,
{ cwd = process.cwd() } = {},
) {
const matches = listSnapshots(`__${slug}.md`, cwd)
.map(readSnapshot)
.filter((snapshot) => snapshotTargetIdentity(snapshot) === targetIdentity);
return matches.at(-1) || null;
}
/** /**
* Return the most recent snapshot for `slug`, or null. Polish reads this * Return the most recent snapshot for `slug`, or null. Polish reads this
* to find its fix backlog when the slug matches. * to find its fix backlog when the slug matches.
*/ */
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
const latest = readNewestSnapshot(slug, { cwd }); return readLatestSnapshotMatching(`__${slug}.md`, cwd);
return latest?.meta.closed === true ? null : latest;
}
/**
* Mark one exact snapshot closed without deleting the score history consumed
* by `trend`. Exact identity matters: a newer critique may land after polish
* reads its backlog, and that newer snapshot must remain live. `snapshotFile`
* may be the absolute path returned by readLatestSnapshot() or the basename
* emitted by `latest --json`. Returns the path marked closed, or null.
*/
export function closeSnapshot(snapshotFile, { cwd = process.cwd() } = {}) {
if (!snapshotFile || typeof snapshotFile !== 'string') return null;
const dir = path.resolve(getCritiqueDir(cwd));
const snapshotPath = path.isAbsolute(snapshotFile)
? path.resolve(snapshotFile)
: path.resolve(dir, snapshotFile);
const filename = path.basename(snapshotPath);
if (
path.dirname(snapshotPath) !== dir
|| !SNAPSHOT_FILENAME.test(filename)
) return null;
let snapshot;
try {
if (!fs.lstatSync(snapshotPath).isFile()) return null;
snapshot = readSnapshot(snapshotPath);
} catch {
return null;
}
if (!snapshot || snapshot.meta.closed === true) return null;
const closedBody = snapshot.body.replace(
/^(---\r?\n[\s\S]*?)(\r?\n---)/,
'$1\nclosed: true$2',
);
if (closedBody === snapshot.body) {
throw new Error(`Cannot close snapshot without frontmatter: ${snapshot.path}`);
}
fs.writeFileSync(snapshot.path, closedBody, 'utf-8');
return snapshot.path;
} }
/** Return the most recent snapshot across all targets, or null. */ /** Return the most recent snapshot across all targets, or null. */
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
const snapshots = listSnapshots('.md', cwd).map(readSnapshot); return readLatestSnapshotMatching('.md', cwd);
const identifiedSlugs = new Set(
snapshots
.filter((snapshot) => snapshotTargetIdentity(snapshot))
.map((snapshot) => snapshot.meta.slug),
);
const latestByTarget = new Map();
for (const snapshot of snapshots) {
if (!snapshot?.meta.slug) continue;
// Slugs are lossy: distinct targets such as foo/bar and foo-bar can share
// one. Keep each known identity's latest open/closed state independent so
// closing one target cannot hide another target's live backlog. Once a
// slug has any identity-aware snapshot, its older legacy records are no
// longer independently routable and must not resurface as zombie work.
const targetIdentity = snapshotTargetIdentity(snapshot);
if (!targetIdentity && identifiedSlugs.has(snapshot.meta.slug)) continue;
const streamKey = targetIdentity || `slug:${snapshot.meta.slug}`;
latestByTarget.set(streamKey, snapshot);
}
return [...latestByTarget.values()]
.filter((snapshot) => snapshot.meta.closed !== true)
.sort((a, b) => a.path.localeCompare(b.path))
.at(-1) || null;
} }
/** /**
@@ -290,13 +153,9 @@ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
// Accept either a ready slug or a concrete target (path/URL) everywhere, so // Accept either a ready slug or a concrete target (path/URL) everywhere, so
// callers never have to run the slug step separately. Anything containing a // callers never have to run the slug step separately. Anything containing a
// path or URL marker is resolved through slugFromTarget. // path or URL marker is resolved through slugFromTarget.
function isReadySlug(value) {
return /^[a-z0-9-]+$/.test(value || '') && !value.includes('/');
}
function coerceSlug(value) { function coerceSlug(value) {
if (!value) return null; if (!value) return null;
if (isReadySlug(value)) return value; if (/^[a-z0-9-]+$/.test(value) && !value.includes('/')) return value;
return slugFromTarget(value); return slugFromTarget(value);
} }
@@ -322,124 +181,14 @@ function main(argv) {
if (metaArg) { if (metaArg) {
try { meta = JSON.parse(metaArg); } catch { /* ignore */ } try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
} }
// The helper, not caller-provided metadata, owns the target fingerprint.
// This makes the snapshot describe the exact file bytes critique saw.
delete meta.target_fingerprint;
delete meta.target_path;
delete meta.target_identity;
const targetIdentity = resolveTargetIdentity(slugArg);
if (targetIdentity) meta.target_identity = targetIdentity;
const targetFingerprint = fingerprintTarget(slugArg);
if (targetFingerprint) {
meta.target_fingerprint = targetFingerprint;
meta.target_path = resolveLocalTargetPath(slugArg);
}
const out = writeSnapshot({ slug, meta, body: raw }); const out = writeSnapshot({ slug, meta, body: raw });
process.stdout.write(`${out}\n`); process.stdout.write(`${out}\n`);
return; return;
} }
case 'latest': { case 'latest': {
const target = args[0]; const latest = readLatestSnapshot(coerceSlug(args[0]));
const format = args[1]; if (!latest) { process.exit(2); }
const slug = coerceSlug(target); process.stdout.write(latest.body);
if (!slug || (format && format !== '--json')) {
process.stderr.write('usage: latest <slug-or-target> [--json]\n');
process.exit(1);
}
const targetFingerprint = fingerprintTarget(target);
const targetPath = resolveLocalTargetPath(target);
const targetIdentity = resolveTargetIdentity(target);
const readySlug = isReadySlug(target);
const newestForSlug = readNewestSnapshot(slug);
if (!newestForSlug) { process.exit(2); }
// Concrete targets select the newest snapshot for their exact identity,
// not merely the newest filename for a lossy slug. This keeps distinct
// targets such as foo/bar and foo-bar from hiding each other's backlog.
const exactSnapshot = readNewestSnapshotForIdentity(slug, targetIdentity);
let latest = exactSnapshot;
if (!latest && !readySlug) {
// Legacy snapshots have no identity. Preserve their old explicit
// path/URL behavior only when no known target identity was selected.
latest = readNewestSnapshotForIdentity(slug, null);
}
if (!latest) latest = newestForSlug;
if (latest.meta.closed === true) { process.exit(2); }
const recordedTargetPath = latest.meta.target_path;
const recordedTargetIdentity = snapshotTargetIdentity(latest);
const matchingIdentity = recordedTargetIdentity === targetIdentity;
// Bare slugs remain a supported lookup mode, including for URL
// snapshots. But when a same-named local file exists, the request is
// ambiguous unless that exact file owns the snapshot identity.
if (readySlug && !recordedTargetIdentity) {
process.stderr.write(
'ambiguous legacy snapshot target; use an explicit ./path or full URL\n',
);
process.exit(2);
}
if (readySlug && targetPath && fs.existsSync(targetPath) && !matchingIdentity) {
process.stderr.write(
'ambiguous snapshot slug; use an explicit ./path or remove the local name collision\n',
);
process.exit(2);
}
const concreteTarget = !readySlug || matchingIdentity;
if (concreteTarget && recordedTargetIdentity && !matchingIdentity) {
process.exit(2);
}
const concreteLocalTarget = concreteTarget && targetPath;
if (concreteLocalTarget && latest.meta.target_fingerprint !== targetFingerprint) {
closeSnapshot(latest.path);
process.exit(2);
}
if (format === '--json') {
process.stdout.write(JSON.stringify({
snapshot_file: path.basename(latest.path),
body: latest.body,
}, null, 2) + '\n');
} else {
process.stdout.write(latest.body);
}
return;
}
case 'close': {
const [slugArg, snapshotFile, ...extra] = args;
const slug = coerceSlug(slugArg);
if (!slug || !snapshotFile || extra.length > 0) {
process.stderr.write('usage: close <resolved-target> <snapshot-file>\n');
process.exit(1);
}
if (
path.basename(snapshotFile) !== snapshotFile
|| !SNAPSHOT_FILENAME.test(snapshotFile)
|| !snapshotFile.endsWith(`__${slug}.md`)
) process.exit(2);
// A slug and filename are not enough to prove ownership because two
// distinct targets can normalize to the same slug. Modern snapshots
// carry a canonical identity, so require the supplied resolved target
// to match it before allowing the exact snapshot to be closed. Legacy
// snapshots without identity retain their historical close behavior.
const snapshotPath = path.join(getCritiqueDir(process.cwd()), snapshotFile);
let snapshot;
try {
if (!fs.lstatSync(snapshotPath).isFile()) process.exit(2);
snapshot = readSnapshot(snapshotPath);
} catch {
process.exit(2);
}
const recordedTargetIdentity = snapshotTargetIdentity(snapshot);
if (
recordedTargetIdentity
&& recordedTargetIdentity !== resolveTargetIdentity(slugArg)
) process.exit(2);
const closed = closeSnapshot(snapshotFile);
if (!closed) { process.exit(2); }
process.stdout.write(`${closed}\n`);
return; return;
} }
case 'trend': { case 'trend': {
@@ -448,7 +197,7 @@ function main(argv) {
return; return;
} }
default: default:
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend|close> [args]\n'); process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
process.exit(1); process.exit(1);
} }
} }
@@ -70,27 +70,13 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
} }
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([ const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS, 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui', '-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]); ]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and // WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1605,7 +1591,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) { function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false }; if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = primaryFontFace(fontFamily, GENERIC_FONTS); const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
if (!primary) return { primary: null, isSerif: false }; if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5204,7 +5190,8 @@ function checkTypography() {
const style = getComputedStyle(el); const style = getComputedStyle(el);
const ff = style.fontFamily; const ff = style.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++; totalTextElements++;
@@ -5449,7 +5436,8 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue; if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily; const ff = rule.style?.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (primary) { if (primary) {
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5468,10 +5456,11 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi; const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm; let fm;
while ((fm = ffRe.exec(html)) !== null) { while ((fm = ffRe.exec(html)) !== null) {
const primary = primaryFontFace(fm[1]); for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (primary) { if (f && !GENERIC_FONTS.has(f)) {
fonts.add(primary); fonts.add(f);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
} }
} }
@@ -1,7 +1,7 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { import {
checkSourceDesignSystem, checkSourceDesignSystem,
collectStaticDesignSystemFindings, collectStaticDesignSystemFindings,
@@ -51,7 +51,9 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue; if (!hasText) continue;
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,7 +9,6 @@ import {
WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX, WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain, isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs'; } from '../shared/constants.mjs';
import { import {
CSS_NAMED_COLORS, CSS_NAMED_COLORS,
@@ -332,7 +331,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) { function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false }; if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = primaryFontFace(fontFamily, GENERIC_FONTS); const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
if (!primary) return { primary: null, isSerif: false }; if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3931,7 +3930,8 @@ function checkTypography() {
const style = getComputedStyle(el); const style = getComputedStyle(el);
const ff = style.fontFamily; const ff = style.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++; totalTextElements++;
@@ -4176,7 +4176,8 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue; if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily; const ff = rule.style?.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (primary) { if (primary) {
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4195,10 +4196,11 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi; const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm; let fm;
while ((fm = ffRe.exec(html)) !== null) { while ((fm = ffRe.exec(html)) !== null) {
const primary = primaryFontFace(fm[1]); for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (primary) { if (f && !GENERIC_FONTS.has(f)) {
fonts.add(primary); fonts.add(f);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
} }
} }
@@ -56,27 +56,13 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
} }
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([ const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS, 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui', '-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]); ]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and // WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -118,7 +104,6 @@ export {
BRAND_FONT_DOMAINS, BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain, isBrandFontOnOwnDomain,
GENERIC_FONTS, GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX, WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR, EM_DASH_FLOOR,
@@ -21,24 +21,22 @@ import zlib from 'node:zlib';
const KEYWORD = 'impeccable:prompt'; const KEYWORD = 'impeccable:prompt';
const args = process.argv.slice(2); const args = process.argv.slice(2);
const file = args.find(a => !a.startsWith('--')); const file = args.find(a => !a.startsWith('--'));
const readMode = args.includes('--read');
const scanMode = args.includes('--scan');
const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; }; const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; };
function imageType(buffer) { function promptOf(imagePath) {
if (buffer.length > 8 && buffer.readUInt32BE(0) === 0x89504e47) return 'png'; const b = fs.readFileSync(imagePath);
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8) return 'jpeg'; let prompt = null;
return null; if (b.length > 8 && b.readUInt32BE(0) === 0x89504e47) prompt = readPngText(b);
} else if (b.length > 3 && b[0] === 0xff && b[1] === 0xd8) prompt = readJpegCom(b);
function readPrompt(imagePath, buffer = fs.readFileSync(imagePath)) {
const type = imageType(buffer);
let prompt = type === 'png' ? parsePng(buffer).prompt : type === 'jpeg' ? readJpegCom(buffer) : null;
if (prompt == null && fs.existsSync(`${imagePath}.json`)) { if (prompt == null && fs.existsSync(`${imagePath}.json`)) {
try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ } try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ }
} }
return prompt; return prompt;
} }
if (args.includes('--scan')) { if (scanMode) {
const targets = args.filter(a => !a.startsWith('--')); const targets = args.filter(a => !a.startsWith('--'));
if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); } if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); }
const RASTER = /\.(png|jpe?g|webp)$/i; const RASTER = /\.(png|jpe?g|webp)$/i;
@@ -61,7 +59,7 @@ if (args.includes('--scan')) {
} }
let missing = 0; let missing = 0;
for (const raster of rasters) { for (const raster of rasters) {
if (readPrompt(raster) == null) { console.log(`MISSING: ${raster}`); missing++; } if (promptOf(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
} }
console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`); console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`);
process.exit(missing > 0 ? 3 : 0); process.exit(missing > 0 ? 3 : 0);
@@ -70,7 +68,8 @@ if (args.includes('--scan')) {
if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); }
const buf = fs.readFileSync(file); const buf = fs.readFileSync(file);
const type = imageType(buf); const isPng = buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47;
const isJpeg = buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8;
const crcTable = (() => { const crcTable = (() => {
const t = new Uint32Array(256); const t = new Uint32Array(256);
@@ -88,26 +87,22 @@ function pngChunk(type, data) {
return out; return out;
} }
function parsePng(buffer) { function readPngText(b) {
const chunks = []; let off = 8;
let prompt = null; while (off + 12 <= b.length) {
let offset = 8; const len = b.readUInt32BE(off);
while (offset + 12 <= buffer.length) { const type = b.toString('ascii', off + 4, off + 8);
const length = buffer.readUInt32BE(offset); if (type === 'tEXt' || type === 'zTXt') {
const type = buffer.toString('ascii', offset + 4, offset + 8); const data = b.subarray(off + 8, off + 8 + len);
const data = buffer.subarray(offset + 8, offset + 8 + length); const nul = data.indexOf(0);
const nul = data.indexOf(0); if (nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD) {
const promptChunk = (type === 'tEXt' || type === 'zTXt') if (type === 'tEXt') return data.toString('utf8', nul + 1);
&& nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD; return zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
if (prompt == null && promptChunk) { }
prompt = type === 'tEXt'
? data.toString('utf8', nul + 1)
: zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
} }
chunks.push({ offset, type, promptChunk, bytes: buffer.subarray(offset, offset + 12 + length) }); off += 12 + len;
offset += 12 + length;
} }
return { chunks, prompt }; return null;
} }
function readJpegCom(b) { function readJpegCom(b) {
@@ -126,34 +121,48 @@ function readJpegCom(b) {
} }
const sidecar = `${file}.json`; const sidecar = `${file}.json`;
if (args.includes('--read')) { if (readMode) {
const prompt = readPrompt(file, buf); let prompt = null;
if (isPng) prompt = readPngText(buf);
else if (isJpeg) prompt = readJpegCom(buf);
if (prompt == null && fs.existsSync(sidecar)) {
try { prompt = JSON.parse(fs.readFileSync(sidecar, 'utf8')).prompt ?? null; } catch { /* fall through */ }
}
if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); } if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); }
console.log(prompt); console.log(prompt);
process.exit(0); process.exit(0);
} }
const promptFile = argOf('--prompt-file'); const prompt = argOf('--prompt') ?? (argOf('--prompt-file') ? fs.readFileSync(argOf('--prompt-file'), 'utf8') : null);
const prompt = argOf('--prompt') ?? (promptFile ? fs.readFileSync(promptFile, 'utf8') : null);
if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); } if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); }
if (type === 'png') { if (isPng) {
// Insert (or replace) our tEXt chunk immediately before IEND. // Insert (or replace) our tEXt chunk immediately before IEND.
const { chunks, prompt: existingPrompt } = parsePng(buf); const iend = buf.indexOf(Buffer.from('IEND', 'ascii')) - 4;
const iend = chunks.find((chunk) => chunk.type === 'IEND')?.offset ?? -1;
if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); } if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); }
// Drop any existing chunk with our keyword to keep embedding idempotent. // Drop any existing chunk with our keyword to keep embedding idempotent.
const replacing = existingPrompt != null; let body = buf.subarray(8, iend);
const body = replacing const existing = readPngText(buf);
? Buffer.concat(chunks if (existing != null) {
.filter((chunk) => chunk.offset < iend && !chunk.promptChunk) const parts = [];
.map((chunk) => chunk.bytes)) let off = 8;
: buf.subarray(8, iend); while (off + 12 <= buf.length && off < iend + 12) {
const promptChunk = pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])); const len = buf.readUInt32BE(off);
const end = replacing ? pngChunk('IEND', Buffer.alloc(0)) : buf.subarray(iend); const type = buf.toString('ascii', off + 4, off + 8);
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, promptChunk, end])); const chunk = buf.subarray(off, off + 12 + len);
const data = buf.subarray(off + 8, off + 8 + len);
const nul = data.indexOf(0);
const ours = (type === 'tEXt' || type === 'zTXt') && nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
if (!ours && type !== 'IEND') parts.push(chunk);
off += 12 + len;
}
body = Buffer.concat(parts).subarray(8 * 0); // parts exclude signature
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), pngChunk('IEND', Buffer.alloc(0))]));
} else {
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, iend), pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), buf.subarray(iend)]));
}
console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`); console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`);
} else if (type === 'jpeg') { } else if (isJpeg) {
const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8'); const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8');
if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); } if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); }
const com = Buffer.alloc(4 + seg.length); const com = Buffer.alloc(4 + seg.length);
@@ -431,15 +431,12 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64'));
// The prompt travels with the asset: embedded in the file itself (EXIF-class // The prompt travels with the asset: embedded in the file itself (EXIF-class
// metadata via embed-prompt.mjs) so intent survives copies across harnesses, // metadata via embed-prompt.mjs) so intent survives copies across harnesses,
// plus a sidecar for anything that indexes rather than opens the image. // plus a sidecar for anything that indexes rather than opens the image.
let embedded = false;
try { try {
const { spawnSync } = await import('node:child_process'); const { spawnSync } = await import('node:child_process');
const result = spawnSync(process.execPath, [fileURLToPath(new URL('./embed-prompt.mjs', import.meta.url)), out, '--prompt', prompt], { stdio: 'ignore' }); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' });
embedded = !result.error && result.status === 0;
if (!embedded) console.warn('generate-image: failed to embed prompt in the image');
fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2));
} catch { /* embedding is best-effort */ } } catch { /* embedding is best-effort */ }
console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); ${embedded ? 'prompt embedded + sidecar' : 'sidecar'} at ${out}.json`); console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`);
if (plateCtx && plateCtx.chroma) { if (plateCtx && plateCtx.chroma) {
const frac = await keyChroma(out, plateCtx.chroma); const frac = await keyChroma(out, plateCtx.chroma);
console.log(`PLATE-CHROMA keyed ${(frac * 100).toFixed(0)}% of pixels to alpha (${plateCtx.chroma}); place with a plain <img> over the page's own ground, no background on the plate. If the keyed fraction is under 20% the generator ignored the key: regenerate with --no-chroma and use mix-blend-mode: multiply instead.`); console.log(`PLATE-CHROMA keyed ${(frac * 100).toFixed(0)}% of pixels to alpha (${plateCtx.chroma}); place with a plain <img> over the page's own ground, no background on the plate. If the keyed fraction is under 20% the generator ignored the key: regenerate with --no-chroma and use mix-blend-mode: multiply instead.`);
@@ -770,22 +770,9 @@ function reset(cwd) {
} }
} catch { /* ignore */ } } catch { /* ignore */ }
} }
// `on` writes three things: config, consent, and hook entries in the return removed.length
// provider manifests. Reset must undo all three (issue #512): a leftover ? `Reset design hook config and cache (removed: ${removed.join(', ')}).`
// manifest entry kept invoking the hook after the config that said "off" : 'No hook config or cache to remove. Already at defaults.';
// was deleted. Local destRel only, since `on` never writes the team-shared
// sharedDestRel. No skill-folder gate: a reset mid-uninstall (skill files
// gone, manifest still wired) is the case that most needs the prune.
const pruned = [];
for (const target of HOOK_MANIFEST_TARGETS) {
try {
if (pruneImpeccableHookFromManifest(path.join(cwd, target.destRel))) pruned.push(target.provider);
} catch { /* ignore */ }
}
const parts = [];
if (removed.length) parts.push(`Reset design hook config and cache (removed: ${removed.join(', ')}).`);
if (pruned.length) parts.push(`Removed hook entries from: ${pruned.join(', ')}.`);
return parts.length ? parts.join(' ') : 'No hook config or cache to remove. Already at defaults.';
} }
function main() { function main() {
@@ -329,37 +329,47 @@ function stripBold(s) {
function extractNamedRules(lines) { function extractNamedRules(lines) {
const rules = []; const rules = [];
const seen = new Set(); const seen = new Set();
const addRule = (name, body, { allowDuplicate = false } = {}) => {
const key = name.toLowerCase();
if (!allowDuplicate && seen.has(key)) return;
seen.add(key);
rules.push({ name, body });
};
// Style A (Impeccable): "**The X Rule.** body body body" — can span lines. // Style A (Impeccable): "**The X Rule.** body body body" — can span lines.
const joined = lines.join('\n'); const joined = lines.join('\n');
const inlineMatches = [...joined.matchAll(/\*\*(The [^*]+?Rule)\.\*\*/g)]; const inlineStart = /\*\*(The [^*]+?Rule)\.\*\*/g;
const inlineMatches = [];
let m;
while ((m = inlineStart.exec(joined)) !== null) {
inlineMatches.push({ name: m[1], start: m.index, end: inlineStart.lastIndex });
}
for (let i = 0; i < inlineMatches.length; i++) { for (let i = 0; i < inlineMatches.length; i++) {
const match = inlineMatches[i]; const mm = inlineMatches[i];
const bodyEnd = inlineMatches[i + 1]?.index ?? joined.length; const bodyEnd = i + 1 < inlineMatches.length ? inlineMatches[i + 1].start : joined.length;
const body = joined const body = joined
.slice(match.index + match[0].length, bodyEnd) .slice(mm.end, bodyEnd)
.replace(/\n##[^\n]*$/s, '') .replace(/\n##[^\n]*$/s, '')
.replace(/\n###[^\n]*$/s, '') .replace(/\n###[^\n]*$/s, '')
.trim(); .trim();
// Preserve the inline format's historical behavior: repeated inline rules const name = stripBold(mm.name).trim();
// remain visible, while the later heading and bullet formats dedupe. seen.add(name.toLowerCase());
addRule(stripBold(match[1]).trim(), stripBold(body), { allowDuplicate: true }); rules.push({ name, body: stripBold(body) });
} }
// Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the // Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the
// bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle. // bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle.
for (const subsection of splitSubsections(lines).slice(1)) { for (let i = 0; i < lines.length; i++) {
const headerName = stripBold(subsection.name).replace(/["“”]/g, '').trim(); const h3 = lines[i].match(/^###\s+(.+?)\s*$/);
if (!h3) continue;
const headerName = stripBold(h3[1]).replace(/["“”]/g, '').trim();
if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue; if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue;
if (seen.has(headerName.toLowerCase())) continue;
const body = stripBold(subsection.lines.join('\n').replace(/\n+/g, ' ')).trim(); const bodyLines = [];
if (body) addRule(headerName, body); for (let j = i + 1; j < lines.length; j++) {
if (/^##\s|^###\s/.test(lines[j])) break;
bodyLines.push(lines[j]);
}
const body = stripBold(bodyLines.join('\n').replace(/\n+/g, ' ')).trim();
if (body) {
seen.add(headerName.toLowerCase());
rules.push({ name: headerName, body });
}
} }
// Style C (Stitch bullet form): "* **The Layering Principle:** body" // Style C (Stitch bullet form): "* **The Layering Principle:** body"
@@ -369,7 +379,9 @@ function extractNamedRules(lines) {
if (!mm) continue; if (!mm) continue;
const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim(); const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim();
if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue; if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue;
addRule(nameRaw, stripBold(mm[2]).trim()); if (seen.has(nameRaw.toLowerCase())) continue;
seen.add(nameRaw.toLowerCase());
rules.push({ name: nameRaw, body: stripBold(mm[2]).trim() });
} }
return rules; return rules;
+1 -1
View File
@@ -7,7 +7,7 @@ argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layo
license: Apache 2.0 license: Apache 2.0
--- ---
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft. This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
Core principles: Core principles:
- Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide).
@@ -1,6 +1,6 @@
### Purpose ### Purpose
Resolve one stable target, run two independent assessments, synthesize a design critique, persist a snapshot, and ask the user what to improve next. The chat response is the primary deliverable; the snapshot is an archive of that run. Resolve one stable target, run two independent assessments, synthesize a design critique, persist a snapshot, and ask the user what to improve next. The chat response is the primary deliverable; the snapshot is an archive/backlog for future commands.
### Hard Invariants ### Hard Invariants
@@ -84,7 +84,7 @@ After Assessment B returns usable CLI findings, reuse them. Do not rerun `detect
Synthesize both assessments into a single report. Do NOT simply concatenate. Weave the findings together, noting where the LLM review and detector agree, where the detector caught issues the LLM missed, and where detector findings are false positives. Synthesize both assessments into a single report. Do NOT simply concatenate. Weave the findings together, noting where the LLM review and detector agree, where the detector caught issues the LLM missed, and where detector findings are false positives.
The chat response is the primary user-facing deliverable. Present the full structured critique below in chat; do not replace it with a summary and a link. The persisted snapshot is an archive of that run. The chat response is the primary user-facing deliverable. Present the full structured critique below in chat; do not replace it with a summary and a link. The persisted snapshot is only an archive/backlog for later commands.
Structure your feedback as a design director would: Structure your feedback as a design director would:
@@ -197,7 +197,7 @@ Skip this step if the Setup slug was null (vague or root-level target).
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"max_score":<n>,"na_heuristics":"<comma-separated numbers, or empty>","p0_count":<n>,"p1_count":<n>}' \ IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"max_score":<n>,"na_heuristics":"<comma-separated numbers, or empty>","p0_count":<n>,"p1_count":<n>}' \
node .github/skills/impeccable/scripts/critique-storage.mjs write "<resolved target>" <body-file> node .github/skills/impeccable/scripts/critique-storage.mjs write "<resolved target>" <body-file>
``` ```
`max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. For a local file target, the helper also records an exact content fingerprint so polish can distinguish the assessed bytes from later edits without relying on Git state or timestamps. The helper prints the absolute path it wrote. Leave that file on disk. Polish closes it; this run does not. `max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. The helper prints the absolute path it wrote.
3. **Delete the temp body file** after the write attempt completes, whether the write succeeded or failed. If deletion fails, mention `temp-file cleanup failed: <reason>` briefly in the final output, but do not block the critique. 3. **Delete the temp body file** after the write attempt completes, whether the write succeeded or failed. If deletion fails, mention `temp-file cleanup failed: <reason>` briefly in the final output, but do not block the critique.
+1 -1
View File
@@ -32,7 +32,7 @@ The first argument is the action. Defaults to `status`.
| `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. | | `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. |
| `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. | | `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. |
| `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. | | `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. |
| `reset` | Delete the project config, dedup cache, and Cursor pending queue, and remove the hook's entries from every provider manifest `on` installs, the committed Copilot file included (a team-shared `settings.json` that `on` never writes is never touched). | | `reset` | Delete the project config, dedup cache, and Cursor pending queue. |
## Flow ## Flow
@@ -68,20 +68,16 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su
## 5. Record the decision ## 5. Record the decision
Before code, record the chosen direction as a development-only contract under `## Direction contract` in the relevant surface brief. A direction contract is durable route or artifact strategy, so create or update the brief even when no other surface strategy needs persistence. Keep the contract to six short blocks and 150 words at most. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The surface brief is the reminder later agents reload across edits and sessions: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
Never copy the direction contract into implementation source or any browser-delivered artifact. This includes HTML or framework comments, hidden DOM, `<template>` elements, `data-*` attributes, rendered JSX or TSX output, serialized props or state, React Server Component payloads, client bundles, metadata or JSON-LD, accessibility-only text, and files served beside the artifact. A compiler or optimizer removing development metadata is not a safety boundary. Reviewers and documenters receive the contract from the surface brief.
On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md.
Read the existing surface brief before updating it: If the work establishes durable strategy for a route or artifact, read its existing surface brief, then update it:
`node .github/skills/impeccable/scripts/surface-brief.mjs read <primary-target>` `node .github/skills/impeccable/scripts/surface-brief.mjs read <primary-target>`
`node .github/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]` `node .github/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]`
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it.
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior.
+2 -10
View File
@@ -29,10 +29,10 @@ Use the feature yourself at the surface's representative sizes: desktop and mobi
If a prior critique exists, use it as one input: If a prior critique exists, use it as one input:
```bash ```bash
node .github/skills/impeccable/scripts/critique-storage.mjs latest "<resolved target>" --json node .github/skills/impeccable/scripts/critique-storage.mjs latest "<resolved target>"
``` ```
Exit 0 returns JSON with the latest snapshot's `body` and an exact `snapshot_file` identity. Retain `snapshot_file` until the end of the pass. For a local file target, the helper compares the file's exact current content fingerprint with the fingerprint captured by critique. Unchanged staged, unstaged, or untracked content remains current; any byte change, deletion, or replacement with a non-file closes the backlog it identified while preserving its trend history and exits 2. A URL target has no local fingerprint and remains current until explicitly closed. When current, incorporate relevant P0/P1 findings from `body` and name the snapshot read. Exit 2 means none exists or the target changed. Perform an independent pass either way. Exit 0 returns the latest snapshot; incorporate relevant P0/P1 findings and name the snapshot read. Exit 2 means none exists. Perform an independent pass either way.
## 3. Triage ## 3. Triage
@@ -95,11 +95,3 @@ Walk the complete path again with mouse, keyboard, and touch where applicable. C
Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment.
Finish with a source diff: remove accidental churn, orphaned code, redundant values, and temporary artifacts. Ship only when the feature is functionally complete and consistently finished across the path. Finish with a source diff: remove accidental churn, orphaned code, redundant values, and temporary artifacts. Ship only when the feature is functionally complete and consistently finished across the path.
When this pass clears every Priority Issue it took from a snapshot, close that snapshot:
```bash
node .github/skills/impeccable/scripts/critique-storage.mjs close "<resolved target>" "<snapshot_file returned by latest>"
```
This closes only the snapshot this pass actually processed; if a newer critique landed meanwhile, its backlog stays live. Do not close when no snapshot was read, when `snapshot_file` was not retained, or when Priority Issues remain.
@@ -8,7 +8,7 @@ Reason over the signals; there is no score to obey:
- `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system).
- `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default. - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default.
- `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared). - `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale.
- `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them.
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `detect.mjs` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code. - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `detect.mjs` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code.
- Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`. - Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`.
@@ -16,9 +16,8 @@
* CLI entry points (called from skill instructions): * CLI entry points (called from skill instructions):
* node critique-storage.mjs slug <resolved-target> * node critique-storage.mjs slug <resolved-target>
* node critique-storage.mjs write <slug> <snapshot-body-file> * node critique-storage.mjs write <slug> <snapshot-body-file>
* node critique-storage.mjs latest <slug> [--json] * node critique-storage.mjs latest <slug>
* node critique-storage.mjs trend <slug> [limit] * node critique-storage.mjs trend <slug> [limit]
* node critique-storage.mjs close <resolved-target> <snapshot-file>
* *
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain * Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
* markdown file; the model reads it directly with its file-read tool. This * markdown file; the model reads it directly with its file-read tool. This
@@ -28,7 +27,6 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { createHash } from 'node:crypto';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
import { getCritiqueDir } from './lib/impeccable-paths.mjs'; import { getCritiqueDir } from './lib/impeccable-paths.mjs';
import { slugFromTarget } from './lib/target-slug.mjs'; import { slugFromTarget } from './lib/target-slug.mjs';
@@ -52,45 +50,6 @@ export function nowFilenameStamp(date = new Date()) {
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z'); return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
} }
/**
* Return an exact content fingerprint for a local file target. URLs and
* non-files return null because their content is not available here.
*
* The fingerprint deliberately describes bytes, not Git state or mtimes:
* critique often assesses an uncommitted file, and a later polish run should
* inherit that backlog when the bytes are unchanged regardless of staging.
*/
function resolveLocalTargetPath(target, { cwd = process.cwd() } = {}) {
if (!target || /^https?:\/\//i.test(target)) return null;
return path.isAbsolute(target) ? path.resolve(target) : path.resolve(cwd, target);
}
function resolveTargetIdentity(target, { cwd = process.cwd() } = {}) {
if (!target || typeof target !== 'string') return null;
if (/^https?:\/\//i.test(target)) {
try {
const url = new URL(target);
const pathname = url.pathname.replace(/\/+$/, '') || '/';
return `url:${url.origin}${pathname}`;
} catch {
return null;
}
}
const filePath = resolveLocalTargetPath(target, { cwd });
return filePath ? `file:${filePath}` : null;
}
export function fingerprintTarget(target, { cwd = process.cwd() } = {}) {
const filePath = resolveLocalTargetPath(target, { cwd });
if (!filePath) return null;
try {
if (!fs.statSync(filePath).isFile()) return null;
return `sha256:${createHash('sha256').update(fs.readFileSync(filePath)).digest('hex')}`;
} catch {
return null;
}
}
/** /**
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter * Write a snapshot for `slug`. `meta` carries the small structured frontmatter
* keys read back by readTrend(). `body` is the human-readable critique * keys read back by readTrend(). `body` is the human-readable critique
@@ -103,27 +62,14 @@ export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new
const dir = getCritiqueDir(cwd); const dir = getCritiqueDir(cwd);
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
const timestamp = nowFilenameStamp(now); const timestamp = nowFilenameStamp(now);
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
// Spread `meta` first so internally computed `timestamp` and `slug` // Spread `meta` first so internally computed `timestamp` and `slug`
// always win. Otherwise a caller-supplied meta blob (parsed from the // always win. Otherwise a caller-supplied meta blob (parsed from the
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the // IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
// filename in disagreement with its frontmatter and corrupting trends. // filename in disagreement with its frontmatter and corrupting trends.
const front = serializeFrontmatter({ ...meta, timestamp, slug }); const front = serializeFrontmatter({ ...meta, timestamp, slug });
const contents = `${front}\n${body.trim()}\n`; fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
return filePath;
// A second critique can finish in the same UTC second. Use exclusive
// creation and a fixed-width suffix so concurrent writers cannot replace
// history and lexical ordering still keeps collision entries newest.
for (let collision = 0; collision <= 9999; collision += 1) {
const suffix = collision === 0 ? '' : `~${String(collision).padStart(4, '0')}`;
const filePath = path.join(dir, `${timestamp}${suffix}__${slug}.md`);
try {
fs.writeFileSync(filePath, contents, { encoding: 'utf-8', flag: 'wx' });
return filePath;
} catch (error) {
if (error?.code !== 'EEXIST') throw error;
}
}
throw new Error(`Too many critique snapshots for ${slug} at ${timestamp}`);
} }
function serializeFrontmatter(obj) { function serializeFrontmatter(obj) {
@@ -152,8 +98,6 @@ function parseFrontmatter(text) {
try { value = JSON.parse(value); } catch { /* leave as-is */ } try { value = JSON.parse(value); } catch { /* leave as-is */ }
} else if (/^-?\d+$/.test(value)) { } else if (/^-?\d+$/.test(value)) {
value = Number(value); value = Number(value);
} else if (value === 'true' || value === 'false') {
value = value === 'true';
} }
out[key] = value; out[key] = value;
} }
@@ -163,7 +107,7 @@ function parseFrontmatter(text) {
/** /**
* Return snapshot files matching `suffix`, sorted oldest newest. * Return snapshot files matching `suffix`, sorted oldest newest.
*/ */
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z(?:~\d{4})?__.+\.md$/; const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
function listSnapshots(suffix, cwd) { function listSnapshots(suffix, cwd) {
const dir = getCritiqueDir(cwd); const dir = getCritiqueDir(cwd);
@@ -174,105 +118,24 @@ function listSnapshots(suffix, cwd) {
.map((f) => path.join(dir, f)); .map((f) => path.join(dir, f));
} }
function readSnapshot(filePath) { function readLatestSnapshotMatching(suffix, cwd) {
const filePath = listSnapshots(suffix, cwd).at(-1);
if (!filePath) return null; if (!filePath) return null;
const body = fs.readFileSync(filePath, 'utf-8'); const body = fs.readFileSync(filePath, 'utf-8');
return { path: filePath, body, meta: parseFrontmatter(body) }; return { path: filePath, body, meta: parseFrontmatter(body) };
} }
function snapshotTargetIdentity(snapshot) {
const targetPath = snapshot?.meta.target_path;
return snapshot?.meta.target_identity
|| (targetPath ? `file:${targetPath}` : null);
}
function readNewestSnapshot(slug, { cwd = process.cwd() } = {}) {
return readSnapshot(listSnapshots(`__${slug}.md`, cwd).at(-1));
}
function readNewestSnapshotForIdentity(
slug,
targetIdentity,
{ cwd = process.cwd() } = {},
) {
const matches = listSnapshots(`__${slug}.md`, cwd)
.map(readSnapshot)
.filter((snapshot) => snapshotTargetIdentity(snapshot) === targetIdentity);
return matches.at(-1) || null;
}
/** /**
* Return the most recent snapshot for `slug`, or null. Polish reads this * Return the most recent snapshot for `slug`, or null. Polish reads this
* to find its fix backlog when the slug matches. * to find its fix backlog when the slug matches.
*/ */
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
const latest = readNewestSnapshot(slug, { cwd }); return readLatestSnapshotMatching(`__${slug}.md`, cwd);
return latest?.meta.closed === true ? null : latest;
}
/**
* Mark one exact snapshot closed without deleting the score history consumed
* by `trend`. Exact identity matters: a newer critique may land after polish
* reads its backlog, and that newer snapshot must remain live. `snapshotFile`
* may be the absolute path returned by readLatestSnapshot() or the basename
* emitted by `latest --json`. Returns the path marked closed, or null.
*/
export function closeSnapshot(snapshotFile, { cwd = process.cwd() } = {}) {
if (!snapshotFile || typeof snapshotFile !== 'string') return null;
const dir = path.resolve(getCritiqueDir(cwd));
const snapshotPath = path.isAbsolute(snapshotFile)
? path.resolve(snapshotFile)
: path.resolve(dir, snapshotFile);
const filename = path.basename(snapshotPath);
if (
path.dirname(snapshotPath) !== dir
|| !SNAPSHOT_FILENAME.test(filename)
) return null;
let snapshot;
try {
if (!fs.lstatSync(snapshotPath).isFile()) return null;
snapshot = readSnapshot(snapshotPath);
} catch {
return null;
}
if (!snapshot || snapshot.meta.closed === true) return null;
const closedBody = snapshot.body.replace(
/^(---\r?\n[\s\S]*?)(\r?\n---)/,
'$1\nclosed: true$2',
);
if (closedBody === snapshot.body) {
throw new Error(`Cannot close snapshot without frontmatter: ${snapshot.path}`);
}
fs.writeFileSync(snapshot.path, closedBody, 'utf-8');
return snapshot.path;
} }
/** Return the most recent snapshot across all targets, or null. */ /** Return the most recent snapshot across all targets, or null. */
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
const snapshots = listSnapshots('.md', cwd).map(readSnapshot); return readLatestSnapshotMatching('.md', cwd);
const identifiedSlugs = new Set(
snapshots
.filter((snapshot) => snapshotTargetIdentity(snapshot))
.map((snapshot) => snapshot.meta.slug),
);
const latestByTarget = new Map();
for (const snapshot of snapshots) {
if (!snapshot?.meta.slug) continue;
// Slugs are lossy: distinct targets such as foo/bar and foo-bar can share
// one. Keep each known identity's latest open/closed state independent so
// closing one target cannot hide another target's live backlog. Once a
// slug has any identity-aware snapshot, its older legacy records are no
// longer independently routable and must not resurface as zombie work.
const targetIdentity = snapshotTargetIdentity(snapshot);
if (!targetIdentity && identifiedSlugs.has(snapshot.meta.slug)) continue;
const streamKey = targetIdentity || `slug:${snapshot.meta.slug}`;
latestByTarget.set(streamKey, snapshot);
}
return [...latestByTarget.values()]
.filter((snapshot) => snapshot.meta.closed !== true)
.sort((a, b) => a.path.localeCompare(b.path))
.at(-1) || null;
} }
/** /**
@@ -290,13 +153,9 @@ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
// Accept either a ready slug or a concrete target (path/URL) everywhere, so // Accept either a ready slug or a concrete target (path/URL) everywhere, so
// callers never have to run the slug step separately. Anything containing a // callers never have to run the slug step separately. Anything containing a
// path or URL marker is resolved through slugFromTarget. // path or URL marker is resolved through slugFromTarget.
function isReadySlug(value) {
return /^[a-z0-9-]+$/.test(value || '') && !value.includes('/');
}
function coerceSlug(value) { function coerceSlug(value) {
if (!value) return null; if (!value) return null;
if (isReadySlug(value)) return value; if (/^[a-z0-9-]+$/.test(value) && !value.includes('/')) return value;
return slugFromTarget(value); return slugFromTarget(value);
} }
@@ -322,124 +181,14 @@ function main(argv) {
if (metaArg) { if (metaArg) {
try { meta = JSON.parse(metaArg); } catch { /* ignore */ } try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
} }
// The helper, not caller-provided metadata, owns the target fingerprint.
// This makes the snapshot describe the exact file bytes critique saw.
delete meta.target_fingerprint;
delete meta.target_path;
delete meta.target_identity;
const targetIdentity = resolveTargetIdentity(slugArg);
if (targetIdentity) meta.target_identity = targetIdentity;
const targetFingerprint = fingerprintTarget(slugArg);
if (targetFingerprint) {
meta.target_fingerprint = targetFingerprint;
meta.target_path = resolveLocalTargetPath(slugArg);
}
const out = writeSnapshot({ slug, meta, body: raw }); const out = writeSnapshot({ slug, meta, body: raw });
process.stdout.write(`${out}\n`); process.stdout.write(`${out}\n`);
return; return;
} }
case 'latest': { case 'latest': {
const target = args[0]; const latest = readLatestSnapshot(coerceSlug(args[0]));
const format = args[1]; if (!latest) { process.exit(2); }
const slug = coerceSlug(target); process.stdout.write(latest.body);
if (!slug || (format && format !== '--json')) {
process.stderr.write('usage: latest <slug-or-target> [--json]\n');
process.exit(1);
}
const targetFingerprint = fingerprintTarget(target);
const targetPath = resolveLocalTargetPath(target);
const targetIdentity = resolveTargetIdentity(target);
const readySlug = isReadySlug(target);
const newestForSlug = readNewestSnapshot(slug);
if (!newestForSlug) { process.exit(2); }
// Concrete targets select the newest snapshot for their exact identity,
// not merely the newest filename for a lossy slug. This keeps distinct
// targets such as foo/bar and foo-bar from hiding each other's backlog.
const exactSnapshot = readNewestSnapshotForIdentity(slug, targetIdentity);
let latest = exactSnapshot;
if (!latest && !readySlug) {
// Legacy snapshots have no identity. Preserve their old explicit
// path/URL behavior only when no known target identity was selected.
latest = readNewestSnapshotForIdentity(slug, null);
}
if (!latest) latest = newestForSlug;
if (latest.meta.closed === true) { process.exit(2); }
const recordedTargetPath = latest.meta.target_path;
const recordedTargetIdentity = snapshotTargetIdentity(latest);
const matchingIdentity = recordedTargetIdentity === targetIdentity;
// Bare slugs remain a supported lookup mode, including for URL
// snapshots. But when a same-named local file exists, the request is
// ambiguous unless that exact file owns the snapshot identity.
if (readySlug && !recordedTargetIdentity) {
process.stderr.write(
'ambiguous legacy snapshot target; use an explicit ./path or full URL\n',
);
process.exit(2);
}
if (readySlug && targetPath && fs.existsSync(targetPath) && !matchingIdentity) {
process.stderr.write(
'ambiguous snapshot slug; use an explicit ./path or remove the local name collision\n',
);
process.exit(2);
}
const concreteTarget = !readySlug || matchingIdentity;
if (concreteTarget && recordedTargetIdentity && !matchingIdentity) {
process.exit(2);
}
const concreteLocalTarget = concreteTarget && targetPath;
if (concreteLocalTarget && latest.meta.target_fingerprint !== targetFingerprint) {
closeSnapshot(latest.path);
process.exit(2);
}
if (format === '--json') {
process.stdout.write(JSON.stringify({
snapshot_file: path.basename(latest.path),
body: latest.body,
}, null, 2) + '\n');
} else {
process.stdout.write(latest.body);
}
return;
}
case 'close': {
const [slugArg, snapshotFile, ...extra] = args;
const slug = coerceSlug(slugArg);
if (!slug || !snapshotFile || extra.length > 0) {
process.stderr.write('usage: close <resolved-target> <snapshot-file>\n');
process.exit(1);
}
if (
path.basename(snapshotFile) !== snapshotFile
|| !SNAPSHOT_FILENAME.test(snapshotFile)
|| !snapshotFile.endsWith(`__${slug}.md`)
) process.exit(2);
// A slug and filename are not enough to prove ownership because two
// distinct targets can normalize to the same slug. Modern snapshots
// carry a canonical identity, so require the supplied resolved target
// to match it before allowing the exact snapshot to be closed. Legacy
// snapshots without identity retain their historical close behavior.
const snapshotPath = path.join(getCritiqueDir(process.cwd()), snapshotFile);
let snapshot;
try {
if (!fs.lstatSync(snapshotPath).isFile()) process.exit(2);
snapshot = readSnapshot(snapshotPath);
} catch {
process.exit(2);
}
const recordedTargetIdentity = snapshotTargetIdentity(snapshot);
if (
recordedTargetIdentity
&& recordedTargetIdentity !== resolveTargetIdentity(slugArg)
) process.exit(2);
const closed = closeSnapshot(snapshotFile);
if (!closed) { process.exit(2); }
process.stdout.write(`${closed}\n`);
return; return;
} }
case 'trend': { case 'trend': {
@@ -448,7 +197,7 @@ function main(argv) {
return; return;
} }
default: default:
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend|close> [args]\n'); process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
process.exit(1); process.exit(1);
} }
} }
@@ -70,27 +70,13 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
} }
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([ const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS, 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui', '-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]); ]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and // WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1605,7 +1591,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) { function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false }; if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = primaryFontFace(fontFamily, GENERIC_FONTS); const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
if (!primary) return { primary: null, isSerif: false }; if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5204,7 +5190,8 @@ function checkTypography() {
const style = getComputedStyle(el); const style = getComputedStyle(el);
const ff = style.fontFamily; const ff = style.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++; totalTextElements++;
@@ -5449,7 +5436,8 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue; if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily; const ff = rule.style?.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (primary) { if (primary) {
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5468,10 +5456,11 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi; const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm; let fm;
while ((fm = ffRe.exec(html)) !== null) { while ((fm = ffRe.exec(html)) !== null) {
const primary = primaryFontFace(fm[1]); for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (primary) { if (f && !GENERIC_FONTS.has(f)) {
fonts.add(primary); fonts.add(f);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
} }
} }
@@ -1,7 +1,7 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { import {
checkSourceDesignSystem, checkSourceDesignSystem,
collectStaticDesignSystemFindings, collectStaticDesignSystemFindings,
@@ -51,7 +51,9 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue; if (!hasText) continue;
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,7 +9,6 @@ import {
WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX, WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain, isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs'; } from '../shared/constants.mjs';
import { import {
CSS_NAMED_COLORS, CSS_NAMED_COLORS,
@@ -332,7 +331,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) { function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false }; if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = primaryFontFace(fontFamily, GENERIC_FONTS); const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
if (!primary) return { primary: null, isSerif: false }; if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3931,7 +3930,8 @@ function checkTypography() {
const style = getComputedStyle(el); const style = getComputedStyle(el);
const ff = style.fontFamily; const ff = style.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++; totalTextElements++;
@@ -4176,7 +4176,8 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue; if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily; const ff = rule.style?.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (primary) { if (primary) {
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4195,10 +4196,11 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi; const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm; let fm;
while ((fm = ffRe.exec(html)) !== null) { while ((fm = ffRe.exec(html)) !== null) {
const primary = primaryFontFace(fm[1]); for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (primary) { if (f && !GENERIC_FONTS.has(f)) {
fonts.add(primary); fonts.add(f);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
} }
} }
@@ -56,27 +56,13 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
} }
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([ const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS, 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui', '-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]); ]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and // WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -118,7 +104,6 @@ export {
BRAND_FONT_DOMAINS, BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain, isBrandFontOnOwnDomain,
GENERIC_FONTS, GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX, WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR, EM_DASH_FLOOR,
@@ -21,24 +21,22 @@ import zlib from 'node:zlib';
const KEYWORD = 'impeccable:prompt'; const KEYWORD = 'impeccable:prompt';
const args = process.argv.slice(2); const args = process.argv.slice(2);
const file = args.find(a => !a.startsWith('--')); const file = args.find(a => !a.startsWith('--'));
const readMode = args.includes('--read');
const scanMode = args.includes('--scan');
const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; }; const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; };
function imageType(buffer) { function promptOf(imagePath) {
if (buffer.length > 8 && buffer.readUInt32BE(0) === 0x89504e47) return 'png'; const b = fs.readFileSync(imagePath);
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8) return 'jpeg'; let prompt = null;
return null; if (b.length > 8 && b.readUInt32BE(0) === 0x89504e47) prompt = readPngText(b);
} else if (b.length > 3 && b[0] === 0xff && b[1] === 0xd8) prompt = readJpegCom(b);
function readPrompt(imagePath, buffer = fs.readFileSync(imagePath)) {
const type = imageType(buffer);
let prompt = type === 'png' ? parsePng(buffer).prompt : type === 'jpeg' ? readJpegCom(buffer) : null;
if (prompt == null && fs.existsSync(`${imagePath}.json`)) { if (prompt == null && fs.existsSync(`${imagePath}.json`)) {
try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ } try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ }
} }
return prompt; return prompt;
} }
if (args.includes('--scan')) { if (scanMode) {
const targets = args.filter(a => !a.startsWith('--')); const targets = args.filter(a => !a.startsWith('--'));
if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); } if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); }
const RASTER = /\.(png|jpe?g|webp)$/i; const RASTER = /\.(png|jpe?g|webp)$/i;
@@ -61,7 +59,7 @@ if (args.includes('--scan')) {
} }
let missing = 0; let missing = 0;
for (const raster of rasters) { for (const raster of rasters) {
if (readPrompt(raster) == null) { console.log(`MISSING: ${raster}`); missing++; } if (promptOf(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
} }
console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`); console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`);
process.exit(missing > 0 ? 3 : 0); process.exit(missing > 0 ? 3 : 0);
@@ -70,7 +68,8 @@ if (args.includes('--scan')) {
if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); }
const buf = fs.readFileSync(file); const buf = fs.readFileSync(file);
const type = imageType(buf); const isPng = buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47;
const isJpeg = buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8;
const crcTable = (() => { const crcTable = (() => {
const t = new Uint32Array(256); const t = new Uint32Array(256);
@@ -88,26 +87,22 @@ function pngChunk(type, data) {
return out; return out;
} }
function parsePng(buffer) { function readPngText(b) {
const chunks = []; let off = 8;
let prompt = null; while (off + 12 <= b.length) {
let offset = 8; const len = b.readUInt32BE(off);
while (offset + 12 <= buffer.length) { const type = b.toString('ascii', off + 4, off + 8);
const length = buffer.readUInt32BE(offset); if (type === 'tEXt' || type === 'zTXt') {
const type = buffer.toString('ascii', offset + 4, offset + 8); const data = b.subarray(off + 8, off + 8 + len);
const data = buffer.subarray(offset + 8, offset + 8 + length); const nul = data.indexOf(0);
const nul = data.indexOf(0); if (nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD) {
const promptChunk = (type === 'tEXt' || type === 'zTXt') if (type === 'tEXt') return data.toString('utf8', nul + 1);
&& nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD; return zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
if (prompt == null && promptChunk) { }
prompt = type === 'tEXt'
? data.toString('utf8', nul + 1)
: zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
} }
chunks.push({ offset, type, promptChunk, bytes: buffer.subarray(offset, offset + 12 + length) }); off += 12 + len;
offset += 12 + length;
} }
return { chunks, prompt }; return null;
} }
function readJpegCom(b) { function readJpegCom(b) {
@@ -126,34 +121,48 @@ function readJpegCom(b) {
} }
const sidecar = `${file}.json`; const sidecar = `${file}.json`;
if (args.includes('--read')) { if (readMode) {
const prompt = readPrompt(file, buf); let prompt = null;
if (isPng) prompt = readPngText(buf);
else if (isJpeg) prompt = readJpegCom(buf);
if (prompt == null && fs.existsSync(sidecar)) {
try { prompt = JSON.parse(fs.readFileSync(sidecar, 'utf8')).prompt ?? null; } catch { /* fall through */ }
}
if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); } if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); }
console.log(prompt); console.log(prompt);
process.exit(0); process.exit(0);
} }
const promptFile = argOf('--prompt-file'); const prompt = argOf('--prompt') ?? (argOf('--prompt-file') ? fs.readFileSync(argOf('--prompt-file'), 'utf8') : null);
const prompt = argOf('--prompt') ?? (promptFile ? fs.readFileSync(promptFile, 'utf8') : null);
if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); } if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); }
if (type === 'png') { if (isPng) {
// Insert (or replace) our tEXt chunk immediately before IEND. // Insert (or replace) our tEXt chunk immediately before IEND.
const { chunks, prompt: existingPrompt } = parsePng(buf); const iend = buf.indexOf(Buffer.from('IEND', 'ascii')) - 4;
const iend = chunks.find((chunk) => chunk.type === 'IEND')?.offset ?? -1;
if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); } if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); }
// Drop any existing chunk with our keyword to keep embedding idempotent. // Drop any existing chunk with our keyword to keep embedding idempotent.
const replacing = existingPrompt != null; let body = buf.subarray(8, iend);
const body = replacing const existing = readPngText(buf);
? Buffer.concat(chunks if (existing != null) {
.filter((chunk) => chunk.offset < iend && !chunk.promptChunk) const parts = [];
.map((chunk) => chunk.bytes)) let off = 8;
: buf.subarray(8, iend); while (off + 12 <= buf.length && off < iend + 12) {
const promptChunk = pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])); const len = buf.readUInt32BE(off);
const end = replacing ? pngChunk('IEND', Buffer.alloc(0)) : buf.subarray(iend); const type = buf.toString('ascii', off + 4, off + 8);
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, promptChunk, end])); const chunk = buf.subarray(off, off + 12 + len);
const data = buf.subarray(off + 8, off + 8 + len);
const nul = data.indexOf(0);
const ours = (type === 'tEXt' || type === 'zTXt') && nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
if (!ours && type !== 'IEND') parts.push(chunk);
off += 12 + len;
}
body = Buffer.concat(parts).subarray(8 * 0); // parts exclude signature
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), pngChunk('IEND', Buffer.alloc(0))]));
} else {
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, iend), pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), buf.subarray(iend)]));
}
console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`); console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`);
} else if (type === 'jpeg') { } else if (isJpeg) {
const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8'); const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8');
if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); } if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); }
const com = Buffer.alloc(4 + seg.length); const com = Buffer.alloc(4 + seg.length);
@@ -431,15 +431,12 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64'));
// The prompt travels with the asset: embedded in the file itself (EXIF-class // The prompt travels with the asset: embedded in the file itself (EXIF-class
// metadata via embed-prompt.mjs) so intent survives copies across harnesses, // metadata via embed-prompt.mjs) so intent survives copies across harnesses,
// plus a sidecar for anything that indexes rather than opens the image. // plus a sidecar for anything that indexes rather than opens the image.
let embedded = false;
try { try {
const { spawnSync } = await import('node:child_process'); const { spawnSync } = await import('node:child_process');
const result = spawnSync(process.execPath, [fileURLToPath(new URL('./embed-prompt.mjs', import.meta.url)), out, '--prompt', prompt], { stdio: 'ignore' }); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' });
embedded = !result.error && result.status === 0;
if (!embedded) console.warn('generate-image: failed to embed prompt in the image');
fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2));
} catch { /* embedding is best-effort */ } } catch { /* embedding is best-effort */ }
console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); ${embedded ? 'prompt embedded + sidecar' : 'sidecar'} at ${out}.json`); console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`);
if (plateCtx && plateCtx.chroma) { if (plateCtx && plateCtx.chroma) {
const frac = await keyChroma(out, plateCtx.chroma); const frac = await keyChroma(out, plateCtx.chroma);
console.log(`PLATE-CHROMA keyed ${(frac * 100).toFixed(0)}% of pixels to alpha (${plateCtx.chroma}); place with a plain <img> over the page's own ground, no background on the plate. If the keyed fraction is under 20% the generator ignored the key: regenerate with --no-chroma and use mix-blend-mode: multiply instead.`); console.log(`PLATE-CHROMA keyed ${(frac * 100).toFixed(0)}% of pixels to alpha (${plateCtx.chroma}); place with a plain <img> over the page's own ground, no background on the plate. If the keyed fraction is under 20% the generator ignored the key: regenerate with --no-chroma and use mix-blend-mode: multiply instead.`);
@@ -770,22 +770,9 @@ function reset(cwd) {
} }
} catch { /* ignore */ } } catch { /* ignore */ }
} }
// `on` writes three things: config, consent, and hook entries in the return removed.length
// provider manifests. Reset must undo all three (issue #512): a leftover ? `Reset design hook config and cache (removed: ${removed.join(', ')}).`
// manifest entry kept invoking the hook after the config that said "off" : 'No hook config or cache to remove. Already at defaults.';
// was deleted. Local destRel only, since `on` never writes the team-shared
// sharedDestRel. No skill-folder gate: a reset mid-uninstall (skill files
// gone, manifest still wired) is the case that most needs the prune.
const pruned = [];
for (const target of HOOK_MANIFEST_TARGETS) {
try {
if (pruneImpeccableHookFromManifest(path.join(cwd, target.destRel))) pruned.push(target.provider);
} catch { /* ignore */ }
}
const parts = [];
if (removed.length) parts.push(`Reset design hook config and cache (removed: ${removed.join(', ')}).`);
if (pruned.length) parts.push(`Removed hook entries from: ${pruned.join(', ')}.`);
return parts.length ? parts.join(' ') : 'No hook config or cache to remove. Already at defaults.';
} }
function main() { function main() {
@@ -329,37 +329,47 @@ function stripBold(s) {
function extractNamedRules(lines) { function extractNamedRules(lines) {
const rules = []; const rules = [];
const seen = new Set(); const seen = new Set();
const addRule = (name, body, { allowDuplicate = false } = {}) => {
const key = name.toLowerCase();
if (!allowDuplicate && seen.has(key)) return;
seen.add(key);
rules.push({ name, body });
};
// Style A (Impeccable): "**The X Rule.** body body body" — can span lines. // Style A (Impeccable): "**The X Rule.** body body body" — can span lines.
const joined = lines.join('\n'); const joined = lines.join('\n');
const inlineMatches = [...joined.matchAll(/\*\*(The [^*]+?Rule)\.\*\*/g)]; const inlineStart = /\*\*(The [^*]+?Rule)\.\*\*/g;
const inlineMatches = [];
let m;
while ((m = inlineStart.exec(joined)) !== null) {
inlineMatches.push({ name: m[1], start: m.index, end: inlineStart.lastIndex });
}
for (let i = 0; i < inlineMatches.length; i++) { for (let i = 0; i < inlineMatches.length; i++) {
const match = inlineMatches[i]; const mm = inlineMatches[i];
const bodyEnd = inlineMatches[i + 1]?.index ?? joined.length; const bodyEnd = i + 1 < inlineMatches.length ? inlineMatches[i + 1].start : joined.length;
const body = joined const body = joined
.slice(match.index + match[0].length, bodyEnd) .slice(mm.end, bodyEnd)
.replace(/\n##[^\n]*$/s, '') .replace(/\n##[^\n]*$/s, '')
.replace(/\n###[^\n]*$/s, '') .replace(/\n###[^\n]*$/s, '')
.trim(); .trim();
// Preserve the inline format's historical behavior: repeated inline rules const name = stripBold(mm.name).trim();
// remain visible, while the later heading and bullet formats dedupe. seen.add(name.toLowerCase());
addRule(stripBold(match[1]).trim(), stripBold(body), { allowDuplicate: true }); rules.push({ name, body: stripBold(body) });
} }
// Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the // Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the
// bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle. // bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle.
for (const subsection of splitSubsections(lines).slice(1)) { for (let i = 0; i < lines.length; i++) {
const headerName = stripBold(subsection.name).replace(/["“”]/g, '').trim(); const h3 = lines[i].match(/^###\s+(.+?)\s*$/);
if (!h3) continue;
const headerName = stripBold(h3[1]).replace(/["“”]/g, '').trim();
if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue; if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue;
if (seen.has(headerName.toLowerCase())) continue;
const body = stripBold(subsection.lines.join('\n').replace(/\n+/g, ' ')).trim(); const bodyLines = [];
if (body) addRule(headerName, body); for (let j = i + 1; j < lines.length; j++) {
if (/^##\s|^###\s/.test(lines[j])) break;
bodyLines.push(lines[j]);
}
const body = stripBold(bodyLines.join('\n').replace(/\n+/g, ' ')).trim();
if (body) {
seen.add(headerName.toLowerCase());
rules.push({ name: headerName, body });
}
} }
// Style C (Stitch bullet form): "* **The Layering Principle:** body" // Style C (Stitch bullet form): "* **The Layering Principle:** body"
@@ -369,7 +379,9 @@ function extractNamedRules(lines) {
if (!mm) continue; if (!mm) continue;
const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim(); const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim();
if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue; if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue;
addRule(nameRaw, stripBold(mm[2]).trim()); if (seen.has(nameRaw.toLowerCase())) continue;
seen.add(nameRaw.toLowerCase());
rules.push({ name: nameRaw, body: stripBold(mm[2]).trim() });
} }
return rules; return rules;
+1 -22
View File
@@ -1,8 +1,6 @@
name: PR Sheriff name: PR Sheriff
on: on:
issues:
types: [opened, reopened]
schedule: schedule:
# Daily UTC afternoon pass. The script uses an aggressive 7/14 day window: # Daily UTC afternoon pass. The script uses an aggressive 7/14 day window:
# warn contributor-blocked PRs after 7 days open, close after 14. # warn contributor-blocked PRs after 7 days open, close after 14.
@@ -26,25 +24,7 @@ concurrency:
cancel-in-progress: false cancel-in-progress: false
jobs: jobs:
issue-inbox:
if: github.event_name == 'issues'
runs-on: ubuntu-latest
steps:
- name: Mark issue for triage
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: |
gh label create "needs triage" \
--repo "$GH_REPO" \
--color "d4c5f9" \
--description "New or reopened issue awaiting maintainer triage" \
--force
gh issue edit "$ISSUE_NUMBER" --repo "$GH_REPO" --add-label "needs triage"
sheriff: sheriff:
if: github.event_name != 'issues'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -69,5 +49,4 @@ jobs:
--warning-days 7 \ --warning-days 7 \
--close-days 14 \ --close-days 14 \
--maintainers "pbakaus" \ --maintainers "pbakaus" \
--regular-contributors "pbakaus,abdulwahabone" \ --regular-contributors "pbakaus,abdulwahabone"
--auto-close-regulars
+1 -1
View File
@@ -10,7 +10,7 @@ allowed-tools:
- Bash(node .grok/skills/impeccable/scripts/*) - Bash(node .grok/skills/impeccable/scripts/*)
--- ---
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft. This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
Core principles: Core principles:
- Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide).
@@ -1,6 +1,6 @@
### Purpose ### Purpose
Resolve one stable target, run two independent assessments, synthesize a design critique, persist a snapshot, and ask the user what to improve next. The chat response is the primary deliverable; the snapshot is an archive of that run. Resolve one stable target, run two independent assessments, synthesize a design critique, persist a snapshot, and ask the user what to improve next. The chat response is the primary deliverable; the snapshot is an archive/backlog for future commands.
### Hard Invariants ### Hard Invariants
@@ -84,7 +84,7 @@ After Assessment B returns usable CLI findings, reuse them. Do not rerun `detect
Synthesize both assessments into a single report. Do NOT simply concatenate. Weave the findings together, noting where the LLM review and detector agree, where the detector caught issues the LLM missed, and where detector findings are false positives. Synthesize both assessments into a single report. Do NOT simply concatenate. Weave the findings together, noting where the LLM review and detector agree, where the detector caught issues the LLM missed, and where detector findings are false positives.
The chat response is the primary user-facing deliverable. Present the full structured critique below in chat; do not replace it with a summary and a link. The persisted snapshot is an archive of that run. The chat response is the primary user-facing deliverable. Present the full structured critique below in chat; do not replace it with a summary and a link. The persisted snapshot is only an archive/backlog for later commands.
Structure your feedback as a design director would: Structure your feedback as a design director would:
@@ -197,7 +197,7 @@ Skip this step if the Setup slug was null (vague or root-level target).
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"max_score":<n>,"na_heuristics":"<comma-separated numbers, or empty>","p0_count":<n>,"p1_count":<n>}' \ IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"max_score":<n>,"na_heuristics":"<comma-separated numbers, or empty>","p0_count":<n>,"p1_count":<n>}' \
node .grok/skills/impeccable/scripts/critique-storage.mjs write "<resolved target>" <body-file> node .grok/skills/impeccable/scripts/critique-storage.mjs write "<resolved target>" <body-file>
``` ```
`max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. For a local file target, the helper also records an exact content fingerprint so polish can distinguish the assessed bytes from later edits without relying on Git state or timestamps. The helper prints the absolute path it wrote. Leave that file on disk. Polish closes it; this run does not. `max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. The helper prints the absolute path it wrote.
3. **Delete the temp body file** after the write attempt completes, whether the write succeeded or failed. If deletion fails, mention `temp-file cleanup failed: <reason>` briefly in the final output, but do not block the critique. 3. **Delete the temp body file** after the write attempt completes, whether the write succeeded or failed. If deletion fails, mention `temp-file cleanup failed: <reason>` briefly in the final output, but do not block the critique.
+1 -1
View File
@@ -32,7 +32,7 @@ The first argument is the action. Defaults to `status`.
| `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. | | `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. |
| `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. | | `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. |
| `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. | | `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. |
| `reset` | Delete the project config, dedup cache, and Cursor pending queue, and remove the hook's entries from every provider manifest `on` installs, the committed Copilot file included (a team-shared `settings.json` that `on` never writes is never touched). | | `reset` | Delete the project config, dedup cache, and Cursor pending queue. |
## Flow ## Flow
@@ -68,20 +68,16 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su
## 5. Record the decision ## 5. Record the decision
Before code, record the chosen direction as a development-only contract under `## Direction contract` in the relevant surface brief. A direction contract is durable route or artifact strategy, so create or update the brief even when no other surface strategy needs persistence. Keep the contract to six short blocks and 150 words at most. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The surface brief is the reminder later agents reload across edits and sessions: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
Never copy the direction contract into implementation source or any browser-delivered artifact. This includes HTML or framework comments, hidden DOM, `<template>` elements, `data-*` attributes, rendered JSX or TSX output, serialized props or state, React Server Component payloads, client bundles, metadata or JSON-LD, accessibility-only text, and files served beside the artifact. A compiler or optimizer removing development metadata is not a safety boundary. Reviewers and documenters receive the contract from the surface brief.
On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md.
Read the existing surface brief before updating it: If the work establishes durable strategy for a route or artifact, read its existing surface brief, then update it:
`node .grok/skills/impeccable/scripts/surface-brief.mjs read <primary-target>` `node .grok/skills/impeccable/scripts/surface-brief.mjs read <primary-target>`
`node .grok/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]` `node .grok/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]`
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it.
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior.
+2 -10
View File
@@ -29,10 +29,10 @@ Use the feature yourself at the surface's representative sizes: desktop and mobi
If a prior critique exists, use it as one input: If a prior critique exists, use it as one input:
```bash ```bash
node .grok/skills/impeccable/scripts/critique-storage.mjs latest "<resolved target>" --json node .grok/skills/impeccable/scripts/critique-storage.mjs latest "<resolved target>"
``` ```
Exit 0 returns JSON with the latest snapshot's `body` and an exact `snapshot_file` identity. Retain `snapshot_file` until the end of the pass. For a local file target, the helper compares the file's exact current content fingerprint with the fingerprint captured by critique. Unchanged staged, unstaged, or untracked content remains current; any byte change, deletion, or replacement with a non-file closes the backlog it identified while preserving its trend history and exits 2. A URL target has no local fingerprint and remains current until explicitly closed. When current, incorporate relevant P0/P1 findings from `body` and name the snapshot read. Exit 2 means none exists or the target changed. Perform an independent pass either way. Exit 0 returns the latest snapshot; incorporate relevant P0/P1 findings and name the snapshot read. Exit 2 means none exists. Perform an independent pass either way.
## 3. Triage ## 3. Triage
@@ -95,11 +95,3 @@ Walk the complete path again with mouse, keyboard, and touch where applicable. C
Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment.
Finish with a source diff: remove accidental churn, orphaned code, redundant values, and temporary artifacts. Ship only when the feature is functionally complete and consistently finished across the path. Finish with a source diff: remove accidental churn, orphaned code, redundant values, and temporary artifacts. Ship only when the feature is functionally complete and consistently finished across the path.
When this pass clears every Priority Issue it took from a snapshot, close that snapshot:
```bash
node .grok/skills/impeccable/scripts/critique-storage.mjs close "<resolved target>" "<snapshot_file returned by latest>"
```
This closes only the snapshot this pass actually processed; if a newer critique landed meanwhile, its backlog stays live. Do not close when no snapshot was read, when `snapshot_file` was not retained, or when Priority Issues remain.
+1 -1
View File
@@ -8,7 +8,7 @@ Reason over the signals; there is no score to obey:
- `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system).
- `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default. - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default.
- `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared). - `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale.
- `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them.
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `detect.mjs` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code. - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `detect.mjs` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code.
- Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`. - Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`.
@@ -16,9 +16,8 @@
* CLI entry points (called from skill instructions): * CLI entry points (called from skill instructions):
* node critique-storage.mjs slug <resolved-target> * node critique-storage.mjs slug <resolved-target>
* node critique-storage.mjs write <slug> <snapshot-body-file> * node critique-storage.mjs write <slug> <snapshot-body-file>
* node critique-storage.mjs latest <slug> [--json] * node critique-storage.mjs latest <slug>
* node critique-storage.mjs trend <slug> [limit] * node critique-storage.mjs trend <slug> [limit]
* node critique-storage.mjs close <resolved-target> <snapshot-file>
* *
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain * Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
* markdown file; the model reads it directly with its file-read tool. This * markdown file; the model reads it directly with its file-read tool. This
@@ -28,7 +27,6 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { createHash } from 'node:crypto';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
import { getCritiqueDir } from './lib/impeccable-paths.mjs'; import { getCritiqueDir } from './lib/impeccable-paths.mjs';
import { slugFromTarget } from './lib/target-slug.mjs'; import { slugFromTarget } from './lib/target-slug.mjs';
@@ -52,45 +50,6 @@ export function nowFilenameStamp(date = new Date()) {
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z'); return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
} }
/**
* Return an exact content fingerprint for a local file target. URLs and
* non-files return null because their content is not available here.
*
* The fingerprint deliberately describes bytes, not Git state or mtimes:
* critique often assesses an uncommitted file, and a later polish run should
* inherit that backlog when the bytes are unchanged regardless of staging.
*/
function resolveLocalTargetPath(target, { cwd = process.cwd() } = {}) {
if (!target || /^https?:\/\//i.test(target)) return null;
return path.isAbsolute(target) ? path.resolve(target) : path.resolve(cwd, target);
}
function resolveTargetIdentity(target, { cwd = process.cwd() } = {}) {
if (!target || typeof target !== 'string') return null;
if (/^https?:\/\//i.test(target)) {
try {
const url = new URL(target);
const pathname = url.pathname.replace(/\/+$/, '') || '/';
return `url:${url.origin}${pathname}`;
} catch {
return null;
}
}
const filePath = resolveLocalTargetPath(target, { cwd });
return filePath ? `file:${filePath}` : null;
}
export function fingerprintTarget(target, { cwd = process.cwd() } = {}) {
const filePath = resolveLocalTargetPath(target, { cwd });
if (!filePath) return null;
try {
if (!fs.statSync(filePath).isFile()) return null;
return `sha256:${createHash('sha256').update(fs.readFileSync(filePath)).digest('hex')}`;
} catch {
return null;
}
}
/** /**
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter * Write a snapshot for `slug`. `meta` carries the small structured frontmatter
* keys read back by readTrend(). `body` is the human-readable critique * keys read back by readTrend(). `body` is the human-readable critique
@@ -103,27 +62,14 @@ export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new
const dir = getCritiqueDir(cwd); const dir = getCritiqueDir(cwd);
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
const timestamp = nowFilenameStamp(now); const timestamp = nowFilenameStamp(now);
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
// Spread `meta` first so internally computed `timestamp` and `slug` // Spread `meta` first so internally computed `timestamp` and `slug`
// always win. Otherwise a caller-supplied meta blob (parsed from the // always win. Otherwise a caller-supplied meta blob (parsed from the
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the // IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
// filename in disagreement with its frontmatter and corrupting trends. // filename in disagreement with its frontmatter and corrupting trends.
const front = serializeFrontmatter({ ...meta, timestamp, slug }); const front = serializeFrontmatter({ ...meta, timestamp, slug });
const contents = `${front}\n${body.trim()}\n`; fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
return filePath;
// A second critique can finish in the same UTC second. Use exclusive
// creation and a fixed-width suffix so concurrent writers cannot replace
// history and lexical ordering still keeps collision entries newest.
for (let collision = 0; collision <= 9999; collision += 1) {
const suffix = collision === 0 ? '' : `~${String(collision).padStart(4, '0')}`;
const filePath = path.join(dir, `${timestamp}${suffix}__${slug}.md`);
try {
fs.writeFileSync(filePath, contents, { encoding: 'utf-8', flag: 'wx' });
return filePath;
} catch (error) {
if (error?.code !== 'EEXIST') throw error;
}
}
throw new Error(`Too many critique snapshots for ${slug} at ${timestamp}`);
} }
function serializeFrontmatter(obj) { function serializeFrontmatter(obj) {
@@ -152,8 +98,6 @@ function parseFrontmatter(text) {
try { value = JSON.parse(value); } catch { /* leave as-is */ } try { value = JSON.parse(value); } catch { /* leave as-is */ }
} else if (/^-?\d+$/.test(value)) { } else if (/^-?\d+$/.test(value)) {
value = Number(value); value = Number(value);
} else if (value === 'true' || value === 'false') {
value = value === 'true';
} }
out[key] = value; out[key] = value;
} }
@@ -163,7 +107,7 @@ function parseFrontmatter(text) {
/** /**
* Return snapshot files matching `suffix`, sorted oldest newest. * Return snapshot files matching `suffix`, sorted oldest newest.
*/ */
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z(?:~\d{4})?__.+\.md$/; const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
function listSnapshots(suffix, cwd) { function listSnapshots(suffix, cwd) {
const dir = getCritiqueDir(cwd); const dir = getCritiqueDir(cwd);
@@ -174,105 +118,24 @@ function listSnapshots(suffix, cwd) {
.map((f) => path.join(dir, f)); .map((f) => path.join(dir, f));
} }
function readSnapshot(filePath) { function readLatestSnapshotMatching(suffix, cwd) {
const filePath = listSnapshots(suffix, cwd).at(-1);
if (!filePath) return null; if (!filePath) return null;
const body = fs.readFileSync(filePath, 'utf-8'); const body = fs.readFileSync(filePath, 'utf-8');
return { path: filePath, body, meta: parseFrontmatter(body) }; return { path: filePath, body, meta: parseFrontmatter(body) };
} }
function snapshotTargetIdentity(snapshot) {
const targetPath = snapshot?.meta.target_path;
return snapshot?.meta.target_identity
|| (targetPath ? `file:${targetPath}` : null);
}
function readNewestSnapshot(slug, { cwd = process.cwd() } = {}) {
return readSnapshot(listSnapshots(`__${slug}.md`, cwd).at(-1));
}
function readNewestSnapshotForIdentity(
slug,
targetIdentity,
{ cwd = process.cwd() } = {},
) {
const matches = listSnapshots(`__${slug}.md`, cwd)
.map(readSnapshot)
.filter((snapshot) => snapshotTargetIdentity(snapshot) === targetIdentity);
return matches.at(-1) || null;
}
/** /**
* Return the most recent snapshot for `slug`, or null. Polish reads this * Return the most recent snapshot for `slug`, or null. Polish reads this
* to find its fix backlog when the slug matches. * to find its fix backlog when the slug matches.
*/ */
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
const latest = readNewestSnapshot(slug, { cwd }); return readLatestSnapshotMatching(`__${slug}.md`, cwd);
return latest?.meta.closed === true ? null : latest;
}
/**
* Mark one exact snapshot closed without deleting the score history consumed
* by `trend`. Exact identity matters: a newer critique may land after polish
* reads its backlog, and that newer snapshot must remain live. `snapshotFile`
* may be the absolute path returned by readLatestSnapshot() or the basename
* emitted by `latest --json`. Returns the path marked closed, or null.
*/
export function closeSnapshot(snapshotFile, { cwd = process.cwd() } = {}) {
if (!snapshotFile || typeof snapshotFile !== 'string') return null;
const dir = path.resolve(getCritiqueDir(cwd));
const snapshotPath = path.isAbsolute(snapshotFile)
? path.resolve(snapshotFile)
: path.resolve(dir, snapshotFile);
const filename = path.basename(snapshotPath);
if (
path.dirname(snapshotPath) !== dir
|| !SNAPSHOT_FILENAME.test(filename)
) return null;
let snapshot;
try {
if (!fs.lstatSync(snapshotPath).isFile()) return null;
snapshot = readSnapshot(snapshotPath);
} catch {
return null;
}
if (!snapshot || snapshot.meta.closed === true) return null;
const closedBody = snapshot.body.replace(
/^(---\r?\n[\s\S]*?)(\r?\n---)/,
'$1\nclosed: true$2',
);
if (closedBody === snapshot.body) {
throw new Error(`Cannot close snapshot without frontmatter: ${snapshot.path}`);
}
fs.writeFileSync(snapshot.path, closedBody, 'utf-8');
return snapshot.path;
} }
/** Return the most recent snapshot across all targets, or null. */ /** Return the most recent snapshot across all targets, or null. */
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
const snapshots = listSnapshots('.md', cwd).map(readSnapshot); return readLatestSnapshotMatching('.md', cwd);
const identifiedSlugs = new Set(
snapshots
.filter((snapshot) => snapshotTargetIdentity(snapshot))
.map((snapshot) => snapshot.meta.slug),
);
const latestByTarget = new Map();
for (const snapshot of snapshots) {
if (!snapshot?.meta.slug) continue;
// Slugs are lossy: distinct targets such as foo/bar and foo-bar can share
// one. Keep each known identity's latest open/closed state independent so
// closing one target cannot hide another target's live backlog. Once a
// slug has any identity-aware snapshot, its older legacy records are no
// longer independently routable and must not resurface as zombie work.
const targetIdentity = snapshotTargetIdentity(snapshot);
if (!targetIdentity && identifiedSlugs.has(snapshot.meta.slug)) continue;
const streamKey = targetIdentity || `slug:${snapshot.meta.slug}`;
latestByTarget.set(streamKey, snapshot);
}
return [...latestByTarget.values()]
.filter((snapshot) => snapshot.meta.closed !== true)
.sort((a, b) => a.path.localeCompare(b.path))
.at(-1) || null;
} }
/** /**
@@ -290,13 +153,9 @@ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
// Accept either a ready slug or a concrete target (path/URL) everywhere, so // Accept either a ready slug or a concrete target (path/URL) everywhere, so
// callers never have to run the slug step separately. Anything containing a // callers never have to run the slug step separately. Anything containing a
// path or URL marker is resolved through slugFromTarget. // path or URL marker is resolved through slugFromTarget.
function isReadySlug(value) {
return /^[a-z0-9-]+$/.test(value || '') && !value.includes('/');
}
function coerceSlug(value) { function coerceSlug(value) {
if (!value) return null; if (!value) return null;
if (isReadySlug(value)) return value; if (/^[a-z0-9-]+$/.test(value) && !value.includes('/')) return value;
return slugFromTarget(value); return slugFromTarget(value);
} }
@@ -322,124 +181,14 @@ function main(argv) {
if (metaArg) { if (metaArg) {
try { meta = JSON.parse(metaArg); } catch { /* ignore */ } try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
} }
// The helper, not caller-provided metadata, owns the target fingerprint.
// This makes the snapshot describe the exact file bytes critique saw.
delete meta.target_fingerprint;
delete meta.target_path;
delete meta.target_identity;
const targetIdentity = resolveTargetIdentity(slugArg);
if (targetIdentity) meta.target_identity = targetIdentity;
const targetFingerprint = fingerprintTarget(slugArg);
if (targetFingerprint) {
meta.target_fingerprint = targetFingerprint;
meta.target_path = resolveLocalTargetPath(slugArg);
}
const out = writeSnapshot({ slug, meta, body: raw }); const out = writeSnapshot({ slug, meta, body: raw });
process.stdout.write(`${out}\n`); process.stdout.write(`${out}\n`);
return; return;
} }
case 'latest': { case 'latest': {
const target = args[0]; const latest = readLatestSnapshot(coerceSlug(args[0]));
const format = args[1]; if (!latest) { process.exit(2); }
const slug = coerceSlug(target); process.stdout.write(latest.body);
if (!slug || (format && format !== '--json')) {
process.stderr.write('usage: latest <slug-or-target> [--json]\n');
process.exit(1);
}
const targetFingerprint = fingerprintTarget(target);
const targetPath = resolveLocalTargetPath(target);
const targetIdentity = resolveTargetIdentity(target);
const readySlug = isReadySlug(target);
const newestForSlug = readNewestSnapshot(slug);
if (!newestForSlug) { process.exit(2); }
// Concrete targets select the newest snapshot for their exact identity,
// not merely the newest filename for a lossy slug. This keeps distinct
// targets such as foo/bar and foo-bar from hiding each other's backlog.
const exactSnapshot = readNewestSnapshotForIdentity(slug, targetIdentity);
let latest = exactSnapshot;
if (!latest && !readySlug) {
// Legacy snapshots have no identity. Preserve their old explicit
// path/URL behavior only when no known target identity was selected.
latest = readNewestSnapshotForIdentity(slug, null);
}
if (!latest) latest = newestForSlug;
if (latest.meta.closed === true) { process.exit(2); }
const recordedTargetPath = latest.meta.target_path;
const recordedTargetIdentity = snapshotTargetIdentity(latest);
const matchingIdentity = recordedTargetIdentity === targetIdentity;
// Bare slugs remain a supported lookup mode, including for URL
// snapshots. But when a same-named local file exists, the request is
// ambiguous unless that exact file owns the snapshot identity.
if (readySlug && !recordedTargetIdentity) {
process.stderr.write(
'ambiguous legacy snapshot target; use an explicit ./path or full URL\n',
);
process.exit(2);
}
if (readySlug && targetPath && fs.existsSync(targetPath) && !matchingIdentity) {
process.stderr.write(
'ambiguous snapshot slug; use an explicit ./path or remove the local name collision\n',
);
process.exit(2);
}
const concreteTarget = !readySlug || matchingIdentity;
if (concreteTarget && recordedTargetIdentity && !matchingIdentity) {
process.exit(2);
}
const concreteLocalTarget = concreteTarget && targetPath;
if (concreteLocalTarget && latest.meta.target_fingerprint !== targetFingerprint) {
closeSnapshot(latest.path);
process.exit(2);
}
if (format === '--json') {
process.stdout.write(JSON.stringify({
snapshot_file: path.basename(latest.path),
body: latest.body,
}, null, 2) + '\n');
} else {
process.stdout.write(latest.body);
}
return;
}
case 'close': {
const [slugArg, snapshotFile, ...extra] = args;
const slug = coerceSlug(slugArg);
if (!slug || !snapshotFile || extra.length > 0) {
process.stderr.write('usage: close <resolved-target> <snapshot-file>\n');
process.exit(1);
}
if (
path.basename(snapshotFile) !== snapshotFile
|| !SNAPSHOT_FILENAME.test(snapshotFile)
|| !snapshotFile.endsWith(`__${slug}.md`)
) process.exit(2);
// A slug and filename are not enough to prove ownership because two
// distinct targets can normalize to the same slug. Modern snapshots
// carry a canonical identity, so require the supplied resolved target
// to match it before allowing the exact snapshot to be closed. Legacy
// snapshots without identity retain their historical close behavior.
const snapshotPath = path.join(getCritiqueDir(process.cwd()), snapshotFile);
let snapshot;
try {
if (!fs.lstatSync(snapshotPath).isFile()) process.exit(2);
snapshot = readSnapshot(snapshotPath);
} catch {
process.exit(2);
}
const recordedTargetIdentity = snapshotTargetIdentity(snapshot);
if (
recordedTargetIdentity
&& recordedTargetIdentity !== resolveTargetIdentity(slugArg)
) process.exit(2);
const closed = closeSnapshot(snapshotFile);
if (!closed) { process.exit(2); }
process.stdout.write(`${closed}\n`);
return; return;
} }
case 'trend': { case 'trend': {
@@ -448,7 +197,7 @@ function main(argv) {
return; return;
} }
default: default:
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend|close> [args]\n'); process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
process.exit(1); process.exit(1);
} }
} }
@@ -70,27 +70,13 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
} }
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([ const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS, 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui', '-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]); ]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and // WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1605,7 +1591,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) { function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false }; if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = primaryFontFace(fontFamily, GENERIC_FONTS); const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
if (!primary) return { primary: null, isSerif: false }; if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5204,7 +5190,8 @@ function checkTypography() {
const style = getComputedStyle(el); const style = getComputedStyle(el);
const ff = style.fontFamily; const ff = style.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++; totalTextElements++;
@@ -5449,7 +5436,8 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue; if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily; const ff = rule.style?.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (primary) { if (primary) {
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5468,10 +5456,11 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi; const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm; let fm;
while ((fm = ffRe.exec(html)) !== null) { while ((fm = ffRe.exec(html)) !== null) {
const primary = primaryFontFace(fm[1]); for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (primary) { if (f && !GENERIC_FONTS.has(f)) {
fonts.add(primary); fonts.add(f);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
} }
} }
@@ -1,7 +1,7 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { import {
checkSourceDesignSystem, checkSourceDesignSystem,
collectStaticDesignSystemFindings, collectStaticDesignSystemFindings,
@@ -51,7 +51,9 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue; if (!hasText) continue;
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,7 +9,6 @@ import {
WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX, WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain, isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs'; } from '../shared/constants.mjs';
import { import {
CSS_NAMED_COLORS, CSS_NAMED_COLORS,
@@ -332,7 +331,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) { function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false }; if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = primaryFontFace(fontFamily, GENERIC_FONTS); const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
if (!primary) return { primary: null, isSerif: false }; if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3931,7 +3930,8 @@ function checkTypography() {
const style = getComputedStyle(el); const style = getComputedStyle(el);
const ff = style.fontFamily; const ff = style.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++; totalTextElements++;
@@ -4176,7 +4176,8 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue; if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily; const ff = rule.style?.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (primary) { if (primary) {
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4195,10 +4196,11 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi; const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm; let fm;
while ((fm = ffRe.exec(html)) !== null) { while ((fm = ffRe.exec(html)) !== null) {
const primary = primaryFontFace(fm[1]); for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (primary) { if (f && !GENERIC_FONTS.has(f)) {
fonts.add(primary); fonts.add(f);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
} }
} }
@@ -56,27 +56,13 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
} }
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([ const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS, 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui', '-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]); ]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and // WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -118,7 +104,6 @@ export {
BRAND_FONT_DOMAINS, BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain, isBrandFontOnOwnDomain,
GENERIC_FONTS, GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX, WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR, EM_DASH_FLOOR,
@@ -21,24 +21,22 @@ import zlib from 'node:zlib';
const KEYWORD = 'impeccable:prompt'; const KEYWORD = 'impeccable:prompt';
const args = process.argv.slice(2); const args = process.argv.slice(2);
const file = args.find(a => !a.startsWith('--')); const file = args.find(a => !a.startsWith('--'));
const readMode = args.includes('--read');
const scanMode = args.includes('--scan');
const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; }; const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; };
function imageType(buffer) { function promptOf(imagePath) {
if (buffer.length > 8 && buffer.readUInt32BE(0) === 0x89504e47) return 'png'; const b = fs.readFileSync(imagePath);
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8) return 'jpeg'; let prompt = null;
return null; if (b.length > 8 && b.readUInt32BE(0) === 0x89504e47) prompt = readPngText(b);
} else if (b.length > 3 && b[0] === 0xff && b[1] === 0xd8) prompt = readJpegCom(b);
function readPrompt(imagePath, buffer = fs.readFileSync(imagePath)) {
const type = imageType(buffer);
let prompt = type === 'png' ? parsePng(buffer).prompt : type === 'jpeg' ? readJpegCom(buffer) : null;
if (prompt == null && fs.existsSync(`${imagePath}.json`)) { if (prompt == null && fs.existsSync(`${imagePath}.json`)) {
try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ } try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ }
} }
return prompt; return prompt;
} }
if (args.includes('--scan')) { if (scanMode) {
const targets = args.filter(a => !a.startsWith('--')); const targets = args.filter(a => !a.startsWith('--'));
if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); } if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); }
const RASTER = /\.(png|jpe?g|webp)$/i; const RASTER = /\.(png|jpe?g|webp)$/i;
@@ -61,7 +59,7 @@ if (args.includes('--scan')) {
} }
let missing = 0; let missing = 0;
for (const raster of rasters) { for (const raster of rasters) {
if (readPrompt(raster) == null) { console.log(`MISSING: ${raster}`); missing++; } if (promptOf(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
} }
console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`); console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`);
process.exit(missing > 0 ? 3 : 0); process.exit(missing > 0 ? 3 : 0);
@@ -70,7 +68,8 @@ if (args.includes('--scan')) {
if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); }
const buf = fs.readFileSync(file); const buf = fs.readFileSync(file);
const type = imageType(buf); const isPng = buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47;
const isJpeg = buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8;
const crcTable = (() => { const crcTable = (() => {
const t = new Uint32Array(256); const t = new Uint32Array(256);
@@ -88,26 +87,22 @@ function pngChunk(type, data) {
return out; return out;
} }
function parsePng(buffer) { function readPngText(b) {
const chunks = []; let off = 8;
let prompt = null; while (off + 12 <= b.length) {
let offset = 8; const len = b.readUInt32BE(off);
while (offset + 12 <= buffer.length) { const type = b.toString('ascii', off + 4, off + 8);
const length = buffer.readUInt32BE(offset); if (type === 'tEXt' || type === 'zTXt') {
const type = buffer.toString('ascii', offset + 4, offset + 8); const data = b.subarray(off + 8, off + 8 + len);
const data = buffer.subarray(offset + 8, offset + 8 + length); const nul = data.indexOf(0);
const nul = data.indexOf(0); if (nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD) {
const promptChunk = (type === 'tEXt' || type === 'zTXt') if (type === 'tEXt') return data.toString('utf8', nul + 1);
&& nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD; return zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
if (prompt == null && promptChunk) { }
prompt = type === 'tEXt'
? data.toString('utf8', nul + 1)
: zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
} }
chunks.push({ offset, type, promptChunk, bytes: buffer.subarray(offset, offset + 12 + length) }); off += 12 + len;
offset += 12 + length;
} }
return { chunks, prompt }; return null;
} }
function readJpegCom(b) { function readJpegCom(b) {
@@ -126,34 +121,48 @@ function readJpegCom(b) {
} }
const sidecar = `${file}.json`; const sidecar = `${file}.json`;
if (args.includes('--read')) { if (readMode) {
const prompt = readPrompt(file, buf); let prompt = null;
if (isPng) prompt = readPngText(buf);
else if (isJpeg) prompt = readJpegCom(buf);
if (prompt == null && fs.existsSync(sidecar)) {
try { prompt = JSON.parse(fs.readFileSync(sidecar, 'utf8')).prompt ?? null; } catch { /* fall through */ }
}
if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); } if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); }
console.log(prompt); console.log(prompt);
process.exit(0); process.exit(0);
} }
const promptFile = argOf('--prompt-file'); const prompt = argOf('--prompt') ?? (argOf('--prompt-file') ? fs.readFileSync(argOf('--prompt-file'), 'utf8') : null);
const prompt = argOf('--prompt') ?? (promptFile ? fs.readFileSync(promptFile, 'utf8') : null);
if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); } if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); }
if (type === 'png') { if (isPng) {
// Insert (or replace) our tEXt chunk immediately before IEND. // Insert (or replace) our tEXt chunk immediately before IEND.
const { chunks, prompt: existingPrompt } = parsePng(buf); const iend = buf.indexOf(Buffer.from('IEND', 'ascii')) - 4;
const iend = chunks.find((chunk) => chunk.type === 'IEND')?.offset ?? -1;
if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); } if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); }
// Drop any existing chunk with our keyword to keep embedding idempotent. // Drop any existing chunk with our keyword to keep embedding idempotent.
const replacing = existingPrompt != null; let body = buf.subarray(8, iend);
const body = replacing const existing = readPngText(buf);
? Buffer.concat(chunks if (existing != null) {
.filter((chunk) => chunk.offset < iend && !chunk.promptChunk) const parts = [];
.map((chunk) => chunk.bytes)) let off = 8;
: buf.subarray(8, iend); while (off + 12 <= buf.length && off < iend + 12) {
const promptChunk = pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])); const len = buf.readUInt32BE(off);
const end = replacing ? pngChunk('IEND', Buffer.alloc(0)) : buf.subarray(iend); const type = buf.toString('ascii', off + 4, off + 8);
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, promptChunk, end])); const chunk = buf.subarray(off, off + 12 + len);
const data = buf.subarray(off + 8, off + 8 + len);
const nul = data.indexOf(0);
const ours = (type === 'tEXt' || type === 'zTXt') && nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
if (!ours && type !== 'IEND') parts.push(chunk);
off += 12 + len;
}
body = Buffer.concat(parts).subarray(8 * 0); // parts exclude signature
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), pngChunk('IEND', Buffer.alloc(0))]));
} else {
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, iend), pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), buf.subarray(iend)]));
}
console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`); console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`);
} else if (type === 'jpeg') { } else if (isJpeg) {
const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8'); const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8');
if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); } if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); }
const com = Buffer.alloc(4 + seg.length); const com = Buffer.alloc(4 + seg.length);
@@ -431,15 +431,12 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64'));
// The prompt travels with the asset: embedded in the file itself (EXIF-class // The prompt travels with the asset: embedded in the file itself (EXIF-class
// metadata via embed-prompt.mjs) so intent survives copies across harnesses, // metadata via embed-prompt.mjs) so intent survives copies across harnesses,
// plus a sidecar for anything that indexes rather than opens the image. // plus a sidecar for anything that indexes rather than opens the image.
let embedded = false;
try { try {
const { spawnSync } = await import('node:child_process'); const { spawnSync } = await import('node:child_process');
const result = spawnSync(process.execPath, [fileURLToPath(new URL('./embed-prompt.mjs', import.meta.url)), out, '--prompt', prompt], { stdio: 'ignore' }); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' });
embedded = !result.error && result.status === 0;
if (!embedded) console.warn('generate-image: failed to embed prompt in the image');
fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2));
} catch { /* embedding is best-effort */ } } catch { /* embedding is best-effort */ }
console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); ${embedded ? 'prompt embedded + sidecar' : 'sidecar'} at ${out}.json`); console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`);
if (plateCtx && plateCtx.chroma) { if (plateCtx && plateCtx.chroma) {
const frac = await keyChroma(out, plateCtx.chroma); const frac = await keyChroma(out, plateCtx.chroma);
console.log(`PLATE-CHROMA keyed ${(frac * 100).toFixed(0)}% of pixels to alpha (${plateCtx.chroma}); place with a plain <img> over the page's own ground, no background on the plate. If the keyed fraction is under 20% the generator ignored the key: regenerate with --no-chroma and use mix-blend-mode: multiply instead.`); console.log(`PLATE-CHROMA keyed ${(frac * 100).toFixed(0)}% of pixels to alpha (${plateCtx.chroma}); place with a plain <img> over the page's own ground, no background on the plate. If the keyed fraction is under 20% the generator ignored the key: regenerate with --no-chroma and use mix-blend-mode: multiply instead.`);
+3 -16
View File
@@ -770,22 +770,9 @@ function reset(cwd) {
} }
} catch { /* ignore */ } } catch { /* ignore */ }
} }
// `on` writes three things: config, consent, and hook entries in the return removed.length
// provider manifests. Reset must undo all three (issue #512): a leftover ? `Reset design hook config and cache (removed: ${removed.join(', ')}).`
// manifest entry kept invoking the hook after the config that said "off" : 'No hook config or cache to remove. Already at defaults.';
// was deleted. Local destRel only, since `on` never writes the team-shared
// sharedDestRel. No skill-folder gate: a reset mid-uninstall (skill files
// gone, manifest still wired) is the case that most needs the prune.
const pruned = [];
for (const target of HOOK_MANIFEST_TARGETS) {
try {
if (pruneImpeccableHookFromManifest(path.join(cwd, target.destRel))) pruned.push(target.provider);
} catch { /* ignore */ }
}
const parts = [];
if (removed.length) parts.push(`Reset design hook config and cache (removed: ${removed.join(', ')}).`);
if (pruned.length) parts.push(`Removed hook entries from: ${pruned.join(', ')}.`);
return parts.length ? parts.join(' ') : 'No hook config or cache to remove. Already at defaults.';
} }
function main() { function main() {
@@ -329,37 +329,47 @@ function stripBold(s) {
function extractNamedRules(lines) { function extractNamedRules(lines) {
const rules = []; const rules = [];
const seen = new Set(); const seen = new Set();
const addRule = (name, body, { allowDuplicate = false } = {}) => {
const key = name.toLowerCase();
if (!allowDuplicate && seen.has(key)) return;
seen.add(key);
rules.push({ name, body });
};
// Style A (Impeccable): "**The X Rule.** body body body" — can span lines. // Style A (Impeccable): "**The X Rule.** body body body" — can span lines.
const joined = lines.join('\n'); const joined = lines.join('\n');
const inlineMatches = [...joined.matchAll(/\*\*(The [^*]+?Rule)\.\*\*/g)]; const inlineStart = /\*\*(The [^*]+?Rule)\.\*\*/g;
const inlineMatches = [];
let m;
while ((m = inlineStart.exec(joined)) !== null) {
inlineMatches.push({ name: m[1], start: m.index, end: inlineStart.lastIndex });
}
for (let i = 0; i < inlineMatches.length; i++) { for (let i = 0; i < inlineMatches.length; i++) {
const match = inlineMatches[i]; const mm = inlineMatches[i];
const bodyEnd = inlineMatches[i + 1]?.index ?? joined.length; const bodyEnd = i + 1 < inlineMatches.length ? inlineMatches[i + 1].start : joined.length;
const body = joined const body = joined
.slice(match.index + match[0].length, bodyEnd) .slice(mm.end, bodyEnd)
.replace(/\n##[^\n]*$/s, '') .replace(/\n##[^\n]*$/s, '')
.replace(/\n###[^\n]*$/s, '') .replace(/\n###[^\n]*$/s, '')
.trim(); .trim();
// Preserve the inline format's historical behavior: repeated inline rules const name = stripBold(mm.name).trim();
// remain visible, while the later heading and bullet formats dedupe. seen.add(name.toLowerCase());
addRule(stripBold(match[1]).trim(), stripBold(body), { allowDuplicate: true }); rules.push({ name, body: stripBold(body) });
} }
// Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the // Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the
// bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle. // bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle.
for (const subsection of splitSubsections(lines).slice(1)) { for (let i = 0; i < lines.length; i++) {
const headerName = stripBold(subsection.name).replace(/["“”]/g, '').trim(); const h3 = lines[i].match(/^###\s+(.+?)\s*$/);
if (!h3) continue;
const headerName = stripBold(h3[1]).replace(/["“”]/g, '').trim();
if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue; if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue;
if (seen.has(headerName.toLowerCase())) continue;
const body = stripBold(subsection.lines.join('\n').replace(/\n+/g, ' ')).trim(); const bodyLines = [];
if (body) addRule(headerName, body); for (let j = i + 1; j < lines.length; j++) {
if (/^##\s|^###\s/.test(lines[j])) break;
bodyLines.push(lines[j]);
}
const body = stripBold(bodyLines.join('\n').replace(/\n+/g, ' ')).trim();
if (body) {
seen.add(headerName.toLowerCase());
rules.push({ name: headerName, body });
}
} }
// Style C (Stitch bullet form): "* **The Layering Principle:** body" // Style C (Stitch bullet form): "* **The Layering Principle:** body"
@@ -369,7 +379,9 @@ function extractNamedRules(lines) {
if (!mm) continue; if (!mm) continue;
const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim(); const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim();
if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue; if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue;
addRule(nameRaw, stripBold(mm[2]).trim()); if (seen.has(nameRaw.toLowerCase())) continue;
seen.add(nameRaw.toLowerCase());
rules.push({ name: nameRaw, body: stripBold(mm[2]).trim() });
} }
return rules; return rules;
+1 -1
View File
@@ -5,7 +5,7 @@ version: 4.1.2
license: Apache 2.0 license: Apache 2.0
--- ---
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft. This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
Core principles: Core principles:
- Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide).
@@ -1,6 +1,6 @@
### Purpose ### Purpose
Resolve one stable target, run two independent assessments, synthesize a design critique, persist a snapshot, and ask the user what to improve next. The chat response is the primary deliverable; the snapshot is an archive of that run. Resolve one stable target, run two independent assessments, synthesize a design critique, persist a snapshot, and ask the user what to improve next. The chat response is the primary deliverable; the snapshot is an archive/backlog for future commands.
### Hard Invariants ### Hard Invariants
@@ -84,7 +84,7 @@ After Assessment B returns usable CLI findings, reuse them. Do not rerun `detect
Synthesize both assessments into a single report. Do NOT simply concatenate. Weave the findings together, noting where the LLM review and detector agree, where the detector caught issues the LLM missed, and where detector findings are false positives. Synthesize both assessments into a single report. Do NOT simply concatenate. Weave the findings together, noting where the LLM review and detector agree, where the detector caught issues the LLM missed, and where detector findings are false positives.
The chat response is the primary user-facing deliverable. Present the full structured critique below in chat; do not replace it with a summary and a link. The persisted snapshot is an archive of that run. The chat response is the primary user-facing deliverable. Present the full structured critique below in chat; do not replace it with a summary and a link. The persisted snapshot is only an archive/backlog for later commands.
Structure your feedback as a design director would: Structure your feedback as a design director would:
@@ -197,7 +197,7 @@ Skip this step if the Setup slug was null (vague or root-level target).
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"max_score":<n>,"na_heuristics":"<comma-separated numbers, or empty>","p0_count":<n>,"p1_count":<n>}' \ IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"max_score":<n>,"na_heuristics":"<comma-separated numbers, or empty>","p0_count":<n>,"p1_count":<n>}' \
node .hermes/skills/impeccable/scripts/critique-storage.mjs write "<resolved target>" <body-file> node .hermes/skills/impeccable/scripts/critique-storage.mjs write "<resolved target>" <body-file>
``` ```
`max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. For a local file target, the helper also records an exact content fingerprint so polish can distinguish the assessed bytes from later edits without relying on Git state or timestamps. The helper prints the absolute path it wrote. Leave that file on disk. Polish closes it; this run does not. `max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. The helper prints the absolute path it wrote.
3. **Delete the temp body file** after the write attempt completes, whether the write succeeded or failed. If deletion fails, mention `temp-file cleanup failed: <reason>` briefly in the final output, but do not block the critique. 3. **Delete the temp body file** after the write attempt completes, whether the write succeeded or failed. If deletion fails, mention `temp-file cleanup failed: <reason>` briefly in the final output, but do not block the critique.
+1 -1
View File
@@ -32,7 +32,7 @@ The first argument is the action. Defaults to `status`.
| `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. | | `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. |
| `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. | | `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. |
| `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. | | `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. |
| `reset` | Delete the project config, dedup cache, and Cursor pending queue, and remove the hook's entries from every provider manifest `on` installs, the committed Copilot file included (a team-shared `settings.json` that `on` never writes is never touched). | | `reset` | Delete the project config, dedup cache, and Cursor pending queue. |
## Flow ## Flow
@@ -68,20 +68,16 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su
## 5. Record the decision ## 5. Record the decision
Before code, record the chosen direction as a development-only contract under `## Direction contract` in the relevant surface brief. A direction contract is durable route or artifact strategy, so create or update the brief even when no other surface strategy needs persistence. Keep the contract to six short blocks and 150 words at most. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The surface brief is the reminder later agents reload across edits and sessions: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
Never copy the direction contract into implementation source or any browser-delivered artifact. This includes HTML or framework comments, hidden DOM, `<template>` elements, `data-*` attributes, rendered JSX or TSX output, serialized props or state, React Server Component payloads, client bundles, metadata or JSON-LD, accessibility-only text, and files served beside the artifact. A compiler or optimizer removing development metadata is not a safety boundary. Reviewers and documenters receive the contract from the surface brief.
On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md.
Read the existing surface brief before updating it: If the work establishes durable strategy for a route or artifact, read its existing surface brief, then update it:
`node .hermes/skills/impeccable/scripts/surface-brief.mjs read <primary-target>` `node .hermes/skills/impeccable/scripts/surface-brief.mjs read <primary-target>`
`node .hermes/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]` `node .hermes/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]`
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it.
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior.
+2 -10
View File
@@ -29,10 +29,10 @@ Use the feature yourself at the surface's representative sizes: desktop and mobi
If a prior critique exists, use it as one input: If a prior critique exists, use it as one input:
```bash ```bash
node .hermes/skills/impeccable/scripts/critique-storage.mjs latest "<resolved target>" --json node .hermes/skills/impeccable/scripts/critique-storage.mjs latest "<resolved target>"
``` ```
Exit 0 returns JSON with the latest snapshot's `body` and an exact `snapshot_file` identity. Retain `snapshot_file` until the end of the pass. For a local file target, the helper compares the file's exact current content fingerprint with the fingerprint captured by critique. Unchanged staged, unstaged, or untracked content remains current; any byte change, deletion, or replacement with a non-file closes the backlog it identified while preserving its trend history and exits 2. A URL target has no local fingerprint and remains current until explicitly closed. When current, incorporate relevant P0/P1 findings from `body` and name the snapshot read. Exit 2 means none exists or the target changed. Perform an independent pass either way. Exit 0 returns the latest snapshot; incorporate relevant P0/P1 findings and name the snapshot read. Exit 2 means none exists. Perform an independent pass either way.
## 3. Triage ## 3. Triage
@@ -95,11 +95,3 @@ Walk the complete path again with mouse, keyboard, and touch where applicable. C
Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment.
Finish with a source diff: remove accidental churn, orphaned code, redundant values, and temporary artifacts. Ship only when the feature is functionally complete and consistently finished across the path. Finish with a source diff: remove accidental churn, orphaned code, redundant values, and temporary artifacts. Ship only when the feature is functionally complete and consistently finished across the path.
When this pass clears every Priority Issue it took from a snapshot, close that snapshot:
```bash
node .hermes/skills/impeccable/scripts/critique-storage.mjs close "<resolved target>" "<snapshot_file returned by latest>"
```
This closes only the snapshot this pass actually processed; if a newer critique landed meanwhile, its backlog stays live. Do not close when no snapshot was read, when `snapshot_file` was not retained, or when Priority Issues remain.
@@ -8,7 +8,7 @@ Reason over the signals; there is no score to obey:
- `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system).
- `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default. - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default.
- `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared). - `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale.
- `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them.
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `detect.mjs` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code. - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `detect.mjs` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code.
- Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`. - Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`.
@@ -16,9 +16,8 @@
* CLI entry points (called from skill instructions): * CLI entry points (called from skill instructions):
* node critique-storage.mjs slug <resolved-target> * node critique-storage.mjs slug <resolved-target>
* node critique-storage.mjs write <slug> <snapshot-body-file> * node critique-storage.mjs write <slug> <snapshot-body-file>
* node critique-storage.mjs latest <slug> [--json] * node critique-storage.mjs latest <slug>
* node critique-storage.mjs trend <slug> [limit] * node critique-storage.mjs trend <slug> [limit]
* node critique-storage.mjs close <resolved-target> <snapshot-file>
* *
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain * Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
* markdown file; the model reads it directly with its file-read tool. This * markdown file; the model reads it directly with its file-read tool. This
@@ -28,7 +27,6 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { createHash } from 'node:crypto';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
import { getCritiqueDir } from './lib/impeccable-paths.mjs'; import { getCritiqueDir } from './lib/impeccable-paths.mjs';
import { slugFromTarget } from './lib/target-slug.mjs'; import { slugFromTarget } from './lib/target-slug.mjs';
@@ -52,45 +50,6 @@ export function nowFilenameStamp(date = new Date()) {
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z'); return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
} }
/**
* Return an exact content fingerprint for a local file target. URLs and
* non-files return null because their content is not available here.
*
* The fingerprint deliberately describes bytes, not Git state or mtimes:
* critique often assesses an uncommitted file, and a later polish run should
* inherit that backlog when the bytes are unchanged regardless of staging.
*/
function resolveLocalTargetPath(target, { cwd = process.cwd() } = {}) {
if (!target || /^https?:\/\//i.test(target)) return null;
return path.isAbsolute(target) ? path.resolve(target) : path.resolve(cwd, target);
}
function resolveTargetIdentity(target, { cwd = process.cwd() } = {}) {
if (!target || typeof target !== 'string') return null;
if (/^https?:\/\//i.test(target)) {
try {
const url = new URL(target);
const pathname = url.pathname.replace(/\/+$/, '') || '/';
return `url:${url.origin}${pathname}`;
} catch {
return null;
}
}
const filePath = resolveLocalTargetPath(target, { cwd });
return filePath ? `file:${filePath}` : null;
}
export function fingerprintTarget(target, { cwd = process.cwd() } = {}) {
const filePath = resolveLocalTargetPath(target, { cwd });
if (!filePath) return null;
try {
if (!fs.statSync(filePath).isFile()) return null;
return `sha256:${createHash('sha256').update(fs.readFileSync(filePath)).digest('hex')}`;
} catch {
return null;
}
}
/** /**
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter * Write a snapshot for `slug`. `meta` carries the small structured frontmatter
* keys read back by readTrend(). `body` is the human-readable critique * keys read back by readTrend(). `body` is the human-readable critique
@@ -103,27 +62,14 @@ export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new
const dir = getCritiqueDir(cwd); const dir = getCritiqueDir(cwd);
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
const timestamp = nowFilenameStamp(now); const timestamp = nowFilenameStamp(now);
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
// Spread `meta` first so internally computed `timestamp` and `slug` // Spread `meta` first so internally computed `timestamp` and `slug`
// always win. Otherwise a caller-supplied meta blob (parsed from the // always win. Otherwise a caller-supplied meta blob (parsed from the
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the // IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
// filename in disagreement with its frontmatter and corrupting trends. // filename in disagreement with its frontmatter and corrupting trends.
const front = serializeFrontmatter({ ...meta, timestamp, slug }); const front = serializeFrontmatter({ ...meta, timestamp, slug });
const contents = `${front}\n${body.trim()}\n`; fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
return filePath;
// A second critique can finish in the same UTC second. Use exclusive
// creation and a fixed-width suffix so concurrent writers cannot replace
// history and lexical ordering still keeps collision entries newest.
for (let collision = 0; collision <= 9999; collision += 1) {
const suffix = collision === 0 ? '' : `~${String(collision).padStart(4, '0')}`;
const filePath = path.join(dir, `${timestamp}${suffix}__${slug}.md`);
try {
fs.writeFileSync(filePath, contents, { encoding: 'utf-8', flag: 'wx' });
return filePath;
} catch (error) {
if (error?.code !== 'EEXIST') throw error;
}
}
throw new Error(`Too many critique snapshots for ${slug} at ${timestamp}`);
} }
function serializeFrontmatter(obj) { function serializeFrontmatter(obj) {
@@ -152,8 +98,6 @@ function parseFrontmatter(text) {
try { value = JSON.parse(value); } catch { /* leave as-is */ } try { value = JSON.parse(value); } catch { /* leave as-is */ }
} else if (/^-?\d+$/.test(value)) { } else if (/^-?\d+$/.test(value)) {
value = Number(value); value = Number(value);
} else if (value === 'true' || value === 'false') {
value = value === 'true';
} }
out[key] = value; out[key] = value;
} }
@@ -163,7 +107,7 @@ function parseFrontmatter(text) {
/** /**
* Return snapshot files matching `suffix`, sorted oldest newest. * Return snapshot files matching `suffix`, sorted oldest newest.
*/ */
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z(?:~\d{4})?__.+\.md$/; const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
function listSnapshots(suffix, cwd) { function listSnapshots(suffix, cwd) {
const dir = getCritiqueDir(cwd); const dir = getCritiqueDir(cwd);
@@ -174,105 +118,24 @@ function listSnapshots(suffix, cwd) {
.map((f) => path.join(dir, f)); .map((f) => path.join(dir, f));
} }
function readSnapshot(filePath) { function readLatestSnapshotMatching(suffix, cwd) {
const filePath = listSnapshots(suffix, cwd).at(-1);
if (!filePath) return null; if (!filePath) return null;
const body = fs.readFileSync(filePath, 'utf-8'); const body = fs.readFileSync(filePath, 'utf-8');
return { path: filePath, body, meta: parseFrontmatter(body) }; return { path: filePath, body, meta: parseFrontmatter(body) };
} }
function snapshotTargetIdentity(snapshot) {
const targetPath = snapshot?.meta.target_path;
return snapshot?.meta.target_identity
|| (targetPath ? `file:${targetPath}` : null);
}
function readNewestSnapshot(slug, { cwd = process.cwd() } = {}) {
return readSnapshot(listSnapshots(`__${slug}.md`, cwd).at(-1));
}
function readNewestSnapshotForIdentity(
slug,
targetIdentity,
{ cwd = process.cwd() } = {},
) {
const matches = listSnapshots(`__${slug}.md`, cwd)
.map(readSnapshot)
.filter((snapshot) => snapshotTargetIdentity(snapshot) === targetIdentity);
return matches.at(-1) || null;
}
/** /**
* Return the most recent snapshot for `slug`, or null. Polish reads this * Return the most recent snapshot for `slug`, or null. Polish reads this
* to find its fix backlog when the slug matches. * to find its fix backlog when the slug matches.
*/ */
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
const latest = readNewestSnapshot(slug, { cwd }); return readLatestSnapshotMatching(`__${slug}.md`, cwd);
return latest?.meta.closed === true ? null : latest;
}
/**
* Mark one exact snapshot closed without deleting the score history consumed
* by `trend`. Exact identity matters: a newer critique may land after polish
* reads its backlog, and that newer snapshot must remain live. `snapshotFile`
* may be the absolute path returned by readLatestSnapshot() or the basename
* emitted by `latest --json`. Returns the path marked closed, or null.
*/
export function closeSnapshot(snapshotFile, { cwd = process.cwd() } = {}) {
if (!snapshotFile || typeof snapshotFile !== 'string') return null;
const dir = path.resolve(getCritiqueDir(cwd));
const snapshotPath = path.isAbsolute(snapshotFile)
? path.resolve(snapshotFile)
: path.resolve(dir, snapshotFile);
const filename = path.basename(snapshotPath);
if (
path.dirname(snapshotPath) !== dir
|| !SNAPSHOT_FILENAME.test(filename)
) return null;
let snapshot;
try {
if (!fs.lstatSync(snapshotPath).isFile()) return null;
snapshot = readSnapshot(snapshotPath);
} catch {
return null;
}
if (!snapshot || snapshot.meta.closed === true) return null;
const closedBody = snapshot.body.replace(
/^(---\r?\n[\s\S]*?)(\r?\n---)/,
'$1\nclosed: true$2',
);
if (closedBody === snapshot.body) {
throw new Error(`Cannot close snapshot without frontmatter: ${snapshot.path}`);
}
fs.writeFileSync(snapshot.path, closedBody, 'utf-8');
return snapshot.path;
} }
/** Return the most recent snapshot across all targets, or null. */ /** Return the most recent snapshot across all targets, or null. */
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
const snapshots = listSnapshots('.md', cwd).map(readSnapshot); return readLatestSnapshotMatching('.md', cwd);
const identifiedSlugs = new Set(
snapshots
.filter((snapshot) => snapshotTargetIdentity(snapshot))
.map((snapshot) => snapshot.meta.slug),
);
const latestByTarget = new Map();
for (const snapshot of snapshots) {
if (!snapshot?.meta.slug) continue;
// Slugs are lossy: distinct targets such as foo/bar and foo-bar can share
// one. Keep each known identity's latest open/closed state independent so
// closing one target cannot hide another target's live backlog. Once a
// slug has any identity-aware snapshot, its older legacy records are no
// longer independently routable and must not resurface as zombie work.
const targetIdentity = snapshotTargetIdentity(snapshot);
if (!targetIdentity && identifiedSlugs.has(snapshot.meta.slug)) continue;
const streamKey = targetIdentity || `slug:${snapshot.meta.slug}`;
latestByTarget.set(streamKey, snapshot);
}
return [...latestByTarget.values()]
.filter((snapshot) => snapshot.meta.closed !== true)
.sort((a, b) => a.path.localeCompare(b.path))
.at(-1) || null;
} }
/** /**
@@ -290,13 +153,9 @@ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
// Accept either a ready slug or a concrete target (path/URL) everywhere, so // Accept either a ready slug or a concrete target (path/URL) everywhere, so
// callers never have to run the slug step separately. Anything containing a // callers never have to run the slug step separately. Anything containing a
// path or URL marker is resolved through slugFromTarget. // path or URL marker is resolved through slugFromTarget.
function isReadySlug(value) {
return /^[a-z0-9-]+$/.test(value || '') && !value.includes('/');
}
function coerceSlug(value) { function coerceSlug(value) {
if (!value) return null; if (!value) return null;
if (isReadySlug(value)) return value; if (/^[a-z0-9-]+$/.test(value) && !value.includes('/')) return value;
return slugFromTarget(value); return slugFromTarget(value);
} }
@@ -322,124 +181,14 @@ function main(argv) {
if (metaArg) { if (metaArg) {
try { meta = JSON.parse(metaArg); } catch { /* ignore */ } try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
} }
// The helper, not caller-provided metadata, owns the target fingerprint.
// This makes the snapshot describe the exact file bytes critique saw.
delete meta.target_fingerprint;
delete meta.target_path;
delete meta.target_identity;
const targetIdentity = resolveTargetIdentity(slugArg);
if (targetIdentity) meta.target_identity = targetIdentity;
const targetFingerprint = fingerprintTarget(slugArg);
if (targetFingerprint) {
meta.target_fingerprint = targetFingerprint;
meta.target_path = resolveLocalTargetPath(slugArg);
}
const out = writeSnapshot({ slug, meta, body: raw }); const out = writeSnapshot({ slug, meta, body: raw });
process.stdout.write(`${out}\n`); process.stdout.write(`${out}\n`);
return; return;
} }
case 'latest': { case 'latest': {
const target = args[0]; const latest = readLatestSnapshot(coerceSlug(args[0]));
const format = args[1]; if (!latest) { process.exit(2); }
const slug = coerceSlug(target); process.stdout.write(latest.body);
if (!slug || (format && format !== '--json')) {
process.stderr.write('usage: latest <slug-or-target> [--json]\n');
process.exit(1);
}
const targetFingerprint = fingerprintTarget(target);
const targetPath = resolveLocalTargetPath(target);
const targetIdentity = resolveTargetIdentity(target);
const readySlug = isReadySlug(target);
const newestForSlug = readNewestSnapshot(slug);
if (!newestForSlug) { process.exit(2); }
// Concrete targets select the newest snapshot for their exact identity,
// not merely the newest filename for a lossy slug. This keeps distinct
// targets such as foo/bar and foo-bar from hiding each other's backlog.
const exactSnapshot = readNewestSnapshotForIdentity(slug, targetIdentity);
let latest = exactSnapshot;
if (!latest && !readySlug) {
// Legacy snapshots have no identity. Preserve their old explicit
// path/URL behavior only when no known target identity was selected.
latest = readNewestSnapshotForIdentity(slug, null);
}
if (!latest) latest = newestForSlug;
if (latest.meta.closed === true) { process.exit(2); }
const recordedTargetPath = latest.meta.target_path;
const recordedTargetIdentity = snapshotTargetIdentity(latest);
const matchingIdentity = recordedTargetIdentity === targetIdentity;
// Bare slugs remain a supported lookup mode, including for URL
// snapshots. But when a same-named local file exists, the request is
// ambiguous unless that exact file owns the snapshot identity.
if (readySlug && !recordedTargetIdentity) {
process.stderr.write(
'ambiguous legacy snapshot target; use an explicit ./path or full URL\n',
);
process.exit(2);
}
if (readySlug && targetPath && fs.existsSync(targetPath) && !matchingIdentity) {
process.stderr.write(
'ambiguous snapshot slug; use an explicit ./path or remove the local name collision\n',
);
process.exit(2);
}
const concreteTarget = !readySlug || matchingIdentity;
if (concreteTarget && recordedTargetIdentity && !matchingIdentity) {
process.exit(2);
}
const concreteLocalTarget = concreteTarget && targetPath;
if (concreteLocalTarget && latest.meta.target_fingerprint !== targetFingerprint) {
closeSnapshot(latest.path);
process.exit(2);
}
if (format === '--json') {
process.stdout.write(JSON.stringify({
snapshot_file: path.basename(latest.path),
body: latest.body,
}, null, 2) + '\n');
} else {
process.stdout.write(latest.body);
}
return;
}
case 'close': {
const [slugArg, snapshotFile, ...extra] = args;
const slug = coerceSlug(slugArg);
if (!slug || !snapshotFile || extra.length > 0) {
process.stderr.write('usage: close <resolved-target> <snapshot-file>\n');
process.exit(1);
}
if (
path.basename(snapshotFile) !== snapshotFile
|| !SNAPSHOT_FILENAME.test(snapshotFile)
|| !snapshotFile.endsWith(`__${slug}.md`)
) process.exit(2);
// A slug and filename are not enough to prove ownership because two
// distinct targets can normalize to the same slug. Modern snapshots
// carry a canonical identity, so require the supplied resolved target
// to match it before allowing the exact snapshot to be closed. Legacy
// snapshots without identity retain their historical close behavior.
const snapshotPath = path.join(getCritiqueDir(process.cwd()), snapshotFile);
let snapshot;
try {
if (!fs.lstatSync(snapshotPath).isFile()) process.exit(2);
snapshot = readSnapshot(snapshotPath);
} catch {
process.exit(2);
}
const recordedTargetIdentity = snapshotTargetIdentity(snapshot);
if (
recordedTargetIdentity
&& recordedTargetIdentity !== resolveTargetIdentity(slugArg)
) process.exit(2);
const closed = closeSnapshot(snapshotFile);
if (!closed) { process.exit(2); }
process.stdout.write(`${closed}\n`);
return; return;
} }
case 'trend': { case 'trend': {
@@ -448,7 +197,7 @@ function main(argv) {
return; return;
} }
default: default:
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend|close> [args]\n'); process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
process.exit(1); process.exit(1);
} }
} }
@@ -70,27 +70,13 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
} }
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([ const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS, 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui', '-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]); ]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and // WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1605,7 +1591,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) { function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false }; if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = primaryFontFace(fontFamily, GENERIC_FONTS); const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
if (!primary) return { primary: null, isSerif: false }; if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5204,7 +5190,8 @@ function checkTypography() {
const style = getComputedStyle(el); const style = getComputedStyle(el);
const ff = style.fontFamily; const ff = style.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++; totalTextElements++;
@@ -5449,7 +5436,8 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue; if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily; const ff = rule.style?.fontFamily;
if (!ff) continue; if (!ff) continue;
const primary = primaryFontFace(ff); const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (primary) { if (primary) {
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5468,10 +5456,11 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi; const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm; let fm;
while ((fm = ffRe.exec(html)) !== null) { while ((fm = ffRe.exec(html)) !== null) {
const primary = primaryFontFace(fm[1]); for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (primary) { if (f && !GENERIC_FONTS.has(f)) {
fonts.add(primary); fonts.add(f);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
} }
} }
@@ -1,7 +1,7 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { import {
checkSourceDesignSystem, checkSourceDesignSystem,
collectStaticDesignSystemFindings, collectStaticDesignSystemFindings,
@@ -51,7 +51,9 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue; if (!hasText) continue;
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
if (!primary) continue; if (!primary) continue;
fonts.add(primary); fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);

Some files were not shown because too many files have changed in this diff Show More