From d65a08b0644bc2bc9639fcf22b042eadf4136f2d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:17:56 +0000 Subject: [PATCH] Sync generated provider output --- .agents/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .claude/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .cursor/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .gemini/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .github/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .grok/skills/impeccable/reference/bolder.md | 2 + .grok/skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .kiro/skills/impeccable/reference/bolder.md | 2 + .kiro/skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .../skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .pi/skills/impeccable/reference/bolder.md | 2 + .pi/skills/impeccable/reference/new-work.md | 12 +- .pi/skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .pi/skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .qoder/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .../skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .../skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .trae/skills/impeccable/reference/bolder.md | 2 + .trae/skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .vibe/skills/impeccable/reference/bolder.md | 2 + .vibe/skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- plugin/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- 135 files changed, 7365 insertions(+), 2175 deletions(-) create mode 100644 .agents/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .claude/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .cursor/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .gemini/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .github/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .grok/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .kiro/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .opencode/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .pi/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .qoder/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .rovodev/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .trae-cn/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .trae/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .vibe/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 plugin/skills/impeccable/scripts/live/ui-surfaces.mjs diff --git a/.agents/skills/impeccable/reference/bolder.md b/.agents/skills/impeccable/reference/bolder.md index 9fe39ca59..026c10a6a 100644 --- a/.agents/skills/impeccable/reference/bolder.md +++ b/.agents/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.agents/skills/impeccable/reference/new-work.md b/.agents/skills/impeccable/reference/new-work.md index 755d734e3..8f654b7d7 100644 --- a/.agents/skills/impeccable/reference/new-work.md +++ b/.agents/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .agents/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .agents/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .agents/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .agents/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.agents/skills/impeccable/reference/visualize.md b/.agents/skills/impeccable/reference/visualize.md index 815e29d3a..c20d910f3 100644 --- a/.agents/skills/impeccable/reference/visualize.md +++ b/.agents/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.agents/skills/impeccable/scripts/concept-seed.mjs b/.agents/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.agents/skills/impeccable/scripts/concept-seed.mjs +++ b/.agents/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this 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. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.agents/skills/impeccable/scripts/lib/impeccable-config.mjs b/.agents/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.agents/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.agents/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.agents/skills/impeccable/scripts/live-browser.js b/.agents/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.agents/skills/impeccable/scripts/live-browser.js +++ b/.agents/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs b/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.agents/skills/impeccable/scripts/live/ui-surfaces.mjs b/.agents/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.agents/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.claude/skills/impeccable/reference/bolder.md b/.claude/skills/impeccable/reference/bolder.md index fced49456..a5c34cd3e 100644 --- a/.claude/skills/impeccable/reference/bolder.md +++ b/.claude/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.claude/skills/impeccable/reference/new-work.md b/.claude/skills/impeccable/reference/new-work.md index 5161d25cb..718b4bd0b 100644 --- a/.claude/skills/impeccable/reference/new-work.md +++ b/.claude/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .claude/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .claude/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .claude/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .claude/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -80,7 +82,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.claude/skills/impeccable/reference/visualize.md b/.claude/skills/impeccable/reference/visualize.md index 94c337f15..6df08b30b 100644 --- a/.claude/skills/impeccable/reference/visualize.md +++ b/.claude/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.claude/skills/impeccable/scripts/concept-seed.mjs b/.claude/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.claude/skills/impeccable/scripts/concept-seed.mjs +++ b/.claude/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this 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. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.claude/skills/impeccable/scripts/lib/impeccable-config.mjs b/.claude/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.claude/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.claude/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.claude/skills/impeccable/scripts/live-browser.js b/.claude/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.claude/skills/impeccable/scripts/live-browser.js +++ b/.claude/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs b/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.claude/skills/impeccable/scripts/live/ui-surfaces.mjs b/.claude/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.claude/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.cursor/skills/impeccable/reference/bolder.md b/.cursor/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.cursor/skills/impeccable/reference/bolder.md +++ b/.cursor/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.cursor/skills/impeccable/reference/new-work.md b/.cursor/skills/impeccable/reference/new-work.md index 99ed558b5..0dff0aea7 100644 --- a/.cursor/skills/impeccable/reference/new-work.md +++ b/.cursor/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .cursor/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .cursor/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .cursor/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .cursor/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.cursor/skills/impeccable/reference/visualize.md b/.cursor/skills/impeccable/reference/visualize.md index 5877eae67..3a8cc1e4a 100644 --- a/.cursor/skills/impeccable/reference/visualize.md +++ b/.cursor/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.cursor/skills/impeccable/scripts/concept-seed.mjs b/.cursor/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.cursor/skills/impeccable/scripts/concept-seed.mjs +++ b/.cursor/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this 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. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.cursor/skills/impeccable/scripts/lib/impeccable-config.mjs b/.cursor/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.cursor/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.cursor/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.cursor/skills/impeccable/scripts/live-browser.js b/.cursor/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.cursor/skills/impeccable/scripts/live-browser.js +++ b/.cursor/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs b/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.cursor/skills/impeccable/scripts/live/ui-surfaces.mjs b/.cursor/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.cursor/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.gemini/skills/impeccable/reference/bolder.md b/.gemini/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.gemini/skills/impeccable/reference/bolder.md +++ b/.gemini/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.gemini/skills/impeccable/reference/new-work.md b/.gemini/skills/impeccable/reference/new-work.md index 77ec7396f..08a8ed6f0 100644 --- a/.gemini/skills/impeccable/reference/new-work.md +++ b/.gemini/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .gemini/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .gemini/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .gemini/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .gemini/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.gemini/skills/impeccable/reference/visualize.md b/.gemini/skills/impeccable/reference/visualize.md index 6dae2cb0b..82f2cc4de 100644 --- a/.gemini/skills/impeccable/reference/visualize.md +++ b/.gemini/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.gemini/skills/impeccable/scripts/concept-seed.mjs b/.gemini/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.gemini/skills/impeccable/scripts/concept-seed.mjs +++ b/.gemini/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this 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. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.gemini/skills/impeccable/scripts/lib/impeccable-config.mjs b/.gemini/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.gemini/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.gemini/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.gemini/skills/impeccable/scripts/live-browser.js b/.gemini/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.gemini/skills/impeccable/scripts/live-browser.js +++ b/.gemini/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs b/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.gemini/skills/impeccable/scripts/live/ui-surfaces.mjs b/.gemini/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.gemini/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.github/skills/impeccable/reference/bolder.md b/.github/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.github/skills/impeccable/reference/bolder.md +++ b/.github/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.github/skills/impeccable/reference/new-work.md b/.github/skills/impeccable/reference/new-work.md index a249c27ad..959eb081e 100644 --- a/.github/skills/impeccable/reference/new-work.md +++ b/.github/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .github/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .github/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .github/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .github/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.github/skills/impeccable/reference/visualize.md b/.github/skills/impeccable/reference/visualize.md index 285a4c996..60ee2020a 100644 --- a/.github/skills/impeccable/reference/visualize.md +++ b/.github/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.github/skills/impeccable/scripts/concept-seed.mjs b/.github/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.github/skills/impeccable/scripts/concept-seed.mjs +++ b/.github/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this 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. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.github/skills/impeccable/scripts/lib/impeccable-config.mjs b/.github/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.github/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.github/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.github/skills/impeccable/scripts/live-browser.js b/.github/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.github/skills/impeccable/scripts/live-browser.js +++ b/.github/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.github/skills/impeccable/scripts/live/browser-script-parts.mjs b/.github/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.github/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.github/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.github/skills/impeccable/scripts/live/ui-surfaces.mjs b/.github/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.github/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.grok/skills/impeccable/reference/bolder.md b/.grok/skills/impeccable/reference/bolder.md index fced49456..a5c34cd3e 100644 --- a/.grok/skills/impeccable/reference/bolder.md +++ b/.grok/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.grok/skills/impeccable/reference/new-work.md b/.grok/skills/impeccable/reference/new-work.md index 45027be6e..0e92394b7 100644 --- a/.grok/skills/impeccable/reference/new-work.md +++ b/.grok/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .grok/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .grok/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .grok/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .grok/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.grok/skills/impeccable/reference/visualize.md b/.grok/skills/impeccable/reference/visualize.md index 4dd8b3525..063520a6e 100644 --- a/.grok/skills/impeccable/reference/visualize.md +++ b/.grok/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.grok/skills/impeccable/scripts/concept-seed.mjs b/.grok/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.grok/skills/impeccable/scripts/concept-seed.mjs +++ b/.grok/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this 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. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.grok/skills/impeccable/scripts/lib/impeccable-config.mjs b/.grok/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.grok/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.grok/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.grok/skills/impeccable/scripts/live-browser.js b/.grok/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.grok/skills/impeccable/scripts/live-browser.js +++ b/.grok/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.grok/skills/impeccable/scripts/live/browser-script-parts.mjs b/.grok/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.grok/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.grok/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.grok/skills/impeccable/scripts/live/ui-surfaces.mjs b/.grok/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.grok/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.kiro/skills/impeccable/reference/bolder.md b/.kiro/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.kiro/skills/impeccable/reference/bolder.md +++ b/.kiro/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.kiro/skills/impeccable/reference/new-work.md b/.kiro/skills/impeccable/reference/new-work.md index 479e37b10..2402bc90a 100644 --- a/.kiro/skills/impeccable/reference/new-work.md +++ b/.kiro/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .kiro/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .kiro/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .kiro/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .kiro/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.kiro/skills/impeccable/reference/visualize.md b/.kiro/skills/impeccable/reference/visualize.md index f298c6c69..cc0057342 100644 --- a/.kiro/skills/impeccable/reference/visualize.md +++ b/.kiro/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.kiro/skills/impeccable/scripts/concept-seed.mjs b/.kiro/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.kiro/skills/impeccable/scripts/concept-seed.mjs +++ b/.kiro/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this 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. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.kiro/skills/impeccable/scripts/lib/impeccable-config.mjs b/.kiro/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.kiro/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.kiro/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.kiro/skills/impeccable/scripts/live-browser.js b/.kiro/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.kiro/skills/impeccable/scripts/live-browser.js +++ b/.kiro/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs b/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.kiro/skills/impeccable/scripts/live/ui-surfaces.mjs b/.kiro/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.kiro/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.opencode/skills/impeccable/reference/bolder.md b/.opencode/skills/impeccable/reference/bolder.md index 5408e49d0..1055d6a9f 100644 --- a/.opencode/skills/impeccable/reference/bolder.md +++ b/.opencode/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.opencode/skills/impeccable/reference/new-work.md b/.opencode/skills/impeccable/reference/new-work.md index 290c53138..95aa35654 100644 --- a/.opencode/skills/impeccable/reference/new-work.md +++ b/.opencode/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .opencode/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .opencode/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .opencode/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .opencode/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.opencode/skills/impeccable/reference/visualize.md b/.opencode/skills/impeccable/reference/visualize.md index 43aebde6e..5bd14ae50 100644 --- a/.opencode/skills/impeccable/reference/visualize.md +++ b/.opencode/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.opencode/skills/impeccable/scripts/concept-seed.mjs b/.opencode/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.opencode/skills/impeccable/scripts/concept-seed.mjs +++ b/.opencode/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this 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. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.opencode/skills/impeccable/scripts/lib/impeccable-config.mjs b/.opencode/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.opencode/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.opencode/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.opencode/skills/impeccable/scripts/live-browser.js b/.opencode/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.opencode/skills/impeccable/scripts/live-browser.js +++ b/.opencode/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs b/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.opencode/skills/impeccable/scripts/live/ui-surfaces.mjs b/.opencode/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.opencode/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.pi/skills/impeccable/reference/bolder.md b/.pi/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.pi/skills/impeccable/reference/bolder.md +++ b/.pi/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.pi/skills/impeccable/reference/new-work.md b/.pi/skills/impeccable/reference/new-work.md index 57437e7ba..ffc87a930 100644 --- a/.pi/skills/impeccable/reference/new-work.md +++ b/.pi/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .pi/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .pi/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .pi/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .pi/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.pi/skills/impeccable/reference/visualize.md b/.pi/skills/impeccable/reference/visualize.md index 0608624d9..539dd0286 100644 --- a/.pi/skills/impeccable/reference/visualize.md +++ b/.pi/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.pi/skills/impeccable/scripts/concept-seed.mjs b/.pi/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.pi/skills/impeccable/scripts/concept-seed.mjs +++ b/.pi/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this 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. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.pi/skills/impeccable/scripts/lib/impeccable-config.mjs b/.pi/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.pi/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.pi/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.pi/skills/impeccable/scripts/live-browser.js b/.pi/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.pi/skills/impeccable/scripts/live-browser.js +++ b/.pi/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs b/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.pi/skills/impeccable/scripts/live/ui-surfaces.mjs b/.pi/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.pi/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.qoder/skills/impeccable/reference/bolder.md b/.qoder/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.qoder/skills/impeccable/reference/bolder.md +++ b/.qoder/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.qoder/skills/impeccable/reference/new-work.md b/.qoder/skills/impeccable/reference/new-work.md index 983ce2b96..c0ce92c06 100644 --- a/.qoder/skills/impeccable/reference/new-work.md +++ b/.qoder/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .qoder/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .qoder/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .qoder/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .qoder/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.qoder/skills/impeccable/reference/visualize.md b/.qoder/skills/impeccable/reference/visualize.md index 7ccc0af4c..3e6a3a7e9 100644 --- a/.qoder/skills/impeccable/reference/visualize.md +++ b/.qoder/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.qoder/skills/impeccable/scripts/concept-seed.mjs b/.qoder/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.qoder/skills/impeccable/scripts/concept-seed.mjs +++ b/.qoder/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this 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. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.qoder/skills/impeccable/scripts/lib/impeccable-config.mjs b/.qoder/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.qoder/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.qoder/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.qoder/skills/impeccable/scripts/live-browser.js b/.qoder/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.qoder/skills/impeccable/scripts/live-browser.js +++ b/.qoder/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs b/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.qoder/skills/impeccable/scripts/live/ui-surfaces.mjs b/.qoder/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.qoder/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.rovodev/skills/impeccable/reference/bolder.md b/.rovodev/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.rovodev/skills/impeccable/reference/bolder.md +++ b/.rovodev/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.rovodev/skills/impeccable/reference/new-work.md b/.rovodev/skills/impeccable/reference/new-work.md index 9b3edee90..c4a7821fc 100644 --- a/.rovodev/skills/impeccable/reference/new-work.md +++ b/.rovodev/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .rovodev/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .rovodev/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .rovodev/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .rovodev/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.rovodev/skills/impeccable/reference/visualize.md b/.rovodev/skills/impeccable/reference/visualize.md index ad1f4a548..665e8c71c 100644 --- a/.rovodev/skills/impeccable/reference/visualize.md +++ b/.rovodev/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.rovodev/skills/impeccable/scripts/concept-seed.mjs b/.rovodev/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.rovodev/skills/impeccable/scripts/concept-seed.mjs +++ b/.rovodev/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this 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. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.rovodev/skills/impeccable/scripts/lib/impeccable-config.mjs b/.rovodev/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.rovodev/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.rovodev/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.rovodev/skills/impeccable/scripts/live-browser.js b/.rovodev/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.rovodev/skills/impeccable/scripts/live-browser.js +++ b/.rovodev/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs b/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.rovodev/skills/impeccable/scripts/live/ui-surfaces.mjs b/.rovodev/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.rovodev/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.trae-cn/skills/impeccable/reference/bolder.md b/.trae-cn/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.trae-cn/skills/impeccable/reference/bolder.md +++ b/.trae-cn/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.trae-cn/skills/impeccable/reference/new-work.md b/.trae-cn/skills/impeccable/reference/new-work.md index f47a72b20..529b63dbc 100644 --- a/.trae-cn/skills/impeccable/reference/new-work.md +++ b/.trae-cn/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .trae-cn/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .trae-cn/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .trae-cn/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .trae-cn/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.trae-cn/skills/impeccable/reference/visualize.md b/.trae-cn/skills/impeccable/reference/visualize.md index 0780bbf8b..6d39ce00a 100644 --- a/.trae-cn/skills/impeccable/reference/visualize.md +++ b/.trae-cn/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.trae-cn/skills/impeccable/scripts/concept-seed.mjs b/.trae-cn/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.trae-cn/skills/impeccable/scripts/concept-seed.mjs +++ b/.trae-cn/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this 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. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.trae-cn/skills/impeccable/scripts/lib/impeccable-config.mjs b/.trae-cn/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.trae-cn/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.trae-cn/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.trae-cn/skills/impeccable/scripts/live-browser.js b/.trae-cn/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.trae-cn/skills/impeccable/scripts/live-browser.js +++ b/.trae-cn/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs b/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.trae-cn/skills/impeccable/scripts/live/ui-surfaces.mjs b/.trae-cn/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.trae-cn/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.trae/skills/impeccable/reference/bolder.md b/.trae/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.trae/skills/impeccable/reference/bolder.md +++ b/.trae/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.trae/skills/impeccable/reference/new-work.md b/.trae/skills/impeccable/reference/new-work.md index f70c82584..5c805afc8 100644 --- a/.trae/skills/impeccable/reference/new-work.md +++ b/.trae/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .trae/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .trae/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .trae/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .trae/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.trae/skills/impeccable/reference/visualize.md b/.trae/skills/impeccable/reference/visualize.md index ef675864c..247ea2067 100644 --- a/.trae/skills/impeccable/reference/visualize.md +++ b/.trae/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.trae/skills/impeccable/scripts/concept-seed.mjs b/.trae/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.trae/skills/impeccable/scripts/concept-seed.mjs +++ b/.trae/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this 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. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.trae/skills/impeccable/scripts/lib/impeccable-config.mjs b/.trae/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.trae/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.trae/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.trae/skills/impeccable/scripts/live-browser.js b/.trae/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.trae/skills/impeccable/scripts/live-browser.js +++ b/.trae/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs b/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.trae/skills/impeccable/scripts/live/ui-surfaces.mjs b/.trae/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.trae/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.vibe/skills/impeccable/reference/bolder.md b/.vibe/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.vibe/skills/impeccable/reference/bolder.md +++ b/.vibe/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.vibe/skills/impeccable/reference/new-work.md b/.vibe/skills/impeccable/reference/new-work.md index bda245059..4adf881d6 100644 --- a/.vibe/skills/impeccable/reference/new-work.md +++ b/.vibe/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .vibe/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .vibe/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .vibe/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .vibe/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.vibe/skills/impeccable/reference/visualize.md b/.vibe/skills/impeccable/reference/visualize.md index 87e410295..a8bed5230 100644 --- a/.vibe/skills/impeccable/reference/visualize.md +++ b/.vibe/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.vibe/skills/impeccable/scripts/concept-seed.mjs b/.vibe/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.vibe/skills/impeccable/scripts/concept-seed.mjs +++ b/.vibe/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this 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. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.vibe/skills/impeccable/scripts/lib/impeccable-config.mjs b/.vibe/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.vibe/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.vibe/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.vibe/skills/impeccable/scripts/live-browser.js b/.vibe/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.vibe/skills/impeccable/scripts/live-browser.js +++ b/.vibe/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.vibe/skills/impeccable/scripts/live/browser-script-parts.mjs b/.vibe/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.vibe/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.vibe/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.vibe/skills/impeccable/scripts/live/ui-surfaces.mjs b/.vibe/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.vibe/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/plugin/skills/impeccable/reference/bolder.md b/plugin/skills/impeccable/reference/bolder.md index fced49456..a5c34cd3e 100644 --- a/plugin/skills/impeccable/reference/bolder.md +++ b/plugin/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/plugin/skills/impeccable/reference/new-work.md b/plugin/skills/impeccable/reference/new-work.md index 5161d25cb..718b4bd0b 100644 --- a/plugin/skills/impeccable/reference/new-work.md +++ b/plugin/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .claude/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .claude/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .claude/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .claude/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -80,7 +82,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/plugin/skills/impeccable/reference/visualize.md b/plugin/skills/impeccable/reference/visualize.md index 94c337f15..6df08b30b 100644 --- a/plugin/skills/impeccable/reference/visualize.md +++ b/plugin/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/plugin/skills/impeccable/scripts/concept-seed.mjs b/plugin/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/plugin/skills/impeccable/scripts/concept-seed.mjs +++ b/plugin/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this 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. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/plugin/skills/impeccable/scripts/lib/impeccable-config.mjs b/plugin/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/plugin/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/plugin/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/plugin/skills/impeccable/scripts/live-browser.js b/plugin/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/plugin/skills/impeccable/scripts/live-browser.js +++ b/plugin/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs b/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/plugin/skills/impeccable/scripts/live/ui-surfaces.mjs b/plugin/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/plugin/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; }