Cursor's finding was real: gating the density check on a named focal
moment let a busy comp pass whenever the direction named none, which is
the common case on the lane that produced the busy comps. The second leg
reuses the bullet's own distinction: several regions performing the
concept at once is the same shout; regions doing their jobs are not.
AI-assisted (Claude Fable 5).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two independent skeptic passes over the added prose, one hunting
oversteer and example bias, one hunting mode and platform damage. What
they killed, and why:
- The absolute 'never a medium' rule contradicted the file's own
imagery-stance fixity two paragraphs up and stripped legitimate guards
(an illustration-committed world, a native app screen warding off
stock-photo drift). A medium ban now belongs to the committed imagery
stance, never to caution, and the rule appears once per reader context
instead of five times corpus-wide.
- The quoted incident string and the four-example subject list taught
the model the exact framings they existed to prevent. Gone; the
abstract rule plus the point-at-the-subject check carry it.
- 'A first-time visitor learns what this is, why it matters, and what
to do' was Persuade anatomy imposed on all four modes. The guard is
now mode-neutral: a quieted region keeps its information and stops
performing.
- 'Calm is what Operate and Read surfaces are for' contradicted
operate.md's density affordance. Deleted; modes stay defined in one
place.
- The focal-moment count now presupposes nothing: it fires only where
the direction names a focal moment, and only on same-scale rivalry,
so an even, calm field stops reading as a failure.
- The decision-comp clause and the mode bullet no longer restate what
they can reference.
Net: the prose additions drop from roughly 480 words to under 200, with
no quoted strings and no example lists.
AI-assisted (Claude Fable 5), prepared for maintainer review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Proven necessary by its own demo: the first re-render of the declined
moto-forum comp satisfied subject-present and one-dominant-move by
deleting the value proposition, leaving a members' index that told a
first-time visitor nothing about what this is or why to care. Paul
caught it. Quieting a region means it stops performing, not that it
leaves; empty is quieter, not calmer.
AI-assisted (Claude Fable 5), prepared for maintainer review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The declined moto-forum comp's prompt read 'no gradients, no rounded SaaS
cards, no photography, no fake member counts, no badges, no testimonials':
the reflex that rightly bans invented claims swallowed the one medium the
subject lives in, and that is exactly how a motorcycle forum got comped
with no motorcycles. The lektor prompt's 'no AI imagery', written by an
image model, is the same fingerprint.
One counterweight, phrased once per authoring surface: the comp
discipline's subject-presence check (which the decision comps already
bind), the asset producer's own prompt rules (a standalone agent that
never reads visualize.md), and new-work's author-assets law (the path a
code-led build takes without the comp round). Truth binds claims, not
demonstrations; a photo of the subject doing its job is a demonstration.
AI-assisted (Claude Fable 5), prepared for maintainer review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The deliverable died in #545; the word survived as the decision-page
payload's field name, annotated everywhere it appeared with the same
compatibility apology. The page and the skill text ship together and
payloads are per-session, so the compatibility burden is one input alias,
not a frozen name.
serve-question.mjs: the card field, the answer key, the schema docs, the
--schema example, the help text, and every internal identifier (compSrc,
data-comp, .media.comp-pending, img.comp, comp-note) now say comp; a
payload declaring the legacy sketch key still renders and answers
identically. new-work.md and the asset producer drop their wire-name
parentheticals. The unit suite covers the canonical answer key coming
back from a legacy-key payload; the new-work e2e's declined-card stray
comp stays declared as sketch, which doubles as alias coverage.
AI-assisted (Claude Fable 5), prepared for maintainer review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Factory review evidence (two batches, both lanes): generated comps drift
poster-ward. They render the world's atmosphere at high density, drop the
surface's subject (a motorcycle forum comped with no motorcycles), and stop
reading as screens a product would ship. The existing anti-vignette
self-check catches the fully collapsed case but says nothing about density
or subject presence, and new-work's "committed all the way" reads as a
coverage instruction.
Three sibling self-checks in visualize.md's comp discipline, each phrased
per mode (Persuade/Operate/Read/Experience) and platform-neutral: the
subject appears as the content the regions hold; the mode must be readable
from the image alone; commitment is depth, not coverage, with one dominant
move per viewport. new-work.md's decision-comp rule gains a clause binding
the same checks so the direction round inherits them explicitly.
AI-assisted (Claude Fable 5), prepared for maintainer review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Stop assuming white when a background cannot be read
Dark themes came back from a scan buried in low-contrast findings that
all claimed the light text sat on #ffffff. Two live runs against
impeccable.style produced 102 and 95 of them.
Two causes, both fixed here.
Parsing. Browsers keep the authored color space in getComputedStyle
output: oklch() stayed oklch, but color-mix results come back as
color(srgb 1.04 0.72 -0.21), wide-gamut authors get color(display-p3
...), and lch()/lab() survive verbatim. The parser read none of those, so
those surfaces registered as unset. parseGradientColors was worse: it
matched only rgba() and #hex, so a ground painted as
linear-gradient(oklch(...), oklch(...)) counted as a gradient with no
stops at all.
Guessing. When the ancestor walk ran out of readable color it returned
white, and on a body-level gradient it returned white without even
looking. Light copy on a lacquer-black page then measured 1.3:1 against a
canvas the visitor never sees.
resolveBackgroundInfo now separates three outcomes: a resolved surface, a
gradient the caller should fall back to stops for, and an unreadable
layer. The last one makes both color adapters skip their contrast checks
entirely. White survives in exactly one case, the one that earns it:
every layer up to the document root was genuinely transparent.
Color conversions moved to cli/engine/shared/color.mjs and gained lab,
lch, and color() for srgb, srgb-linear, and display-p3. Spaces outside
that set return null, which now routes to abstention rather than to a
color nobody painted. Every conversion is pinned against what Chrome
itself paints for the same string.
Rescanning impeccable.style: 102 low-contrast findings down to 30, none
of them on an invented white ground.
Assisted-by: Claude Code
* fix: address PR review bot findings on background resolution
- Treat a url() image layer stacked above a gradient as an occluding,
unreadable surface: resolveBackgroundInfo now returns unresolved so the
gradient-stop fallback never measures stops the image hides
(greptile-apps finding, reproduced in Chrome).
- Route the glow and AI-palette DOM adapters through resolveBackgroundInfo
so an unresolved surface makes them abstain instead of hunting gradient
ancestors past an unreadable layer (Cursor Bugbot finding).
- Resolve background-color keywords jsdom hands through verbatim:
inherit now reads as no-paint (the ancestor walk IS its resolution) and
currentcolor substitutes the element's own computed text color instead
of forcing an abstention (Copilot finding).
- Regression coverage in the dark-theme fixture for all three, asserted in
both the jsdom and real-Chrome suites; browser detector regenerated.
AI-assisted: prepared with Claude Code at the maintainer's direction.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: keep zero-offset glow findings when the surface is unreadable
The browser glow adapter abstained from the whole element when
resolveBackgroundInfo reported an unreadable surface, which also dropped
zero-offset chromatic halo findings that do not depend on the background
at all. It now skips only the gradient hunt past the unreadable layer and
scores the halo tell against a null surface, matching what the static
loop already did. Fixture cases pin both sides: the halo over a url()
image ancestor flags in both engines, and an offset chromatic shadow on
the same unknown surface stays abstained.
Also hardens the currentcolor background substitution with the
parseColorResolved fallback used by the text-color path, and adds fixture
coverage proving tokenized currentcolor surfaces already resolve through
the static cascade (flag when knowable, abstain when the token is
undefined).
Addresses Cursor Bugbot review findings on PR #541.
AI-assisted-by: Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: abstain on translucent gradients over images, drop phantom color-mix stops
Two follow-up review findings on the merge with main.
A gradient leading a url() layer was treated as a resolvable surface even
when its stops are translucent, so the glow and AI-palette hunts averaged
wash stops (a 20% black wash reads as pure black) while the real surface
blends with image pixels the engine cannot read. resolveBackgroundInfo now
marks gradient-over-image unresolved unless every readable stop of the
leading gradient is opaque, in which case the gradient provably covers the
image and remains the scorable surface.
parseGradientColorsModern predated this branch's parseGradientColors
rewrite: its second regex pass re-extracted color tokens nested inside
color-mix() stops that the shared parser already captures whole via
balanced-paren tokens, appending ingredient colors that are never painted.
The worst-case stop ratio then invented low-contrast findings against a
color nobody sees. The helper is removed; all callers use the shared
parser, which covers the modern syntaxes it existed for.
Fixture coverage pins both: the translucent-wash-over-image glow abstains
in both engines, an opaque gradient over an image still flags in the
browser, and the color-mix wash case stays clean in the static engine.
Each new assertion was verified to fail against the previous engine.
Addresses Greptile and Cursor Bugbot review findings on PR #541.
AI-assisted-by: Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add data-impeccable-ignore scoped waivers; fix occlusion and image-backed contrast FPs
Three changes that let a page hosting deliberate anti-pattern exhibits
scan clean without losing coverage, prepared with AI assistance (Claude
Code) on maintainer instruction:
- data-impeccable-ignore="rule-a rule-b" (or "*" / bare) on any element
suppresses matching findings for its whole subtree, in the browser
overlay, the extension, and the static engine. The DOM twin of the
line-based impeccable-disable comments (which a live DOM cannot
apply) and the generalization of data-impeccable-allow-kickers.
Applied at the addBrowserFindings choke point, at the static element
walk, and for regex findings that carry a live selector.
- text-occlusion: an occluder whose effective opacity multiplies out to
~0 paints nothing. An opacity-0 range scrubber stretched over a
before/after comparison produced 16 "100% covered by an opaque
element" findings on one page because elementFromPoint returns it and
its UA background-color read as opaque paint. Invisible-at-rest
elements are also no longer probed as victims.
- Analytic contrast now skips what it cannot measure: a url() image
layer anywhere in the background stack ends the gradient-stops walk
(dark ink on a bright gold-leaf image measured 2.6:1 against the wash
composited over the wrong base), and elements that are invisible at
rest (visibility hidden, effective opacity ~0 — hidden scene decks)
are skipped by the color checks in both engines. The static cascade
now tracks opacity to support this.
Covered by a new scoped-ignore fixture (exact rule, star, comma list,
nested depth, wrong-rule control) tested in both engines, a scrubber
pass case in the occlusion fixture, and image-backed / photo-panel /
hidden-scene pass cases in the gradient-ground fixture. Full suite
passes; browser and extension bundles regenerated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* CSS-scan findings carry their enclosing selector; browser pass resolves them
Page-level CSS-text findings (marquee, dark-glow, radial-halo,
repeating-stripes, codex-grid, ai-color-palette, image-hover-transform,
pseudo/inset side-tab stripes) now attach the selector of the rule that
matched, via a best-effort enclosingCssSelector() helper or the
selector already in scope. The browser pass resolves that selector
against the live DOM: pseudo segments are stripped, a selector that
renders nowhere on the page drops the finding (the CSS ships there but
the pattern never paints — the live DOM is ground truth in a browser
scan), and matches under a data-impeccable-ignore ancestor are waived.
Static scans are unchanged: partial documents keep the text-level
findings. Applied with AI assistance (Claude Code).
Covered in the scoped-ignore fixture: a live marquee under a marquee
waiver is suppressed, and dead two-axis grid CSS matching no element is
dropped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Attribute selectors on gradient-text and bounce-easing page emitters too
Same mechanism as the previous commit, extended to the three page-level
motion/text emitters that were still selector-less. Applied with AI
assistance (Claude Code).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* text-overflow: skip SVG content; scrollWidth lies there
Chrome reports arbitrary non-zero scrollWidth/clientWidth on SVG
elements (a <text> gave 78/48 while its rendered length sat inside its
box), so the box-metric delta is noise. SVG clips to its own viewport
anyway. Pass case added to the quality fixture. Applied with AI
assistance (Claude Code).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Overlay samples image-backed text at the pixel level by default
Visual contrast gains a third mode. Explicit true still runs the full
sampled pass, explicit false still disables everything (the mode the
test suites use), and unset — the default overlay run — now samples
ONLY image-backed text: the one class the analytic walk deliberately
skips, because a url() layer's pixels are unknowable without looking.
The cost is bounded and the method is precise: at most a 3x3 grid of
sample points per candidate (degrading to 3 or 1 for small rects), the
source image drawn once to a canvas with only those pixels read, and
glyph ink never pollutes the samples because the image is drawn alone.
A cross-origin image without CORS headers reports unresolved rather
than guessing. Applied with AI assistance (Claude Code).
Covered by a new fixture: white text on a near-white same-origin
data-URI image background flags via sampled pixels under default
options; dark ink on the same image passes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Review fixes: root opacity, keyframe steps, static parity, attributed fixtures
Applied with AI assistance (Claude Code), addressing all seven findings
from the automated reviews:
- effectiveOpacityDOM walks through body and html: a page-fade wrapper
with body/html opacity 0 hides every descendant (Greptile executed a
Chromium repro of the false positive).
- enclosingCssSelector refuses `from`/`to` keyframe steps, which read
as never-matching type selectors and got valid findings wrongly
dropped by the zero-match rule (Bugbot, high). Regression case: an
overshoot bezier inside a `to` step must survive as page-level.
- The static cascade now inherits visibility, so descendants of a
hidden container compute as hidden like the browser path; a declared
visibility:visible still overrides.
- The static engine applies scoped waivers to selector-backed
html-pattern findings, mirroring the browser — but keeps findings
whose selector matches nothing, since static scans see partial
documents.
- The scoped-ignore fixture grows to the mandated matrix: 4 flag cases
(control, other-rule waiver, sibling waiver, misspelled rule id) and
5 waived shapes (exact rule, nested depth, star, comma list, self),
each with a unique border width so every finding attributes to
exactly one case in both engines' tests.
- The image-backed contrast test pins its cases via the sampled
finding's candidate text: the white-on-light specimen must flag and
the dark-ink control must stay clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Review fixes: image-only starvation, selector rejection class, inset stripes
Second review round, applied with AI assistance (Claude Code):
- The image-only filter moves inside the candidate collector, before the
cap: gradient/opacity/filter candidates earlier in DOM order no longer
consume the 12-candidate budget and starve the url()-backed texts the
mode exists to sample (Bugbot, high). The regression fixture packs 14
gradient decoys ahead of the photo panels, and the test now drives the
overlay entry (impeccableDetectAsync, default options) rather than
detectUrl's Node-side full fallback, which is where the image-only
mode actually lives.
- enclosingCssSelector no longer rejects the child combinator or quoted
attribute selectors; only braces and angle brackets disqualify.
- The inset box-shadow side-tab scanner attaches its selector like the
pseudo-element scanner does, so those findings waive and dead-drop
the same way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Fix: measure gradient body grounds instead of assuming white (browser mode)
A page whose ground is set via background: linear-gradient(...) on body
leaves backgroundColor transparent, and resolveBackground assumed white
for any body/html-level gradient. In a real browser that assumption is
wrong: the shorthand is always decomposed there, so reaching that branch
means the ground truly is the gradient. On a dark oklch gradient ground
(impeccable.style's lacquer) this turned every light-on-dark text into a
~1.3:1 "on #ffffff" low-contrast finding, ~120 false positives on one
site. Browser mode now returns null so the caller measures against the
actual gradient stops; the white assumption stays for jsdom, where the
undecomposed-shorthand rationale still holds.
Gradient stops also now parse modern color syntax: computed
backgroundImage keeps oklch()/oklab()/hsl()/hwb() stops as authored, and
parseGradientColors only read rgb()/hex, so a token-driven gradient
ground was invisible even once the walk deferred to it. New
parseGradientColorsModern routes those stops through parseAnyColor.
Covered by a Puppeteer fixture (dark oklch body gradient): light text on
the ground must not flag, muted dark-gray ink must, proving the stops
are measured rather than the checks silently skipping.
Prepared with AI assistance (Claude Code), on maintainer instruction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Composite translucent layers over gradient stops; parse modern glow stops
Review fixes from PR #557's automated reviews, applied with AI
assistance (Claude Code):
- Cursor Bugbot found the new browser-mode early return discarded the
translucent ancestors resolveBackground had collected: text on a
frosted wash over a body gradient was measured against raw stops.
resolveGradientStops now collects translucent layers during its own
walk (through readCascadeBackgroundColor, extracted so both walks
read surfaces identically) and composites every stop under them.
- Copilot flagged the other legacy parseGradientColors call sites. The
glow-context fallback now uses parseGradientColorsModern, since body
gradients reach it more often after this change. The AI-palette rule
and the injected analytic sampler stay on the legacy parser
deliberately: the former is a rule-behavior expansion deserving its
own fixtures, the latter degrades to pixel sampling or a skip.
- Greptile asked for standard fixture structure: the fixture now has
labeled flag/pass cases (3 flag, 5 pass) including the frosted-wash
pair that locks the overlay compositing in both directions and a
legacy hex-stop gradient guarding the original parser path.
The test scopes itself to the DOM path via visualContrast: false, the
suite's established pattern; the screenshot sampler is a separate
subsystem with its own coverage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Pin gradient-ground flag cases to their snippet signatures
Bugbot follow-up: a count-only assertion let an offsetting miss and
false positive cancel, especially the frosted pair. Each flag case now
asserts its full text-on-background signature, so the frosted case must
measure against the composited wash and the count guard excludes any
pass case flagging in its place. Applied with AI assistance (Claude
Code).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Comments: the static path is the custom engine now, not jsdom
jsdom left the dependency tree when the static-html engine (StaticElement
+ css-cascade.mjs) replaced it, and that engine does decompose the
background shorthand, so the comments this PR added were dated in both
name and rationale. Only comments touched by this PR are renamed; the
~40 legacy jsdom mentions elsewhere in checks.mjs are a separate sweep.
Applied with AI assistance (Claude Code).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Static engine: measure body gradients too, dropping the white assumption
Follow-up to the browser-mode fix: the white assumption for body/html
gradients was a jsdom guard, and jsdom is gone. The static cascade
decomposes the background shorthand (expandStaticDeclaration) and
preserves var() colors for later resolution, so a missing solid under a
body gradient is now as real in static mode as in a browser — and the
static engine had the identical false-positive class (light text on a
dark gradient ground flagged "on #ffffff") while missing the muted-ink
true positives on the same page.
The old catastrophic case cannot recur: opaque stops fully cover any
hidden solid (they are the ground), alpha stops composite over the
resolved base or the white canvas default, and unresolvable stops drop
rather than guess.
Static twin of the browser test added over the same fixture; the full
suite, the url()-ancestor guard, and a source scan of impeccable.style
(0 low-contrast findings) all stay clean. Applied with AI assistance
(Claude Code).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Bound copy-edit prompt context
Whitelist and truncate staged operation context before it reaches the local agent prompt.
AI assistance: Implemented and validated with OpenAI Codex under maintainer authorization.
* Harden copy-edit prompt bounds
Bound repair, candidate, and element context consistently and preserve absent source positions as null.\n\nAI assistance: Implemented and validated with OpenAI Codex under maintainer authorization.
* Preserve bounded repair context
Keep repair attempt metadata and nested diagnostics while retaining prompt limits.\n\nAI assistance: Implemented and validated with OpenAI Codex under maintainer authorization.
Pass staged copy-edit prompts over stdin so large batches do not exceed platform argv limits.
AI assistance: Implemented and validated with OpenAI Codex under maintainer authorization.
* Map polish's evidence and verify steps per platform
polish.md was the last command reference verifying through web-only
vocabulary after #546 gave the pipeline its native leg. Three targeted
mappings, following the in-file precedent new-work.md set (the
classify-triage-polish-verify flow itself is platform-neutral, so no
polish.native.md):
- Evidence gathering: desktop and mobile sizes on the web; the shipped
device classes on simulator, emulator, or hardware on native, per the
platform reference's Verifying the build section.
- Verify checklist layouts: phone and tablet size classes, both
orientations where supported, on native.
- Verify checklist "supported browsers": native has none, so the
analogues are named (runtime warnings, dropped frames, supported OS
versions).
Assisted-by: Claude Code
* fix: branch the verify checklist web-vs-native explicitly
Copilot follow-up: the parenthetical style could read as both term
sets applying on native. The two bullets now branch explicitly, and
the shared items (console errors, layout shift, latency, image
loading) stay unbranched since they apply everywhere.
Assisted-by: Claude Code
* fix: restore runtime warnings to the native verify branch
greptile follow-up: the explicit-branch restyle dropped the runtime
warnings requirement the parenthetical carried; folding it into
"console errors everywhere" hid it behind web vocabulary. It is back
as its own item in the native branch.
Assisted-by: Claude Code
* Give the verify-and-review pipeline a native leg
The build-verify-review loop assumed a browser end to end while the
comp side of the system was already platform-aware: new-work.md,
visualize.md, and the asset producer all comp a native app portrait at
its device viewport, and then the verification steps asked for desktop
and mobile browser screenshots of it. Concretely:
- new-work.md step 7 ordered detect.mjs on every hookless build with no
platform guard. routing.md declares the detector web-only and the
design hook skips native projects, so a native build was always
hookless and always ordered to run an HTML rule engine over
Swift/Kotlin/RN code. The playbook now guards it: web-only, and on
native the reviewer's floor check is the named slop gate.
- The inspection round and the SKILL.src.md batched-round principle
named desktop and mobile as the only viewports. Both now map per
platform: web keeps desktop and mobile; native inspects the shipped
device classes per OS, captured from the simulator or emulator.
- ios.md and android.md carried no verification guidance at all, so
nothing told a native run how to produce the screenshots the evidence
chain depends on. Each gains a Verifying the build section: simctl /
adb capture commands, dark-appearance and type-scale checks, and the
simulator-vs-hardware honesty line.
- The finish reviewer judged native builds blind: it never runs
context.mjs and its packet carried no platform guidance. On native
the packet now includes the platform reference path(s) and a
no-detector-ran line, and the reviewer's Input Contract says to judge
in the platform's conventions.
Assisted-by: Claude Code
* fix: address PR review bot findings
- greptile: carry the capture's device selector through the
state-changing verification commands (simctl appearance, adb uimode
and font_scale); unqualified forms fail with several targets attached
- Copilot: align new-work.md's cross-reference with the actual heading
(Verifying the build)
- Copilot: give the finish reviewer's Input Contract the native
filename example new-work.md establishes (phone.png / tablet.png,
suffixed per OS on adaptive)
Assisted-by: Claude Code
* fix: identify simulators by UDID, not display name
greptile follow-up: display names can collide across booted simulators,
so the capture and appearance commands now both key on the UDID from
simctl list devices booted.
Assisted-by: Claude Code
* Resolve {{scripts_path}} in the agent bodies Codex ships
Three code paths emit an agent body: the degraded fallback reference, the
.toml nested inside the skill for Codex, and the native agent file. Only the
nested .toml skipped placeholder substitution and rule-marker stripping, so
the codex and .agents dists shipped `node {{scripts_path}}/embed-prompt.mjs`
verbatim in the asset producer, and every caller had to substitute the token
itself at load time.
All three now render through renderAgentBody(), and the new regression test
asserts a runnable embed-prompt command on each emitted surface plus a
synthetic agent proving markers and placeholders resolve in the nested .toml.
Prepared by an AI agent (Claude Code) under pbakaus's instruction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Give the finish reviewer's screenshots one fixed address
The Input Contract asked for "desktop and mobile screenshot paths captured by
the parent" and named none, so each session invented a filename and the
verdict pass went looking for a recapture that was never written there. Two
reviewer passes burned on that in the eval runs.
The parent now captures and recaptures to .impeccable/review/desktop.png and
.impeccable/review/mobile.png, and the reviewer reads those two first,
treating a brief-named path as the fallback for a parent that wrote elsewhere.
Prepared by an AI agent (Claude Code) under pbakaus's instruction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Lead Setup with the base directory the runtime reports
The rendered claude and codex skills opened with
`node .claude/skills/impeccable/scripts/context.mjs`, a project-relative path
that resolves in this repo and in nothing a user installs: a personal or
plugin install puts the scripts outside the project entirely. The working form
was already in the text, parenthesized, after the one that fails.
Setup now leads with `node <skill-base-dir>/scripts/context.mjs` and says once
that the base directory resolves every scripts-path command in the skill and
its references, leaving the project-relative path as the fallback for runtimes
that report no base directory.
Prepared by an AI agent (Claude Code) under pbakaus's instruction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Answer the Copilot review: brittle model assertion, missing review dir
Assert that {{model}} resolved rather than that it resolved to "GPT", which
belongs to PROVIDER_PLACEHOLDERS and can change without touching what the test
guards. And have the parent create .impeccable/review/ when the harness does
not, so a fresh project's first capture has somewhere to land.
Prepared by an AI agent (Claude Code) under pbakaus's instruction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Make the review-screenshot contract directory-based, not web-viewport-named
Two amendments to the recapture contract from review feedback:
1. The canonical location is the directory .impeccable/review/, one file
per captured viewport; desktop.png and mobile.png are the web case,
not the contract. Baking web-viewport names into the reviewer's spec
would have hardened a web assumption into paths that a native
(ios/android/adaptive) build cannot honestly write.
2. Precedence restored to explicit-beats-convention: paths the calling
brief names are authoritative when the files exist; the canonical
directory is where the reviewer looks when the brief names none or a
named path is missing. This avoids stale canonical files from an
earlier run silently winning over fresh explicit paths. The observed
failure (the verdict round inventing a round-stamped filename) stays
fixed: recapture happens over the same files, and invented filenames
are still called out as pointing at nothing.
Assisted-by: Claude Code
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Restore reduced-motion build guidance
Restores the accessibility requirement and verification step to the animation playbook, with a regression test that keeps it on the build path.
Implemented and validated with OpenAI Codex assistance under standing maintainer authorization.
* Harden reduced-motion guidance regression
Normalizes CRLF input and accepts either reduced-motion spelling so the contract stays portable and intent-focused.
Implemented and validated with OpenAI Codex assistance under standing maintainer authorization.
* Anchor skill reference test to its module
Resolve the repository fixture path from the test module so the regression test is independent of the caller's working directory.
This change was prepared with AI assistance under maintainer authorization.
* Clarify reduced-motion guidance
Replace the double negative in the canonical animation guidance and keep the source contract aligned with the clearer wording.
This change was prepared with AI assistance under maintainer authorization.
* Polish the decision page: raise cycler, declined height, canon order, full card anatomy
Field feedback from the first real rolls of the verdict-routed hand:
- Several raises stacked on the assigned card blew it out of proportion.
More than one raise now renders as a compact cycler: one visible, a
counter, click or Enter advances. A single raise stays inline.
- Declined cards inherited the row's stretch alignment, so a narrow card
stood at the tallest contender's height, a strange stilt beside the
hand. They now size to their content.
- Deck order becomes a gradient of standing: contenders, then the canon,
then declined dead last. The canon between full alternates and the
demoted row reads as the familiar door rather than the last resort
after the rejects.
- Root cause of bare-bones challenger and canon cards in the field: the
--schema example only gave the assigned card palette, materials, and
risk, and models author payloads by imitating the example, so the
"same anatomy on every card" instruction lost to it every time. The
example now carries full anatomy on every card and the schema note says
a card with no palette chips is an authoring gap, not a data gap.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Decision cards carry full-fidelity comps instead of sketches
Field verdict on the sketch contract: the sketches came back too simple
to inform the choice, and generation takes the same time at any
fidelity, so the deliberately-unfinished frame paid comp cost for sketch
quality. The decision card's image is now that direction's north-star
comp, produced under visualize.md's comp discipline (structure-led
prompt, real name and content, no invented commercial claims), saved
under .impeccable/mocks/ with its prompt sidecar. Fairness between cards
comes from equal fidelity in each card's own grammar rather than shared
unfinishedness.
The chosen card's comp is never spent by the choice: on a comp-led build
it enters the comp round as compositional option one (visualize.md now
generates two variations beside it; a round arriving with no decision
comp still renders all three), and on a code-led build it returns at the
finish review as the critique reference. Produce order still front-loads
a re-roll's spend onto the cards read first.
serve-question keeps the sketch field's wire name for payload
compatibility; docs, schema paths, shimmer labels, and the answer
directive (CHOSEN COMP) speak comp.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: address PR review bot findings on the comp round
- Producer still forced sketches (cursor, high): the asset producer's
Decision Sketches contract still mandated deliberately unfinished matte
sketches, so the parallel path would keep shipping sketch-era images.
The section is now Decision Comps: full-fidelity north-star comp,
structure-led prompt, equal commitment across siblings, no invented
claims, sidecar written.
- Mocks collided with the approval check (cursor, high): decision comps
now live under .impeccable/mocks/decision/, visualize.md scopes the
no-approval finding to comp-round output, new-work.md states the
unchosen hand implies no approval, and the code-led finish packet names
the chosen decision comp as the critique reference in the approved-comp
slot.
- Raise cycler announces (greptile, both P1s): a visually hidden
aria-live region reads out the newly active raise and its position on
advance; initial render stays quiet.
- Declined width in the vertical deck (cursor, medium): align-self:
flex-start shrank declined cards to content width in the portrait
column layout, where the cross axis is horizontal; they stretch there
and keep content height in the row layout.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: raise cycler tooltip and label name both input modes
Copilot: the tooltip said Click while the control also answers Enter and
Space; the title and a new aria-label now say activate/press Enter.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: finish reviewer exempts decision comps from the approval check
cursor[bot] follow-through: the reviewer's Persistence check still
treated any comps under .impeccable/mocks/ as approval-gated, and the
reviewer never reads visualize.md by design, so code-led and spent-hand
rounds could draw a false skipped-approval finding. The check now scopes
to comp-round comps, exempts .impeccable/mocks/decision/ as the direction
round's dealt hand, and defines how a code-led build's decision comp is
judged in the approved-comp slot: the critique reference, under the
no-approved-comp fidelity rules plus what the image dared that the build
did not.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: the critique reference is its own reviewer input, not the approved-comp slot
cursor[bot]: passing the code-led decision comp through the approved-comp
slot dragged in that slot's obligations (inventory-first reading, the
fidelity matrix, Truth's shipped-asset demand for every image-native
region), which contradicts code-led's premise. The input contract now
names it a separate labeled critique-reference input that nothing binding
"the approved comp" touches, and Fidelity defines its treatment where the
no-approved-comp rules live: provocation, not spec; no matrix, citations,
or asset obligations; its dares enter material_fixes as ordinary fixes.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
* Route the direction hand by verdict, add the pick card, enforce salience parity
The decision round previously rendered every dealt challenger as an equal
full card whatever the weighing said, so a world that fused poorly (an
underwater world dealt to a flower shop) sat at the same visual weight as
the assigned direction, and concept-level fusion had no surviving output.
Three changes, all presentation-layer; the dice, the assignment, and the
two-axis weighing are untouched:
- Verdict routing: the weighing closes with wins / competitive / declined
per challenger, decided before any borrowing. Declined challengers render
demoted (narrow, quiet, catalog art as a labeled thumb, "Adopt anyway"),
reordered to the end of the deck by the page itself, still adoptable,
never silently dropped. Donations return as named "raised by" lines on
the assigned card: a declined challenger donates ambition and system
discipline, never its clothes.
- The pick card: one card for the model's top-ranked grounded candidate
when the dice assigned another, kicker MY PICK, honest familiarity risk
on its face. One card, never a ranked list, never the lead position; the
anti-menu rule survives with exactly this carve-out.
- Salience parity: a card's imagery weight is capped by the assigned
card's. With a text-only assigned card (no image generation in the
harness), full-bleed catalog heroes demote to labeled thumbs, so what
looks important is the verdict's call, never rendering luck.
serve-question payload gains additive fields (verdict, kept, raised); old
payloads render unchanged. concept-seed's rendered instructions carry the
verdict/donation contract and the pick-card carve-out. Covered by two
Playwright tests in the new-work e2e suite (verdict routing + parity).
Design exploration and rationale were worked through with the maintainer;
research grounding is impeccable.style/research lessons 3-5.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Add Safer/Bolder re-roll registers to the direction round
The re-roll gains the user's steering wheel on the familiar-to-bold axis.
The decision page renders two register buttons beside the plain re-roll
(payload: reroll: { registers: ["safer", "bolder"] }; booleans still work),
the answer carries the chosen register, and concept-seed gains --register.
The design constraint that shaped the implementation: a register changes
only what a round INSTRUCTS, never what it DEALT. The same key and reroll
count reproduce the same deal whatever the register, so the exclusion chain
never forks and the reproduction contract holds with no API change.
- bolder: the dealt foreign forms become the whole hand, every challenger a
full card; the first-dealt challenger leads (assignment by deal order, so
the dice still choose). The pick card sits out; the canon stays.
- safer: the round's dealt hand is spent unseen and stays excluded; the
model presents its remaining conventional grounded candidates (at most
three) plus the canon executed against named competitors. This is the one
sanctioned lineup of the model's own ranked list, existing only by
explicit user request. Works degraded (needs no catalog); bolder degrades
to a plain grounded round, disclosed.
Registers are user steering, never the model's to pre-select. Covered by a
concept-seed unit test (same-deal invariant, validation) and a Playwright
test (button, answer field, REGISTER directive).
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Add the execution-contract round: comp-led or code-led, chosen after the direction
The build previously went comp-led for everyone, silently: a generated comp
led and the build chased it, which produces the boldest compositions and
also the measured worst-of-both-worlds failure (ambitious design landed
poorly, no motion, fix rounds after). Models already defect from it by
quietly skipping comp generation, which is unsanctioned code-led with no
contract to catch it. This makes the fork explicit and both paths
defection-proof:
- Comp-led: the comp is law and non-optional once chosen; visualize.md and
the comp-is-king build phases run as today.
- Code-led: no comp of this page, skipped by contract rather than drift.
The QUALITY BAR boards still calibrate finish, and the ambition moves
into the written direction contract (FIRST VIEWPORT plus a named
signature interaction and motion grammar), audited by the finish
reviewer in behavior. Not a discount on commitment.
Placement: a second round on the same open table, right after the
direction lands. Sketches stay in the direction round (they pick the
world); comps are what code-led skips (they bind the composition). The
chosen world sets the default lead; the user flips freely; a standing
preference recorded in PRODUCT.md skips the round on later surfaces; with
no image generation there is no fork, code-led is the only path.
Mechanism: serve-question gains payload-level followup: true, which keeps
the detached server alive after a pick (exactly like re-roll), swaps the
page to the loading hand instead of goodbye, marks the answer with
followup: true so --wait keeps the table, and prints a FOLLOWUP OPEN
directive telling the agent to deliver the next round via --update.
Covered by a Playwright test driving the full two-round flow.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: address PR review bot findings
- Degraded safer register no longer contradicts itself (greptile,
Copilot, cursor): the degraded template previously said "the assigned
index is suspended; the user picks" and then emitted ASSIGNED INDEX,
the mandatory build instruction, and the restated footer anyway. The
degraded safer path now suppresses the assignment machinery entirely,
matching the non-degraded safer round, and restates the user-picks
behavior for truncated readers instead.
- A declined card's declared sketch no longer renders a full media face
(Copilot): the renderer ignores sketch slots on declined cards
outright, so a stray sketch cannot buy back the salience the verdict
took away.
- Bolder rounds no longer carry the generic weighing instruction
(cursor): it measures against the assigned grounded direction, which
the bolder register suspends; a leader-relative variant weighs the
fused challengers against the first-dealt leader instead.
All three pinned by new assertions in tests/concept-seed.test.mjs and
tests/new-work-e2e.test.mjs.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: followup never arms the loading hand in blocking serve mode
cursor[bot] caught a client/server disagreement: the page interpolated its
FOLLOWUP constant from the payload alone, so a followup: true payload served
in blocking mode (no --start) would leave the browser on a loading hand that
nothing resolves, since a blocking server exits on any pick and has no
update channel. The page constant is now armed only when the server is
detached, blocking rounds get the goodbye screen as before, and new-work.md
states that followup belongs only on a detached round; blocking and
structured-tool channels run the build-path round as its own second
question. Pinned in tests/serve-question.test.mjs.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Add card-kind choice telemetry and the bolder routing disambiguation
The choice ping previously fired only when a dealt catalog challenger won,
so pick-share and canon-share had no denominator and the decision page's
new spectrum could not be measured. The ping now fires once per resolved
attended round on API-dealt rolls: --kind names which card class won
(assigned / pick / challenger / canon), --chosen carries the catalog id
only when a dealt challenger won, and --register rides along when the
round came from a steered hand. Grounded candidates' names never leave the
machine (the ping carries the kind alone), the legacy id-only shape stays
valid, and DO_NOT_TRACK / IMPECCABLE_NO_TELEMETRY still disable the ping
entirely. The seed's TELEMETRY block teaches the new invocation.
Also the naming-collision guard: "bolder" said while a direction round is
open routes to the Bolder hand register, never the bolder refinement
command; one line each in bolder.md and new-work.md.
The /api/chosen field additions land in a sister impeccable-site PR; the
API ignores unknown fields meanwhile, so this is safe to ship first.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: ping test survives a DO_NOT_TRACK shell
cursor[bot]: the pingChosen unit test cleared only IMPECCABLE_NO_TELEMETRY,
so a developer shell with DO_NOT_TRACK set failed the success-path
assertions. The test now clears both, restores prior values in finally,
and passes under DO_NOT_TRACK=1.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
The list of Live chrome surfaces was inlined into live-browser.js as a
function-scope const when live/ui-core.mjs was deleted for having zero
in-repo references. It had one out-of-repo reference. The private
impeccable-site repo imports it at build time: its Live UI lab must hold
a snapshot for every surface Live defines, and the site build fails with
the surface name when one is missing. Inlining put the list out of reach
of every Node importer, so the site had to regex it back out of the
browser script, and the guard only kept passing because the site's
materialized copy of skill/ was stale.
A guard that reads a list the site itself maintains guards nothing, so
the fix is a real export rather than a better parser.
skill/scripts/live/ui-surfaces.mjs is now the single definition. The
browser-runtime constraint is unchanged and satisfied the same way the
command palette already solves it: live-browser.js is served raw and
injected as a classic <script>, so it cannot import an ES module. The
/live.js assembler serializes the module into
window.__IMPECCABLE_LIVE_UI_SURFACES__ in the prelude it already writes
for the token, port and vocabulary, and live-browser.js reads the global.
assembleLiveBrowserScript defaults the value from the module rather than
taking it from live-server.mjs, so the bundle carries the canonical
inventory by construction instead of by a caller remembering to pass it.
The emitted inventory is byte-identical to the inlined one.
tests/live-ui-surfaces.test.mjs pins both halves of the seam: the module
is the definition (live-browser.js must not redeclare it), the prefix the
module builds ids from matches the PREFIX live-browser.js hardcodes, and
the assembled bundle still carries the list. live-server.test.mjs gets
the matching integration check against a served /live.js.
One existing assertion changed. live-browser-regression.test.mjs checked
that the steer Send control is registered as live chrome by matching the
text of the inline literal's last line. That encoded where the list was
written, not what it contains; it now asserts membership in the imported
LIVE_UI_COMPONENT_IDS, which is the behaviour it was after.
Verified with the full default suite plus a live-e2e fixture run
(vite8-react-modal), so the overlay is exercised end to end in a browser.
AI-assisted via Claude Code under maintainer direction.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Centralize CSS numeric token parsing and characterize every supported color unit while preserving config and filtering behavior.
AI-assisted: Codex implemented this refactor under pbakaus’s scheduled architecture-simplification authorization.
Make critique storage the single owner of snapshot discovery and frontmatter parsing, and keep context signals focused on summarizing the canonical result.
AI-assisted: Prepared by Codex under pbakaus's scheduled architecture-refactor authorization.
Emit Codex-compatible top-level keys while preserving the argument hint under metadata. Keep existing Claude-style pin frontmatter unchanged for other harnesses.
AI assistance: Codex implemented and validated this change under maintainer pbakaus's standing authorization.
* Fix Blade directory detection
AI assistance: Codex reproduced the issue, implemented the fix, and ran the validation described in the pull request.
* Fix compound scan suffix matching
AI assistance: Codex addressed review findings and ran the validation described in the pull request.
Optional `avoid`, two or three negations of 12 to 160 characters. A world built
from posters is at risk of shouting; one built from instruments is at risk of
dead greys. The global detector cannot know which and the author can, so the
"do not" belongs beside the "do" rather than in a rulebook that applies to
everything equally.
Optional on purpose: 541 entries predate it and none of them are wrong for
lacking it, so nothing needs backfilling.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A well-made site was audited for what separates it from a competent one, and the
answer was not its ingredients. It runs the default stack, Next and Tailwind and
Geist, with no world and no unusual technique. What it has is attention to the
surfaces a browser renders for you: 29 focus-visible rules, 15 scrollbar rules,
and styled text selection, caret, underline offset and scroll behaviour.
Those are the cheapest signal that a page was built rather than assembled, and
the ones a model skips most reliably, because nobody asks for them and nothing
looks broken without them. The floor already covers contrast, depth, spacing,
measure, motion and states; this is the layer under all of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A 3-star held two tickets and a 1-star held none. On a pool this size that is
not a nudge, it is the shape of the draw. Measured against the live catalog:
3-star worlds absorbed 57% of the graphic draw from 65 of 163 eligible worlds,
46% of atmosphere from 13 of 43, and 75% of interaction from 15 of 25. The
reviewer's report that the same worlds keep returning is exactly what a rating
multiplier does to a corpus whose thinnest tier holds 25 worlds.
Now a 3-star draws level with a 2-star, and a 1-star draws at half rather than
not at all. Excluding a marginal keep made rating do a job breadth already does
properly: breadth still removes a niche world from the pool entirely, which is
the honest way to say "too narrow to challenge an arbitrary build", while a
1-star records "unexceptional" and is still worth showing sometimes.
Effect on the same catalog: the 3-star share falls to 39% on graphic, 30% on
atmosphere and 60% on interaction. That last one is no longer a weighting
artefact, it is simply what the tier contains, since 15 of its 25 eligible
worlds are rated 3.
Compositions get the same treatment; the two ticket functions had the identical
shape and no reason to disagree. Both tests asserted the old policy directly
and now assert the new one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
probe-compking-sol-4 (evals) executed its staged inventory faithfully and
still lost the comp: the brief had already recorded the comp's materials
down (low-contrast textures, a 70-path lake against the comp's hundreds,
a sculpted plate as flat CSS), and the build thread never loads
visualize.md, so nothing told it the comp wins that disagreement. The
comp-is-king block now says the record gets corrected upward, that the
comparison runs against the freshly reopened comp rather than memory,
and that a texture under a near-opaque wash is not shipped material.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Gracefully handle the no-image decision page
Tested the new-work path without image generation and fixed what broke:
- A text-only card's back face (First viewport, The case) was unreachable:
the Details flip chip only rendered inside the media block. Cards with no
imagery now render their full read on the front and skip the back face.
- A hero/board that fails to load (retired catalog URL, offline shell) sat
as a dark void with a zoom cursor. The slot now collapses to a field
painted from the card's own palette with an "artwork unavailable" pill;
broken inspiration PIPs remove themselves.
- Sketchless catalog art rendered unlabeled as the card's face, reading as
the promise of the build. It now carries the same "inspiration" label and
hover title the PIP uses.
- The --schema example pointed at catalog URLs that 404 (missing family
prefix); updated to the real asset paths and noted the text-only front
behavior in the schema prose.
Extends e2e test (e) with the front-read and label assertions and adds
test (f) for the broken-image fallback.
AI-assisted (Claude Code).
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: address PR review bot findings
- cursor[bot]: the unavailable-art scrim painted over the flip chips and
swallowed their clicks; it now passes pointer events through and the
chips render above it.
- Copilot: a palette-less card whose art failed still read as a dark void
and kept the stale Inspiration tooltip; the slot now falls back to the
graphite field in CSS and the tooltip is removed with the art.
Test (f) now covers both: a broken card with back facts must still flip
via Details, and a palette-less broken card gets the labeled fallback.
AI-assisted (Claude Code).
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
* test: harden the test strategy (triggers, runner speed, release guards)
Follow-ups from an end-to-end testing strategy review:
- Suite triggers are now auto-generated from each suite's own file list,
so change-based CI can never miss a test file again (four files were
unreachable by their own edits, and tests/lib/detector-bundle.test.js
triggered core while running in detector). Two new meta-tests pin the
invariant. Hand-written trigger patterns now carry only source paths
and fixture dirs; palette dropped from the live triggers since no
suite tests it.
- The node runner batches all files into one node --test invocation at
concurrency 4 instead of spawning per file. Default suite drops from
~159s to ~100s; the live suite soaked clean three times.
- scripts/release.mjs gets its first tests: 12 scenarios spawning the
real script inside a disposable git repo with a local bare origin,
covering every refusal guard plus notes/tweet rendering, all under
--dry-run.
- skill/scripts/live/ui-core.mjs deleted: zero references repo-wide,
superseded by the July live rewrite, yet still shipping to users.
cli/lib/download-providers.js annotated with its cross-repo consumers
(impeccable-site Pages Functions) so it is not mistaken for dead code.
- CLAUDE.md gains an area-to-suite table for the opt-in suites a change
owes; AGENTS.md syncs the plugin-e2e commands and obligations.
AI-assisted via Claude Code under maintainer direction.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: exclude peeled tag lines from release-test origin cleanup
Copilot: git ls-remote --tags emits ^{} peel lines for annotated tags,
which are not deletable refs; --refs filters them so the cleanup loop
survives a future scenario that pushes an annotated tag.
AI-assisted via Claude Code under maintainer direction.
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
* Fix broken-image comment false positives
AI assistance was used to reproduce the issue, implement the fix, and add regression coverage.
* Harden JavaScript comment scanning
AI assistance was used to address automated review feedback, add regression coverage, and run validation.
* Handle comments in template expressions
AI assistance was used to reproduce and fix automated review feedback, add regression coverage, and run validation.
* Preserve JSX around URL and regex syntax
AI assistance was used to reproduce and fix automated review feedback, add regression coverage, and run validation.
* Fix regex keyword property context
AI assistance: Codex identified, implemented, and validated this review follow-up under maintainer authorization.
* Handle JSX slash edge cases
AI assistance: Codex addressed review findings and validated this follow-up under maintainer authorization.
* Ignore CSS-in-JS comments
AI assistance: Codex addressed top-level review findings and validated this follow-up under maintainer authorization.
* Handle remaining slash contexts
Fix JavaScript keyword separation and JSX protocol-relative URL classification so comment stripping preserves only live source. Add focused regressions for the reviewed edge cases.\n\nAI assistance: Codex implemented and validated this change under maintainer authorization.
* Handle generic styled templates
Recognize TypeScript generic arguments consistently in CSS-in-JS extraction and comment sanitization. Add focused regressions for extraction and comment-only styled templates.\n\nAI assistance: Codex implemented and validated this change under maintainer authorization.
* Handle nested styled generics
Teach CSS-in-JS extraction and comment sanitization to scan balanced nested TypeScript generic arguments before template literals. Add regressions for live and commented nested-generic styles.\n\nAI assistance disclosure: Codex implemented and validated this review follow-up under maintainer authorization.
* Handle nested source contexts
Keep regex detection correct after postfix operators, distinguish JSX expression comments from protocol-relative text, and scan nested template literals inside CSS-in-JS interpolations. Add focused regressions for each review finding.\n\nAI assistance disclosure: Codex implemented and validated these review follow-ups under maintainer authorization.
* Complete comment-safe source scanning
Recognize regex literals after for-of, comparisons, and block braces without confusing object-literal division. Route grid-background detection through the offset-preserving comment-neutralized source and add negative and positive controls.\n\nAI assistance disclosure: Codex implemented and validated these review follow-ups under maintainer authorization.
* Handle remaining lexer contexts
Recognize JSX attribute expressions and regex literals inside CSS-in-JS interpolations so comment stripping remains source-safe.\n\nAI-assisted: Codex implemented and validated this change under maintainer authorization.
* Align interpolation regex contexts
Match postfix-update and statement-block regex classification in CSS-in-JS interpolation parsing so templates remain extractable.\n\nAI-assisted: Codex implemented and validated this change under maintainer authorization.
Centralize the browser-safe module set and source transformation so the browser and extension builders cannot drift.
AI assistance: This refactor was prepared by Codex under pbakaus's scheduled architecture-simplification authorization.
* test: guard the plugin loader contract that PR #494 exposed
The agents manifest key shipped for months and silently loaded zero of
the four subagents; no validator looked at the generated plugin
manifest's shape and claude plugin validate never checks it. Three
layers now do:
- scripts/lib/validate-plugin-manifest.js pins the verified loader
contract (KNOWN_LOADER_KEYS allowlist, no agents key, trailing-slash
skills path from issue #86, every skill/agents/*.md shipped in
plugin/agents/), unit-tested in tests/validate-plugin-manifest.test.js
including a check of the real committed subtree.
- The same check gates bun run build next to the version-drift guard.
- tests/plugin-e2e.test.mjs installs the committed ./plugin subtree into
a real Claude Code (sandboxed via CLAUDE_CONFIG_DIR in a temp dir) and
asserts the component inventory: skill parses, all agents visible,
hooks discovered. In the default suite; runs in about a second and
skips cleanly when the claude CLI is absent, so CI is unaffected.
All three failed against the pre-#494 tree for the shipped reason
(Agents 0 of 4) and pass against current main.
AI-assisted via Claude Code under maintainer direction.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: address PR review bot findings
- Copilot: guard collectPluginManifestFindings against valid JSON that is
not an object (null, string, number, array) so a broken manifest is a
finding instead of a build crash; unit test added
- Copilot: update the plugin-e2e header comment, the suite is in the
default lineup rather than opt-in
AI-assisted via Claude Code under maintainer direction.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: harden plugin E2E sandbox isolation
Bugbot: create the sandbox CLAUDE_CONFIG_DIR up front and redirect HOME
and USERPROFILE into the temp workDir too, so a CLI code path that
derives config or cache locations from the home directory instead of
CLAUDE_CONFIG_DIR still cannot touch the developer's real Claude config
when the default suite runs.
AI-assisted via Claude Code under maintainer direction.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: agent parity check mirrors the build's emit rules
Bugbot: the shipped filename is `${claude-name || name}.md` and a
providers: list may exclude claude-code, so comparing raw source
basenames could fail the build on a renamed or provider-scoped agent
with a build:release hint that cannot fix it. The validator now derives
expected filenames the same way the transformer factory does (shared
parseFrontmatter, same providers gate) with unit coverage for renames,
name overrides, and provider-scoped agents.
AI-assisted via Claude Code under maintainer direction.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: run the plugin E2E through a shell on Windows
Bugbot: the claude CLI is a .cmd shim on Windows and Node refuses to
spawn those via execFile without a shell, so the availability probe
always failed and the suite silently skipped there. Windows now invokes
through a shell with every argument double-quoted (temp paths routinely
contain spaces); the POSIX path is unchanged.
AI-assisted via Claude Code under maintainer direction.
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
Keep unmanaged detector fields when ignore-file updates the canonical detector configuration. Add a regression covering existing extension mappings.\n\nAI assistance: Codex implemented and validated this change under maintainer authorization.
Phase one is near-pixel-perfect reproduction at the comp's breakpoint,
with exactly three concessions (closest font, icons unless a library
was chosen, genuine comp defects); the overlap comparison is the
authority because models systematically believe their code recreation
succeeded when it did not, and regions that keep losing the comparison
ship as composited rendered assets instead. Phase two brings the
reproduction to life (interaction, motion, responsiveness), and
anything beyond the comp inherits the recorded system, never invented
container chrome.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The containment gate re-canonicalized projectCwd for every target file
in the per-edit and Stop loops. The hook runs as a fresh process per
tool event, so a module-level memo makes it once-per-event work; the
size cap only matters to long-lived importers like the test runner.
Addresses Copilot review feedback on PR #471.
Written with AI assistance (Claude Code).
Co-Authored-By: Claude Code <noreply@anthropic.com>
Replace three duplicate matcher loops with one declarative pattern list while preserving import resolution behavior. Add Sass @use and @forward characterization coverage.\n\nAI-assisted change prepared under pbakaus's scheduled architecture-refactor authorization.
hook-before-edit.mjs kept its own string-based isInsideProject; it now
uses the shared isScanTargetInsideProject so all three hook passes
apply one containment semantic, symlink canonicalization included.
Because the before-edit hook gates proposed Writes whose target does
not exist yet, canonicalPath now resolves the nearest existing
ancestor and re-appends the remainder instead of falling back to the
raw resolved path — a new file under a symlinked root compares equal
to its canonical project.
Written with AI assistance (Claude Code).
Co-Authored-By: Claude Code <noreply@anthropic.com>
The per-edit and Stop deep passes gated on sensitive paths, generated
paths, extension, config ignores, and size, but never on containment.
Any file the session touched outside the project (harness scratchpad
dirs under the system temp root, sibling checkouts) was scanned and
judged against THIS project's config and DESIGN.md palette, producing
design-system findings that are wrong by construction.
Both loops now check isScanTargetInsideProject() (audit reason:
outside-project), matching the gate hook-before-edit.mjs already had.
Paths are canonicalized so a symlinked root doesn't split the
comparison. The Stop pass re-checks containment itself because caches
written by older hook versions can still list out-of-project paths.
Umbrella-dir launches (issue #305) are unaffected: their projectCwd
resolves to the edited file's own project root, so containment holds.
Written with AI assistance (Claude Code).
Co-Authored-By: Claude Code <noreply@anthropic.com>
Review follow-up: the regex stopped at the literal ?token= and tolerated
anything after it, so removing the encoded token value from the URL
still passed. Requiring encodeURIComponent(TOKEN) right after the
prefix makes the mutation fail.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up: the reflected-origin contract matters most on the
OPTIONS preflight, where a cached response authorized for one origin
must never be served to another.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Covers the fix for the ddev breakage reported in #304: the live server
now reflects Access-Control-Allow-Origin for any request bearing the
valid session token, so dev servers on loopback aliases (https://*.ddev.site,
Valet's *.test, hosts-file entries) work again while tokenless remote
origins stay blocked. The server/browser source changes shipped in
b1c5707f; this adds the test coverage that was written alongside them:
- tokenless remote origins get no ACAO on any route, token'd or not
- a non-loopback origin with the valid token is reflected, with
Vary: Origin, on both the real request and its OPTIONS preflight
- the /manual-edit-stash source assertion tracks the token-bearing URL
Prepared with AI assistance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
context.mjs now probes cwebp/sips/magick/ffmpeg once (which/where per
OS) and prints IMAGE_TOOLS, replacing macOS-specific prose; the
IMAGE_GEN_AVAILABLE directive leads with the harness-native tool so a
present OpenAI key stops reading as an instruction to bill it; and the
sandboxed board-start guidance sheds codex vocabulary.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sandboxed shells cannot bind the board's port; every codex session paid
one failed start before retrying escalated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Recognize both slash- and dollar-prefixed prescribed seed markers and exercise each variant in coverage tests.
AI assistance: Codex addressed Cursor and Copilot review feedback and reran validation under maintainer authorization.
Treat Components as optional only when DESIGN.md carries the prescribed seed marker, while retaining Colors and Typography checks.
AI assistance: Codex reproduced the issue, implemented the fix, and added regression coverage under maintainer authorization.
Join indented Markdown bullet continuations and keep them out of Overview philosophy text.
AI assistance: Codex reproduced the issue, implemented the fix, and added regression coverage under maintainer authorization.
Describe the catalog as independent of SKILL.md extraction rather than repository content, addressing Copilot's review feedback.
Prepared with Codex assistance under pbakaus's scheduled architecture cleanup authorization.
Remove the unreachable legacy SKILL.md pattern parser now that readPatterns uses the curated catalog exclusively.
Prepared with Codex assistance under pbakaus's scheduled architecture cleanup authorization.
Three lessons from the Tortuga containment-map run. The first rebuild
directive now executes immediately, informing the user instead of
asking permission to fix a failure; consultation waits for a second
rebuild verdict or user-approved content at risk. The asset producer
spawns on every subagent-capable run even when produce looks empty,
because its manifest is the independent check on the inventory's media
and the skipped spawn marks every all-CSS failure to date. And the
inventory now opens by reading the comp as a design system (component
grammar, corners, line weights, elevation, type ramp), because the
sections the comp does not show get built from that record, and without
it the fallback is the stock kit: square boxes, 1px grids, bentos, hard
shadows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The condensation pass folded the old textures-are-raster-by-default
sentence into the medium gate's lighting-and-depth clause, and the next
codex run drove straight through the gap: woven cotton as 'layered CSS
textures', a black nylon band as plain CSS, and a physical evidence-tag
CTA as CSS shapes, so the produce bucket stayed empty and the asset
producer was never called. The gate now names textures explicitly,
woven cloth, paper grain, fabric, leather, brushed metal, with no depth
argument owed, and calls 'layered CSS textures' what it is: not a
medium.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three of the six axes cannot be read from a world's prose, and widening their
keywords manufactures signal rather than finding it. Depth's probe matched
worlds that said "no cast shadow anywhere" and "without perspective or depth";
motion and colour strategy describe properties the system rules never state, so
they place 28% and 7%.
An optional axes object on the concept records the value instead. Absent means
inferred from the rules as before, so nothing needs backfilling. Validated
against the axes definition when the caller supplies it, because a typo would
read as "unrecorded" and fall back to a probe already known not to work, which
is the quietest way for this to fail.
This is what makes an assigned wave measurable. If a wave draws "drenched" and
"simulated physics" before designing anything, the world it produces has to
carry those values or the assignment is lost the moment it lands, and occupancy
goes back to guessing at prose.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Tortuga comp dissolves the Install CTA's edge into the storm's
particles; the build shipped a plain rectangle with four decorative
dots, and neither the builder nor the reviewer's rebuild findings named
it. The inventory now gives the primary action its own row and medium,
naming the shrink-to-border-trick failure as the compliance-token
version of commitment, and the reviewer's fidelity matrix lists the
primary action's treatment among the salient elements, with a
physically-worked CTA rendered as a plain rectangle scored contradicted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Tortuga run showed the finish machinery working end to end, roll,
challengers, approved comp, honest labels, independent reviewer, an
earned rebuild verdict at the user checkpoint, and still cost the user
a full build to learn the hero undersold the comp: a glyph storm at a
tenth of the approved density under type half as compressed. Both
misses were visible the moment the first viewport rendered.
Two cheap gates front-load that discovery. The build section gains a
hero checkpoint: capture the first viewport and set it beside the
comp's before any later section, judging scale and density as
quantities. The inventory gains the same quantitative discipline:
field and texture regions record density and coverage, and TYPE rows
name the compression class and render one headline word against the
comp before anything is built on the face.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codex's risk reviewer rejected the network-escalated roll rerun for
'contacting an unspecified external domain' and the assumed export of
project context, so the run degraded to no challengers. Both concerns
are answerable: the script's only network contact is one GET to
impeccable.style/api/roll carrying scope, mode, an eight-hex key, and a
re-roll counter, nothing project-derived. The degraded message now
states that verbatim and tells the model to put the URL and payload in
its approval request, so the reviewer judges the real action instead of
an unknown.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
single-font's retirement made the detector 59 rules; both READMEs still
said 60 in five places, and the count validator reported clean because
'deterministic detector rules' puts a word the regex never expected
between the qualifier and the noun, and README.npm.md was never in the
checked file list. The regex now tolerates the detector infix, counts
qualified 'issues' claims, and README.npm.md joins the list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Version bumps for all three components plus the build:release sync of
the tracked harness dirs and the plugin subtree at 4.0.4, rebased onto
the composition-axes work so the release carries both threads.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The composition pool (stagings) is not ready: too thin to help, and its
draws crowd the decision it rides along with. concept-seed.mjs stops
rendering the staging block by default; IMPECCABLE_COMPOSITIONS=1
re-enables it for catalog development, and the draw machinery,
rating-weighted selection, and mode scoping stay intact and tested for
its return. new-work.md drops the dress-the-staging-challengers
instruction and the FORM contract's staging clauses; the surface-scope
roll still assigns which of the model's own structures gets built.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One family with weight and size contrast carrying the hierarchy is a
legitimate type system, and in practice the rule mostly punished
minimal pages: it was the loudest cross-rule noise on the fixture
corpus's should-pass columns. Removed from the registry, both engine
paths, the regex page analyzers, and the devtools category map; the
negative assertions stay as resurrection guards, and the text-content
analyzer index base shifts down one with the removal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codex's own post-mortem of the second hamster-wheel session: it loaded
the kicker ban, shipped five kickers anyway, and then the reviewer and
documenter 'compounded it by accepting, and even canonizing, the
invented label style'. Nothing downstream of the builder ever re-read
the floor.
The reviewer gains check 6, Floor: read craft-floor.md (now the one
skill reference it may read, passed in its inputs) and hold the
screenshots against the Refuse list; a banned element is a material fix
even when it matches nothing in the comp, because fidelity cannot
authorize what the floor refuses. The documenter gains the mirror rule:
a floor refusal lands in its not-canonized line as a carried defect,
never in DESIGN.md as a rule future surfaces inherit.
Also updates the live-reference contract test to match the applier's
condensed no-server sentence, which still carries the same guarantee.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul's codex build carried an element literally named class="kicker"
and the detector returned one finding. Two independent blind spots:
- The linked stylesheet was styles.css?v=3, and the href resolved as a
literal path with the query string in it, so the whole sheet was
invisible to every element-level check: 1 finding with the link, 18
with the CSS inlined. Hrefs now strip query and hash before resolving.
- The kicker gate demanded letter-spacing >= max(1px, 0.08 * size). The
wild's most common recipe, 0.08em at 12px, computes to 0.973px and
lost to the absolute floor by a fraction. The floor is now purely
proportional (0.06 * size), with a fixture case pinning the exact
shape that slipped through.
With both fixed, the failed codex build scans at 18 findings including
its numbered section kickers (numbered-section-labels), side-tab
stripe, and grid background.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three subagent audits reviewed the files that grew through the last
rounds of patches. Their honest verdict: dense, not bloated; roughly
430 words of true redundancy came out with no rule lost, and every cut
they flagged as removing compliance pressure was skipped. Highlights:
approval recording now has one owner in visualize.md, the asset
producer's crop ban went from three statements to the deliberate pair,
its two transparency passages carried contradictory defaults (resolved
toward true alpha first), and the 450-word medium-gate wall split into
three paragraphs at zero cost. The producer also gained a mode-seam
sentence so a sketch run cannot return an asset manifest.
The reviewer's verdict is no longer soft: a derived disposition line
(rebuild / fix / ship) opens every return, computed from the matrix
rather than felt, recomputed after the verdict pass, and never
softenable by the parent, who must report it verbatim. The second
hamster-wheel run showed the parent inventing 'PASS WITH FIXES' over a
matrix with MATERIAL contradicted on the focal element.
Two additions from the same session's evidence: hard offset shadows
outside a neobrutalist world join the craft floor's refusals (codex
invents them without fail), and hookless harnesses must run detect.mjs
once before the finish review, because codex has no hooks and the
detector otherwise never sees the build at all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The second codex hamster-wheel run read the medium gate and still
assigned a shaded, perspectived technical illustration to 'Authored SVG
geometry': the world was an instruction booklet, so the affinity
clause's 'diagrams' blessed the downgrade, and the page shipped as flat
clipart against an illustration-grade comp. The gate now says style
does not move the boundary: perspective, shading, figure drawing, or
dense mechanical detail is illustration however line-drawn it looks,
and authored SVG ends where drawing skill begins. The craft floor's
sketchy-SVG rule carries the same sentence.
The reviewer in that run built an honest matrix, MATERIAL contradicted
on the focal element, and still emitted it as a fixable item the parent
answered with CSS. The rebuild directive now fires when MATERIAL is
contradicted on the focal element, not only when TYPE falls with it,
and every asset-requiring fix must say 'produce: <region>' so it cannot
be answered as a style tweak.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A landscape frame was the silent default at every generation site,
which is a composition error before the build starts for native apps
and mobile-first surfaces. The sketch frame, the asset producer's
single-sketch contract, and the comp instruction now state it: portrait
at device viewport when the surface is a phone screen, landscape for
desktop web. The decision page adapts in kind: portrait art overrides
the 16/10 slot with its own exact ratio so nothing crops, and the deck
narrows so portrait cards line up side by side. The --schema guidance
tells the model the page handles either.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cards widen from 27vw to 34vw and the media slot matches the 16:10
sketch frame instead of cropping it to 16:9: at the old width the
imagery read as a thumbnail above a column of copy, and the copy won
the attention contest the sketch exists to win. The whole image is now
a zoom target with a zoom-in cursor, not just the expand chip; chip and
PIP handlers already stop propagation, so the art click is unambiguous.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A codex greenfield build produced an excellent approved comp and then an
abysmal page, and the reviewer approved it. The failure chain: the
implementation inventory downgraded a photographic hero to 'silhouette
in SVG' and sculpted panels to 'material finish: CSS'; the builder read
'no photography on hand' as a license to avoid photographic rendering;
QA looked at one full-page thumbnail; the reviewer was spawned with the
builder's forked history and then scored fix claims instead of pixels;
and the output contract had no way to say 'rejected'.
The fixes, stage by stage:
- The inventory's medium column gets a gate: a human figure, product
object, machinery, or lit material is raster whatever the stack, and
such regions are regenerated cleanly at asset resolution with the
comp and its embedded prompt as reference. Never cropped from the
comp, whose effective resolution is reference grade; the asset
producer's direct bucket closes the same hole. Dropping an
image-native region is a user decision at the approval point.
- Generated imagery is a material, not a claim: evidence rules bind
assertions, never render fidelity.
- The build thread's inspection becomes a region-by-region side-by-side
against the comp at legible scale, never one full-page thumbnail.
- The reviewer spawns fresh, never with forked history (fork_turns: 0
in codex), and gains a rejection lane: when TYPE, MATERIAL, and the
focal element are all contradicted, the first material fix is a
rebuild directive the parent surfaces to the user instead of
patching. Verdict passes score recaptures only; the parent's fix
narration is not evidence.
- The verdict-loop ceiling softens: two rounds ends an unattended run,
but an attended session puts the open-items table in front of the
user and lets them fund another round; any round that resolves
nothing stops the loop.
- Comp approval joins the roll as skip-proof: question-tool errors fall
back to the decision page, delegation is recorded in the brief and
the sidecar and disclosed up front, and the reviewer treats comps
with no recorded pick as a material finding.
- Craft floor: system display faces (Impact, Arial Black) as an
own-world display voice and unicode glyphs standing in for icon
systems are named failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The surface brief was the only carrier of which comp got approved, and
eval transcripts show models routinely skip writing it, leaving the
choice unrecoverable. The comp's .json prompt sidecar already travels
with the mocks folder across sessions and machines, so the approval now
gets marked there too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The area taxonomy was wrong, and wrong in a way worth recording. It was
derived from Mobbin-style categories in the abstract rather than from what
the skill can be asked for, and measured against the catalog most of it
described problems that were not there: onboarding, settings, empty-state and
search each had zero entries.
Reframed against demand instead. A user asks for a docs site, an onboarding
flow, a landing page, or a data table, and those differ in how much of the
product is in play. Register already says what kind of work it is; grain says
how much: product, flow, view, region.
Named grain rather than scope because scope already means direction-or-surface
on every roll and 'surface' is already a register value, so a scope of
'surface' would have collided with both.
Platform is the second axis: web, ios, android. Unlike grain it is a hard
filter with no fallback, because a composition that leans on hover or a
pointer does not degrade on a phone into something slightly worse, it stops
working, and an empty deal is a visible gap where a broken one is not.
Both fields are optional and absence means eligible everywhere, so nothing
needs backfilling and no existing roll changes.
The third piece is the one a trace turned up. Asking for an onboarding flow
resolves to register=operate, grain=flow, and the catalog holds zero
flow-grain compositions, so the top-up would have dealt three plausible
single-screen compositions with no signal that none matched. The model would
have improvised the flow structure while believing it was handed one, which is
the same silent plausibility the axis exists to remove. Selection now returns
a match alongside the picks, and the rendered seed says when the structure is
borrowed and why.
Measured at the time of writing: 137 of 173 approved compositions are view
grain, product grain is empty, flow grain holds one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The roll API validates its `area` parameter against the surface's list, which
meant importing the taxonomy into a Pages Function. composition-catalog.mjs
reads the filesystem, so importing from there would have pulled node:fs into
the Worker bundle, the same trap WELL_TIERS hit. roll-selection.mjs has no
imports at all and is what both callers already load, so it owns the taxonomy
and composition-catalog re-exports it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two gaps, both reported from real use. Worlds were drawn with no mode
awareness at all: selectApprovedChallengers never received the mode, so a
build asking for an app UI could draw six worlds that only make sense on a
landing page. And surface alone is too coarse for compositions, because
"operate" spans onboarding, dashboards, editors and settings, so an
onboarding flow could legitimately be dealt a settings composition.
Worlds gain `allowedModes` on the review record, beside breadth and rating,
because it is a reviewer judgment rather than authored content. Absent means
eligible in every mode, so nothing needs backfilling and no existing roll
changes. Applied per tier and skipped where it would empty one, matching how
minRating and strength already degrade. It is a ceiling the reviewer lowers,
not a category they assign: a world is an identity, and identities transfer
across modes further than compositions do.
Compositions gain an optional `area`, one level below surface, with a
taxonomy per surface (COMPOSITION_AREAS). Area is a preference rather than a
filter: a request reorders the ranking to put area matches first and tops up
from the rest of the surface, because the per-area pools are small and
dealing one on-target composition would be worse than three good ones. A
stable partition of an already deterministic ranking stays deterministic.
`--area` on the CLI requires `--mode`, since areas are scoped to a surface,
and is validated against that surface's list so a wrong-surface area fails
loudly instead of silently matching nothing.
Also validated `breadth`, which selection has honoured for a while with
nothing checking it, so a typo read as "general" and quietly returned a
narrow world to the pool.
Four new tests: worlds excluded from a mode stay out, absent allowedModes
stays eligible everywhere, a tier whose every world excludes the mode falls
back instead of starving, and an area-scoped deal prefers its area, tops up
to three, and reproduces from its key.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The data layer has said compositions since the catalog was split, while the
code, the model-facing text, and the UI still said stagings. The rename was
held back by the selection logic existing twice; it exists once now, so this
is one pass instead of two coordinated ones.
Renamed: selectApprovedStagings, selectApprovedStaging, renderStaging, and
the model-facing STAGING GRAMMAR / STAGING CHALLENGERS / FIRST-SURFACE
STAGING INPUTS headings. The block that introduces them now states what
they are for rather than only what they are not: what is the cleverest way
to present, organize, or make interactive the problem in front of you.
Three places keep the old word on purpose:
- The rank salt, `${scope}:${key}:staging`. It is hash input, so renaming
it would re-deal every roll anyone has ever reproduced by key. Verified:
240 seeder rolls and 252 API rolls reproduce exactly.
- `Staging/hierarchy:`, the first composition grammar prefix. Inside a
composition, staging names one of its four aspects, which is a different
word-sense from staging as the name for the whole artifact. It is also a
schema constant that 317 catalog entries are validated against.
- The wire fields. The API keeps emitting `stagings` and `staging` beside
`compositions`, because the wire is the one place a rename cannot be
coordinated with already-installed skills. Clients prefer the new field
and fall back through both old ones.
Separately, WELL_TIERS had two definitions after the extraction.
roll-selection.mjs owns it now and concept-catalog.mjs imports it, in that
direction because concept-catalog reads the filesystem and a Pages Function
must not pull node:fs into its bundle. Imported and re-exported rather than
re-exported alone: a bare `export { X } from` does not bind X locally, and
validateConceptCatalog needs it, which cost one round of red tests.
Dropped concept-catalog's synchronous deterministicRank. Nothing imports it
since selection moved out, and leaving a second ranking implementation
around is how the first drift started.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
concept-seed.mjs and the service repo's functions/api/_worldroll-core.js
were two implementations of the same selection, and the API core's header
claimed they matched "exactly". They did not: it had no breadth gate on
either pool, no rating weighting for compositions, and dealt one
composition where the seeder dealt three. Since the catalog never ships
with the skill, every real user rolls through that API, so those gates
reached nobody. Two copies is the defect; this removes the second.
Written as generators rather than plain functions because the callers
cannot agree on a hash. Node has a synchronous one, Workers only have
async crypto.subtle, and renderConceptSeed's local path is deliberately
synchronous so prepared eval sessions and tests can call it without
awaiting. The selection yields batches of strings to hash and resumes
with their digests; runSyncSelection and runAsyncSelection are the only
runtime-specific code, eight lines each. Forcing the seeder async would
have broken the eval harness; forking the logic is what got us here.
No roll changes. Node's crypto.createHash('sha256') and Web Crypto's
SHA-256 return the same bytes, verified, and 240 seeder rolls plus 252
API rolls across both scopes, five modes, three reroll depths and the
rating gate reproduce their pre-refactor output exactly. The 23 existing
concept-seed tests pass unmodified, which is the point: the synchronous
contract survived.
The service repo's core keeps its own copy until this is on main, because
its deploy materializes skill/ from main and would fail to resolve an
import that is not there yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>