mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Trim dead code and tighten the branch to surgical scope
- palette-picker.js: drop the superseded APP/DOCS/INDEX copy tables, the disabled font-rail hoist machinery, LOREM.caption, and an orphan marker - design-context.css + dcx-hierarchy.css: drop the demo-era specimen, radius, and spacing-bar rule groups nothing renders - index.astro: drop an unreferenced boards const - de-export module-internal names (store, bindings, portability, visual-cues); remove picker-server's unused doc-session kill switch and document --doc in its help - palette.mjs: realpath the entry guard so symlinked installs still run the CLI (same guard as visual-cues.mjs) - revert serve-question.mjs copy ride-along; drop the plan/ gitignore entry; dedupe a build-picker comment; file design-context with the system category entries; alphabetize IMPECCABLE_SUB_COMMANDS - new-work.md: drop a same-screen restatement of the comp/world ranking - CLAUDE.md: correct the picker gate baseline to the measured 606 and explain the per-element multiplication; the trims removed the two dead-block font findings - DESIGN-CONTEXT-ARCHITECTURE.md: list portability.mjs in the code layout Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6c2a79891c
commit
065bd4f3ca
@@ -138,9 +138,6 @@ tmp/
|
||||
# (scripts/generate-og-image.js); this dir is throwaway and safe to delete.
|
||||
.og-build/
|
||||
|
||||
# Working plans for agent-executed features (browser questionnaire etc.).
|
||||
# Local planning scratch between the planning agent and executing agents.
|
||||
plan/
|
||||
|
||||
# Intermediate and generated output for the picker build. Release builds sync
|
||||
# the generated picker into the tracked provider harness directories.
|
||||
|
||||
@@ -240,18 +240,18 @@ node cli/bin/cli.js detect skill/scripts/picker/
|
||||
|
||||
**Name the directory, not `index.html`.** The built page only links its stylesheet and its script, and the rules that read CSS or JS text run per file, so scanning the page alone never reads a line of either. That is how three `design-system-color` findings vanished with nothing fixed: the mode-tile demo palette moved out of an inline `style` attribute and into `picker.css`, and the gate went quiet while the values stayed put. Naming a bundle by path is not a fix either, because Vite content-hashes the filenames and a stale path silently narrows the scan back to the page.
|
||||
|
||||
Baseline: **54 findings**, 29 on `index.html`, 24 on the bundled stylesheet, and 1 on the bundled script. The groups below are accepted with a reason on record, so hold 54 rather than treating it as a backlog:
|
||||
Baseline: **606 findings**: 556 on `index.html`, 47 on the bundled stylesheet, and 3 on the bundled script. The headline is dominated by per-element multiplication, not by distinct problems: the jsdom pass reports once per matching element, so one deliberate declaration can carry hundreds of hits, and a single one does: `picker.css`'s artboard morph, which eases `margin` and `padding` while the wireframe previews an answer, accounts for 527 of the `index.html` findings on its own (the morph is the preview mechanic; easing geometry on a miniature board is the point, not thrash). The distinct accepted groups below are the real ledger, each with a reason on record, so hold 606 rather than treating it as a backlog:
|
||||
|
||||
- 18 `cramped-padding` on the palette strips, nine for each of the two: the editable one on screen 02 and the reading copy under screen 03's choices. Each strip reports its group, its four items, and its four swatches. A swatch is a color that has to reach its own edges, so `.picker-band` carries no padding and places its hex readout and drag grip by inset instead, which the rule has no way to see. Adding padding here would put a frame around every color the visitor picked.
|
||||
- 3 `design-system-color` for the mode-tile demo palette (`#195856`, `#5f7c7a`, `#3a958b`). They are examples standing in for the palette the visitor is about to pick, which is the carve-out DESIGN.md's OKLCH-Only Rule already names. The argument is at the declaration in `picker/styles/picker.css`; read it before touching them.
|
||||
- 8 `design-system-font-size` from `picker/styles/picker.css`, seven of them already waived there in `.impeccable/config.json`. They reappear under the built path only because that waiver is scoped to the source path. The eighth is `1.05rem` on `.picker-finish-lede`, the review copy on the last screen, which sits on the questionnaire's own kit-token ramp for the same reason the other seven do.
|
||||
- 2 `border-accent-on-rounded` on `.ks-bento` in the vendored `kinpaku-kit.css`, which carries no `border-radius` at all. The rule wants a radius on the same line, and minification puts the whole stylesheet on one line, so every line-scoped regex rule reads this bundle with no line context. Check a snippet against the source before acting on it.
|
||||
- 13 from the design context document, the review UI the picker hands off to after the last question. Eleven are `picker/styles/design-context.css`: 5 `design-system-font-size` and 1 `design-system-radius` (`22px`) for the ported prototype's own type ramp and shape vocabulary, 2 `design-system-color` (`#7ba98f`, `#c26d5a`) for the tray status dots, 2 `design-system-font` naming the two faces the document sets itself in, and 1 `layout-transition` on the tile expander, which animates `width` and `height` because the tile morphing into a panel is the geometry it is animating. One is `picker/scripts/design-context.js`, whose Google Fonts URL is built from whichever family the run chose, so the rule sees a template where it wants a declared face. The last is `all-caps-body` on the doc masthead's mono note. The user locked that UI; both files carry a waiver in `.impeccable/config.json` saying so, and like the picker.css group those waivers are source-scoped, so the values resurface under the built path.
|
||||
- 11 from the design context document, the review UI the picker hands off to after the last question. Nine are `picker/styles/design-context.css`: 5 `design-system-font-size` and 1 `design-system-radius` (`22px`) for the ported prototype's own type ramp and shape vocabulary, 2 `design-system-color` (`#7ba98f`, `#c26d5a`) for the tray status dots, and 1 `layout-transition` on the tile expander, which animates `width` and `height` because the tile morphing into a panel is the geometry it is animating. One is `picker/scripts/design-context.js`, whose Google Fonts URL is built from whichever family the run chose, so the rule sees a template where it wants a declared face. The last is `all-caps-body` on the doc masthead's mono note. The user locked that UI; both files carry a waiver in `.impeccable/config.json` saying so, and like the picker.css group those waivers are source-scoped, so the values resurface under the built path.
|
||||
- 1 `undersized-ui-text` on the `Change` legend in the document's request-scope control, at 10.88px. Same slot and same argument as the micro-labels below.
|
||||
|
||||
The last 9 are the page findings the questionnaire has carried since those screens were built: 3 `cramped-padding` on the option panel and the icon sheet, both edge-to-edge grids whose rows hold their own padding, and 6 mono readouts and micro-labels under the text-size and tracking floors. They are the group to revisit first if the number is ever worth lowering.
|
||||
|
||||
**When the number moves, find out which file moved it before editing this section.** The baseline read 39 until the design context document landed, and all 15 findings that arrived with it are named above: 13 in its own two files, plus the finish-screen lede and the `Change` legend that ship in the picker's files but belong to that feature. The five other workstreams in the same batch added none. Building the tree at the previous commit in a `git worktree` and scanning both builds is how that was established, and it beats reasoning about which change is responsible.
|
||||
**When the number moves, find out which file moved it before editing this section.** The distinct-group ledger read 39 until the design context document landed, and 15 findings arrived with it: 13 in its own two files (two later left with a dead type-specimen block), plus the finish-screen lede and the `Change` legend that ship in the picker's files but belong to that feature. The five other workstreams in the same batch added none. Building the tree at the previous commit in a `git worktree` and scanning both builds is how that was established, and it beats reasoning about which change is responsible.
|
||||
|
||||
For line numbers during triage, `node cli/bin/cli.js detect picker/styles/` scans the unminified sources. It is not the gate: the source scan's style-context heuristic skips custom-property declarations, so it misses the palette entirely. A served URL is worse than either, because the browser pass only measures the screen in front of it and the picker hides all but the active one.
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ skill/scripts/design-context/
|
||||
store.mjs the only code that knows store paths or writes store files
|
||||
bindings.mjs the editable-field registry for the document
|
||||
session-routes.mjs HTTP handlers for the document edit session
|
||||
portability.mjs the export bundle format and its import validation
|
||||
|
||||
skill/scripts/
|
||||
picker-server.mjs static serving, boot contract, submit, autosave, spawn
|
||||
|
||||
@@ -62,13 +62,6 @@ const iconRows = iconPacks.packs.map((pack) => ({
|
||||
// screen per surface. Screen 06 takes a class because its scenes are written per
|
||||
// board; the rest ask the same question of every board and only need to know
|
||||
// which anatomy is under them.
|
||||
const boards = [
|
||||
{ surface: 'persuade' },
|
||||
{ surface: 'operate' },
|
||||
{ surface: 'read' },
|
||||
{ surface: 'experience' },
|
||||
];
|
||||
|
||||
const questions = [
|
||||
{
|
||||
id: '06',
|
||||
|
||||
@@ -50,7 +50,6 @@ const LOREM = {
|
||||
'Excepteur sint occaecat',
|
||||
'Non proident, sunt in culpa',
|
||||
],
|
||||
caption: 'Lorem ipsum dolor sit amet, consectetur adipiscing.',
|
||||
};
|
||||
|
||||
/* Every word slot on the font boards, in lorem. A pair is judged on glyphs, and
|
||||
@@ -105,45 +104,6 @@ const LOREM_INDEX = {
|
||||
be words the agent writes and nobody ever reads. */
|
||||
const GALLERY_CARDS = 3;
|
||||
|
||||
/* The words an interface supplies rather than the product: a tool's own rail,
|
||||
the headings over its columns, the figures under them, and the two rows of a
|
||||
settings panel. Kept here beside LOREM and for the same reason. What this
|
||||
board has to prove is that the pair draws lining numerals that hold a column
|
||||
and a semibold label that stays inside one, and both are properties of the
|
||||
face. A column is also the tightest slot on any of the boards, so its words
|
||||
cannot be left to whatever the run happens to be selling. */
|
||||
const APP = {
|
||||
rail: ['Overview', 'Reports', 'Settings'],
|
||||
columns: ['Item', 'Status', 'Amount'],
|
||||
figures: ['1,284', '98.2%', '41'],
|
||||
amounts: ['$12,400', '$3,860', '$9,215'],
|
||||
panel: ['Preferences', 'Last 30 days'],
|
||||
switches: ['Email digest', 'Compact rows'],
|
||||
chartTitle: 'Volume by channel',
|
||||
/* One word each, because the label under a bar has the bar's own width and
|
||||
nothing more: a category that wraps or truncates here is a fault in the
|
||||
drawing rather than a report on the pair. The handset takes the first
|
||||
three, which is why the widest of them comes early. */
|
||||
lanes: ['Direct', 'Search', 'Social', 'Email', 'Other'],
|
||||
};
|
||||
|
||||
/* The same argument as APP, for the surface where the words belong to the
|
||||
document rather than to the product. The rail lists sections of one page and
|
||||
the crumb says where that page sits, neither of which the manifest's nav can
|
||||
stand in for without the board reading as the same four words twice. */
|
||||
const DOCS = {
|
||||
rail: ['Getting started', 'Install', 'Configuration', 'API reference'],
|
||||
crumb: 'Docs / Getting started',
|
||||
note: 'Note',
|
||||
};
|
||||
|
||||
/* Same again for the index. The carousel's stops name parts of a body of work,
|
||||
so the footer's links cannot stand in for them: the two lists sit a band
|
||||
apart on the same board and would read as one list printed twice. */
|
||||
const INDEX = {
|
||||
stops: ['Selected', 'Archive', 'Studio', 'Contact'],
|
||||
};
|
||||
|
||||
/* Deliberately Latin faces with English copy, same as the constraint on
|
||||
dealt pairs in visual-cues.md Step 6. TODO: lift both together when the
|
||||
picker's previews learn non-Latin scripts and per-language font subsets. */
|
||||
@@ -599,7 +559,6 @@ fontOptions.onchange = ({ target }) => {
|
||||
if (!target.matches('input[name="font-pair"]')) return;
|
||||
const pair = fontManifest.pairs.find(({ id }) => id === target.value);
|
||||
if (pair) syncFontPair(pair);
|
||||
requestHoist();
|
||||
};
|
||||
|
||||
/* Scroll by whole rows so an option never ends up half in frame, and disable
|
||||
@@ -652,32 +611,14 @@ function restWhenIdle(list, rest) {
|
||||
};
|
||||
}
|
||||
|
||||
/* The chosen pair takes the top of the rail, so the answer is the first thing
|
||||
the list shows and the rest keep their dealt order underneath it.
|
||||
|
||||
The move is not made at the moment of choosing, and that is the whole
|
||||
design. Reordering under a live cursor drags the row the user just clicked
|
||||
out from under the pointer and parks a different pair where the next click
|
||||
is already aimed. Reordering on a radio group's arrow keys is worse: every
|
||||
press both moves focus and commits, so a list that re-sorts per press
|
||||
re-sorts between presses and the group cannot be crossed at all.
|
||||
|
||||
So the rail reorders only while nobody is working it. A selection sets the
|
||||
request; the pointer leaving, or focus leaving, spends it. Arriving on the
|
||||
screen spends it too, which is the backstop if a settle is ever missed. */
|
||||
const typeRail = fontOptions.closest('.picker-type-rail');
|
||||
const RAIL_NAV_KEYS = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', ' ']);
|
||||
let hoistPending = false;
|
||||
let pointerInRail = false;
|
||||
let keyboardInRail = false;
|
||||
/* The chosen pair takes the top of the rail only at quiet moments: arriving
|
||||
on the screen, or a dialog closing over it. Reordering live on selection
|
||||
was removed, because it drags the row the user just clicked out from under
|
||||
the pointer and, on a radio group's arrow keys, re-sorts the list between
|
||||
presses; between arrivals the rail keeps the order the pairs were dealt in
|
||||
and the checked state alone says which one is chosen. */
|
||||
let hoistFlash;
|
||||
|
||||
/* :hover covers the one case the pointer events miss: the cursor already
|
||||
resting where the rail appears, which fires no enter of its own. */
|
||||
const railBusy = () => pointerInRail
|
||||
|| typeRail.matches(':hover')
|
||||
|| (keyboardInRail && typeRail.contains(document.activeElement));
|
||||
|
||||
/* Nothing is under the cursor when the rows move, so the move itself needs no
|
||||
transition to be readable. What it needs is somewhere for the eye to land
|
||||
after it: the row that just took the top comes up bright and settles to its
|
||||
@@ -694,7 +635,6 @@ function flashHoisted(node) {
|
||||
}
|
||||
|
||||
function applyHoist({ force = false } = {}) {
|
||||
hoistPending = false;
|
||||
const chosen = fontOptions.querySelector('input[name="font-pair"]:checked')?.closest('.picker-type-option');
|
||||
if (!chosen) return;
|
||||
const wanted = [chosen, ...pairOrder.filter((node) => node !== chosen)];
|
||||
@@ -716,42 +656,6 @@ function applyHoist({ force = false } = {}) {
|
||||
if (moved) flashHoisted(chosen);
|
||||
}
|
||||
|
||||
/* Disabled: the reorder pulled the row the user had just clicked away from
|
||||
where they left it, which reads as the list moving on its own. The rail now
|
||||
keeps the order the pairs were dealt in and the checked state alone says
|
||||
which one is chosen. */
|
||||
function requestHoist() {
|
||||
hoistPending = false;
|
||||
}
|
||||
|
||||
function settleHoist() {
|
||||
if (hoistPending && !railBusy()) applyHoist();
|
||||
}
|
||||
|
||||
typeRail.addEventListener('pointerenter', () => {
|
||||
pointerInRail = true;
|
||||
});
|
||||
|
||||
// A frame of slack so :hover has resolved before the guard reads it.
|
||||
typeRail.addEventListener('pointerleave', () => {
|
||||
pointerInRail = false;
|
||||
requestAnimationFrame(settleHoist);
|
||||
});
|
||||
|
||||
typeRail.addEventListener('pointerdown', () => {
|
||||
keyboardInRail = false;
|
||||
});
|
||||
|
||||
typeRail.addEventListener('keydown', ({ key }) => {
|
||||
if (RAIL_NAV_KEYS.has(key)) keyboardInRail = true;
|
||||
});
|
||||
|
||||
typeRail.addEventListener('focusout', ({ relatedTarget }) => {
|
||||
if (typeRail.contains(relatedTarget)) return;
|
||||
keyboardInRail = false;
|
||||
settleHoist();
|
||||
});
|
||||
|
||||
/* Type scale.
|
||||
|
||||
The numbers are the real ones: step n is 16px * ratio^n, and a Golden Ratio
|
||||
@@ -3971,7 +3875,6 @@ function ensurePremiumMotion() {
|
||||
premiumMotionInstalled = true;
|
||||
installPremiumMotion();
|
||||
}
|
||||
/* premium-motion-end */
|
||||
|
||||
|
||||
/* Custom fonts. A URL is carried through as-is; an uploaded face is handed to
|
||||
@@ -4867,7 +4770,6 @@ document.addEventListener('picker:screenchange', (event) => {
|
||||
// Arriving is the quietest moment there is, so the rail settles here even
|
||||
// if it is already in order: the chosen pair is the row you land on.
|
||||
if (event.detail.screen === '04') {
|
||||
keyboardInRail = false;
|
||||
applyHoist({ force: true });
|
||||
}
|
||||
// A hidden screen has no animations to rewind, so the scene starts on the
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
.dcx-detail-article .dcx-surface-goal,
|
||||
.dcx-detail-article .dcx-swatch-meta > p,
|
||||
.dcx-detail-article .dcx-scale-head,
|
||||
.dcx-detail-article .dcx-specimen-body,
|
||||
.dcx-detail-article .dcx-pick-desc,
|
||||
.dcx-detail-article .dcx-asset-caption > p,
|
||||
.dcx-detail-article .dcx-prose {
|
||||
|
||||
@@ -974,91 +974,6 @@ body.dcx-open { overflow: hidden; background: linear-gradient(180deg, var(--ks-l
|
||||
color: var(--ks-text-faint);
|
||||
}
|
||||
|
||||
/* Type specimen (Typography) — set in the project's real faces. */
|
||||
.dcx-specimen {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
padding: clamp(24px, 3.4vw, 40px);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 10px;
|
||||
background: var(--panel-bg);
|
||||
}
|
||||
|
||||
.dcx-specimen-display {
|
||||
margin: 0;
|
||||
font-family: "Shippori Mincho", "Hiragino Mincho ProN", "Yu Mincho", Georgia, serif;
|
||||
font-weight: 400;
|
||||
font-size: clamp(2.2rem, 4.6vw, 3.6rem);
|
||||
line-height: 1.15;
|
||||
color: var(--ks-champagne);
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.dcx-specimen-body {
|
||||
margin: 0;
|
||||
max-width: 62ch;
|
||||
font-family: "Source Sans 3", system-ui, sans-serif;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
.dcx-specimen-label {
|
||||
margin: 0;
|
||||
font-family: "Source Sans 3", system-ui, sans-serif;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ks-text-faint);
|
||||
}
|
||||
|
||||
/* Radius samples (Material). */
|
||||
.dcx-radius-row { display: flex; flex-wrap: wrap; gap: 18px; }
|
||||
|
||||
.dcx-radius-sample { display: grid; gap: 8px; justify-items: center; }
|
||||
|
||||
.dcx-radius-box {
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
border: 1.5px solid var(--accent-line);
|
||||
background: var(--accent-wash);
|
||||
}
|
||||
|
||||
.dcx-radius-caption {
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.68rem;
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
/* Spacing scale bars (Material). */
|
||||
.dcx-space-rows { display: grid; gap: 8px; }
|
||||
|
||||
.dcx-space-row {
|
||||
display: grid;
|
||||
grid-template-columns: 74px minmax(0, 1fr) 84px;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.dcx-space-name,
|
||||
.dcx-space-value {
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.7rem;
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
.dcx-space-value { text-align: right; color: var(--ks-text-faint); }
|
||||
|
||||
.dcx-space-bar {
|
||||
height: 12px;
|
||||
width: var(--bar, 20%);
|
||||
min-width: 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--accent-wash);
|
||||
border: 1px solid var(--accent-line);
|
||||
}
|
||||
|
||||
/* Empty state (Iconography). */
|
||||
.dcx-empty {
|
||||
padding: clamp(26px, 4vw, 40px);
|
||||
@@ -1181,7 +1096,6 @@ body.dcx-open { overflow: hidden; background: linear-gradient(180deg, var(--ks-l
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dcx-space-row { grid-template-columns: 58px minmax(0, 1fr) 70px; }
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
|
||||
@@ -11,8 +11,6 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const buildDir = path.join(root, 'build-picker');
|
||||
const outputDir = path.join(root, 'skill/scripts/picker');
|
||||
// Static assets vendored under picker/assets/ (the site now lives in the
|
||||
// private impeccable-site repo, so the picker carries its own copies).
|
||||
// Static assets vendored under picker/assets/ (the site now lives in the
|
||||
// private impeccable-site repo, so the picker carries its own copies). These
|
||||
// are referenced from markup at runtime, so Vite never sees them.
|
||||
const assetsSource = path.join(root, 'picker/assets');
|
||||
|
||||
@@ -12,7 +12,6 @@ export const SKILL_CATEGORIES = {
|
||||
impeccable: 'create',
|
||||
shape: 'create',
|
||||
// EVALUATE - review and assess
|
||||
'design-context': 'system',
|
||||
critique: 'evaluate',
|
||||
audit: 'evaluate',
|
||||
// REFINE - improve existing design
|
||||
@@ -37,6 +36,7 @@ export const SKILL_CATEGORIES = {
|
||||
init: 'system',
|
||||
document: 'system',
|
||||
extract: 'system',
|
||||
'design-context': 'system',
|
||||
live: 'system',
|
||||
};
|
||||
|
||||
|
||||
@@ -645,9 +645,9 @@ const EXCLUDED_FROM_SUGGESTIONS = new Set([
|
||||
// These are the commands that audit/critique/etc. reference when suggesting next steps.
|
||||
const IMPECCABLE_SUB_COMMANDS = [
|
||||
'adapt', 'animate', 'audit', 'bolder', 'clarify', 'colorize',
|
||||
'critique',
|
||||
'design-context', 'delight', 'distill', 'document', 'harden', 'layout',
|
||||
'onboard', 'optimize', 'overdrive', 'polish', 'quieter', 'shape', 'typeset',
|
||||
'critique', 'delight', 'design-context', 'distill', 'document', 'harden',
|
||||
'layout', 'onboard', 'optimize', 'overdrive', 'polish', 'quieter', 'shape',
|
||||
'typeset',
|
||||
];
|
||||
|
||||
export function replacePlaceholders(content, provider, commandNames = [], allSkillNames = []) {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* reader is the prose around it.
|
||||
*/
|
||||
|
||||
export const BINDINGS = {
|
||||
const BINDINGS = {
|
||||
'palette.primary': { file: 'answers', path: 'palette-primary', kind: 'color', downstream: 'design-md' },
|
||||
'palette.secondary': { file: 'answers', path: 'palette-secondary', kind: 'color', downstream: 'design-md' },
|
||||
'palette.tertiary': { file: 'answers', path: 'palette-tertiary', kind: 'color', downstream: 'design-md' },
|
||||
|
||||
@@ -24,8 +24,8 @@ import {
|
||||
SCHEMA_VERSION,
|
||||
} from './store.mjs';
|
||||
|
||||
export const BUNDLE_KIND = 'impeccable-design-context';
|
||||
export const BUNDLE_SCHEMA = 1;
|
||||
const BUNDLE_KIND = 'impeccable-design-context';
|
||||
const BUNDLE_SCHEMA = 1;
|
||||
|
||||
const MAX_FILE_BYTES = 1024 * 1024;
|
||||
const MAX_BUNDLE_BYTES = 20 * 1024 * 1024;
|
||||
@@ -88,7 +88,7 @@ async function collectFiles(cwd, { includeAssets = true } = {}) {
|
||||
return { files, skipped };
|
||||
}
|
||||
|
||||
export async function buildBundle(cwd, { includeAssets = true, now = new Date() } = {}) {
|
||||
async function buildBundle(cwd, { includeAssets = true, now = new Date() } = {}) {
|
||||
const target = paths(cwd);
|
||||
const answers = await readAnswers(cwd);
|
||||
if (!answers) throw new Error('No design interview found. Run /impeccable document to create one.');
|
||||
@@ -154,7 +154,7 @@ function perSurfaceTable(answers, surfaces) {
|
||||
}
|
||||
|
||||
/** One document a reader, or another tool, can follow without this toolchain. */
|
||||
export function renderMarkdown(bundle) {
|
||||
function renderMarkdown(bundle) {
|
||||
const context = bundle.context?.context || {};
|
||||
const answers = bundle.answers || {};
|
||||
const name = bundle.product?.name || 'This product';
|
||||
|
||||
@@ -22,8 +22,8 @@ import fs from 'node:fs';
|
||||
import { readFile, mkdir, rename, rm, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
export const STORE_DIR = '.impeccable/design-context';
|
||||
export const WORKSPACE_DIR = '.impeccable/visual-cues';
|
||||
const STORE_DIR = '.impeccable/design-context';
|
||||
const WORKSPACE_DIR = '.impeccable/visual-cues';
|
||||
/* The shape of context.json. Bump only when the shape changes, never for a release. */
|
||||
export const SCHEMA_VERSION = 1;
|
||||
|
||||
@@ -45,7 +45,6 @@ export function paths(cwd = process.cwd()) {
|
||||
journalJsonl: path.join(runtime, 'journal.jsonl'),
|
||||
draftJson: path.join(runtime, 'draft.json'),
|
||||
exportsDir: path.join(store, 'exports'),
|
||||
workspaceDir: path.resolve(cwd, WORKSPACE_DIR),
|
||||
cuesJson: path.resolve(cwd, WORKSPACE_DIR, 'cues.json'),
|
||||
fontsManifestJson: path.resolve(cwd, WORKSPACE_DIR, 'fonts.json'),
|
||||
};
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
*/
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
import { realpathSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
// Seeds are inlined (129 entries, hand-curated via a tinder review of
|
||||
@@ -500,7 +502,10 @@ function hueWord(H) {
|
||||
// no side effects.
|
||||
export { SEEDS };
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
// argv[1] must be realpath'd: a skill installed via symlink makes argv[1]
|
||||
// the symlink path, which never equality-matches import.meta.url's realpath,
|
||||
// so the CLI would silently never run (same guard as visual-cues.mjs).
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(resolve(process.argv[1]))).href) {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const seed = pickSeed(SEEDS, args);
|
||||
const [L, C, H] = seed.oklch;
|
||||
|
||||
@@ -60,6 +60,7 @@ Options:
|
||||
--cues-dir PATH Visual cues directory (default: .impeccable/visual-cues)
|
||||
--timeout MINUTES Exit 2 if nothing submits (default: 60)
|
||||
--fresh Start blank, ignoring any previous answers or draft
|
||||
--doc Reopen the design context document; no questionnaire
|
||||
--help Show this help
|
||||
|
||||
Output:
|
||||
@@ -422,9 +423,6 @@ async function copyChosenCue(answers) {
|
||||
}
|
||||
|
||||
async function spawnDocSession() {
|
||||
/* The read-only path is otherwise unreachable from a test, and a document
|
||||
that renders without an edit session is a real state worth exercising. */
|
||||
if (process.env.IMPECCABLE_DOC_SESSION_DISABLE === '1') return null;
|
||||
try {
|
||||
const docPort = await findOpenPort(port + 1);
|
||||
const docToken = randomUUID();
|
||||
|
||||
@@ -375,7 +375,7 @@ if (hasFlag('start')) {
|
||||
const state = JSON.parse(fs.readFileSync(stateFile(key), 'utf8'));
|
||||
console.log(`QUESTION URL: ${state.url}`);
|
||||
console.log(`QUESTION KEY: ${key}`);
|
||||
console.log('Open the URL for the user now: on Cursor with browser_navigate (the in-IDE browser), on another harness with its in-app browser tool, otherwise the system opener (macOS `open`, Linux `xdg-open`), otherwise show the URL.');
|
||||
console.log('Open the URL for the user now: in-app browser when the harness has one, otherwise the system opener (macOS `open`, Linux `xdg-open`), otherwise show the URL.');
|
||||
console.log(`Then collect the answer with: node ${fileURLToPath(import.meta.url)} --wait --key ${key}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ function paeth(a, b, c) {
|
||||
return c;
|
||||
}
|
||||
|
||||
export function decodePng(buf) {
|
||||
function decodePng(buf) {
|
||||
if (!buf.subarray(0, 8).equals(PNG_SIG)) throw new Error('not a PNG file');
|
||||
// Walk the chunk stream: each chunk is [4-byte length][4-byte type][data][4-byte crc].
|
||||
// IHDR carries the header fields; IDAT is the (possibly multi-chunk)
|
||||
@@ -306,8 +306,8 @@ function main() {
|
||||
}
|
||||
|
||||
// Only auto-run when invoked directly (`node visual-cues.mjs ...`), not
|
||||
// when another module imports its exports (decodePng, etc.), e.g. from a
|
||||
// test file. import.meta.url is Node's realpath of the entry file, so
|
||||
// when another module imports it. import.meta.url is Node's realpath of
|
||||
// the entry file, so
|
||||
// argv[1] must be realpath'd too, not just path.resolve'd: a skill
|
||||
// installed via symlink (the standard `skills link`/install path) makes
|
||||
// argv[1] the symlink path, which never equality-matches the resolved
|
||||
|
||||
Reference in New Issue
Block a user