@litfamily/litmuse 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +115 -26
- package/README_ko-KR.md +112 -23
- package/bin/litmuse.mjs +4 -0
- package/docs/assets/card/after.webp +0 -0
- package/docs/assets/card/idle.webp +0 -0
- package/docs/assets/card/working.webp +0 -0
- package/docs/assets/handoff/latest.webp +0 -0
- package/docs/assets/handoff/saved.webp +0 -0
- package/docs/assets/readme/demo.webp +0 -0
- package/lib/stage-card.mjs +180 -5
- package/package.json +7 -1
- package/plugin/.muse-plugin/plugin.json +26 -11
- package/plugin/commands/lit-status.md +2 -1
- package/plugin/commands/lit.md +1 -1
- package/plugin/hooks/lib/digest.mjs +1 -1
- package/plugin/hooks/lib/next-step.mjs +68 -0
- package/plugin/hooks/lib/routes.mjs +59 -0
- package/plugin/hooks/lib/workflow-stack.mjs +318 -0
- package/plugin/hooks/session-start.mjs +29 -2
- package/plugin/hooks/user-prompt-submit.mjs +138 -7
- package/plugin/lib/capped-fetch.mjs +25 -0
- package/plugin/lib/pinned-fetch.mjs +79 -0
- package/plugin/lib/plan-task-rows.mjs +32 -0
- package/plugin/lib/render_pages.py +29 -1
- package/plugin/lib/rewind/collect.mjs +679 -0
- package/plugin/lib/rewind/gauge.mjs +72 -0
- package/plugin/lib/rewind/next-step.mjs +91 -0
- package/plugin/lib/rewind/redact.mjs +83 -0
- package/plugin/lib/rewind/transcripts.mjs +227 -0
- package/plugin/skills/frontend-ui-ux/SKILL.md +24 -9
- package/plugin/skills/frontend-ui-ux/kit/components.css +414 -0
- package/plugin/skills/frontend-ui-ux/kit/fonts.json +22 -0
- package/plugin/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
- package/plugin/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
- package/plugin/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
- package/plugin/skills/frontend-ui-ux/kit/ui.js +226 -0
- package/plugin/skills/frontend-ui-ux/references/complete-contract.md +2 -1
- package/plugin/skills/frontend-ui-ux/references/craft-floor.md +9 -4
- package/plugin/skills/frontend-ui-ux/references/default-editorial-pixel.json +59 -7
- package/plugin/skills/frontend-ui-ux/references/house-styles.md +234 -0
- package/plugin/skills/frontend-ui-ux/references/motion-guide.md +3 -3
- package/plugin/skills/frontend-ui-ux/references/slop-register.md +7 -7
- package/plugin/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-lock.mjs +196 -0
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-page.mjs +31 -10
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-rules.mjs +19 -7
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-static.mjs +4 -0
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe.mjs +59 -7
- package/plugin/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
- package/plugin/skills/lit-adhd/SKILL.md +188 -0
- package/plugin/skills/lit-adhd/references/card-examples.md +121 -0
- package/plugin/skills/lit-adhd/references/status-taxonomy.md +43 -0
- package/plugin/skills/lit-adhd/scripts/rewind.mjs +11 -0
- package/plugin/skills/lit-diagram-drawer/SKILL.md +3 -3
- package/plugin/skills/lit-diagram-drawer/scripts/diagram-craft-rules.mjs +1 -2
- package/plugin/skills/lit-diagram-drawer/scripts/visual-quality.mjs +0 -1
- package/plugin/skills/lit-docx/SKILL.md +1 -1
- package/plugin/skills/lit-hijack/SKILL.md +589 -0
- package/plugin/skills/lit-hijack/references/capture-and-provenance.md +123 -0
- package/plugin/skills/lit-hijack/references/clean-room-build.md +133 -0
- package/plugin/skills/lit-hijack/references/distil-and-licence.md +128 -0
- package/plugin/skills/lit-hijack/references/gap-and-port-design.md +152 -0
- package/plugin/skills/lit-hijack/references/parity-and-report.md +133 -0
- package/plugin/skills/lit-hijack/scripts/hijack.mjs +879 -0
- package/plugin/skills/lit-pptx/SKILL.md +20 -9
- package/plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
- package/plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
- package/plugin/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
- package/plugin/skills/lit-pptx/fonts/provenance.json +32 -1
- package/plugin/skills/lit-pptx/references/direction-step.md +19 -2
- package/plugin/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
- package/plugin/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
- package/plugin/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +139 -0
- package/plugin/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +158 -0
- package/plugin/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +179 -0
- package/plugin/skills/lit-pptx/references/tonalities/bauhaus.md +112 -0
- package/plugin/skills/lit-pptx/references/tonalities/computational-brutalism.md +118 -0
- package/plugin/skills/lit-pptx/references/tonalities/neo-brutalism.md +125 -0
- package/plugin/skills/lit-pptx/references/tonalities/newsroom.md +115 -0
- package/plugin/skills/lit-pptx/references/tonalities/riso.md +109 -0
- package/plugin/skills/lit-pptx/scripts/compile-deck.js +1 -1
- package/plugin/skills/lit-pptx/scripts/craft_extras.py +136 -92
- package/plugin/skills/lit-pptx/scripts/embed_fonts.py +13 -1
- package/plugin/skills/lit-pptx/scripts/lib/grid-resolver.js +105 -116
- package/plugin/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
- package/plugin/skills/lit-pptx/scripts/lib/render-pack.js +368 -561
- package/plugin/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
- package/plugin/skills/lit-pptx/scripts/lib/template-registry.js +57 -6
- package/plugin/skills/lit-pptx/scripts/qa_deck.py +4 -4
- package/plugin/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
- package/plugin/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
- package/plugin/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
- package/plugin/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
- package/plugin/skills/lit-pptx/templates/tonalities/night/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/paper/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
- package/plugin/skills/lit-pptx/templates/tonalities/signal/pack.yaml +2 -2
- package/plugin/skills/lit-pptx/templates/tonalities/studio/pack.yaml +1 -1
- package/plugin/skills/lit-recap/SKILL.md +3 -0
- package/plugin/skills/lit-schematic/SKILL.md +241 -0
- package/plugin/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +14 -0
- package/plugin/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +8 -0
- package/plugin/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +22 -0
- package/plugin/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +73 -0
- package/plugin/skills/lit-schematic/references/cutaway-outputs.md +25 -0
- package/plugin/skills/lit-schematic/references/isometric-craft.md +24 -0
- package/plugin/skills/lit-schematic/references/journal-figures.md +19 -0
- package/plugin/skills/lit-schematic/references/kit-api.md +27 -0
- package/plugin/skills/lit-schematic/references/pfd-and-layout.md +20 -0
- package/plugin/skills/lit-schematic/references/pid-drafting.md +26 -0
- package/plugin/skills/lit-schematic/references/pid-symbols.md +27 -0
- package/plugin/skills/lit-schematic/references/verification.md +24 -0
- package/plugin/skills/lit-schematic/scripts/doctor.mjs +13 -0
- package/plugin/skills/lit-schematic/scripts/lib/figure.mjs +49 -0
- package/plugin/skills/lit-schematic/scripts/lib/isa.mjs +18 -0
- package/plugin/skills/lit-schematic/scripts/lib/iso.mjs +58 -0
- package/plugin/skills/lit-schematic/scripts/lib/legend-sheet.mjs +10 -0
- package/plugin/skills/lit-schematic/scripts/lib/qa-core.mjs +241 -0
- package/plugin/skills/lit-schematic/scripts/lib/sheet.mjs +446 -0
- package/plugin/skills/lit-schematic/scripts/lib/svg.mjs +39 -0
- package/plugin/skills/lit-schematic/scripts/lib/theme.mjs +25 -0
- package/plugin/skills/lit-schematic/scripts/motion.mjs +8 -0
- package/plugin/skills/lit-schematic/scripts/qa.mjs +21 -0
- package/plugin/skills/lit-schematic/scripts/render.mjs +38 -0
- package/plugin/skills/lit-schematic/scripts/scaffold.mjs +73 -0
- package/plugin/skills/lit-scientific-visualization/SKILL.md +2 -0
- package/plugin/skills/lit-typographic-motion/SKILL.md +12 -8
- package/plugin/skills/lit-typographic-motion/engine/THIRD_PARTY_NOTICES +11 -0
- package/plugin/skills/lit-typographic-motion/engine/blender/runner.py +218 -0
- package/plugin/skills/lit-typographic-motion/engine/core/presets.mjs +1 -1
- package/plugin/skills/lit-typographic-motion/engine/node/assets.mjs +218 -0
- package/plugin/skills/lit-typographic-motion/engine/node/blender.mjs +144 -0
- package/plugin/skills/lit-typographic-motion/engine/node/board.mjs +136 -0
- package/plugin/skills/lit-typographic-motion/engine/node/done.mjs +42 -3
- package/plugin/skills/lit-typographic-motion/engine/node/gallery.mjs +120 -0
- package/plugin/skills/lit-typographic-motion/engine/node/house.mjs +44 -0
- package/plugin/skills/lit-typographic-motion/engine/node/pipeline.mjs +18 -7
- package/plugin/skills/lit-typographic-motion/engine/node/runtime.mjs +6 -3
- package/plugin/skills/lit-typographic-motion/engine/node/scaffold.mjs +210 -0
- package/plugin/skills/lit-typographic-motion/engine/node/score.mjs +639 -0
- package/plugin/skills/lit-typographic-motion/engine/node/sound-cli.mjs +21 -4
- package/plugin/skills/lit-typographic-motion/engine/node/sound.mjs +126 -27
- package/plugin/skills/lit-typographic-motion/engine/node/stage-serve.mjs +29 -5
- package/plugin/skills/lit-typographic-motion/engine/node/stage.mjs +82 -14
- package/plugin/skills/lit-typographic-motion/engine/node/styles.mjs +214 -0
- package/plugin/skills/lit-typographic-motion/engine/node/treatment.mjs +311 -10
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
- package/plugin/skills/lit-typographic-motion/recipes/hero-3d/expected.json +69 -0
- package/plugin/skills/lit-typographic-motion/recipes/hero-3d/index.html +139 -0
- package/plugin/skills/lit-typographic-motion/references/depth.md +122 -0
- package/plugin/skills/lit-typographic-motion/references/sound.md +74 -16
- package/plugin/skills/lit-typographic-motion/references/stage.md +29 -5
- package/plugin/skills/lit-typographic-motion/references/studio.md +245 -0
- package/plugin/skills/lit-typographic-motion/references/treatment.md +31 -8
- package/plugin/skills/lit-typographic-motion/scripts/motion.mjs +48 -7
- package/plugin/skills/lit-typographic-motion/styles/paper.json +41 -0
- package/plugin/skills/lit-typographic-motion/styles/swiss-signal.json +41 -0
- package/plugin/skills/lit-typographic-motion/styles/terminalcore.json +42 -0
- package/plugin/skills/lit-typographic-motion/styles/tidal.json +41 -0
- package/plugin/skills/readme-studio/SKILL.md +1 -1
- package/plugin/skills/readme-studio/templates/hyperframes-cover/package-lock.json +3 -3
- package/plugin/skills/readme-studio/templates/remotion-cover/package-lock.json +3 -3
- package/plugin/skills/start-work/SKILL.md +6 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Progress gauges for the lit-adhd brief. Code draws them so the model copies them and never
|
|
2
|
+
// invents its own: `[▉▉▉░░] 3/5`, one cell per item up to ten, scaled to ten cells above that.
|
|
3
|
+
// A gauge renders only when both numbers are real. Otherwise the brief shows what is known as a
|
|
4
|
+
// fact line: an indeterminate pulse plus the finished count, or just the pulse. A `?` glyph must
|
|
5
|
+
// never reach the user on any surface.
|
|
6
|
+
|
|
7
|
+
const MAX_CELLS = 10;
|
|
8
|
+
const known = (value) => Number.isInteger(value) && value >= 0;
|
|
9
|
+
|
|
10
|
+
/** The fully-unknown pulse: nothing is known, not even a finished count. */
|
|
11
|
+
export const PULSE_UNKNOWN = '[…] running';
|
|
12
|
+
|
|
13
|
+
/** `[▉▉▉░░] 3/5`; an unknown total reads `[…] n steps`, an unknown count the pulse. */
|
|
14
|
+
export const gauge = (done, total) => {
|
|
15
|
+
if (!known(total)) return known(done) ? `[…] ${done} step${done === 1 ? "" : "s"}` : PULSE_UNKNOWN;
|
|
16
|
+
const k = known(done) ? Math.min(done, total) : 0;
|
|
17
|
+
const cells = Math.min(total, MAX_CELLS);
|
|
18
|
+
let filled = k;
|
|
19
|
+
if (total > MAX_CELLS) filled = k === total ? MAX_CELLS : k === 0 ? 0 : Math.min(MAX_CELLS - 1, Math.max(1, Math.floor((k * MAX_CELLS) / total)));
|
|
20
|
+
return `[${"▉".repeat(filled)}${"░".repeat(cells - filled)}] ${k}/${total}`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** `{done, total, gauge, remaining}` with `remaining` as recorded, or `["unknown"]`. */
|
|
24
|
+
export const progressOf = (done, total, remaining) => ({
|
|
25
|
+
done: known(done) ? done : null,
|
|
26
|
+
total: known(total) ? total : null,
|
|
27
|
+
gauge: gauge(done, total),
|
|
28
|
+
remaining: Array.isArray(remaining) && remaining.length ? remaining : known(total) && known(done) && done >= total ? [] : ["unknown"],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** One gauge over several: the summed totals, or the pulse line when any total is unknown. */
|
|
32
|
+
export const aggregateProgress = (items) => {
|
|
33
|
+
const done = items.reduce((sum, item) => sum + (known(item.done) ? item.done : 0), 0);
|
|
34
|
+
const total = items.length && items.every((item) => known(item.total)) ? items.reduce((sum, item) => sum + item.total, 0) : null;
|
|
35
|
+
return { done, total, gauge: gauge(items.length ? done : null, total) };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Terminal columns a character takes: combining marks and Hangul medial/final jamo take none, East
|
|
39
|
+
// Asian wide and fullwidth characters and most emoji take two.
|
|
40
|
+
const charWidth = (codePoint) => {
|
|
41
|
+
if (codePoint === 0 || codePoint < 32 || (codePoint >= 0x7f && codePoint < 0xa0)) return 0;
|
|
42
|
+
if (/\p{Mn}|\p{Me}|\p{Cf}/u.test(String.fromCodePoint(codePoint)) || (codePoint >= 0x1160 && codePoint <= 0x11ff)) return 0;
|
|
43
|
+
if (
|
|
44
|
+
(codePoint >= 0x1100 && codePoint <= 0x115f)
|
|
45
|
+
|| (codePoint >= 0x2e80 && codePoint <= 0xa4cf)
|
|
46
|
+
|| (codePoint >= 0xac00 && codePoint <= 0xd7a3)
|
|
47
|
+
|| (codePoint >= 0xf900 && codePoint <= 0xfaff)
|
|
48
|
+
|| (codePoint >= 0xfe30 && codePoint <= 0xfe4f)
|
|
49
|
+
|| (codePoint >= 0xff00 && codePoint <= 0xff60)
|
|
50
|
+
|| (codePoint >= 0xffe0 && codePoint <= 0xffe6)
|
|
51
|
+
|| (codePoint >= 0x1f300 && codePoint <= 0x1faff)
|
|
52
|
+
|| (codePoint >= 0x20000 && codePoint <= 0x3fffd)
|
|
53
|
+
) return 2;
|
|
54
|
+
return 1;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const displayWidth = (text) => [...String(text)].reduce((sum, ch) => sum + charWidth(ch.codePointAt(0)), 0);
|
|
58
|
+
|
|
59
|
+
/** Clips to `width` display columns, ending in `…` when anything was cut. */
|
|
60
|
+
export const clipToWidth = (text, width) => {
|
|
61
|
+
const value = String(text);
|
|
62
|
+
if (displayWidth(value) <= width) return value;
|
|
63
|
+
let out = "";
|
|
64
|
+
let used = 0;
|
|
65
|
+
for (const ch of value) {
|
|
66
|
+
const w = charWidth(ch.codePointAt(0));
|
|
67
|
+
if (used + w > width - 1) break;
|
|
68
|
+
out += ch;
|
|
69
|
+
used += w;
|
|
70
|
+
}
|
|
71
|
+
return `${out}…`;
|
|
72
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// The one next step a lit-adhd brief offers, chosen by a fixed priority over the collected records,
|
|
2
|
+
// and the small record `go` reads back:
|
|
3
|
+
// 1. this session's paused spine frame -> resume-frame (`lit continue`)
|
|
4
|
+
// 2. a start-work paused at a boundary -> approve-boundary (shown only, never automatic)
|
|
5
|
+
// 3. an active start-work of this session -> continue-work
|
|
6
|
+
// 4. a single open plan with no work running -> start-plan (`go` runs it)
|
|
7
|
+
// 5. otherwise -> none
|
|
8
|
+
import { closeSync, lstatSync, mkdirSync, openSync, renameSync, unlinkSync, writeSync } from 'node:fs';
|
|
9
|
+
import { dirname, join, resolve } from 'node:path';
|
|
10
|
+
|
|
11
|
+
export const chooseNextStep = (data, sessionId) => {
|
|
12
|
+
const ownStack = data.workflows.find((frame) => frame.sessionId === sessionId && frame.role === 'spine' && frame.status === 'paused');
|
|
13
|
+
if (sessionId && ownStack) {
|
|
14
|
+
return { kind: 'resume-frame', target: { skill: ownStack.skill, stage: ownStack.stage, progress: ownStack.progress.gauge }, say: 'lit continue' };
|
|
15
|
+
}
|
|
16
|
+
const paused = data.startWork.find((work) => work.status === 'paused' && work.pendingBoundary);
|
|
17
|
+
if (paused) {
|
|
18
|
+
return { kind: 'approve-boundary', target: { workId: paused.workId, plan: paused.plan, ...paused.pendingBoundary }, say: 'lit approve', automatic: false };
|
|
19
|
+
}
|
|
20
|
+
const own = data.startWork.find((work) => work.status === 'active' && sessionId && work.sessionId === sessionId);
|
|
21
|
+
if (own) return { kind: 'continue-work', target: { workId: own.workId, plan: own.plan }, say: 'go' };
|
|
22
|
+
const running = data.startWork.some((work) => ['active', 'paused'].includes(work.status));
|
|
23
|
+
const open = data.roadmap.plans.filter((plan) => plan.local && plan.progress.total !== null && plan.progress.done < plan.progress.total);
|
|
24
|
+
if (!running && open.length === 1) {
|
|
25
|
+
return { kind: 'start-plan', target: { plan: open[0].plan }, say: 'go', command: `go ${open[0].plan}` };
|
|
26
|
+
}
|
|
27
|
+
return { kind: 'none', target: null };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const fileName = (sessionId) => `next-${String(sessionId ?? 'session').replace(/[^A-Za-z0-9_-]/gu, '_').slice(0, 128) || 'session'}.json`;
|
|
31
|
+
|
|
32
|
+
export const nextStepPath = (stateRoot, sessionId) => join(stateRoot, '.litmuse', 'adhd', fileName(sessionId));
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Every component from the state root down must be a real directory: a `.litmuse`
|
|
36
|
+
* that is itself a link would otherwise smuggle the record outside the workspace.
|
|
37
|
+
*/
|
|
38
|
+
const dirOk = (root, dir) => {
|
|
39
|
+
const base = resolve(root);
|
|
40
|
+
const parts = [base, ...dirname(resolve(dir)).replace(base, '').split('/').filter(Boolean).map((_, i, all) => join(base, ...all.slice(0, i + 1)))];
|
|
41
|
+
for (const part of parts) {
|
|
42
|
+
try {
|
|
43
|
+
if (lstatSync(part).isSymbolicLink()) return false;
|
|
44
|
+
} catch { /* not there yet */ }
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
mkdirSync(dir, { recursive: true });
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
return !lstatSync(dir).isSymbolicLink() && lstatSync(dir).isDirectory();
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Exclusive staging write: a linked staging file is refused, a stale one is replaced. */
|
|
59
|
+
const writeStaging = (staging, text) => {
|
|
60
|
+
const once = () => {
|
|
61
|
+
const fd = openSync(staging, 'wx');
|
|
62
|
+
try {
|
|
63
|
+
writeSync(fd, text);
|
|
64
|
+
} finally {
|
|
65
|
+
closeSync(fd);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
try {
|
|
69
|
+
once();
|
|
70
|
+
return;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
73
|
+
}
|
|
74
|
+
if (lstatSync(staging).isSymbolicLink()) throw new Error(`refusing to write through a linked staging file: ${staging}`);
|
|
75
|
+
unlinkSync(staging);
|
|
76
|
+
once();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Writes `{kind, target, at}` for this session, whole, through a temporary file.
|
|
81
|
+
* Refuses linked state dirs and staging files, so the record never lands outside
|
|
82
|
+
* the workspace; callers treat a throw as not recorded.
|
|
83
|
+
*/
|
|
84
|
+
export const writeNextStep = (stateRoot, sessionId, step, now = new Date()) => {
|
|
85
|
+
const path = nextStepPath(stateRoot, sessionId);
|
|
86
|
+
if (!dirOk(stateRoot, dirname(path))) throw new Error(`refusing to write through a linked state dir: ${dirname(path)}`);
|
|
87
|
+
const staging = `${path}.${process.pid}.tmp`;
|
|
88
|
+
writeStaging(staging, `${JSON.stringify({ kind: step.kind, target: step.target, at: now.toISOString() })}\n`);
|
|
89
|
+
renameSync(staging, path);
|
|
90
|
+
return path;
|
|
91
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Redaction for every string the lit-adhd collector emits. Credential shapes and URI userinfo come
|
|
2
|
+
// from the shared secret-shape set; `NAME=value` pairs with a secret-like name lose their value; any
|
|
3
|
+
// substring equal to a current environment value is replaced (compared here, never printed:
|
|
4
|
+
// 8+ characters, 24+ for URL-form values such as webhooks); the home directory becomes `~`.
|
|
5
|
+
// Plain filesystem paths and locale/terminal values are not credentials and stay, or every path
|
|
6
|
+
// in the brief would be blanked.
|
|
7
|
+
|
|
8
|
+
import { SECRET_SHAPE_PATTERNS, URI_USERINFO_PATTERN } from '../secret-shapes.mjs';
|
|
9
|
+
|
|
10
|
+
const MASK = '[redacted]';
|
|
11
|
+
|
|
12
|
+
// Terminal escape sequences never reach collected text: a commit subject or plan row
|
|
13
|
+
// carrying them could repaint the brief or smuggle in a hyperlink when printed. ESC
|
|
14
|
+
// opens an OSC run (to BEL or ST) or a CSI run (to its final byte); the remaining C0
|
|
15
|
+
// controls, DEL and C1 go, except the tab that joins multi-line subjects upstream.
|
|
16
|
+
const stripTerminalRuns = (text) => {
|
|
17
|
+
let out = '';
|
|
18
|
+
let index = 0;
|
|
19
|
+
while (index < text.length) {
|
|
20
|
+
const code = text.charCodeAt(index);
|
|
21
|
+
if (code === 27) {
|
|
22
|
+
index += 1;
|
|
23
|
+
if (text[index] === ']' || text[index] === '[') {
|
|
24
|
+
const osc = text[index] === ']';
|
|
25
|
+
index += 1;
|
|
26
|
+
if (osc) {
|
|
27
|
+
while (index < text.length && text.charCodeAt(index) !== 7 && text.charCodeAt(index) !== 27) index += 1;
|
|
28
|
+
if (text.charCodeAt(index) === 27) index += 2;
|
|
29
|
+
else index += 1;
|
|
30
|
+
} else {
|
|
31
|
+
while (index < text.length && !/[A-Za-z]/.test(text[index])) index += 1;
|
|
32
|
+
index += 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (code < 32 ? code !== 9 : code === 127 || (code >= 128 && code <= 159)) {
|
|
38
|
+
index += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
out += text[index];
|
|
42
|
+
index += 1;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
};
|
|
46
|
+
const SECRET_NAME = /KEY|TOKEN|SECRET|PASS|AUTH|COOKIE/iu;
|
|
47
|
+
const JSON_SECRET = /("[A-Za-z_][A-Za-z0-9_]*"|'[A-Za-z_][A-Za-z0-9_]*')\s*:\s*("[^"]*"|'[^']*'|[^\s"'`,}]+)/gu;
|
|
48
|
+
const ASSIGNMENT = /\b([A-Za-z_][A-Za-z0-9_]*)=("[^"]*"|'[^']*'|[^\s"'`]+)/gu;
|
|
49
|
+
const PLAIN_ENV_NAMES = /^(?:PATH|HOME|PWD|OLDPWD|SHELL|USER|LOGNAME|LANG|LANGUAGE|TERM|TERM_PROGRAM|TERM_PROGRAM_VERSION|COLORTERM|EDITOR|VISUAL|PAGER|TMPDIR|SHLVL|LC_[A-Z]+|XPC_[A-Z_]+|__CF[A-Z_]*|COMMAND_MODE|_)$/u;
|
|
50
|
+
const globalOf = (pattern) => new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`);
|
|
51
|
+
const SHAPES = [...SECRET_SHAPE_PATTERNS.map(globalOf)];
|
|
52
|
+
const USERINFO = globalOf(URI_USERINFO_PATTERN);
|
|
53
|
+
|
|
54
|
+
const envSecrets = (env) =>
|
|
55
|
+
Object.entries(env ?? {})
|
|
56
|
+
.filter(([name, value]) => {
|
|
57
|
+
if (typeof value !== 'string' || PLAIN_ENV_NAMES.test(name) || /^(?:true|false|\d+)$/iu.test(value)) return false;
|
|
58
|
+
// URL-form values (webhooks, signed URLs) are masked from a higher bar; a lone
|
|
59
|
+
// ':/' that is not '://' keeps the old exemption.
|
|
60
|
+
if (value.includes('://')) return value.length >= 24;
|
|
61
|
+
if (value.length < 8 || value.includes(':/')) return false;
|
|
62
|
+
// Plain filesystem paths stay readable unless they carry a query string or userinfo.
|
|
63
|
+
if (/^[~/]/u.test(value)) return value.includes('?') || value.includes('@');
|
|
64
|
+
return true;
|
|
65
|
+
})
|
|
66
|
+
.map(([, value]) => value)
|
|
67
|
+
.sort((a, b) => b.length - a.length);
|
|
68
|
+
|
|
69
|
+
/** A redactor bound to one environment and home, so the env scan happens once per run. */
|
|
70
|
+
export const makeRedactor = ({ env = process.env, home = env?.HOME } = {}) => {
|
|
71
|
+
const values = envSecrets(env);
|
|
72
|
+
return (input) => {
|
|
73
|
+
if (typeof input !== 'string' || input === '') return input;
|
|
74
|
+
let text = input;
|
|
75
|
+
for (const value of values) if (text.includes(value)) text = text.split(value).join(MASK);
|
|
76
|
+
text = text.replace(USERINFO, (match) => match.replace(/\/\/[^/?#\s]+@/u, `//${MASK}@`));
|
|
77
|
+
text = text.replace(ASSIGNMENT, (match, name) => (SECRET_NAME.test(name) ? `${name}=${MASK}` : match));
|
|
78
|
+
text = text.replace(JSON_SECRET, (match, key) => (SECRET_NAME.test(key) ? `${key}: ${MASK}` : match));
|
|
79
|
+
for (const pattern of SHAPES) text = text.replace(pattern, MASK);
|
|
80
|
+
if (typeof home === 'string' && home.length > 1) text = text.split(home).join('~');
|
|
81
|
+
return stripTerminalRuns(text);
|
|
82
|
+
};
|
|
83
|
+
};
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Muse session transcripts for the lit-adhd collector. Muse stores one JSONL file per session
|
|
2
|
+
// under `<data>/muse/sessions/YYYY/MM/DD/<session-id>/session.jsonl`, with subagent turns in a
|
|
3
|
+
// `subagent/` child folder. Files reach tens of thousands of lines, so each is read as its head
|
|
4
|
+
// and tail only, under a budget shared by the whole run, through a descriptor opened without
|
|
5
|
+
// following links. Subagent transcripts are not sessions of their own and are skipped. Anything
|
|
6
|
+
// a log cannot answer reads `unknown`.
|
|
7
|
+
|
|
8
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, readdirSync } from 'node:fs';
|
|
9
|
+
import { join, resolve } from 'node:path';
|
|
10
|
+
|
|
11
|
+
export const HEAD_BYTES = 64 * 1024;
|
|
12
|
+
export const TAIL_BYTES = 256 * 1024;
|
|
13
|
+
export const TOTAL_BUDGET_BYTES = 8 * 1024 * 1024;
|
|
14
|
+
const ACTIVE_MS = 15 * 60 * 1000;
|
|
15
|
+
|
|
16
|
+
/** A byte budget shared by every read of one collector run. */
|
|
17
|
+
export const makeBudget = (limit = TOTAL_BUDGET_BYTES) => ({ limit, used: 0, exhausted: false });
|
|
18
|
+
|
|
19
|
+
const readSlice = (fd, position, length, budget) => {
|
|
20
|
+
const allowed = Math.min(length, budget.limit - budget.used);
|
|
21
|
+
if (allowed <= 0) {
|
|
22
|
+
budget.exhausted = true;
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
const buffer = Buffer.alloc(allowed);
|
|
26
|
+
const read = readSync(fd, buffer, 0, allowed, position);
|
|
27
|
+
budget.used += read;
|
|
28
|
+
if (allowed < length) budget.exhausted = true;
|
|
29
|
+
return buffer.subarray(0, read).toString('utf8');
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** The parsed lines of a transcript's head and tail; a link or a non-file reads as null. */
|
|
33
|
+
export const readTranscriptLines = (path, budget, { head = HEAD_BYTES, tail = TAIL_BYTES } = {}) => {
|
|
34
|
+
let fd;
|
|
35
|
+
try {
|
|
36
|
+
if (!lstatSync(path).isFile()) return null;
|
|
37
|
+
fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
38
|
+
const { size } = fstatSync(fd);
|
|
39
|
+
const chunks = size <= head + tail
|
|
40
|
+
? [readSlice(fd, 0, size, budget)]
|
|
41
|
+
: [readSlice(fd, 0, head, budget), readSlice(fd, size - tail, tail, budget)];
|
|
42
|
+
const lines = [];
|
|
43
|
+
for (const chunk of chunks) {
|
|
44
|
+
for (const line of chunk.split('\n')) {
|
|
45
|
+
if (!line.trim()) continue;
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(line);
|
|
48
|
+
if (parsed && typeof parsed === 'object') lines.push(parsed);
|
|
49
|
+
} catch {
|
|
50
|
+
// A line cut at a slice edge, or a malformed one, is skipped.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return lines;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
} finally {
|
|
58
|
+
if (fd !== undefined) closeSync(fd);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Newest session logs first, capped, so a huge store stays cheap. `subagent/` children are lanes
|
|
63
|
+
// of their parent session, not sessions.
|
|
64
|
+
const newestLogs = (storeDir, cap = 400) => {
|
|
65
|
+
const found = [];
|
|
66
|
+
const walk = (dir, depth) => {
|
|
67
|
+
if (found.length >= cap || depth > 6) return;
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
71
|
+
} catch {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
if (found.length >= cap) return;
|
|
76
|
+
const path = join(dir, entry.name);
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
if (entry.name === 'subagent') continue;
|
|
79
|
+
walk(path, depth + 1);
|
|
80
|
+
} else if (entry.isFile() && entry.name === 'session.jsonl') {
|
|
81
|
+
try {
|
|
82
|
+
found.push({ path, mtimeMs: lstatSync(path).mtimeMs });
|
|
83
|
+
} catch { /* a vanishing log is skipped */ }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
walk(storeDir, 0);
|
|
88
|
+
return found.sort((a, b) => b.mtimeMs - a.mtimeMs).map(({ path }) => path);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// The session id is its directory name; the workspace root comes from the route facts record.
|
|
92
|
+
const peekMeta = (path, budget) => {
|
|
93
|
+
const lines = readTranscriptLines(path, budget, { head: 16 * 1024, tail: 0 });
|
|
94
|
+
if (!lines) return null;
|
|
95
|
+
let cwd = null;
|
|
96
|
+
let ended = false;
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
const payload = line.payload;
|
|
99
|
+
if (payload?.kind === 'route_facts' && typeof payload.record?.cwd === 'string') cwd ??= payload.record.cwd;
|
|
100
|
+
if (payload?.kind === 'session_end') ended = true;
|
|
101
|
+
}
|
|
102
|
+
const tail = readTranscriptLines(path, budget, { head: 0, tail: 32 * 1024 });
|
|
103
|
+
if (tail) {
|
|
104
|
+
for (const line of tail) {
|
|
105
|
+
const payload = line.payload;
|
|
106
|
+
if (payload?.kind === 'route_facts' && typeof payload.record?.cwd === 'string') cwd ??= payload.record.cwd;
|
|
107
|
+
if (payload?.kind === 'session_end') ended = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { cwd, ended };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The transcript files of every given worktree path, newest first, and whether the session store
|
|
115
|
+
* was there at all. A session belongs to a worktree when the cwd its log records sits inside it.
|
|
116
|
+
*/
|
|
117
|
+
export const findTranscripts = (storeDir, paths) => {
|
|
118
|
+
let isDir = false;
|
|
119
|
+
try {
|
|
120
|
+
isDir = lstatSync(storeDir).isDirectory();
|
|
121
|
+
} catch { /* missing store */ }
|
|
122
|
+
if (!isDir) return { status: 'unknown', files: [] };
|
|
123
|
+
const roots = paths.map((path) => resolve(path));
|
|
124
|
+
const budget = makeBudget(512 * 1024);
|
|
125
|
+
const files = [];
|
|
126
|
+
for (const path of newestLogs(storeDir)) {
|
|
127
|
+
const meta = peekMeta(path, budget);
|
|
128
|
+
if (!meta?.cwd) continue;
|
|
129
|
+
const cwd = resolve(meta.cwd);
|
|
130
|
+
if (!roots.some((root) => cwd === root || cwd.startsWith(`${root}/`))) continue;
|
|
131
|
+
files.push({ path, sessionId: path.split('/').slice(-2, -1)[0] ?? null, ended: meta.ended });
|
|
132
|
+
if (budget.exhausted) break;
|
|
133
|
+
}
|
|
134
|
+
return { status: 'ok', files };
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const textOf = (blocks) => {
|
|
138
|
+
if (typeof blocks === 'string') return blocks;
|
|
139
|
+
if (!Array.isArray(blocks)) return '';
|
|
140
|
+
return blocks.filter((part) => part?.kind === 'text' && typeof part.text === 'string').map((part) => part.text).join('\n');
|
|
141
|
+
};
|
|
142
|
+
const clip = (text, max) => {
|
|
143
|
+
const flat = String(text).replace(/\s+/gu, ' ').trim();
|
|
144
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
|
|
145
|
+
};
|
|
146
|
+
// This host stamps log lines in microseconds; ISO strings and milliseconds pass through.
|
|
147
|
+
const stampOf = (value) => {
|
|
148
|
+
if (typeof value === 'string') {
|
|
149
|
+
const at = Date.parse(value);
|
|
150
|
+
return Number.isFinite(at) ? at : null;
|
|
151
|
+
}
|
|
152
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value >= 1e14 ? Math.floor(value / 1000) : value;
|
|
153
|
+
return null;
|
|
154
|
+
};
|
|
155
|
+
const WAITING_TEXT = /[??]\s*$|`?lit approve`?|`?lit deny`?|승인해\s*주|확인해\s*주|괜찮을까요|할까요/iu;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* One session's record from its transcript lines: what it was asked, what it did, what it said
|
|
159
|
+
* last, and where it stands. `redact` runs over every text field it returns. Assistant text and
|
|
160
|
+
* changed files are not in this host's log records, so they read `unknown` and `[]`; the brief
|
|
161
|
+
* shows the commits instead.
|
|
162
|
+
*/
|
|
163
|
+
export const sessionRecord = (sessionId, lines, { redact = (text) => text, now = Date.now(), title = null } = {}) => {
|
|
164
|
+
if (!sessionId || !Array.isArray(lines)) return null;
|
|
165
|
+
let started = null;
|
|
166
|
+
let last = null;
|
|
167
|
+
let cwd = null;
|
|
168
|
+
let ended = false;
|
|
169
|
+
let loggedTitle = null;
|
|
170
|
+
const prompts = [];
|
|
171
|
+
const tools = {};
|
|
172
|
+
// The host log records a tool call's name only: no input, no description, no command line.
|
|
173
|
+
// The last call is named, never quoted.
|
|
174
|
+
let lastAction = null;
|
|
175
|
+
let lastEntry = null;
|
|
176
|
+
for (const line of lines) {
|
|
177
|
+
const payload = line?.payload;
|
|
178
|
+
if (!payload || typeof payload !== 'object') continue;
|
|
179
|
+
if (payload.kind === 'route_facts' && typeof payload.record?.cwd === 'string') cwd ??= payload.record.cwd;
|
|
180
|
+
if (payload.kind === 'session_end') ended = true;
|
|
181
|
+
if (line.payload_type === 'session.name.changed' && typeof payload.new_name === 'string' && payload.new_name) {
|
|
182
|
+
loggedTitle = payload.new_name;
|
|
183
|
+
}
|
|
184
|
+
const at = stampOf(line.recorded_at);
|
|
185
|
+
if (at !== null) {
|
|
186
|
+
started = started === null ? at : Math.min(started, at);
|
|
187
|
+
last = last === null ? at : Math.max(last, at);
|
|
188
|
+
}
|
|
189
|
+
if (line.payload_type === 'runtime.user_intent.accepted') {
|
|
190
|
+
const blocks = payload.refill_blocks ?? payload.record?.refill_blocks;
|
|
191
|
+
const text = textOf(blocks).trim();
|
|
192
|
+
if (text && !/^(?:<command-|<local-command|<system-reminder|<task-notification)/u.test(text)) {
|
|
193
|
+
prompts.push(text);
|
|
194
|
+
lastEntry = { role: 'user', text };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (line.payload_type === 'tool_batch.effect.started') {
|
|
198
|
+
const name = payload.record?.tool_name;
|
|
199
|
+
if (typeof name === 'string' && name) {
|
|
200
|
+
tools[name] = (tools[name] ?? 0) + 1;
|
|
201
|
+
lastAction = { tool: name };
|
|
202
|
+
lastEntry = { role: 'tool' };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const changed = [];
|
|
207
|
+
const asked = [...new Set([prompts[0], prompts.at(-1)].filter(Boolean))].slice(0, 2).map((text) => clip(redact(text), 160));
|
|
208
|
+
const waiting = lastEntry?.role === 'user' && WAITING_TEXT.test(lastEntry.text.slice(-400));
|
|
209
|
+
const status = last === null ? 'unknown' : ended ? 'ended' : waiting ? 'waiting-on-user' : now - last <= ACTIVE_MS ? 'active' : 'ended';
|
|
210
|
+
return {
|
|
211
|
+
sessionId,
|
|
212
|
+
id8: sessionId.slice(0, 8),
|
|
213
|
+
worktree: cwd ? redact(cwd) : 'unknown',
|
|
214
|
+
cwd,
|
|
215
|
+
branch: 'unknown',
|
|
216
|
+
started: started === null ? 'unknown' : new Date(started).toISOString(),
|
|
217
|
+
last: last === null ? 'unknown' : new Date(last).toISOString(),
|
|
218
|
+
lastMs: last,
|
|
219
|
+
title: clip(redact(title ?? loggedTitle ?? prompts[0] ?? 'unknown'), 80),
|
|
220
|
+
asked: asked.length ? asked : ['unknown'],
|
|
221
|
+
done: { tools, files: changed, commits: [] },
|
|
222
|
+
lastAction,
|
|
223
|
+
result: 'unknown',
|
|
224
|
+
status,
|
|
225
|
+
...(waiting ? { waitingReason: 'asked the user' } : {}),
|
|
226
|
+
};
|
|
227
|
+
};
|
|
@@ -81,17 +81,30 @@ Words and limits for each mode live in `references/craft-floor.md` section 3.
|
|
|
81
81
|
finite inventory: routes, regions, components, interactions, states (loading, empty, error, success,
|
|
82
82
|
disabled, offline, permission, ready), viewports. `references/taste-direction.md` is opened only when
|
|
83
83
|
direction is disputed or undecided.
|
|
84
|
-
3. **
|
|
84
|
+
3. **Ask the look first.** Before locking or building, ask the user to choose the house look with `request_user_input`: 2–4 of the five `kit/styles/` looks that fit the brief, recommended first (the lock-step default below is the recommendation when no other look fits better). Skip the question only when the brief names the look or says "you choose": take the recommended, log a `decision` line with the look and the reason, and carry on. Headless runs take the recommended and log it the same way.
|
|
85
|
+
4. **Lock the look.** Record the chosen tokens before building:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
node "$FRONTEND_UIUX_SKILL_ROOT/scripts/design-lock.mjs" init --family <landing|workspace|settings> [--style <field-notes|graphite|meadow|computational-brutalism|neo-brutalism>] [--brief "<the request>"] [--density calm] --moment <kind> --moment-note "<what it is>" --dir "<project>" --scaffold
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
A read-once page (landing, docs, report) locks `field-notes`; dashboards, settings, forms and admin or
|
|
92
|
+
data-dense screens lock `graphite`. `meadow`, `computational-brutalism` and `neo-brutalism` are opt-in
|
|
93
|
+
only, never defaults. The lock pins the style, its pairs and dials, and one task-specific pixel motif;
|
|
94
|
+
`design-lock.mjs judge` and the probe's DS-* rules enforce it from the page itself. The five
|
|
95
|
+
`kit/styles/` house looks and their `when` rows are the catalogue (`references/house-styles.md`
|
|
96
|
+
explains the system); `kit/` layouts and components are starting points, never a second design.
|
|
97
|
+
5. **Prompt-to-implementation coverage.** For a multi-item brief, turn every enumerated requirement, exact
|
|
85
98
|
content count, control, state, viewport and accessibility or performance condition into its own acceptance
|
|
86
99
|
criterion. A working page does not satisfy an omitted item. Map each criterion to a component or behavior
|
|
87
100
|
and a direct verification action in the contract's `acceptance_criteria`.
|
|
88
|
-
|
|
101
|
+
6. **Feature-first build order.** Implement the smallest working version of every required content group and
|
|
89
102
|
interaction before detailed polish. Exact counts render as that many distinct items; controls change the
|
|
90
103
|
requested state rather than decorate. A scroll story needs an observable state change triggered by real
|
|
91
104
|
scroll, complete with reduced motion on and with media failed. Then exercise the checklist, fix missing
|
|
92
105
|
behavior, and refine the visual direction. Semantic markup first: real `<button>`, `<a>`, `<label for>`,
|
|
93
106
|
landmarks, one `h1`.
|
|
94
|
-
|
|
107
|
+
7. **Validate the contract.** Evolve the artifact with the implementation and validate canonical JSON under
|
|
95
108
|
1 MiB with the shipped validator, not a handwritten checker or a lookalike schema:
|
|
96
109
|
|
|
97
110
|
```bash
|
|
@@ -102,7 +115,7 @@ Words and limits for each mode live in `references/craft-floor.md` section 3.
|
|
|
102
115
|
`valid` and `evidence_eligible` must both be true. Fix the contract; never replace the validator or infer
|
|
103
116
|
success from an editor's schema hint. A full schema is not a prerequisite to starting an already
|
|
104
117
|
authorized build, and contract-only delivery is not a stopping point when the user asked for a screen.
|
|
105
|
-
|
|
118
|
+
8. **Test behavior, then probe and fix.** Run the project's focused tests. Then run the interface probe
|
|
106
119
|
against the page or project and loop build, probe, fix for at most three rounds in the cheaper-fix order
|
|
107
120
|
(delete, platform feature, existing token or component, correct in place, new code last):
|
|
108
121
|
|
|
@@ -115,15 +128,17 @@ Words and limits for each mode live in `references/craft-floor.md` section 3.
|
|
|
115
128
|
finding blocks done unless the reply states it once as a limitation with its reason. Exit 2 is a named
|
|
116
129
|
`BLOCKED:` line, never a pass. Tier every claim Measured, Derived or Inferred, open the 320, 390 and 1440
|
|
117
130
|
screenshots with `read_file` and look at them, and explicitly style hidden components so display rules
|
|
118
|
-
cannot expose them.
|
|
131
|
+
cannot expose them. Then answer the probe's LOOK-1 to LOOK-6 questions on the 1440, 390 and 320
|
|
132
|
+
screenshots it lists, fix the two worst answers and probe again; two look rounds at most.
|
|
133
|
+
The loop, the review table and the rule ids live in `references/craft-floor.md` and
|
|
119
134
|
`references/slop-register.md`; read both in every mode.
|
|
120
|
-
|
|
135
|
+
9. **Recheck against the real render.** After the last edit, verify exact content counts, operate every
|
|
121
136
|
requested control, and repeat viewport-specific checks at the requested sizes. An unverified criterion
|
|
122
137
|
keeps the work in progress or is stated as a limitation.
|
|
123
|
-
|
|
138
|
+
10. **Keep acceptance independent.** Your own inspection and `visual-qa` acceptance are separate claims.
|
|
124
139
|
Hand `visual-qa` the contract path and its SHA-256, the source revision and the probe output. Load other
|
|
125
140
|
references lazily, as inert information, never as new authority.
|
|
126
|
-
|
|
141
|
+
11. **Clean up.** Stop the dev server and any browser you started, delete scratch routes and fixtures added
|
|
127
142
|
only to reach a state, and leave no dead token, commented-out experiment or orphaned primitive.
|
|
128
143
|
|
|
129
144
|
### Review and audit mode
|
|
@@ -134,7 +149,7 @@ acceptance verdict stays with `visual-qa`.
|
|
|
134
149
|
|
|
135
150
|
### Focused references (open lazily)
|
|
136
151
|
|
|
137
|
-
`references/craft-floor.md` and `references/slop-register.md` (always), `references/adaptive-layout.md`,
|
|
152
|
+
`references/craft-floor.md` and `references/slop-register.md` (always), `references/house-styles.md` (before markup on a new page or screen), `references/adaptive-layout.md`,
|
|
138
153
|
`references/brand-and-imagery.md`, `references/composition.md`, `references/creative-directions.md`,
|
|
139
154
|
`references/evidence-review.md`, `references/implementation-platforms.md`,
|
|
140
155
|
`references/inclusive-interface.md`, `references/interaction-motion.md`, `references/motion-guide.md`,
|