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,102 @@
|
|
|
1
|
+
// cli-support.mjs - argument parsing, typed usage errors, exit-code mapping, and JSON file IO
|
|
2
|
+
// shared by every report-tools subcommand. node: builtins only.
|
|
3
|
+
|
|
4
|
+
import { readFileSync, renameSync, writeFileSync } from "node:fs"
|
|
5
|
+
import { basename, dirname, join } from "node:path"
|
|
6
|
+
|
|
7
|
+
/** A usage or input error: always exit code 2. */
|
|
8
|
+
export class CliError extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message)
|
|
11
|
+
this.name = "CliError"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const IO_CODES = new Set(["ENOENT", "EACCES", "EISDIR", "ENOTDIR", "EPERM"])
|
|
16
|
+
|
|
17
|
+
/** 0 pass, 1 semantic failure (result.exitCode), 2 usage or IO error. */
|
|
18
|
+
export function exitCodeFor(resultOrError) {
|
|
19
|
+
if (resultOrError instanceof CliError) return 2
|
|
20
|
+
if (resultOrError instanceof Error) return IO_CODES.has(resultOrError.code) ? 2 : 1
|
|
21
|
+
return resultOrError?.exitCode === 1 ? 1 : 0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse `--name value`, `--name=value`, boolean flags, and repeatable flags.
|
|
26
|
+
* @param {readonly string[]} argv
|
|
27
|
+
* @param {{ repeatable?: string[], booleans?: string[] }} spec
|
|
28
|
+
*/
|
|
29
|
+
export function parseArgs(argv, { repeatable = [], booleans = [] } = {}) {
|
|
30
|
+
const repeat = new Set(repeatable)
|
|
31
|
+
const bools = new Set(booleans)
|
|
32
|
+
const positionals = []
|
|
33
|
+
const flags = {}
|
|
34
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
35
|
+
const arg = argv[index]
|
|
36
|
+
if (!arg.startsWith("--")) {
|
|
37
|
+
positionals.push(arg)
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
const eq = arg.indexOf("=")
|
|
41
|
+
const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq)
|
|
42
|
+
let value
|
|
43
|
+
if (bools.has(name) && eq === -1) value = true
|
|
44
|
+
else if (eq !== -1) value = arg.slice(eq + 1)
|
|
45
|
+
else {
|
|
46
|
+
value = argv[index + 1]
|
|
47
|
+
if (value === undefined || value.startsWith("--")) throw new CliError(`--${name} requires a value`)
|
|
48
|
+
index += 1
|
|
49
|
+
}
|
|
50
|
+
if (repeat.has(name)) flags[name] = [...(flags[name] ?? []), value]
|
|
51
|
+
else flags[name] = value
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
positionals,
|
|
55
|
+
flags,
|
|
56
|
+
/** @param {string} name @param {number} [fallback] */
|
|
57
|
+
int(name, fallback) {
|
|
58
|
+
if (flags[name] === undefined) {
|
|
59
|
+
if (fallback === undefined) throw new CliError(`--${name} is required`)
|
|
60
|
+
return fallback
|
|
61
|
+
}
|
|
62
|
+
const parsed = Number(flags[name])
|
|
63
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new CliError(`--${name} requires a positive integer`)
|
|
64
|
+
return parsed
|
|
65
|
+
},
|
|
66
|
+
/** @param {string} name */
|
|
67
|
+
required(name) {
|
|
68
|
+
const value = flags[name]
|
|
69
|
+
if (value === undefined || value === true) throw new CliError(`--${name} is required`)
|
|
70
|
+
return value
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Read and parse a JSON file; a missing or malformed file is a CliError naming the path. */
|
|
76
|
+
export function readJsonFile(path) {
|
|
77
|
+
let text
|
|
78
|
+
try {
|
|
79
|
+
text = readFileSync(path, "utf8")
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new CliError(`cannot read ${path}: ${error.code ?? error.message}`)
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(text)
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new CliError(`${path} is not valid JSON: ${error.message}`)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Write JSON through a sibling temp file + rename, so a crash never leaves a half-written file. */
|
|
91
|
+
export function writeJsonAtomic(path, value) {
|
|
92
|
+
const temp = join(dirname(path), `.${basename(path)}.${process.pid}.${Date.now()}.tmp`)
|
|
93
|
+
writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`)
|
|
94
|
+
renameSync(temp, path)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Print a result: JSON on stdout, one summary line on stderr. */
|
|
98
|
+
export function emit(result, { stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
99
|
+
if (typeof result.text === "string") stdout.write(result.text.endsWith("\n") ? result.text : `${result.text}\n`)
|
|
100
|
+
else stdout.write(`${JSON.stringify(result.json ?? result, null, 2)}\n`)
|
|
101
|
+
if (result.summary) stderr.write(`${result.summary}\n`)
|
|
102
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// contracts.mjs - the fixed vocabulary every report-tools module shares.
|
|
2
|
+
//
|
|
3
|
+
// Severities live here and nowhere else: no module exports a way to change them, so a
|
|
4
|
+
// content-integrity defect can never be demoted by a caller. Plain Node ESM, node: builtins
|
|
5
|
+
// only, runnable by `node` and `bun` alike.
|
|
6
|
+
|
|
7
|
+
export const SCHEMA_VERSION = 1
|
|
8
|
+
|
|
9
|
+
function deepFreeze(value) {
|
|
10
|
+
if (value === null || typeof value !== "object") return value
|
|
11
|
+
for (const key of Object.keys(value)) deepFreeze(value[key])
|
|
12
|
+
return Object.freeze(value)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** code -> { severity, integrity, hint }. integrity = blocks delivery even on repair exhaustion. */
|
|
16
|
+
export const DEFECT_CODES = deepFreeze({
|
|
17
|
+
korean_no_keep_all: { severity: "major", integrity: false, hint: 'Add "word-break: keep-all; overflow-wrap: anywhere" to the body or the slide text container.' },
|
|
18
|
+
palette_off_token: { severity: "minor", integrity: false, hint: "Replace the literal color with a design-spec token (var(--...)) or add the color to the spec palette." },
|
|
19
|
+
emoji_in_prose: { severity: "major", integrity: false, hint: "Remove the emoji; use an inline SVG icon or plain text." },
|
|
20
|
+
em_dash_in_prose: { severity: "major", integrity: false, hint: "Rewrite the sentence with a comma, colon, or period instead of an em dash." },
|
|
21
|
+
en_dash_in_prose: { severity: "minor", integrity: false, hint: "Use a hyphen for compounds; keep the en dash only between numbers." },
|
|
22
|
+
heading_too_long: { severity: "minor", integrity: false, hint: "Shorten the heading to one line (26 Korean characters or 12 English words)." },
|
|
23
|
+
heading_generic: { severity: "minor", integrity: false, hint: "Make the heading carry the section's claim instead of a generic label." },
|
|
24
|
+
unsourced_number: { severity: "major", integrity: false, hint: "Tag the number MEASURED / ASSUMED / DERIVED or cite its source in the same paragraph." },
|
|
25
|
+
chart_without_figure: { severity: "major", integrity: false, hint: "Wrap the image or chart in a figure container with a caption." },
|
|
26
|
+
chart_svg_no_text: { severity: "major", integrity: false, hint: "Give the chart a title, axis labels, units, and value labels as SVG text." },
|
|
27
|
+
chart_svg_missing_labels: { severity: "minor", integrity: false, hint: "Add the missing title, axis ticks, or unit label to the chart." },
|
|
28
|
+
korean_serif_font: { severity: "major", integrity: false, hint: "Use a gothic (sans) Korean family; serif and brush families are banned by the design spec." },
|
|
29
|
+
section_without_citation: { severity: "major", integrity: false, hint: "Cite at least one source inside this section or merge it into a cited one." },
|
|
30
|
+
missing_closing_section: { severity: "blocker", integrity: true, hint: "Add the closing section that explains how the report was made (method, sources, gates)." },
|
|
31
|
+
missing_citation_section: { severity: "blocker", integrity: true, hint: "Add a sources or references section, or [S<n>] definition lines." },
|
|
32
|
+
broken_asset_reference: { severity: "blocker", integrity: true, hint: "Re-render or restore the missing asset file before delivery." },
|
|
33
|
+
missing_required_section: { severity: "blocker", integrity: true, hint: "Add the section the brief requires (matched by --require-section)." },
|
|
34
|
+
missing_promised_deliverable: { severity: "blocker", integrity: true, hint: "Produce the promised deliverable or record it as blocked_capability or skipped with a reason." },
|
|
35
|
+
layout_overflow: { severity: "major", integrity: false, hint: "Constrain the element to its container (max-width: 100%, overflow-wrap: anywhere, or a narrower measure)." },
|
|
36
|
+
layout_text_clipped: { severity: "major", integrity: false, hint: "Remove the overflow: hidden clip or give the text room to wrap." },
|
|
37
|
+
layout_scroll_container: { severity: "minor", integrity: false, hint: "Scrolling content prints clipped; make the content fit or wrap the table in a scroll wrapper only for HTML deliverables." },
|
|
38
|
+
layout_image_distorted: { severity: "major", integrity: false, hint: "Preserve the image aspect ratio (object-fit: contain inside a fixed container)." },
|
|
39
|
+
layout_sibling_overlap: { severity: "major", integrity: false, hint: "Separate the overlapping elements (margins, grid, or a slide split)." },
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const LAYOUT_PREFIX = "layout_"
|
|
43
|
+
/** Codes checkStatic can raise (G1-G15). */
|
|
44
|
+
export const STATIC_CODES = Object.freeze(
|
|
45
|
+
Object.keys(DEFECT_CODES).filter((code) => !code.startsWith(LAYOUT_PREFIX) && code !== "missing_promised_deliverable"),
|
|
46
|
+
)
|
|
47
|
+
/** Codes checkLayout can raise (L1-L5). */
|
|
48
|
+
export const LAYOUT_CODES = Object.freeze(Object.keys(DEFECT_CODES).filter((code) => code.startsWith(LAYOUT_PREFIX)))
|
|
49
|
+
/** Codes only the outcome manifest verifier raises. */
|
|
50
|
+
export const MANIFEST_CODES = Object.freeze(["missing_promised_deliverable"])
|
|
51
|
+
|
|
52
|
+
export const INTEGRITY_CODES = Object.freeze(
|
|
53
|
+
new Set(Object.entries(DEFECT_CODES).filter(([, entry]) => entry.integrity).map(([code]) => code)),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
export const LANES = Object.freeze(["template-strict", "template-vibe", "no-format", "edit-existing"])
|
|
57
|
+
export const STATES = Object.freeze(["none", "partial", "complete"])
|
|
58
|
+
export const DELIVERABLE_FORMATS = Object.freeze(["pdf", "docx", "html", "md", "latex", "slides", "post"])
|
|
59
|
+
export const DELIVERABLE_STATUSES = Object.freeze(["pending", "delivered", "blocked_capability", "skipped", "failed"])
|
|
60
|
+
export const GATE_NAMES = Object.freeze(["static", "layout", "visual", "proofread"])
|
|
61
|
+
export const GATE_STATUSES = Object.freeze(["pass", "fail", "not_run"])
|
|
62
|
+
export const LINEAGE_MODES = Object.freeze(["inline", "section"])
|
|
63
|
+
export const ANSWERED_BY = Object.freeze(["user", "default", "request"])
|
|
64
|
+
export const REPAIR_DEFAULTS = Object.freeze({ maxAttempts: 3, plateauLimit: 2, wallClockMs: 15 * 60 * 1000 })
|
|
65
|
+
export const DESIGN_SPEC_SECTIONS = Object.freeze([
|
|
66
|
+
"Extracted from",
|
|
67
|
+
"Tokens",
|
|
68
|
+
"Typography",
|
|
69
|
+
"Layout",
|
|
70
|
+
"Structure",
|
|
71
|
+
"Figures",
|
|
72
|
+
"Citations",
|
|
73
|
+
"Open questions",
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/
|
|
77
|
+
|
|
78
|
+
function isObject(value) {
|
|
79
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function check(errors, condition, message) {
|
|
83
|
+
if (!condition) errors.push(message)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validateDeliverable(row, index, errors) {
|
|
87
|
+
const at = `deliverables[${index}]`
|
|
88
|
+
if (!isObject(row)) return errors.push(`${at} must be an object`)
|
|
89
|
+
check(errors, DELIVERABLE_FORMATS.includes(row.format), `${at}.format must be one of ${DELIVERABLE_FORMATS.join(", ")}`)
|
|
90
|
+
check(errors, DELIVERABLE_STATUSES.includes(row.status), `${at}.status must be one of ${DELIVERABLE_STATUSES.join(", ")}`)
|
|
91
|
+
check(errors, typeof row.promised === "boolean", `${at}.promised must be a boolean`)
|
|
92
|
+
check(errors, typeof row.updatedAt === "string" && ISO_DATE.test(row.updatedAt), `${at}.updatedAt must be an ISO timestamp`)
|
|
93
|
+
for (const key of ["path", "reason"]) {
|
|
94
|
+
if (row[key] !== undefined) check(errors, typeof row[key] === "string", `${at}.${key} must be a string`)
|
|
95
|
+
}
|
|
96
|
+
for (const key of ["bytes", "pages"]) {
|
|
97
|
+
if (row[key] !== undefined) check(errors, Number.isInteger(row[key]) && row[key] >= 0, `${at}.${key} must be a non-negative integer`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function validateDefect(defect, at, errors) {
|
|
102
|
+
if (!isObject(defect)) return errors.push(`${at} must be an object`)
|
|
103
|
+
check(errors, Object.hasOwn(DEFECT_CODES, defect.code), `${at}.code is not a known defect code`)
|
|
104
|
+
check(errors, typeof defect.message === "string", `${at}.message must be a string`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @returns {{ ok: boolean, errors: string[] }} */
|
|
108
|
+
export function validateOutcomeManifest(manifest) {
|
|
109
|
+
const errors = []
|
|
110
|
+
if (!isObject(manifest)) return { ok: false, errors: ["manifest must be an object (schemaVersion missing)"] }
|
|
111
|
+
check(errors, manifest.schemaVersion === SCHEMA_VERSION, `schemaVersion must be ${SCHEMA_VERSION}`)
|
|
112
|
+
check(errors, typeof manifest.sessionDir === "string" && manifest.sessionDir.length > 0, "sessionDir must be a non-empty string")
|
|
113
|
+
check(errors, typeof manifest.startedAt === "string" && ISO_DATE.test(manifest.startedAt), "startedAt must be an ISO timestamp")
|
|
114
|
+
check(errors, manifest.finishedAt === null || (typeof manifest.finishedAt === "string" && ISO_DATE.test(manifest.finishedAt)), "finishedAt must be null or an ISO timestamp")
|
|
115
|
+
check(errors, manifest.elapsedMinutes === null || (Number.isFinite(manifest.elapsedMinutes) && manifest.elapsedMinutes >= 0), "elapsedMinutes must be null or a non-negative number")
|
|
116
|
+
check(errors, manifest.lane === null || LANES.includes(manifest.lane), `lane must be null or one of ${LANES.join(", ")}`)
|
|
117
|
+
check(errors, manifest.interview === null || isObject(manifest.interview), "interview must be null or an object")
|
|
118
|
+
if (Array.isArray(manifest.deliverables)) manifest.deliverables.forEach((row, index) => validateDeliverable(row, index, errors))
|
|
119
|
+
else errors.push("deliverables must be an array")
|
|
120
|
+
if (Array.isArray(manifest.renders)) {
|
|
121
|
+
manifest.renders.forEach((render, index) => {
|
|
122
|
+
check(errors, isObject(render) && typeof render.path === "string" && Number.isInteger(render.page) && render.page >= 1, `renders[${index}] must be { path: string, page: integer >= 1 }`)
|
|
123
|
+
})
|
|
124
|
+
} else errors.push("renders must be an array")
|
|
125
|
+
if (isObject(manifest.gates)) {
|
|
126
|
+
for (const gate of GATE_NAMES) check(errors, GATE_STATUSES.includes(manifest.gates[gate]), `gates.${gate} must be one of ${GATE_STATUSES.join(", ")}`)
|
|
127
|
+
} else errors.push("gates must be an object")
|
|
128
|
+
if (Array.isArray(manifest.residualDefects)) manifest.residualDefects.forEach((defect, index) => validateDefect(defect, `residualDefects[${index}]`, errors))
|
|
129
|
+
else errors.push("residualDefects must be an array")
|
|
130
|
+
check(errors, manifest.repair === null || isObject(manifest.repair), "repair must be null or an object")
|
|
131
|
+
check(errors, manifest.sources === null || isObject(manifest.sources), "sources must be null or an object")
|
|
132
|
+
return { ok: errors.length === 0, errors }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isMeasure(value) {
|
|
136
|
+
return Array.isArray(value) && value.length === 3 && value.every((n) => Number.isInteger(n) && n >= 0)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** @returns {{ ok: boolean, errors: string[] }} */
|
|
140
|
+
export function validateRepairState(state) {
|
|
141
|
+
const errors = []
|
|
142
|
+
if (!isObject(state)) return { ok: false, errors: ["repair state must be an object (schemaVersion missing)"] }
|
|
143
|
+
check(errors, state.schemaVersion === SCHEMA_VERSION, `schemaVersion must be ${SCHEMA_VERSION}`)
|
|
144
|
+
check(errors, typeof state.startedAt === "string" && ISO_DATE.test(state.startedAt), "startedAt must be an ISO timestamp")
|
|
145
|
+
if (isObject(state.budget)) {
|
|
146
|
+
for (const key of ["maxAttempts", "plateauLimit", "wallClockMs"]) {
|
|
147
|
+
check(errors, Number.isInteger(state.budget[key]) && state.budget[key] > 0, `budget.${key} must be a positive integer`)
|
|
148
|
+
}
|
|
149
|
+
} else errors.push("budget must be an object")
|
|
150
|
+
if (Array.isArray(state.attempts)) {
|
|
151
|
+
state.attempts.forEach((attempt, index) => {
|
|
152
|
+
const at = `attempts[${index}]`
|
|
153
|
+
if (!isObject(attempt)) return errors.push(`${at} must be an object`)
|
|
154
|
+
check(errors, typeof attempt.at === "string" && ISO_DATE.test(attempt.at), `${at}.at must be an ISO timestamp`)
|
|
155
|
+
check(errors, isMeasure(attempt.measure), `${at}.measure must be [blocker, major, minor] counts`)
|
|
156
|
+
check(errors, typeof attempt.signature === "string", `${at}.signature must be a string`)
|
|
157
|
+
})
|
|
158
|
+
} else errors.push("attempts must be an array")
|
|
159
|
+
check(errors, Number.isInteger(state.consecutiveNonProgress) && state.consecutiveNonProgress >= 0, "consecutiveNonProgress must be a non-negative integer")
|
|
160
|
+
check(errors, Array.isArray(state.seenSignatures) && state.seenSignatures.every((s) => typeof s === "string"), "seenSignatures must be an array of strings")
|
|
161
|
+
return { ok: errors.length === 0, errors }
|
|
162
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// css-lite.mjs - a tolerant, dependency-free CSS scanner for report gates and spec extraction.
|
|
2
|
+
//
|
|
3
|
+
// Never throws on malformed input: an unbalanced stylesheet yields the rules closed so far.
|
|
4
|
+
// Plain Node ESM, no imports.
|
|
5
|
+
|
|
6
|
+
const GROUP_AT = /^@(?:-[a-z]+-)?(?:media|supports|container|layer|document|scope|starting-style|keyframes)\b/i
|
|
7
|
+
const HEX = /(?<![\w&])#([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})(?![\w-])/gi
|
|
8
|
+
const DARK_MEDIA = /prefers-color-scheme\s*:\s*dark/i
|
|
9
|
+
const DARK_THEME = /\[data-theme\s*=\s*["']?dark/i
|
|
10
|
+
const TOKEN_SCOPE = /(?:^|[\s,>])(?::root|html)\b|^\[data-theme/i
|
|
11
|
+
|
|
12
|
+
function stripComments(css) {
|
|
13
|
+
return css.replace(/\/\*[\s\S]*?(?:\*\/|$)/g, "")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Split on `separator` outside quotes and parentheses. */
|
|
17
|
+
function splitTopLevel(text, separator) {
|
|
18
|
+
const parts = []
|
|
19
|
+
let depth = 0
|
|
20
|
+
let quote = null
|
|
21
|
+
let current = ""
|
|
22
|
+
for (let i = 0; i < text.length; i++) {
|
|
23
|
+
const ch = text[i]
|
|
24
|
+
if (quote) {
|
|
25
|
+
if (ch === "\\") current += ch + (text[++i] ?? "")
|
|
26
|
+
else {
|
|
27
|
+
if (ch === quote) quote = null
|
|
28
|
+
current += ch
|
|
29
|
+
}
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
if (ch === '"' || ch === "'") quote = ch
|
|
33
|
+
else if (ch === "(") depth++
|
|
34
|
+
else if (ch === ")") depth = Math.max(0, depth - 1)
|
|
35
|
+
else if (ch === separator && depth === 0) {
|
|
36
|
+
parts.push(current)
|
|
37
|
+
current = ""
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
current += ch
|
|
41
|
+
}
|
|
42
|
+
parts.push(current)
|
|
43
|
+
return parts
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseDeclarations(text) {
|
|
47
|
+
const declarations = []
|
|
48
|
+
for (const piece of splitTopLevel(text, ";")) {
|
|
49
|
+
const colon = piece.indexOf(":")
|
|
50
|
+
if (colon <= 0) continue
|
|
51
|
+
const rawProp = piece.slice(0, colon).trim()
|
|
52
|
+
const value = piece.slice(colon + 1).trim()
|
|
53
|
+
if (!rawProp || /[\s{}]/.test(rawProp)) continue
|
|
54
|
+
declarations.push({ prop: rawProp.startsWith("--") ? rawProp : rawProp.toLowerCase(), value })
|
|
55
|
+
}
|
|
56
|
+
return declarations
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @returns {{ selector: string, declarations: { prop: string, value: string }[], atRule?: string }[]}
|
|
61
|
+
* atRule is the chain of enclosing grouping at-rule preludes joined by a space.
|
|
62
|
+
*/
|
|
63
|
+
export function scanRules(css) {
|
|
64
|
+
if (typeof css !== "string") return []
|
|
65
|
+
const src = stripComments(css)
|
|
66
|
+
const slots = []
|
|
67
|
+
const stack = []
|
|
68
|
+
let buffer = ""
|
|
69
|
+
for (let i = 0; i < src.length; i++) {
|
|
70
|
+
const ch = src[i]
|
|
71
|
+
if (ch === '"' || ch === "'") {
|
|
72
|
+
let end = i + 1
|
|
73
|
+
while (end < src.length && src[end] !== ch) end += src[end] === "\\" ? 2 : 1
|
|
74
|
+
buffer += src.slice(i, end + 1)
|
|
75
|
+
i = end
|
|
76
|
+
} else if (ch === "{") {
|
|
77
|
+
const pieces = splitTopLevel(buffer, ";")
|
|
78
|
+
const prelude = pieces.pop().trim()
|
|
79
|
+
const frame = stack.at(-1)
|
|
80
|
+
if (frame && pieces.length) frame.body += `${pieces.join(";")};`
|
|
81
|
+
const group = GROUP_AT.test(prelude)
|
|
82
|
+
const slot = group ? -1 : slots.push(null) - 1
|
|
83
|
+
stack.push({ prelude, group, slot, body: "" })
|
|
84
|
+
buffer = ""
|
|
85
|
+
} else if (ch === "}") {
|
|
86
|
+
const frame = stack.pop()
|
|
87
|
+
if (frame && !frame.group) {
|
|
88
|
+
const atChain = stack.filter((entry) => entry.group).map((entry) => entry.prelude)
|
|
89
|
+
const rule = { selector: frame.prelude, declarations: parseDeclarations(frame.body + buffer) }
|
|
90
|
+
if (atChain.length) rule.atRule = atChain.join(" ")
|
|
91
|
+
slots[frame.slot] = rule
|
|
92
|
+
}
|
|
93
|
+
buffer = ""
|
|
94
|
+
} else if (ch === ";" && stack.length === 0) {
|
|
95
|
+
buffer = ""
|
|
96
|
+
} else {
|
|
97
|
+
buffer += ch
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return slots.filter(Boolean)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isDarkRule(rule) {
|
|
104
|
+
if (rule.atRule && DARK_MEDIA.test(rule.atRule)) return true
|
|
105
|
+
return DARK_THEME.test(rule.selector.replace(/:not\([^)]*\)/gi, ""))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Global design tokens from `:root`, `html`, and `[data-theme]` rules. Dark = inside
|
|
110
|
+
* `@media (prefers-color-scheme: dark)` or on a `[data-theme="dark"]` selector. Last value wins.
|
|
111
|
+
* @returns {{ light: Map<string, string>, dark: Map<string, string> }}
|
|
112
|
+
*/
|
|
113
|
+
export function customProperties(css) {
|
|
114
|
+
const light = new Map()
|
|
115
|
+
const dark = new Map()
|
|
116
|
+
for (const rule of scanRules(css)) {
|
|
117
|
+
if (!rule.selector.split(",").some((part) => TOKEN_SCOPE.test(part.trim()))) continue
|
|
118
|
+
const target = isDarkRule(rule) ? dark : light
|
|
119
|
+
for (const { prop, value } of rule.declarations) if (prop.startsWith("--")) target.set(prop, value)
|
|
120
|
+
}
|
|
121
|
+
return { light, dark }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function splitFamilies(value) {
|
|
125
|
+
return splitTopLevel(value, ",")
|
|
126
|
+
.map((family) => family.trim().replace(/^(["'])(.*)\1$/, "$2").trim())
|
|
127
|
+
.filter(Boolean)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Font stack after the size token of a `font` shorthand, or null when the shorthand has none. */
|
|
131
|
+
function shorthandFamilies(value) {
|
|
132
|
+
const match = /(?:^|\s)[\d.]+(?:px|em|rem|pt|%|vw|vh|ch|ex)(?:\s*\/\s*\S+)?\s+(.+)$/i.exec(value)
|
|
133
|
+
return match ? match[1] : null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** @returns {{ selector: string, families: string[] }[]} var() references resolve against light tokens. */
|
|
137
|
+
export function fontFamilies(css) {
|
|
138
|
+
const props = customProperties(css).light
|
|
139
|
+
const result = []
|
|
140
|
+
for (const rule of scanRules(css)) {
|
|
141
|
+
for (const { prop, value } of rule.declarations) {
|
|
142
|
+
const resolved = resolveVar(value, props)
|
|
143
|
+
const stack = prop === "font-family" ? resolved : prop === "font" ? shorthandFamilies(resolved) : null
|
|
144
|
+
if (stack === null) continue
|
|
145
|
+
const families = splitFamilies(stack)
|
|
146
|
+
if (families.length) result.push({ selector: rule.selector, families })
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return result
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** #abc / #abcd / #aabbcc / #aabbccdd -> #aabbcc (lowercase, alpha dropped); anything else -> null. */
|
|
153
|
+
export function normalizeHex(hex) {
|
|
154
|
+
const match = /^#([0-9a-f]{3,8})$/i.exec(String(hex).trim())
|
|
155
|
+
if (!match || ![3, 4, 6, 8].includes(match[1].length)) return null
|
|
156
|
+
const digits = match[1].toLowerCase()
|
|
157
|
+
const six = digits.length <= 4 ? [...digits.slice(0, 3)].map((d) => d + d).join("") : digits.slice(0, 6)
|
|
158
|
+
return `#${six}`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Every hex color in `text`, normalized, in order of appearance, repeats kept. */
|
|
162
|
+
export function hexColors(text) {
|
|
163
|
+
if (typeof text !== "string") return []
|
|
164
|
+
return [...text.matchAll(HEX)].map((match) => normalizeHex(match[0]))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Unique px widths named in @media preludes (min-/max-width and range syntax), ascending. */
|
|
168
|
+
export function mediaBreakpoints(css) {
|
|
169
|
+
if (typeof css !== "string") return []
|
|
170
|
+
const widths = new Set()
|
|
171
|
+
for (const media of stripComments(css).matchAll(/@media\b([^{;]*)/gi)) {
|
|
172
|
+
for (const match of media[1].matchAll(/\b(?:min-|max-)?width\s*(?::|[<>]=?|=)\s*(\d+(?:\.\d+)?)px/gi)) {
|
|
173
|
+
widths.add(Number(match[1]))
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return [...widths].sort((a, b) => a - b)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function lookup(props, name) {
|
|
180
|
+
if (props instanceof Map) return props.get(name)
|
|
181
|
+
return props && Object.hasOwn(props, name) ? props[name] : undefined
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Substitute var(--name[, fallback]) from `props` (Map or object); unresolvable references keep their text. */
|
|
185
|
+
export function resolveVar(value, props, depth = 0) {
|
|
186
|
+
if (typeof value !== "string" || depth > 10) return value
|
|
187
|
+
let out = ""
|
|
188
|
+
let cursor = 0
|
|
189
|
+
for (;;) {
|
|
190
|
+
const start = value.indexOf("var(", cursor)
|
|
191
|
+
if (start === -1) return out + value.slice(cursor)
|
|
192
|
+
let end = start + 4
|
|
193
|
+
for (let open = 1; end < value.length && open > 0; end++) {
|
|
194
|
+
if (value[end] === "(") open++
|
|
195
|
+
else if (value[end] === ")") open--
|
|
196
|
+
}
|
|
197
|
+
const [name, ...rest] = splitTopLevel(value.slice(start + 4, end - 1), ",")
|
|
198
|
+
const fallback = rest.length ? rest.join(",").trim() : undefined
|
|
199
|
+
const replacement = lookup(props, name.trim()) ?? fallback
|
|
200
|
+
const original = value.slice(start, end)
|
|
201
|
+
out += value.slice(cursor, start) + (replacement === undefined ? original : resolveVar(replacement, props, depth + 1))
|
|
202
|
+
cursor = end
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -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,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
|
+
}
|