lazycodex-ai 5.0.0-beta.89 → 5.0.0-beta.90
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/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/README.zh-cn.md +1 -1
- package/dist/cli/config-manager/refresh-opencode-plugin-sandbox.d.ts +31 -0
- package/dist/cli/doctor/checks/tui-plugin-config.d.ts +11 -0
- package/dist/cli/index.js +1661 -859
- package/dist/cli/install-native/index.d.ts +9 -1
- package/dist/cli/install-native/legacy-omo-bin.d.ts +27 -0
- package/dist/cli/install-native/offer-native-setup.d.ts +31 -0
- package/dist/cli/install-native/omo-bin-test-fixtures.d.ts +35 -0
- package/dist/cli/install-native/plan.d.ts +8 -0
- package/dist/cli/install-native/repair-legacy-omo-bin.d.ts +26 -0
- package/dist/cli/install-native/run-native-install.d.ts +11 -1
- package/dist/cli/install-native/verify-omo-command.d.ts +22 -0
- package/dist/cli/native-edition-hint.d.ts +2 -2
- package/dist/cli-node/index.js +1661 -859
- package/package.json +1 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +5 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-6.md +5 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +7 -3
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +7 -3
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +2 -2
- package/packages/omo-codex/plugin/skills/browser/SKILL.md +8 -1
- package/packages/omo-codex/plugin/skills/browser/references/install.md +34 -5
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/index.js +2 -2
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +3 -3
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-doctor.mjs +14 -3
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-install.mjs +13 -1
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +7 -3
- package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +63 -0
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +37 -25
- package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +187 -0
- package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +66 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/cli-support.mjs +102 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +162 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/css-lite.mjs +204 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/design-spec.mjs +161 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/entities.mjs +39 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/entry-guard.mjs +21 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/format-extract-css.mjs +122 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/format-extract.mjs +229 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-figures.mjs +173 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-layout.mjs +146 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +167 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-structure.mjs +118 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-text.mjs +190 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-lite.mjs +239 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/layout-probe.mjs +83 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +250 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/repair-tracker.mjs +171 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +214 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools.mjs +61 -0
- package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +3 -1
- package/packages/omo-codex/plugin/test/sync-skills-codex-compatibility.test.mjs +11 -0
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +2 -2
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +20 -0
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
- package/packages/prompts-core/prompts/ultrawork/codex.md +7 -3
- package/packages/shared-skills/skills/browser/SKILL.md +8 -1
- package/packages/shared-skills/skills/browser/references/install.md +34 -5
- package/packages/shared-skills/skills/browser/runtime/omowright/index.js +2 -2
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +3 -3
- package/packages/shared-skills/skills/browser/scripts/browser-doctor.mjs +14 -3
- package/packages/shared-skills/skills/browser/scripts/browser-install.mjs +13 -1
- package/packages/shared-skills/skills/ulw-research/AGENTS.md +63 -0
- package/packages/shared-skills/skills/ulw-research/SKILL.md +38 -26
- package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +187 -0
- package/packages/shared-skills/skills/ulw-research/references/report-gates.md +66 -0
- package/packages/shared-skills/skills/ulw-research/scripts/cli-support.mjs +102 -0
- package/packages/shared-skills/skills/ulw-research/scripts/cli-support.test.ts +88 -0
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +162 -0
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +177 -0
- package/packages/shared-skills/skills/ulw-research/scripts/css-lite.mjs +204 -0
- package/packages/shared-skills/skills/ulw-research/scripts/css-lite.test.ts +159 -0
- package/packages/shared-skills/skills/ulw-research/scripts/design-spec.mjs +161 -0
- package/packages/shared-skills/skills/ulw-research/scripts/design-spec.test.ts +162 -0
- package/packages/shared-skills/skills/ulw-research/scripts/entities.mjs +39 -0
- package/packages/shared-skills/skills/ulw-research/scripts/entry-guard.mjs +21 -0
- package/packages/shared-skills/skills/ulw-research/scripts/format-extract-css.mjs +122 -0
- package/packages/shared-skills/skills/ulw-research/scripts/format-extract.mjs +229 -0
- package/packages/shared-skills/skills/ulw-research/scripts/format-extract.test.ts +243 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-figures.mjs +173 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-layout.mjs +146 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-layout.test.ts +236 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +167 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-static.test.ts +265 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-structure.mjs +118 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-text.mjs +190 -0
- package/packages/shared-skills/skills/ulw-research/scripts/glossary-vocabulary.test.ts +43 -0
- package/packages/shared-skills/skills/ulw-research/scripts/html-lite.mjs +239 -0
- package/packages/shared-skills/skills/ulw-research/scripts/html-lite.test.ts +191 -0
- package/packages/shared-skills/skills/ulw-research/scripts/layout-probe.mjs +83 -0
- package/packages/shared-skills/skills/ulw-research/scripts/layout-probe.test.ts +89 -0
- package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +250 -0
- package/packages/shared-skills/skills/ulw-research/scripts/outcome.test.ts +281 -0
- package/packages/shared-skills/skills/ulw-research/scripts/repair-tracker.mjs +171 -0
- package/packages/shared-skills/skills/ulw-research/scripts/repair-tracker.test.ts +246 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +214 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools.mjs +61 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +213 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/assets/figure-1.svg +1 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/bad-report.html +44 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/boxes.json +90 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/design-spec.md +29 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/good-report.html +57 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/reference-report.html +86 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/reference.md +23 -0
- package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +3 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// design-spec.mjs - read the palette and lineage mode out of design-spec.md, and render a
|
|
2
|
+
// design-spec.md draft from a format extract. Nothing is guessed: every value the extract
|
|
3
|
+
// does not carry is written `TODO: ask` and listed under Open questions.
|
|
4
|
+
// Plain Node ESM, node: builtins and siblings only.
|
|
5
|
+
|
|
6
|
+
import { DESIGN_SPEC_SECTIONS, LINEAGE_MODES } from "./contracts.mjs"
|
|
7
|
+
import { hexColors } from "./css-lite.mjs"
|
|
8
|
+
|
|
9
|
+
const TODO = "TODO: ask"
|
|
10
|
+
// English and Korean section names (the requester writes specs in either language).
|
|
11
|
+
const PALETTE_HEADING = /^(palette|tokens|colou?rs?|\uD1A0\uD070|\uD314\uB808\uD2B8|\uC0C9\uC0C1|\uCEEC\uB7EC)/i
|
|
12
|
+
const FENCE = /^\s{0,3}(`{3,}|~{3,})/
|
|
13
|
+
const HEADING = /^(#{1,6})\s+(.*?)\s*#*\s*$/
|
|
14
|
+
const TOKEN_DECLARATION = /--[\w-]+\s*:\s*([^;]*)/g
|
|
15
|
+
const FIGURE_STANDARD =
|
|
16
|
+
"Each figure sits in a fixed-size container with a caption; the image scales to fit with its aspect ratio preserved (object-fit: contain), never stretched, cropped, or spilling out. Every chart carries a title, axis labels, units, and value labels."
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Walk markdown lines with fence awareness.
|
|
20
|
+
* visit(line, { fenced, heading: { level, text } | null })
|
|
21
|
+
*/
|
|
22
|
+
function walkLines(markdown, visit) {
|
|
23
|
+
let fence = null
|
|
24
|
+
for (const line of String(markdown ?? "").split(/\r?\n/)) {
|
|
25
|
+
const fenceMatch = FENCE.exec(line)
|
|
26
|
+
if (fence) {
|
|
27
|
+
const closes = fenceMatch && fenceMatch[1][0] === fence[0] && fenceMatch[1].length >= fence.length && line.trim() === fenceMatch[1]
|
|
28
|
+
if (closes) fence = null
|
|
29
|
+
else visit(line, { fenced: true, heading: null })
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
if (fenceMatch) {
|
|
33
|
+
fence = fenceMatch[1]
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
const heading = HEADING.exec(line)
|
|
37
|
+
visit(line, { fenced: false, heading: heading ? { level: heading[1].length, text: heading[2].replace(/^[\d.\s]+/, "") } : null })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Palette = hex values of every `--x: #hex` inside any fenced code block, plus every hex
|
|
43
|
+
* anywhere under a heading named Palette / Tokens / Colors / Colours or their Korean equivalents (subheadings included).
|
|
44
|
+
* @returns {Set<string>} normalized #rrggbb values
|
|
45
|
+
*/
|
|
46
|
+
export function parsePalette(specMarkdown) {
|
|
47
|
+
const palette = new Set()
|
|
48
|
+
let paletteLevel = 0
|
|
49
|
+
walkLines(specMarkdown, (line, { fenced, heading }) => {
|
|
50
|
+
if (heading) {
|
|
51
|
+
if (paletteLevel && heading.level > paletteLevel) return
|
|
52
|
+
paletteLevel = PALETTE_HEADING.test(heading.text) ? heading.level : 0
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
if (fenced) for (const match of line.matchAll(TOKEN_DECLARATION)) for (const hex of hexColors(match[1])) palette.add(hex)
|
|
56
|
+
if (paletteLevel) for (const hex of hexColors(line)) palette.add(hex)
|
|
57
|
+
})
|
|
58
|
+
return palette
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** `Lineage: inline|section` under the Citations heading; absent or unrecognized -> "inline". */
|
|
62
|
+
export function parseLineageMode(specMarkdown) {
|
|
63
|
+
let citationsLevel = 0
|
|
64
|
+
let mode = null
|
|
65
|
+
walkLines(specMarkdown, (line, { fenced, heading }) => {
|
|
66
|
+
if (mode || fenced) return
|
|
67
|
+
if (heading) {
|
|
68
|
+
if (citationsLevel && heading.level > citationsLevel) return
|
|
69
|
+
citationsLevel = /^citations\b/i.test(heading.text) ? heading.level : 0
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
const match = citationsLevel ? /^\s*(?:[-*]\s+)?lineage\s*:\s*(\S+)/i.exec(line) : null
|
|
73
|
+
if (match && LINEAGE_MODES.includes(match[1].toLowerCase())) mode = match[1].toLowerCase()
|
|
74
|
+
})
|
|
75
|
+
return mode ?? "inline"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function entriesOf(value) {
|
|
79
|
+
if (value instanceof Map) return [...value]
|
|
80
|
+
return value !== null && typeof value === "object" ? Object.entries(value) : []
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isKnown(value) {
|
|
84
|
+
if (value === undefined || value === null || value === "" || value === "unknown") return false
|
|
85
|
+
if (Array.isArray(value)) return value.length > 0
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function tokenFence(entries, dark) {
|
|
90
|
+
const lines = entries.map(([name, value]) => `${dark ? " " : " "}${name}: ${value};`)
|
|
91
|
+
const body = dark ? ["@media (prefers-color-scheme: dark) {", " :root {", ...lines, " }", "}"] : [":root {", ...lines, "}"]
|
|
92
|
+
return ["```css", ...body, "```"]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Render a design-spec.md draft. `extract` follows the format-extract shape
|
|
97
|
+
* ({ origin, bytes, date?, tokens: { light, dark }, fonts, colors, breakpoints, headingNumbering,
|
|
98
|
+
* citationStyle, lineageMode, figureContainers, skeleton, measure, notes }); every field is optional.
|
|
99
|
+
* @returns {string}
|
|
100
|
+
*/
|
|
101
|
+
export function renderDesignSpec(extract = {}) {
|
|
102
|
+
const source = extract ?? {}
|
|
103
|
+
const open = []
|
|
104
|
+
const field = (label, value, format = String) => {
|
|
105
|
+
if (isKnown(value)) return format(value)
|
|
106
|
+
open.push(label)
|
|
107
|
+
return TODO
|
|
108
|
+
}
|
|
109
|
+
const fonts = source.fonts ?? {}
|
|
110
|
+
const measure = source.measure ?? {}
|
|
111
|
+
const light = entriesOf(source.tokens?.light)
|
|
112
|
+
const dark = entriesOf(source.tokens?.dark)
|
|
113
|
+
const colors = Array.isArray(source.colors) ? source.colors : []
|
|
114
|
+
const skeleton = Array.isArray(source.skeleton) ? source.skeleton : []
|
|
115
|
+
const notes = Array.isArray(source.notes) ? source.notes : []
|
|
116
|
+
|
|
117
|
+
const sections = {
|
|
118
|
+
"Extracted from": [
|
|
119
|
+
`- Source: ${field("Source path or URL", source.origin)}`,
|
|
120
|
+
`- Bytes: ${field("Source byte count", source.bytes)}`,
|
|
121
|
+
`- Date: ${source.date ?? new Date().toISOString().slice(0, 10)}`,
|
|
122
|
+
...notes.map((note) => `- Note: ${note}`),
|
|
123
|
+
],
|
|
124
|
+
Tokens: [
|
|
125
|
+
...(light.length ? tokenFence(light, false) : [`- Light tokens: ${field("Light tokens", null)}`]),
|
|
126
|
+
...(dark.length ? tokenFence(dark, true) : [`- Dark tokens: ${field("Dark tokens", null)}`]),
|
|
127
|
+
...(colors.length
|
|
128
|
+
? ["- Top colors:", ...colors.map((c) => ` - ${c.hex}${c.count ? ` (${c.count} uses${c.roles?.length ? `; ${c.roles.join(", ")}` : ""})` : ""}`)]
|
|
129
|
+
: [`- Top colors: ${field("Top colors", null)}`]),
|
|
130
|
+
],
|
|
131
|
+
Typography: [
|
|
132
|
+
`- Body font: ${field("Body font", fonts.body)}`,
|
|
133
|
+
`- Heading font: ${field("Heading font", fonts.heading)}`,
|
|
134
|
+
`- Mono font: ${field("Mono font", fonts.mono)}`,
|
|
135
|
+
`- Body size: ${field("Body size", measure.fontSize)}`,
|
|
136
|
+
`- Line height: ${field("Line height", measure.lineHeight)}`,
|
|
137
|
+
],
|
|
138
|
+
Layout: [
|
|
139
|
+
`- Breakpoints: ${field("Breakpoints", source.breakpoints, (list) => list.map((px) => `${px}px`).join(", "))}`,
|
|
140
|
+
`- Measure (max width): ${field("Measure", measure.maxWidth)}`,
|
|
141
|
+
],
|
|
142
|
+
Structure: [
|
|
143
|
+
`- Heading numbering: ${field("Heading numbering", source.headingNumbering)}`,
|
|
144
|
+
skeleton.length ? "- Skeleton:" : `- Skeleton: ${field("Section skeleton", null)}`,
|
|
145
|
+
...skeleton.map((entry) => `${" ".repeat(Math.max(1, entry.level))}- ${entry.text}`),
|
|
146
|
+
],
|
|
147
|
+
Figures: [
|
|
148
|
+
`- Containers: ${field("Figure containers", source.figureContainers, (list) => list.join(", "))}`,
|
|
149
|
+
`- Standard: ${FIGURE_STANDARD}`,
|
|
150
|
+
],
|
|
151
|
+
Citations: [
|
|
152
|
+
`- Style: ${field("Citation style", source.citationStyle)}`,
|
|
153
|
+
`- Lineage: ${field("Lineage mode", LINEAGE_MODES.includes(source.lineageMode) ? source.lineageMode : null)}`,
|
|
154
|
+
],
|
|
155
|
+
}
|
|
156
|
+
sections["Open questions"] = open.length ? open.map((label) => `- ${label}: ${TODO}`) : ["- None"]
|
|
157
|
+
|
|
158
|
+
const out = ["# Design spec", ""]
|
|
159
|
+
for (const heading of DESIGN_SPEC_SECTIONS) out.push(`## ${heading}`, ...(sections[heading] ?? [`- ${TODO}`]), "")
|
|
160
|
+
return out.join("\n")
|
|
161
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
|
|
3
|
+
import { DESIGN_SPEC_SECTIONS } from "./contracts.mjs"
|
|
4
|
+
import { parseLineageMode, parsePalette, renderDesignSpec } from "./design-spec.mjs"
|
|
5
|
+
|
|
6
|
+
function sectionBody(markdown: string, heading: string): string {
|
|
7
|
+
const start = markdown.indexOf(`## ${heading}\n`)
|
|
8
|
+
const rest = markdown.slice(start + heading.length + 4)
|
|
9
|
+
const next = rest.search(/^## /m)
|
|
10
|
+
return next === -1 ? rest : rest.slice(0, next)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("parsePalette", () => {
|
|
14
|
+
test("#given a css fence and a Palette bullet list #when parsed #then the union is returned", () => {
|
|
15
|
+
// given
|
|
16
|
+
const spec = [
|
|
17
|
+
"# Spec",
|
|
18
|
+
"```css",
|
|
19
|
+
":root { --bg: #FAF6EF; --accent: #c2410c; }",
|
|
20
|
+
"```",
|
|
21
|
+
"## Palette",
|
|
22
|
+
"- accent #C2410C",
|
|
23
|
+
"- good #15803d",
|
|
24
|
+
"## Layout",
|
|
25
|
+
"- divider #999999 is prose, not a token",
|
|
26
|
+
].join("\n")
|
|
27
|
+
// when
|
|
28
|
+
const palette = parsePalette(spec)
|
|
29
|
+
// then
|
|
30
|
+
expect([...palette].sort()).toEqual(["#15803d", "#c2410c", "#faf6ef"])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test("#given Tokens and Colours headings with nested subheadings #when parsed #then hex under them counts", () => {
|
|
34
|
+
// given
|
|
35
|
+
const spec = [
|
|
36
|
+
"## Tokens (copied from the reference)",
|
|
37
|
+
"- Light: --bg #faf6ef, --fg #24211b",
|
|
38
|
+
"### Dark",
|
|
39
|
+
"- --bg #0c0b0a",
|
|
40
|
+
"## Colours",
|
|
41
|
+
"good #abc",
|
|
42
|
+
"## Structure",
|
|
43
|
+
"#def is not a color heading",
|
|
44
|
+
].join("\n")
|
|
45
|
+
// then
|
|
46
|
+
expect([...parsePalette(spec)].sort()).toEqual(["#0c0b0a", "#24211b", "#aabbcc", "#faf6ef"])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test("#given a fence line starting with a hash #when parsed #then it is not read as a heading", () => {
|
|
50
|
+
// given
|
|
51
|
+
const spec = ["## Palette", "```", "# not a heading", "--x: #111111", "```", "- #222222"].join("\n")
|
|
52
|
+
// then
|
|
53
|
+
expect([...parsePalette(spec)].sort()).toEqual(["#111111", "#222222"])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("#given a spec with no tokens #when parsed #then the palette is empty", () => {
|
|
57
|
+
expect(parsePalette("# Spec\n\nPlain prose with #fff outside a palette.").size).toBe(0)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe("parseLineageMode", () => {
|
|
62
|
+
test("#given a Lineage line under Citations #when parsed #then that mode is returned", () => {
|
|
63
|
+
expect(parseLineageMode("## Citations\n- Style: bracket-S\n- Lineage: section\n")).toBe("section")
|
|
64
|
+
expect(parseLineageMode("## Citations\nLineage: inline")).toBe("inline")
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test("#given no Citations section or a Lineage line elsewhere #when parsed #then inline is the default", () => {
|
|
68
|
+
expect(parseLineageMode("# Spec")).toBe("inline")
|
|
69
|
+
expect(parseLineageMode("## Layout\nLineage: section\n## Citations\n- Style: footnote")).toBe("inline")
|
|
70
|
+
expect(parseLineageMode("## Citations\nLineage: TODO: ask")).toBe("inline")
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe("renderDesignSpec", () => {
|
|
75
|
+
test("#given an empty extract #when rendered #then every section heading appears in order with TODO: ask under each", () => {
|
|
76
|
+
// when
|
|
77
|
+
const markdown = renderDesignSpec({})
|
|
78
|
+
// then
|
|
79
|
+
let cursor = -1
|
|
80
|
+
for (const heading of DESIGN_SPEC_SECTIONS) {
|
|
81
|
+
const index = markdown.indexOf(`## ${heading}\n`)
|
|
82
|
+
expect(index, heading).toBeGreaterThan(cursor)
|
|
83
|
+
cursor = index
|
|
84
|
+
expect(sectionBody(markdown, heading), heading).toContain("TODO: ask")
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test("#given a full extract #when rendered #then values, provenance and a round-trippable palette and lineage appear", () => {
|
|
89
|
+
// given
|
|
90
|
+
const extract = {
|
|
91
|
+
origin: "tests/fixtures/reference-report.html",
|
|
92
|
+
bytes: 16500,
|
|
93
|
+
date: "2026-09-24",
|
|
94
|
+
tokens: {
|
|
95
|
+
light: new Map([
|
|
96
|
+
["--bg", "#faf6ef"],
|
|
97
|
+
["--accent", "#c2410c"],
|
|
98
|
+
]),
|
|
99
|
+
dark: new Map([["--bg", "#0c0b0a"]]),
|
|
100
|
+
},
|
|
101
|
+
fonts: { body: "Pretendard, sans-serif", heading: "Pretendard, sans-serif", mono: "ui-monospace, monospace" },
|
|
102
|
+
colors: [{ hex: "#24211b", count: 12, roles: ["body"] }],
|
|
103
|
+
breakpoints: [560, 640],
|
|
104
|
+
headingNumbering: "double-digit",
|
|
105
|
+
citationStyle: "bracket-S",
|
|
106
|
+
lineageMode: "section",
|
|
107
|
+
figureContainers: ["fig"],
|
|
108
|
+
skeleton: [
|
|
109
|
+
{ level: 1, text: "Title" },
|
|
110
|
+
{ level: 2, text: "01 Findings" },
|
|
111
|
+
],
|
|
112
|
+
measure: { fontSize: "17px", lineHeight: "1.75", maxWidth: "720px" },
|
|
113
|
+
notes: ["skipped external stylesheet: theme.css"],
|
|
114
|
+
}
|
|
115
|
+
// when
|
|
116
|
+
const markdown = renderDesignSpec(extract)
|
|
117
|
+
// then
|
|
118
|
+
const provenance = sectionBody(markdown, "Extracted from")
|
|
119
|
+
expect(provenance).toContain("tests/fixtures/reference-report.html")
|
|
120
|
+
expect(provenance).toContain("16500")
|
|
121
|
+
expect(provenance).toContain("2026-09-24")
|
|
122
|
+
expect(provenance).toContain("skipped external stylesheet: theme.css")
|
|
123
|
+
expect([...parsePalette(markdown)].sort()).toEqual(["#0c0b0a", "#24211b", "#c2410c", "#faf6ef"])
|
|
124
|
+
expect(parseLineageMode(markdown)).toBe("section")
|
|
125
|
+
expect(sectionBody(markdown, "Layout")).toContain("560px, 640px")
|
|
126
|
+
expect(sectionBody(markdown, "Structure")).toContain(" - 01 Findings")
|
|
127
|
+
expect(sectionBody(markdown, "Figures")).toContain("fig")
|
|
128
|
+
expect(sectionBody(markdown, "Open questions")).not.toContain("TODO: ask")
|
|
129
|
+
for (const heading of ["Tokens", "Typography", "Layout", "Structure", "Figures", "Citations"]) {
|
|
130
|
+
expect(sectionBody(markdown, heading), heading).not.toContain("TODO: ask")
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test("#given unknown enum values and plain-object tokens #when rendered #then unknowns are TODO: ask and listed as open questions", () => {
|
|
135
|
+
// given
|
|
136
|
+
const extract = { tokens: { light: { "--bg": "#ffffff" }, dark: {} }, lineageMode: "unknown", citationStyle: "unknown" }
|
|
137
|
+
// when
|
|
138
|
+
const markdown = renderDesignSpec(extract)
|
|
139
|
+
// then
|
|
140
|
+
expect(sectionBody(markdown, "Tokens")).toContain("--bg: #ffffff;")
|
|
141
|
+
expect(sectionBody(markdown, "Tokens")).toContain("Dark tokens: TODO: ask")
|
|
142
|
+
expect(sectionBody(markdown, "Citations")).toContain("Lineage: TODO: ask")
|
|
143
|
+
expect(parseLineageMode(markdown)).toBe("inline")
|
|
144
|
+
expect(sectionBody(markdown, "Open questions")).toContain("Lineage mode")
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe("parsePalette with Korean headings", () => {
|
|
149
|
+
test("#given a spec whose token section is headed in Korean #when parsed #then the bullet hexes under it are the palette", () => {
|
|
150
|
+
// given
|
|
151
|
+
const md = ["# design-spec", "## 토큰 (변경 금지)", "- 라이트: bg #faf6ef · fg #24211b · accent #c2410c", "## 그림 표준", "- 테두리 #ffffff"].join("\n")
|
|
152
|
+
// when
|
|
153
|
+
const palette = parsePalette(md)
|
|
154
|
+
// then
|
|
155
|
+
expect([...palette].sort()).toEqual(["#24211b", "#c2410c", "#faf6ef"])
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test("#given Korean color and palette headings #when parsed #then each is recognized", () => {
|
|
159
|
+
expect([...parsePalette("## 팔레트\n- #112233")]).toEqual(["#112233"])
|
|
160
|
+
expect([...parsePalette("## 색상\n- #445566")]).toEqual(["#445566"])
|
|
161
|
+
})
|
|
162
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// entities.mjs - HTML character reference decoding for html-lite.mjs.
|
|
2
|
+
//
|
|
3
|
+
// Supports a fixed set of named entities plus decimal and hex numeric references. Unknown or
|
|
4
|
+
// malformed references stay verbatim. Single indexOf scan; plain Node ESM, no dependencies.
|
|
5
|
+
|
|
6
|
+
const NAMED_ENTITIES = {
|
|
7
|
+
amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: "\u00a0", mdash: "\u2014", ndash: "\u2013",
|
|
8
|
+
hellip: "\u2026", copy: "\u00a9", reg: "\u00ae", times: "\u00d7", middot: "\u00b7",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function decodeOne(body) {
|
|
12
|
+
if (Object.hasOwn(NAMED_ENTITIES, body)) return NAMED_ENTITIES[body]
|
|
13
|
+
if (body[0] !== "#") return null
|
|
14
|
+
const hex = body[1] === "x" || body[1] === "X"
|
|
15
|
+
const digits = body.slice(hex ? 2 : 1)
|
|
16
|
+
if (!(hex ? /^[0-9a-fA-F]{1,6}$/ : /^[0-9]{1,7}$/).test(digits)) return null
|
|
17
|
+
const cp = Number.parseInt(digits, hex ? 16 : 10)
|
|
18
|
+
return cp > 0 && cp <= 0x10ffff && (cp < 0xd800 || cp > 0xdfff) ? String.fromCodePoint(cp) : "\ufffd"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Decode the supported named entities plus decimal/hex numeric references; unknown ones stay verbatim. */
|
|
22
|
+
export function decodeEntities(text) {
|
|
23
|
+
const source = String(text)
|
|
24
|
+
let out = ""
|
|
25
|
+
let last = 0
|
|
26
|
+
let at = source.indexOf("&")
|
|
27
|
+
while (at !== -1) {
|
|
28
|
+
const semi = source.indexOf(";", at + 1)
|
|
29
|
+
const decoded = semi !== -1 && semi - at <= 12 ? decodeOne(source.slice(at + 1, semi)) : null
|
|
30
|
+
if (decoded !== null) {
|
|
31
|
+
out += source.slice(last, at) + decoded
|
|
32
|
+
last = semi + 1
|
|
33
|
+
at = source.indexOf("&", last)
|
|
34
|
+
} else {
|
|
35
|
+
at = source.indexOf("&", at + 1)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out + source.slice(last)
|
|
39
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs"
|
|
2
|
+
import { pathToFileURL } from "node:url"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* True when the module at `importMetaUrl` is the process entrypoint (`node <script>`).
|
|
6
|
+
* Compares against the realpath too: Node resolves import.meta.url through symlinks while
|
|
7
|
+
* process.argv[1] keeps the symlinked spelling, so a plain equality check silently skips the CLI
|
|
8
|
+
* body when the skill is reached through a symlinked plugin directory.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} importMetaUrl the caller's import.meta.url
|
|
11
|
+
*/
|
|
12
|
+
export function isCliEntry(importMetaUrl) {
|
|
13
|
+
const argv1 = process.argv[1]
|
|
14
|
+
if (argv1 === undefined) return false
|
|
15
|
+
if (importMetaUrl === pathToFileURL(argv1).href) return true
|
|
16
|
+
try {
|
|
17
|
+
return importMetaUrl === pathToFileURL(realpathSync(argv1)).href
|
|
18
|
+
} catch {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// format-extract-css.mjs - the CSS half of format-extract: tokens, font stacks, ranked colors,
|
|
2
|
+
// breakpoints, and the reading measure. Only values the stylesheet states are returned; a
|
|
3
|
+
// missing value stays undefined so the renderer writes `TODO: ask`.
|
|
4
|
+
// Plain Node ESM, node: builtins and siblings only.
|
|
5
|
+
|
|
6
|
+
import { customProperties, fontFamilies, hexColors, mediaBreakpoints, resolveVar, scanRules } from "./css-lite.mjs"
|
|
7
|
+
|
|
8
|
+
const ROLE_ORDER = ["body", "heading", "link", "background", "border", "accent"]
|
|
9
|
+
const TOP_COLORS = 8
|
|
10
|
+
const ABSOLUTE_LENGTH = /^\d+(?:\.\d+)?(?:px|rem|em|ch)$/i
|
|
11
|
+
|
|
12
|
+
/** Last compound of each comma-separated selector part, pseudo-classes and attributes dropped. */
|
|
13
|
+
function subjects(selector) {
|
|
14
|
+
return selector.split(",").map((part) => {
|
|
15
|
+
const compounds = part.trim().split(/\s*[\s>+~]\s*/)
|
|
16
|
+
return (compounds.at(-1) ?? "").replace(/(?:::?[\w-]+(?:\([^)]*\))?|\[[^\]]*\])+$/g, "").toLowerCase()
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const isBodySubject = (subject) => subject === "body" || subject === "html" || subject === ":root"
|
|
21
|
+
const isHeadingSubject = (subject) => /^h[1-6](?![\w-])/.test(subject)
|
|
22
|
+
const isMonoSubject = (subject) => /^(?:code|pre|kbd|samp)(?![\w-])/.test(subject)
|
|
23
|
+
const isLinkSubject = (subject) => /^a(?![\w-])/.test(subject)
|
|
24
|
+
|
|
25
|
+
function formatStack(families) {
|
|
26
|
+
return families.map((family) => (/^-?[A-Za-z_][\w-]*$/.test(family) ? family : JSON.stringify(family))).join(", ")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** { body?, heading?, mono? } from font-family / font declarations, var() resolved. */
|
|
30
|
+
export function fontStacks(css) {
|
|
31
|
+
const fonts = {}
|
|
32
|
+
const entries = fontFamilies(css)
|
|
33
|
+
const pick = (test) => entries.find((entry) => subjects(entry.selector).some(test))
|
|
34
|
+
const body = pick(isBodySubject)
|
|
35
|
+
const heading = pick(isHeadingSubject)
|
|
36
|
+
const mono = pick(isMonoSubject) ?? entries.find((entry) => entry.families.at(-1)?.toLowerCase() === "monospace")
|
|
37
|
+
if (body) fonts.body = formatStack(body.families)
|
|
38
|
+
if (heading) fonts.heading = formatStack(heading.families)
|
|
39
|
+
if (mono) fonts.mono = formatStack(mono.families)
|
|
40
|
+
return fonts
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function tokenRoles(name) {
|
|
44
|
+
const roles = []
|
|
45
|
+
if (/accent|primary|brand/i.test(name)) roles.push("accent")
|
|
46
|
+
if (/(?:^--|-)bg(?:-|$)|background|surface/i.test(name)) roles.push("background")
|
|
47
|
+
if (/border|(?:^--|-)rule(?:-|$)/i.test(name)) roles.push("border")
|
|
48
|
+
return roles
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function declarationRoles(selector, prop) {
|
|
52
|
+
if (prop.startsWith("--")) return tokenRoles(prop)
|
|
53
|
+
if (prop.startsWith("background")) return ["background"]
|
|
54
|
+
if (prop.startsWith("border") || prop.startsWith("outline")) return ["border"]
|
|
55
|
+
if (prop !== "color") return []
|
|
56
|
+
const parts = subjects(selector)
|
|
57
|
+
const roles = []
|
|
58
|
+
if (parts.some(isLinkSubject)) roles.push("link")
|
|
59
|
+
if (parts.some(isHeadingSubject)) roles.push("heading")
|
|
60
|
+
if (parts.some(isBodySubject)) roles.push("body")
|
|
61
|
+
return roles
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Top hex colors by use count (ties keep first appearance): each token definition counts once,
|
|
66
|
+
* and each other declaration counts once per hex it resolves to (var() against light tokens).
|
|
67
|
+
* Roles are guessed from the selector and property where the color is used.
|
|
68
|
+
*/
|
|
69
|
+
export function rankColors(css, lightTokens) {
|
|
70
|
+
const table = new Map()
|
|
71
|
+
for (const rule of scanRules(css)) {
|
|
72
|
+
for (const { prop, value } of rule.declarations) {
|
|
73
|
+
const roles = declarationRoles(rule.selector, prop)
|
|
74
|
+
for (const hex of hexColors(prop.startsWith("--") ? value : resolveVar(value, lightTokens))) {
|
|
75
|
+
const entry = table.get(hex) ?? { hex, count: 0, roles: new Set() }
|
|
76
|
+
entry.count++
|
|
77
|
+
for (const role of roles) entry.roles.add(role)
|
|
78
|
+
table.set(hex, entry)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [...table.values()]
|
|
83
|
+
.sort((a, b) => b.count - a.count)
|
|
84
|
+
.slice(0, TOP_COLORS)
|
|
85
|
+
.map((entry) => ({ hex: entry.hex, count: entry.count, roles: ROLE_ORDER.filter((role) => entry.roles.has(role)) }))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** { fontSize?, lineHeight?, maxWidth? }: body size and leading, plus the most used absolute max width. */
|
|
89
|
+
export function readingMeasure(css, lightTokens) {
|
|
90
|
+
const measure = {}
|
|
91
|
+
const rules = scanRules(css).filter((rule) => !rule.atRule)
|
|
92
|
+
for (const rule of rules) {
|
|
93
|
+
if (!subjects(rule.selector).some(isBodySubject)) continue
|
|
94
|
+
for (const { prop, value } of rule.declarations) {
|
|
95
|
+
if (prop === "font-size" && !measure.fontSize) measure.fontSize = resolveVar(value, lightTokens)
|
|
96
|
+
if (prop === "line-height" && !measure.lineHeight) measure.lineHeight = resolveVar(value, lightTokens)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const widths = new Map()
|
|
100
|
+
for (const rule of rules) {
|
|
101
|
+
for (const { prop, value } of rule.declarations) {
|
|
102
|
+
if (prop !== "max-width" && prop !== "max-inline-size") continue
|
|
103
|
+
const resolved = resolveVar(value, lightTokens).trim()
|
|
104
|
+
if (ABSOLUTE_LENGTH.test(resolved)) widths.set(resolved, (widths.get(resolved) ?? 0) + 1)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const widest = [...widths].sort((a, b) => b[1] - a[1])[0]
|
|
108
|
+
if (widest) measure.maxWidth = widest[0]
|
|
109
|
+
return measure
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Every CSS-derived Extract field from one concatenated stylesheet. */
|
|
113
|
+
export function cssFacts(css) {
|
|
114
|
+
const tokens = customProperties(css)
|
|
115
|
+
return {
|
|
116
|
+
tokens,
|
|
117
|
+
fonts: fontStacks(css),
|
|
118
|
+
colors: rankColors(css, tokens.light),
|
|
119
|
+
breakpoints: mediaBreakpoints(css),
|
|
120
|
+
measure: readingMeasure(css, tokens.light),
|
|
121
|
+
}
|
|
122
|
+
}
|