@namewta/speculo 0.8.6 → 0.8.7

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.
Files changed (74) hide show
  1. package/package.json +1 -1
  2. package/template/canonical/canonical-specdev-goal-plan.md +14 -14
  3. package/template/canonical/canonical-specdev-grill-with-docs.md +9 -7
  4. package/template/canonical/canonical-specdev-spec.md +9 -7
  5. package/template/canonical/canonical-specdev-tickets.md +13 -13
  6. package/template/canonical/canonical-specdev-wayfinder.md +7 -7
  7. package/template/workflows/specdev/I-init-setup/I-init-setup.md +1 -1
  8. package/template/workflows/specdev/I-init-setup/config-template.json +2 -2
  9. package/template/workflows/specdev/P-goal-plan/planning-modes.md +1 -1
  10. package/template/workflows/specdev/P-prototype/P-prototype.md +30 -26
  11. package/template/workflows/specdev/P-prototype/design-library/INDEX.md +43 -0
  12. package/template/workflows/specdev/P-prototype/design-library/color-and-theme.md +65 -0
  13. package/template/workflows/specdev/P-prototype/design-library/foundations.md +66 -0
  14. package/template/workflows/specdev/P-prototype/design-library/interaction-patterns.md +61 -0
  15. package/template/workflows/specdev/P-prototype/design-library/product-pattern-index.md +73 -0
  16. package/template/workflows/specdev/P-prototype/design-library/research-provenance.md +54 -0
  17. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/METHODOLOGY.md +90 -0
  18. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/README.md +31 -0
  19. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/data/extended-projects.json +241 -0
  20. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/data/projects.json +253 -0
  21. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/reference/USAGE.md +40 -0
  22. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/reference/design-tokens.css +145 -0
  23. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/report/OPEN_SOURCE_UI_RESEARCH_2026.md +519 -0
  24. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/PROJECT_TEMPLATE.md +29 -0
  25. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/README.md +9 -0
  26. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/claude-code-modern-clients.md +407 -0
  27. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/creative-ai-communication.md +574 -0
  28. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/data-dev-tools.md +432 -0
  29. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/personal-multiplatform-apps.md +185 -0
  30. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/productivity-collaboration.md +398 -0
  31. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/01-dense-ide.html +68 -0
  32. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/02-monochrome-console.html +18 -0
  33. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/03-soft-personal-ai.html +17 -0
  34. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/04-responsive-web.html +14 -0
  35. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/05-mobile-supervisor.html +16 -0
  36. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/06-cross-platform-workspace.html +17 -0
  37. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/07-local-first-content.html +9 -0
  38. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/08-media-first.html +17 -0
  39. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/README.md +49 -0
  40. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/ATTRIBUTION.md +9 -0
  41. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/landscape.jpg +0 -0
  42. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/lucide.js +20494 -0
  43. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/mountain.jpg +0 -0
  44. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/workspace.jpg +0 -0
  45. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/index.html +70 -0
  46. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/package-lock.json +78 -0
  47. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/package.json +12 -0
  48. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/screenshots/index-desktop.png +0 -0
  49. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/scripts/gallery.js +134 -0
  50. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/styles/base.css +795 -0
  51. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/styles/pages.css +533 -0
  52. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/tests/gallery.spec.js +56 -0
  53. package/template/workflows/specdev/P-prototype/design-library/responsive-and-platforms.md +53 -0
  54. package/template/workflows/specdev/P-prototype/design-library/style-index.md +32 -0
  55. package/template/workflows/specdev/P-prototype/design-package.schema.json +70 -0
  56. package/template/workflows/specdev/P-prototype/design-system-template.md +366 -0
  57. package/template/workflows/specdev/P-prototype/detect-existing-style.md +53 -0
  58. package/template/workflows/specdev/P-prototype/generate-design-package.md +50 -0
  59. package/template/workflows/specdev/P-prototype/style-selection-protocol.md +41 -0
  60. package/template/workflows/specdev/P-prototype/tools/materialize-prototype.mjs +126 -0
  61. package/template/workflows/specdev/P-prototype/tools/validate-design-package.mjs +155 -0
  62. package/template/workflows/specdev/README.md +11 -5
  63. package/template/workflows/specdev/T-triage/T-triage.md +1 -1
  64. package/template/workflows/specdev/W-wayfinder/W-wayfinder.md +1 -1
  65. package/template/workflows/specdev/common/README.md +2 -2
  66. package/template/workflows/specdev/common/rules/artifact-contract.md +3 -1
  67. package/template/workflows/specdev/common/schemas/config.schema.json +4 -4
  68. package/template/workflows/specdev/common/skills/dev-worktree/SKILL.md +4 -4
  69. package/template/workflows/specdev/common/skills/dev-worktree/references/create.md +1 -3
  70. package/template/workflows/specdev/common/tools/validate-specdev.mjs +88 -29
  71. package/template/workflows/specdev/P-prototype/logic-prototype.md +0 -24
  72. package/template/workflows/specdev/P-prototype/prototype-record-template.md +0 -46
  73. package/template/workflows/specdev/P-prototype/ui-prototype.md +0 -21
  74. package/template/workflows/specdev/common/schemas/prototype-record.schema.json +0 -24
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
4
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ const REQUIRED_TARGETS = new Set([
8
+ "final/index.html",
9
+ "final/styles.css",
10
+ "final/app.js",
11
+ ]);
12
+
13
+ const TARGET_LANGUAGE = new Map([
14
+ ["final/index.html", new Set(["html"])],
15
+ ["final/styles.css", new Set(["css"])],
16
+ ["final/app.js", new Set(["javascript", "js"])],
17
+ ]);
18
+
19
+ export function extractPrototypeSources(markdown) {
20
+ const pattern = /<!-- PROTOTYPE-FILE: ([^\r\n]+) -->\r?\n```(html|css|javascript|js)\r?\n([\s\S]*?)\r?\n```\r?\n<!-- \/PROTOTYPE-FILE -->/g;
21
+ const sources = new Map();
22
+
23
+ for (const match of markdown.matchAll(pattern)) {
24
+ const target = match[1].trim().replaceAll("\\", "/");
25
+ if (isAbsolute(target) || !TARGET_LANGUAGE.has(target)) {
26
+ throw new Error(`unsupported prototype target: ${target}`);
27
+ }
28
+ if (!TARGET_LANGUAGE.get(target).has(match[2])) throw new Error(`invalid code fence language for ${target}: ${match[2]}`);
29
+ if (sources.has(target)) throw new Error(`duplicate prototype target: ${target}`);
30
+ sources.set(target, `${match[3].replaceAll("\r\n", "\n")}\n`);
31
+ }
32
+
33
+ const openingCount = markdown.match(/<!-- PROTOTYPE-FILE:/g)?.length ?? 0;
34
+ const closingCount = markdown.match(/<!-- \/PROTOTYPE-FILE -->/g)?.length ?? 0;
35
+ if (openingCount !== 3 || closingCount !== 3 || sources.size !== 3) {
36
+ throw new Error(`expected exactly three paired PROTOTYPE-FILE blocks; found ${openingCount} openings, ${closingCount} closings, ${sources.size} valid blocks`);
37
+ }
38
+
39
+ for (const target of REQUIRED_TARGETS) {
40
+ if (!sources.has(target)) throw new Error(`missing prototype source block: ${target}`);
41
+ }
42
+
43
+ return sources;
44
+ }
45
+
46
+ function resolveInside(root, target) {
47
+ const destination = resolve(root, ...target.split("/"));
48
+ const local = relative(root, destination);
49
+ if (!local || local.startsWith(`..${sep}`) || local === ".." || isAbsolute(local)) {
50
+ throw new Error(`prototype target escapes design package: ${target}`);
51
+ }
52
+ return destination;
53
+ }
54
+
55
+ async function assertNoSymlink(root, target) {
56
+ let current = root;
57
+ const local = relative(root, target);
58
+ for (const segment of ["", ...local.split(sep).filter(Boolean)]) {
59
+ if (segment) current = join(current, segment);
60
+ try {
61
+ const info = await lstat(current);
62
+ if (info.isSymbolicLink()) throw new Error(`prototype path must not contain symlinks: ${current}`);
63
+ } catch (error) {
64
+ if (error.code !== "ENOENT") throw error;
65
+ }
66
+ }
67
+ }
68
+
69
+ async function atomicWrite(destination, content) {
70
+ await mkdir(dirname(destination), { recursive: true });
71
+ const temporary = `${destination}.tmp-${process.pid}-${Date.now()}`;
72
+ await writeFile(temporary, content, "utf8");
73
+ try {
74
+ await rename(temporary, destination);
75
+ } catch (error) {
76
+ await rm(temporary, { force: true });
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ export async function materialize(markdownPath, { check = false } = {}) {
82
+ const absoluteMarkdown = resolve(markdownPath);
83
+ const root = dirname(absoluteMarkdown);
84
+ const markdown = await readFile(absoluteMarkdown, "utf8");
85
+ const sources = extractPrototypeSources(markdown);
86
+ const mismatches = [];
87
+
88
+ for (const [target, expected] of sources) {
89
+ const destination = resolveInside(root, target);
90
+ await assertNoSymlink(root, destination);
91
+ if (check) {
92
+ let actual;
93
+ try {
94
+ actual = await readFile(destination, "utf8");
95
+ } catch (error) {
96
+ if (error.code === "ENOENT") {
97
+ mismatches.push(`${target}: missing`);
98
+ continue;
99
+ }
100
+ throw error;
101
+ }
102
+ if (actual !== expected) mismatches.push(`${target}: differs from design-system.md`);
103
+ } else {
104
+ await atomicWrite(destination, expected);
105
+ }
106
+ }
107
+
108
+ if (mismatches.length > 0) throw new Error(mismatches.join("\n"));
109
+ return [...sources.keys()];
110
+ }
111
+
112
+ async function main() {
113
+ const args = process.argv.slice(2);
114
+ const check = args.includes("--check");
115
+ const paths = args.filter((arg) => arg !== "--check");
116
+ if (paths.length !== 1) throw new Error("usage: materialize-prototype.mjs [--check] <design-system.md>");
117
+ const targets = await materialize(paths[0], { check });
118
+ console.log(`${check ? "verified" : "materialized"} ${targets.length} prototype files`);
119
+ }
120
+
121
+ if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
122
+ main().catch((error) => {
123
+ console.error(error.message);
124
+ process.exitCode = 1;
125
+ });
126
+ }
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { lstat, readdir, readFile, stat } from "node:fs/promises";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
+ import { extractPrototypeSources, materialize } from "./materialize-prototype.mjs";
6
+
7
+ const REQUIRED_HEADINGS = [
8
+ "## Project Evidence",
9
+ "## Style Decision",
10
+ "## Foundation Tokens",
11
+ "## Component and State Contract",
12
+ "## Interaction Contract",
13
+ "## Responsive Contract",
14
+ "## Prototype Sources",
15
+ "## Validation and Handoff",
16
+ ];
17
+
18
+ function frontmatterValue(header, key) {
19
+ const match = header.match(new RegExp(`^${key}:\\s*(.*?)\\s*$`, "m"));
20
+ if (!match) return undefined;
21
+ const value = match[1].trim();
22
+ if (value === "null") return null;
23
+ if (/^\d+$/.test(value)) return Number(value);
24
+ return value.replace(/^['"]|['"]$/g, "");
25
+ }
26
+
27
+ function frontmatterArray(header, key) {
28
+ const value = frontmatterValue(header, key);
29
+ if (typeof value !== "string" || !value.startsWith("[") || !value.endsWith("]")) return [];
30
+ return value.slice(1, -1).split(",").map((item) => item.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
31
+ }
32
+
33
+ async function isFile(path) {
34
+ try {
35
+ return (await stat(path)).isFile();
36
+ } catch (error) {
37
+ if (error.code === "ENOENT") return false;
38
+ throw error;
39
+ }
40
+ }
41
+
42
+ async function isSymlink(path) {
43
+ try {
44
+ return (await lstat(path)).isSymbolicLink();
45
+ } catch (error) {
46
+ if (error.code === "ENOENT") return false;
47
+ throw error;
48
+ }
49
+ }
50
+
51
+ async function validate(path, { allowDraft = false } = {}) {
52
+ const designPath = resolve(path);
53
+ if (await isSymlink(designPath)) throw new Error("design-system.md must not be a symlink");
54
+ const markdown = await readFile(designPath, "utf8");
55
+ const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
56
+ const errors = [];
57
+ if (!match) throw new Error("design-system.md requires YAML frontmatter");
58
+ const [, header, body] = match;
59
+ const meta = {
60
+ schemaVersion: frontmatterValue(header, "schema_version"),
61
+ artifact: frontmatterValue(header, "artifact"),
62
+ change: frontmatterValue(header, "change"),
63
+ designId: frontmatterValue(header, "design_id"),
64
+ status: frontmatterValue(header, "status"),
65
+ projectKind: frontmatterValue(header, "project_kind"),
66
+ platforms: frontmatterArray(header, "platforms"),
67
+ candidates: frontmatterArray(header, "style_candidates"),
68
+ selectedStyle: frontmatterValue(header, "selected_style"),
69
+ density: frontmatterValue(header, "density"),
70
+ colorMode: frontmatterValue(header, "color_mode"),
71
+ researchSnapshot: frontmatterValue(header, "research_snapshot"),
72
+ prototypeFiles: frontmatterArray(header, "prototype_files"),
73
+ updatedAt: frontmatterValue(header, "updated_at"),
74
+ };
75
+
76
+ const designRoot = dirname(designPath);
77
+ const expectedDesignId = basename(designRoot);
78
+ const expectedChange = basename(dirname(dirname(designRoot)));
79
+ if (meta.schemaVersion !== 2 || meta.artifact !== "ui-design-system") errors.push("artifact/schema_version must be ui-design-system/2");
80
+ if (!/^UI-\d{3,}$/.test(String(meta.designId ?? "")) || meta.designId !== expectedDesignId) errors.push("design_id must match the UI-NNN directory");
81
+ if (meta.change !== expectedChange) errors.push("change must match the owning change directory");
82
+ if (!new Set(["detecting", "selecting", "ready", "blocked"]).has(meta.status)) errors.push("invalid design status");
83
+ if (!new Set(["existing", "new"]).has(meta.projectKind)) errors.push("project_kind must be existing or new");
84
+ if (!meta.platforms.length || meta.platforms.some((value) => !new Set(["desktop", "web", "mobile"]).has(value))) errors.push("platforms must contain desktop, web, or mobile");
85
+ if (meta.candidates.length < 2 || meta.candidates.length > 4 || new Set(meta.candidates).size !== meta.candidates.length) errors.push("style_candidates must contain 2-4 unique values");
86
+ if (meta.researchSnapshot !== "49b5659") errors.push("research_snapshot must equal 49b5659");
87
+ for (const required of ["final/index.html", "final/styles.css", "final/app.js"]) {
88
+ if (!meta.prototypeFiles.includes(required)) errors.push(`prototype_files must include ${required}`);
89
+ }
90
+ if (typeof meta.updatedAt !== "string" || Number.isNaN(Date.parse(meta.updatedAt))) errors.push("updated_at must be ISO-8601");
91
+ for (const heading of REQUIRED_HEADINGS) if (!body.includes(heading)) errors.push(`missing '${heading}'`);
92
+ if (/(?:[A-Za-z]:[\\/]|\/(?:Users|home|tmp)\/)/.test(markdown)) errors.push("design package contains a machine-specific absolute path");
93
+
94
+ let sources;
95
+ try {
96
+ sources = extractPrototypeSources(markdown);
97
+ } catch (error) {
98
+ errors.push(error.message);
99
+ }
100
+
101
+ if (meta.status === "ready" && !allowDraft) {
102
+ if (!meta.selectedStyle || !meta.candidates.includes(meta.selectedStyle)) errors.push("ready design requires selected_style from style_candidates");
103
+ if (!new Set(["compact", "default", "comfortable", "touch"]).has(meta.density)) errors.push("ready design requires a density");
104
+ if (!new Set(["light", "dark", "system", "both"]).has(meta.colorMode)) errors.push("ready design requires a color_mode");
105
+ const comparisonIndex = join(designRoot, "comparison", "index.html");
106
+ const variantsRoot = join(designRoot, "comparison", "variants");
107
+ if (!(await isFile(comparisonIndex))) errors.push("ready design requires comparison/index.html");
108
+ if (await isSymlink(comparisonIndex)) errors.push("comparison/index.html must not be a symlink");
109
+ if (await isSymlink(join(designRoot, "comparison")) || await isSymlink(variantsRoot)) errors.push("comparison directories must not be symlinks");
110
+ let variants = [];
111
+ try {
112
+ variants = (await readdir(variantsRoot)).filter((entry) => entry.endsWith(".html"));
113
+ } catch (error) {
114
+ if (error.code !== "ENOENT") throw error;
115
+ }
116
+ if (variants.length < 2 || variants.length > 4) errors.push("ready design requires 2-4 comparison variant HTML files");
117
+ for (const variant of variants) {
118
+ if (await isSymlink(join(variantsRoot, variant))) errors.push(`comparison variant must not be a symlink: ${variant}`);
119
+ }
120
+ if (await isSymlink(join(designRoot, "final"))) errors.push("final directory must not be a symlink");
121
+ if (sources) {
122
+ try {
123
+ await materialize(designPath, { check: true });
124
+ } catch (error) {
125
+ errors.push(error.message);
126
+ }
127
+ const html = sources.get("final/index.html") ?? "";
128
+ const css = sources.get("final/styles.css") ?? "";
129
+ const js = sources.get("final/app.js") ?? "";
130
+ if (!/href=["']styles\.css["']/.test(html) || !/src=["']app\.js["']/.test(html)) errors.push("final HTML must reference relative styles.css and app.js");
131
+ if (/<(?:script|link)[^>]+(?:src|href)=["']https?:/i.test(html)) errors.push("final HTML must not load remote scripts or styles");
132
+ if (!css.includes(":focus-visible")) errors.push("final CSS must define :focus-visible");
133
+ if (!css.includes("prefers-reduced-motion")) errors.push("final CSS must handle prefers-reduced-motion");
134
+ if (!js.includes("addEventListener")) errors.push("final JS must implement an explicit interaction");
135
+ }
136
+ }
137
+
138
+ if (errors.length) throw new Error(errors.join("\n"));
139
+ return meta;
140
+ }
141
+
142
+ const args = process.argv.slice(2);
143
+ const allowDraft = args.includes("--allow-draft");
144
+ const paths = args.filter((arg) => arg !== "--allow-draft");
145
+ if (paths.length !== 1) {
146
+ console.error("usage: validate-design-package.mjs [--allow-draft] <design-system.md>");
147
+ process.exitCode = 1;
148
+ } else {
149
+ validate(paths[0], { allowDraft })
150
+ .then((meta) => console.log(`valid ${meta.designId} (${meta.status})`))
151
+ .catch((error) => {
152
+ console.error(error.message);
153
+ process.exitCode = 1;
154
+ });
155
+ }
@@ -50,9 +50,13 @@ Archive 归档历史并将经验证知识提升为当前长期知识
50
50
  - `<Path>{roots.state}/specdev/changes/{change}/investigation/</Path>`
51
51
  - `<Path>{roots.state}/specdev/changes/{change}/evidence/</Path>`
52
52
  - `<Path>{roots.state}/specdev/changes/{change}/reviews/</Path>`
53
- - `<Path>{roots.state}/specdev/changes/{change}/prototypes/</Path>`
53
+ - `<Path>{roots.state}/specdev/changes/{change}/prototypes/{design-id}/design-system.md</Path>`
54
+ - `<Path>{roots.state}/specdev/changes/{change}/prototypes/{design-id}/comparison/</Path>`
55
+ - `<Path>{roots.state}/specdev/changes/{change}/prototypes/{design-id}/final/</Path>`
54
56
  - `<Path>{roots.state}/specdev/changes/{change}/questionnaires/</Path>`
55
57
 
58
+ `{design-id}` 由 P-prototype 在当前 change 内分配为最小未占用的 `UI-NNN`;设计系统文档是设计权威,comparison 与 final 是其可运行投影。
59
+
56
60
  工件职责和冲突裁决位于 `<Path>{roots.workflows}/specdev/common/rules/artifact-contract.md</Path>`。
57
61
 
58
62
  ## 持久化约定
@@ -98,7 +102,9 @@ Archive 归档历史并将经验证知识提升为当前长期知识
98
102
  - `<Path>{roots.state}/specdev/changes/{change}/architecture-review.html</Path>`
99
103
  - `<Path>{roots.state}/specdev/changes/{change}/evidence/</Path>`
100
104
  - `<Path>{roots.state}/specdev/changes/{change}/reviews/</Path>`
101
- - `<Path>{roots.state}/specdev/changes/{change}/prototypes/</Path>`
105
+ - `<Path>{roots.state}/specdev/changes/{change}/prototypes/{design-id}/design-system.md</Path>`
106
+ - `<Path>{roots.state}/specdev/changes/{change}/prototypes/{design-id}/comparison/</Path>`
107
+ - `<Path>{roots.state}/specdev/changes/{change}/prototypes/{design-id}/final/</Path>`
102
108
  - `<Path>{roots.state}/specdev/changes/{change}/questionnaires/</Path>`
103
109
 
104
110
  ## 全局治理原则
@@ -156,7 +162,7 @@ Change 从 active/blocked 转为 completed 时加载 `<Path>{roots.workflows}/sp
156
162
 
157
163
  `active[].change` 必须唯一,且不得同时出现在 `archived`。开始 Work 时设置 `current_work`;暂停或可恢复阻塞时保留;成功完成时加入 `works_run` 并清空;取消时清空但不加入。逐次时间、结果和审计证据由 change 自有状态、Work 主产物、Evidence 或 LOG 承载,不写入全局索引。
158
164
 
159
- `<Path>{roots.state}/specdev/config.json</Path>` 的 `execution.max_implementation_agents`、`max_integration_attempts` 和 planning 原型变体字段均为可配置正整数,初始化时写入默认值;仅 implementation subagent 受前者约束且不含 Lead,current workspace 仍保持单 writer 串行安全不变量;只读 review/research/test-observation agent 不设 SpecDev 数字上限。
165
+ `<Path>{roots.state}/specdev/config.json</Path>` 的 `execution.max_implementation_agents`、`max_integration_attempts` 和 planning UI 设计候选字段均为可配置正整数;候选默认值与上限必须落在 2-4 且默认值不大于上限。仅 implementation subagent 受前者约束且不含 Lead,current workspace 仍保持单 writer 串行安全不变量;只读 review/research/test-observation agent 不设 SpecDev 数字上限。
160
166
 
161
167
  `<Path>{roots.state}/specdev/changes/{change}/.status.json</Path>` 的 `worktrees` 保存 Ticket 级 `base_sha`、父分支、workspace/implementation/integration owner、workspace locator、implementation/source checkpoint、适用 candidate/result SHA、验证、E2E disposition 与生命周期状态。current 记录使用 `workspace_ref=current` 和 direct-parent;required 记录使用 source/parent-candidate。每个实现 Ticket 都有一条记录;父分支只有在对应策略的验证通过后推进。`removed` 是 required 集成后来源 branch/worktree 完成清理的终态,必须保留全部集成与 E2E 证据。
162
168
 
@@ -189,7 +195,7 @@ Change 从 active/blocked 转为 completed 时加载 `<Path>{roots.workflows}/sp
189
195
  | 疑难 bug 或性能回归 | D-diagnose-bugs | S / T / I / R / W |
190
196
  | 模糊但可通过决策访谈收敛 | G-grill-with-docs | P / S / T / W |
191
197
  | 路径超出单次上下文 | W-wayfinder | G / P / D / S / T |
192
- | 需要用代码回答逻辑/UI 问题 | P-prototype | G / S / T / I |
198
+ | 需要检测项目 UI、选择设计方向并生成可运行设计包 | P-prototype | G / S / T / I |
193
199
  | 固定点 diff、branch 或 PR review | C-code-review | completed / T / S / G |
194
200
  | 外部行为已清楚 | S-spec | T-tickets |
195
201
  | Ready Spec 需要垂直切片 | T-tickets | P-goal-plan / I |
@@ -210,7 +216,7 @@ Change 从 active/blocked 转为 completed 时加载 `<Path>{roots.workflows}/sp
210
216
  - **I-implement** — 实现:基于 Ready Ticket 或获批小型 Spec 执行设计检查、TDD、动态派单、双轴审查、按 Goal Plan 选择的 current workspace 或 Ticket worktree 提交、直接父分支或候选合并验证和 Lead Evidence 回写。
211
217
  - **I-init-setup** — 初始化设置:初始化 SpecDev 的语言、配置、全局状态、本地 change 追踪、领域知识布局、验证命令和并发治理。
212
218
  - **P-goal-plan** — 目标规划:在跨 Ticket 协调复杂度需要时,以固定 Lead、动态派单、DAG/Gate 和候选合并门禁生成决策完备且可恢复的执行计划。
213
- - **P-prototype** — 原型:在获授权的临时 branch/worktree 中构建一次性 Logic UI 原型,回答一个明确设计问题并持久化答案、资产定位和清理状态。
219
+ - **P-prototype** — UI 设计原型:检测现有项目的 UI 事实,按产品任务推荐并逐步选择设计风格,生成持久化设计系统文档、多风格 HTML 对照和可运行 HTML/CSS/JS 原型。
214
220
  - **R-review-architecture** — 架构审查:从用户指定范围或 Git 热点扫描代码库的深化机会,以持久化可视化 HTML 呈现候选,并对用户选择的一个方案运行设计树访谈。
215
221
  - **S-spec** — 编写 Spec:综合已知事实、设计决定、诊断与代码现状,产出以外部行为和验收合同为权威的 Ready Spec。
216
222
  - **T-tickets** — 拆分 Tickets:将 Spec、计划或已确认对话拆成曳光弹式垂直切片;每个 Ticket 决策完备、可独立验证、适配单一上下文,并建立阻塞 DAG、路径所有权和执行就绪门禁。
@@ -38,7 +38,7 @@ Triage 是 SpecDev 唯一的远程摄入与关闭边界。开发期间,`<Path>
38
38
  - 根因未知的 bug → `<Path>{roots.workflows}/specdev/D-diagnose-bugs/D-diagnose-bugs.md</Path>`;
39
39
  - 产品或架构决定未锁定 → `<Path>{roots.workflows}/specdev/G-grill-with-docs/G-grill-with-docs.md</Path>`;
40
40
  - 路径超出单次上下文 → `<Path>{roots.workflows}/specdev/W-wayfinder/W-wayfinder.md</Path>`;
41
- - 需要可运行原型回答设计问题 → `<Path>{roots.workflows}/specdev/P-prototype/P-prototype.md</Path>`;
41
+ - 需要检测项目 UI、比较风格候选并固化可运行设计包 → `<Path>{roots.workflows}/specdev/P-prototype/P-prototype.md</Path>`;
42
42
  - 外部行为明确 → `<Path>{roots.workflows}/specdev/S-spec/S-spec.md</Path>`;
43
43
  - 固定点 diff 或 PR 审查 → `<Path>{roots.workflows}/specdev/C-code-review/C-code-review.md</Path>`;
44
44
  - 小型明确变更 → `<Path>{roots.workflows}/specdev/T-tickets/T-tickets.md</Path>` 或获批 Direct Spec。
@@ -47,7 +47,7 @@ Wayfinder 默认进行**规划**:每个 Ticket 解决一个决策,当地图
47
47
  每个 Ticket 要么是 **HITL**,与一个代表自己发言的人类一起工作;要么是 **AFK**,由 Agent 独立驱动。HITL Ticket 只能通过实时交流解决,Agent 绝不代替人类一方发言。
48
48
 
49
49
  - **Research(AFK)**:阅读文档、第三方 API 或知识库等资源,揭示某个决策等待的事实。调用 `<Path>{roots.workflows}/specdev/common/skills/research/SKILL.md</Path>`。当需要当前工作目录之外的知识时使用。
50
- - **Prototype(HITL)**:调用 `<Path>{roots.workflows}/specdev/P-prototype/P-prototype.md</Path>` 回答一个 UI/逻辑问题,并把 record、临时 branch/worktree 和运行 URL 链接为 solution comment 资产;P 不实现目的地。
50
+ - **Prototype(HITL)**:调用 `<Path>{roots.workflows}/specdev/P-prototype/P-prototype.md</Path>` 检测项目 UI、比较功能风格候选并逐步确认设计方向,把 `<Path>{roots.state}/specdev/changes/{change}/prototypes/{design-id}/design-system.md</Path>` comparison locator 链接为 solution comment 资产;`{design-id}` 使用 P 返回的 `UI-NNN`,P 不实现目的地。
51
51
  - **Grilling(HITL)**:对话。调用 `<Path>{roots.workflows}/specdev/G-grill-with-docs/G-grill-with-docs.md</Path>` 的 grilling 与 domain-modeling 能力,但本会话只关闭当前 Wayfinder Ticket。
52
52
  - **Task(HITL 或 AFK)**:在决策做出前必须完成的手动工作。它通过为决策解除阻塞赢得位置,不以交付目的地为目标。Agent 能独立驱动时使用 AFK,否则给人类精确清单。
53
53
 
@@ -37,14 +37,14 @@
37
37
  - 分诊:`<Path>{roots.workflows}/specdev/common/schemas/triage.schema.json</Path>`
38
38
  - 诊断:`<Path>{roots.workflows}/specdev/common/schemas/diagnosis.schema.json</Path>`
39
39
  - 代码审查:`<Path>{roots.workflows}/specdev/common/schemas/code-review.schema.json</Path>`
40
- - 原型记录:`<Path>{roots.workflows}/specdev/common/schemas/prototype-record.schema.json</Path>`
40
+ - UI 设计包:`<Path>{roots.workflows}/specdev/P-prototype/design-package.schema.json</Path>`
41
41
 
42
42
  ## 工具与 Skill
43
43
 
44
44
  - 包与 change 校验器:`<Path>{roots.workflows}/specdev/common/tools/validate-specdev.mjs</Path>`
45
45
  - 校验器说明:`<Path>{roots.workflows}/specdev/common/tools/README.md</Path>`
46
46
  - 外部技术研究 Skill:`<Path>{roots.workflows}/specdev/common/skills/research/SKILL.md</Path>`
47
- - Ticket/原型 worktree Skill:`<Path>{roots.workflows}/specdev/common/skills/dev-worktree/SKILL.md</Path>`;仅 required Ticket 使用 source → parent-candidate → parent 状态机,current Ticket 使用 current workspace/direct-parent,原型保持临时生命周期
47
+ - Ticket worktree Skill:`<Path>{roots.workflows}/specdev/common/skills/dev-worktree/SKILL.md</Path>`;仅 required Ticket 使用 source → parent-candidate → parent 状态机,current Ticket 使用 current workspace/direct-parent
48
48
  - 动态 Agent 交付合同 Skill:`<Path>{roots.workflows}/specdev/common/skills/subagent-delivery/SKILL.md</Path>`;P-goal-plan 建立 Lead 合同,I-implement 在执行期派单与验收
49
49
  - 双轴代码审查 Skill:`<Path>{roots.workflows}/specdev/common/skills/code-review/SKILL.md</Path>`
50
50
 
@@ -19,13 +19,15 @@ SpecDev 通过分层工件避免同一决策被多个模型反复重做。每个
19
19
  | Goal Plan | `<Path>{roots.state}/specdev/changes/{change}/goal-plan.md</Path>` | 跨 Ticket 调度、Gate、共享所有权、迁移顺序、集成和偏差治理 | 复制 Ticket 全文 |
20
20
  | Evidence | `<Path>{roots.state}/specdev/changes/{change}/evidence/{ticket-id}.md</Path>` | 实际修改、命令、结果、验收映射、偏差、风险和提交引用 | 新的产品或架构决策 |
21
21
  | 代码审查 | `<Path>{roots.state}/specdev/changes/{change}/reviews/CR-###.md</Path>` | 固定点、标准轴和规范轴 finding | 实施修复或合并两轴排名 |
22
- | 原型记录 | `<Path>{roots.state}/specdev/changes/{change}/prototypes/{prototype-id}/record.md</Path>` | 一个问题、分支、资产、答案、promotion 和清理 | 生产实现或多个问题的计划 |
22
+ | UI 设计包 | `<Path>{roots.state}/specdev/changes/{change}/prototypes/{design-id}/design-system.md</Path>`、`<Path>{roots.state}/specdev/changes/{change}/prototypes/{design-id}/comparison/</Path>` 与 `<Path>{roots.state}/specdev/changes/{change}/prototypes/{design-id}/final/</Path>` | 项目 UI 证据、功能风格候选、逐层用户决定、设计 token、交互合同和可运行 HTML/CSS/JS 投影 | 生产 UI 实现或替用户确认高影响偏好 |
23
23
  | Stakeholder 问卷 | `<Path>{roots.state}/specdev/changes/{change}/questionnaires/{slug}.md</Path>` | 第三方原始回答和恢复条件 | 未经转录确认的产品/架构决定 |
24
24
  | Wayfinder 地图 | `<Path>{roots.state}/specdev/changes/{change}/wayfinder-map.md</Path>` | 目的地、说明、已关闭决策索引、战争迷雾和范围之外 | 开放 Ticket 正文或答案详情 |
25
25
  | Wayfinder Ticket | `<Path>{roots.state}/specdev/changes/{change}/investigation/{investigation-id}.md</Path>` | 一个可精确陈述的问题、类型、阻塞和关闭状态 | 解决方案评论或交付目标 |
26
26
  | Wayfinder solution comment | `<Path>{roots.state}/specdev/changes/{change}/investigation/comments/{investigation-id}/NN-solution.md</Path>` | Ticket 的答案、结果事实和资产指针 | 地图索引或产品实现 |
27
27
  | 架构审查 | `<Path>{roots.state}/specdev/changes/{change}/architecture-review.md</Path>` 与 `<Path>{roots.state}/specdev/changes/{change}/architecture-review.html</Path>` | 深化候选、证据、可视化、选择和访谈状态 | 未经用户选择的执行契约 |
28
28
 
29
+ UI 设计包中的 `{design-id}` 由 P-prototype 分配为当前 change 内最小未占用的 `UI-NNN`;设计系统文档是唯一设计权威,comparison 与 final 不建立第二套规则。
30
+
29
31
  Change CONTEXT/ADR 是 active change 内的执行权威,不是 workflow 级永久知识。G 和其他设计/执行 Works 只读 `<Path>{roots.state}/specdev/context/</Path>` 与 `<Path>{roots.state}/specdev/adr/</Path>`;只有 A 在 change 完成、实现证据验证、毕业评估和用户确认后才能写入永久 namespace。未毕业内容随归档 change 保留,不能从 change 工件消失。
30
32
 
31
33
  ## 2. 权威顺序
@@ -40,19 +40,19 @@
40
40
  },
41
41
  "planning": {
42
42
  "type": "object",
43
- "required": ["default_depth", "require_ready_gate", "require_evidence", "ui_prototype_default_variants", "ui_prototype_max_variants"],
43
+ "required": ["default_depth", "require_ready_gate", "require_evidence", "ui_design_default_candidates", "ui_design_max_candidates"],
44
44
  "properties": {
45
45
  "default_depth": {"enum": ["lite", "standard", "deep"]},
46
46
  "require_ready_gate": {"type": "boolean"},
47
47
  "require_evidence": {"type": "boolean"},
48
- "ui_prototype_default_variants": {"type": "integer", "minimum": 1},
49
- "ui_prototype_max_variants": {"type": "integer", "minimum": 1}
48
+ "ui_design_default_candidates": {"type": "integer", "minimum": 2, "maximum": 4},
49
+ "ui_design_max_candidates": {"type": "integer", "minimum": 2, "maximum": 4}
50
50
  },
51
51
  "additionalProperties": true
52
52
  }
53
53
  },
54
54
  "allOf": [{
55
- "$comment": "ui_prototype_default_variants <= ui_prototype_max_variants is enforced by validate-specdev.mjs because JSON Schema cannot compare sibling numeric values."
55
+ "$comment": "ui_design_default_candidates <= ui_design_max_candidates is enforced by validate-specdev.mjs because JSON Schema cannot compare sibling numeric values."
56
56
  }],
57
57
  "additionalProperties": false
58
58
  }
@@ -1,16 +1,16 @@
1
1
  ---
2
2
  name: dev-worktree
3
- description: 管理 SpecDev required Ticket 或原型的 Git worktree;为 required Ticket 创建隔离来源 workspace、固定实现 commit,并由 Lead 在 parent-candidate checkout 验证后推进父分支。
3
+ description: 管理 SpecDev required Ticket Git worktree;创建隔离来源 workspace、固定实现 commit,并由 Lead 在 parent-candidate checkout 验证后推进父分支。
4
4
  ---
5
5
 
6
6
  # Dev Worktree
7
7
 
8
- 本 Skill 由 T-tickets/P-goal-plan/I-implementP-prototype 复用。`purpose=ticket` 仅在 Goal Plan 选择 `required` 时使用完整 source → candidate → parent 状态机;`current` Ticket 不调用本 Skill。`purpose=prototype` 只使用调用方批准的临时生命周期。
8
+ 本 Skill 由 T-ticketsP-goal-plan 和 I-implement 复用。仅在 Goal Plan 选择 `required` 时使用完整 source → candidate → parent 状态机;`current` Ticket 不调用本 Skill
9
9
 
10
10
  ## 输入
11
11
 
12
12
  - `operation=create | restore | finalize | remove`;
13
- - `purpose=ticket | prototype`;
13
+ - `purpose=ticket`;
14
14
  - repository、父分支、`base_sha`、branch、portable workspace locator;
15
15
  - workspace、implementation 和 integration owner;
16
16
  - 允许动作、路径合同、验证合同、调用方状态记录位置。
@@ -39,7 +39,7 @@ implementation owner 只在来源 worktree 修改授权项目路径,运行 Tic
39
39
 
40
40
  ## 4. 移除
41
41
 
42
- `operation=remove` 先验证 Ticket 已 `integrated` 或 prototype 已结束、目标 worktree clean、checkpoint 可恢复且删除目标精确。只有明确 cleanup 授权时删除来源 branch/worktree;强制删除需要单独确认。删除后重读 `git worktree list` 与 refs,并只把调用方生命周期状态更新为 `removed`;`base_sha`、source checkpoint、candidate/result、验证、E2E 与 Evidence 字段必须原样保留。
42
+ `operation=remove` 先验证 Ticket 已 `integrated`、目标 worktree clean、checkpoint 可恢复且删除目标精确。只有明确 cleanup 授权时删除来源 branch/worktree;强制删除需要单独确认。删除后重读 `git worktree list` 与 refs,并只把调用方生命周期状态更新为 `removed`;`base_sha`、source checkpoint、candidate/result、验证、E2E 与 Evidence 字段必须原样保留。
43
43
 
44
44
  **完成标准**:只删除精确授权目标;失败保留现场与恢复命令。
45
45
 
@@ -8,14 +8,12 @@
8
8
  - `specdev-worktree/` 已由 Speculo init 加入项目 `.gitignore`;
9
9
  - 目标 branch/worktree 不覆盖现有用户 workspace,路径合同无冲突。
10
10
 
11
- Prototype 只要求调用方已记录本次临时 branch/worktree 授权、问题、owner、locator 和清理策略;它不写 Ticket worktree 状态。
12
-
13
11
  ## 创建 Ticket 来源 worktree
14
12
 
15
13
  1. 重读父分支 HEAD、工作树、现有 worktrees 与 refs;父 HEAD 与计划基线不一致时由 Lead决定更新 `base_sha` 或阻塞;
16
14
  2. 固定 branch `speculo/<change>/<ticket-id>` 与 locator `specdev-worktree/<ticket-id>`;
17
15
  3. 确认目标 branch/path 不存在,或其实际记录精确匹配当前 Ticket;
18
- 4. 从 `base_sha` 创建 Git worktree,不复用其他 Ticket/原型目录;
16
+ 4. 从 `base_sha` 创建 Git worktree,不复用其他 Ticket 目录;
19
17
  5. 在来源 worktree 读取项目 Agent 指令、依赖、构建与路径合同;
20
18
  6. 安装实际需要的依赖,运行最小非 E2E 基线;
21
19
  7. Lead 写入 `<Path>{roots.state}/specdev/changes/{change}/.status.json</Path>`,状态为 `active`。
@@ -465,10 +465,21 @@ function validateDocumentReferences(root) {
465
465
  const errors = [];
466
466
  const warnings = [];
467
467
  const entries = walk(root);
468
- const knownFiles = new Set(entries.filter(isFile).map((path) => basename(path)));
469
- const knownDirectories = new Set(entries.filter(isDirectory).map((path) => basename(path)));
468
+ const frozenResearchRoot = join(
469
+ root,
470
+ "P-prototype",
471
+ "design-library",
472
+ "research-snapshot",
473
+ );
474
+ const governedEntries = entries.filter(
475
+ (path) => path !== frozenResearchRoot && !path.startsWith(`${frozenResearchRoot}${sep}`),
476
+ );
477
+ const knownFiles = new Set(governedEntries.filter(isFile).map((path) => basename(path)));
478
+ const knownDirectories = new Set(governedEntries.filter(isDirectory).map((path) => basename(path)));
470
479
 
471
- for (const path of entries.filter(
480
+ // The pinned research snapshot preserves its upstream relative links and HTML
481
+ // asset paths byte-for-byte. Global template link checks still validate it.
482
+ for (const path of governedEntries.filter(
472
483
  (item) => isFile(item) && [".md", ".html"].includes(extname(item).toLowerCase()),
473
484
  )) {
474
485
  const label = toPosix(relative(root, path));
@@ -682,12 +693,13 @@ function validateExecutionContractAssets(root) {
682
693
  configTemplate.execution.max_implementation_agents < 1 ||
683
694
  !Number.isInteger(configTemplate.execution?.max_integration_attempts) ||
684
695
  configTemplate.execution.max_integration_attempts < 1 ||
685
- !Number.isInteger(configTemplate.planning?.ui_prototype_default_variants) ||
686
- !Number.isInteger(configTemplate.planning?.ui_prototype_max_variants) ||
687
- configTemplate.planning.ui_prototype_default_variants < 1 ||
688
- configTemplate.planning.ui_prototype_max_variants < configTemplate.planning.ui_prototype_default_variants
696
+ !Number.isInteger(configTemplate.planning?.ui_design_default_candidates) ||
697
+ !Number.isInteger(configTemplate.planning?.ui_design_max_candidates) ||
698
+ configTemplate.planning.ui_design_default_candidates < 2 ||
699
+ configTemplate.planning.ui_design_max_candidates > 4 ||
700
+ configTemplate.planning.ui_design_max_candidates < configTemplate.planning.ui_design_default_candidates
689
701
  ) {
690
- errors.push("config-template.json must define SpecDev config v5 with positive execution limits and valid prototype variant bounds");
702
+ errors.push("config-template.json must define SpecDev config v5 with positive execution limits and valid UI design candidate bounds");
691
703
  }
692
704
  for (const obsolete of ["auto_commit", "worktree_for_parallel", "max_parallel"]) {
693
705
  if (JSON.stringify(configTemplate).includes(`\"${obsolete}\"`)) {
@@ -816,7 +828,7 @@ function capabilityChecks(root) {
816
828
  "prototype",
817
829
  [
818
830
  join(root, "P-prototype", "P-prototype.md"),
819
- ["一个问题", "Logic", "UI", "临时 branch/worktree", "promotion target", "main"],
831
+ ["设计定向", "风格", "design-system.md", "comparison", "HTML/CSS/JS", "design-library/INDEX.md"],
820
832
  ],
821
833
  ],
822
834
  [
@@ -1196,31 +1208,78 @@ function validateReviews(change, required, errors) {
1196
1208
 
1197
1209
  function validatePrototypes(change, required, errors) {
1198
1210
  const root = join(change, "prototypes");
1199
- const paths = isDirectory(root)
1200
- ? walk(root).filter((path) => isFile(path) && basename(path) === "record.md").sort()
1201
- : [];
1202
- if (required && !paths.length) errors.push("prototype stage requires a prototypes/<id>/record.md artifact");
1203
- for (const path of paths) {
1204
- const { meta, body } = parseFrontmatter(path);
1205
- const label = toPosix(relative(change, path));
1206
- if (meta.schema_version !== 1 || meta.artifact !== "prototype-record") {
1207
- errors.push(`${label}: artifact/schema_version must be prototype-record/1`);
1211
+ if (!isDirectory(root)) {
1212
+ if (required) errors.push("prototype stage requires a prototypes/UI-NNN/design-system.md artifact");
1213
+ return [];
1214
+ }
1215
+
1216
+ const validator = resolve(
1217
+ dirname(fileURLToPath(import.meta.url)),
1218
+ "..",
1219
+ "..",
1220
+ "P-prototype",
1221
+ "tools",
1222
+ "validate-design-package.mjs",
1223
+ );
1224
+ const paths = [];
1225
+ let readyCount = 0;
1226
+
1227
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
1228
+ const entryPath = join(root, entry.name);
1229
+ const label = toPosix(relative(change, entryPath));
1230
+ if (entry.isSymbolicLink()) {
1231
+ errors.push(`${label}: prototype design directories and files must not be symlinks`);
1232
+ continue;
1208
1233
  }
1209
- if (meta.change !== basename(change)) errors.push(`${label}: change must equal directory name`);
1210
- if (!/^PROTO-\d{3,}$/.test(String(meta.prototype_id ?? ""))) errors.push(`${label}: invalid prototype_id`);
1211
- if (!new Set(["logic", "ui"]).has(meta.branch)) errors.push(`${label}: invalid prototype branch`);
1212
- if (!new Set(["active", "answered", "blocked", "discarded"]).has(meta.status)) errors.push(`${label}: invalid prototype status`);
1213
- if (typeof meta.workspace_ref !== "string" || ABSOLUTE_MACHINE_PATH_RE.test(meta.workspace_ref)) {
1214
- errors.push(`${label}: workspace_ref must be a portable locator`);
1234
+ if (!entry.isDirectory()) {
1235
+ errors.push(`${label}: prototypes may only contain UI-NNN design directories`);
1236
+ continue;
1237
+ }
1238
+ if (!/^UI-\d{3,}$/.test(entry.name)) {
1239
+ errors.push(`${label}: prototype design directory must use UI-NNN`);
1240
+ continue;
1215
1241
  }
1216
- if (meta.status === "answered" && (!String(meta.winner ?? "").trim() || !String(meta.promotion_target ?? "").trim())) {
1217
- errors.push(`${label}: answered prototype requires winner and promotion_target`);
1242
+
1243
+ const designPath = join(entryPath, "design-system.md");
1244
+ if (!isFile(designPath)) {
1245
+ errors.push(`${label}: missing design-system.md`);
1246
+ continue;
1247
+ }
1248
+ paths.push(designPath);
1249
+ const { meta } = parseFrontmatter(designPath);
1250
+ if (meta.status === "ready") readyCount += 1;
1251
+ try {
1252
+ execFileSync(process.execPath, [validator, designPath], {
1253
+ encoding: "utf8",
1254
+ stdio: ["ignore", "pipe", "pipe"],
1255
+ });
1256
+ } catch (error) {
1257
+ const details = String(error.stderr || error.stdout || error.message)
1258
+ .trim()
1259
+ .split(/\r?\n/)
1260
+ .filter(Boolean);
1261
+ if (!details.length) errors.push(`${label}: design package validation failed`);
1262
+ for (const detail of details) errors.push(`${label}: ${detail}`);
1218
1263
  }
1219
- if (!new Set(["pending", "clean", "registered"]).has(meta.cleanup_status)) errors.push(`${label}: invalid cleanup_status`);
1220
- for (const heading of ["## Question and Assumption", "## Run and Assets", "## Evaluation", "## Promotion and Cleanup"]) {
1221
- if (!body.includes(heading)) errors.push(`${label}: missing '${heading}'`);
1264
+ }
1265
+
1266
+ const obsoleteRecords = walk(root).filter(
1267
+ (path) => isFile(path) && basename(path) === "record.md",
1268
+ );
1269
+ for (const path of obsoleteRecords) {
1270
+ errors.push(`${toPosix(relative(change, path))}: obsolete prototype record is forbidden; use UI-NNN/design-system.md`);
1271
+ }
1272
+ const discoveredDesigns = walk(root).filter(
1273
+ (path) => isFile(path) && basename(path) === "design-system.md",
1274
+ );
1275
+ for (const path of discoveredDesigns) {
1276
+ if (!paths.includes(path)) {
1277
+ errors.push(`${toPosix(relative(change, path))}: design-system.md must be directly under prototypes/UI-NNN`);
1222
1278
  }
1223
1279
  }
1280
+ if (required && readyCount === 0) {
1281
+ errors.push("prototype stage requires at least one ready UI design package");
1282
+ }
1224
1283
  return paths;
1225
1284
  }
1226
1285