@namewta/speculo 0.8.5 → 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.
- package/README.md +4 -3
- package/dist/src/structured.js +162 -4
- package/dist/src/structured.js.map +1 -1
- package/package.json +2 -2
- package/template/canonical/canonical-specdev-goal-plan.md +14 -15
- package/template/canonical/canonical-specdev-grill-with-docs.md +9 -8
- package/template/canonical/canonical-specdev-spec.md +9 -8
- package/template/canonical/canonical-specdev-tickets.md +13 -14
- package/template/canonical/canonical-specdev-wayfinder.md +7 -7
- package/template/commands/status.md +3 -3
- package/template/skills/archive-and-consolidate/SKILL.md +19 -14
- package/template/workflows/learning/A-archive-and-consolidate/A-archive-and-consolidate.md +38 -0
- package/template/workflows/learning/A-archive-and-consolidate/promotion-plan-template.md +23 -0
- package/template/workflows/learning/A-assess-and-plan/A-assess-and-plan.md +39 -0
- package/template/workflows/learning/A-assess-and-plan/change-status-template.json +28 -0
- package/template/workflows/learning/A-assess-and-plan/learning-plan-template.md +28 -0
- package/template/workflows/learning/E-eli5/E-eli5.md +37 -0
- package/template/workflows/learning/E-eli5/lesson-template.md +31 -0
- package/template/workflows/learning/I-init-setup/I-init-setup.md +36 -0
- package/template/workflows/learning/I-init-setup/context-index-template.md +6 -0
- package/template/workflows/learning/I-init-setup/learner-profile-template.md +17 -0
- package/template/workflows/learning/I-init-setup/review-index-template.md +4 -0
- package/template/workflows/learning/INDEX.md +26 -0
- package/template/workflows/learning/P-practice/P-practice.md +34 -0
- package/template/workflows/learning/P-practice/practice-template.md +16 -0
- package/template/workflows/learning/Q-quiz/Q-quiz.md +34 -0
- package/template/workflows/learning/Q-quiz/quiz-artifact-template.md +16 -0
- package/template/workflows/learning/R-review/R-review.md +35 -0
- package/template/workflows/learning/R-review/review-template.md +12 -0
- package/template/workflows/learning/README.md +121 -0
- package/template/workflows/learning/_state/archive/.gitkeep +1 -0
- package/template/workflows/learning/_state/changes/.gitkeep +1 -0
- package/template/workflows/learning/_state/status.json +6 -0
- package/template/workflows/learning/common/rules/artifact-contract.md +27 -0
- package/template/workflows/learning/common/rules/assessment-policy.md +7 -0
- package/template/workflows/learning/common/rules/knowledge-organization.md +9 -0
- package/template/workflows/learning/common/rules/mastery-policy.md +21 -0
- package/template/workflows/learning/common/rules/path-reference-contract.md +7 -0
- package/template/workflows/learning/common/rules/teaching-policy.md +15 -0
- package/template/workflows/learning/common/schemas/change-status.schema.json +41 -0
- package/template/workflows/learning/common/schemas/status.schema.json +32 -0
- package/template/workflows/learning/common/skills/knowledge-promotion/SKILL.md +46 -0
- package/template/workflows/learning/common/skills/knowledge-promotion/domain-index-template.md +6 -0
- package/template/workflows/learning/common/skills/knowledge-promotion/domain-overview-template.md +19 -0
- package/template/workflows/learning/common/skills/knowledge-promotion/knowledge-template.md +25 -0
- package/template/workflows/learning/common/tools/validate-learning.mjs +356 -0
- package/template/workflows/learning/runtime-contract.json +11 -0
- package/template/workflows/specdev/I-init-setup/I-init-setup.md +1 -1
- package/template/workflows/specdev/I-init-setup/config-template.json +2 -2
- package/template/workflows/specdev/INDEX.md +2 -2
- package/template/workflows/specdev/P-goal-plan/planning-modes.md +1 -1
- package/template/workflows/specdev/P-prototype/P-prototype.md +30 -26
- package/template/workflows/specdev/P-prototype/design-library/INDEX.md +43 -0
- package/template/workflows/specdev/P-prototype/design-library/color-and-theme.md +65 -0
- package/template/workflows/specdev/P-prototype/design-library/foundations.md +66 -0
- package/template/workflows/specdev/P-prototype/design-library/interaction-patterns.md +61 -0
- package/template/workflows/specdev/P-prototype/design-library/product-pattern-index.md +73 -0
- package/template/workflows/specdev/P-prototype/design-library/research-provenance.md +54 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/METHODOLOGY.md +90 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/README.md +31 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/data/extended-projects.json +241 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/data/projects.json +253 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/reference/USAGE.md +40 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/reference/design-tokens.css +145 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/report/OPEN_SOURCE_UI_RESEARCH_2026.md +519 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/PROJECT_TEMPLATE.md +29 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/README.md +9 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/claude-code-modern-clients.md +407 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/creative-ai-communication.md +574 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/data-dev-tools.md +432 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/personal-multiplatform-apps.md +185 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/productivity-collaboration.md +398 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/01-dense-ide.html +68 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/02-monochrome-console.html +18 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/03-soft-personal-ai.html +17 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/04-responsive-web.html +14 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/05-mobile-supervisor.html +16 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/06-cross-platform-workspace.html +17 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/07-local-first-content.html +9 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/08-media-first.html +17 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/README.md +49 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/ATTRIBUTION.md +9 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/landscape.jpg +0 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/lucide.js +20494 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/mountain.jpg +0 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/workspace.jpg +0 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/index.html +70 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/package-lock.json +78 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/package.json +12 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/screenshots/index-desktop.png +0 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/scripts/gallery.js +134 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/styles/base.css +795 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/styles/pages.css +533 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/tests/gallery.spec.js +56 -0
- package/template/workflows/specdev/P-prototype/design-library/responsive-and-platforms.md +53 -0
- package/template/workflows/specdev/P-prototype/design-library/style-index.md +32 -0
- package/template/workflows/specdev/P-prototype/design-package.schema.json +70 -0
- package/template/workflows/specdev/P-prototype/design-system-template.md +366 -0
- package/template/workflows/specdev/P-prototype/detect-existing-style.md +53 -0
- package/template/workflows/specdev/P-prototype/generate-design-package.md +50 -0
- package/template/workflows/specdev/P-prototype/style-selection-protocol.md +41 -0
- package/template/workflows/specdev/P-prototype/tools/materialize-prototype.mjs +126 -0
- package/template/workflows/specdev/P-prototype/tools/validate-design-package.mjs +155 -0
- package/template/workflows/specdev/README.md +12 -11
- package/template/workflows/specdev/T-triage/T-triage.md +1 -1
- package/template/workflows/specdev/W-wayfinder/W-wayfinder.md +1 -1
- package/template/workflows/specdev/common/README.md +2 -2
- package/template/workflows/specdev/common/rules/artifact-contract.md +3 -2
- package/template/workflows/specdev/common/schemas/config.schema.json +4 -4
- package/template/workflows/specdev/common/skills/dev-worktree/SKILL.md +4 -4
- package/template/workflows/specdev/common/skills/dev-worktree/references/create.md +1 -3
- package/template/workflows/specdev/common/tools/validate-specdev.mjs +83 -92
- package/template/canonical/canonical-specdev-engineering-cognitive-mentor.md +0 -1968
- package/template/workflows/specdev/E-eli5/E-eli5.md +0 -97
- package/template/workflows/specdev/E-engineering-cognitive-mentor/E-engineering-cognitive-mentor.md +0 -254
- package/template/workflows/specdev/E-engineering-cognitive-mentor/architecture-guidance.md +0 -90
- package/template/workflows/specdev/E-engineering-cognitive-mentor/bug-guidance.md +0 -80
- package/template/workflows/specdev/E-engineering-cognitive-mentor/codebase-guidance.md +0 -107
- package/template/workflows/specdev/E-engineering-cognitive-mentor/comprehension-and-closure.md +0 -95
- package/template/workflows/specdev/E-engineering-cognitive-mentor/domain-learning-guidance.md +0 -62
- package/template/workflows/specdev/E-engineering-cognitive-mentor/evidence-and-options.md +0 -132
- package/template/workflows/specdev/E-engineering-cognitive-mentor/interaction-protocol.md +0 -116
- package/template/workflows/specdev/E-engineering-cognitive-mentor/mentor-report-template.md +0 -135
- package/template/workflows/specdev/E-engineering-cognitive-mentor/mode-routing.md +0 -47
- package/template/workflows/specdev/E-engineering-cognitive-mentor/persistence-and-resume.md +0 -132
- package/template/workflows/specdev/E-engineering-cognitive-mentor/requirements-guidance.md +0 -92
- package/template/workflows/specdev/P-prototype/logic-prototype.md +0 -24
- package/template/workflows/specdev/P-prototype/prototype-record-template.md +0 -46
- package/template/workflows/specdev/P-prototype/ui-prototype.md +0 -21
- package/template/workflows/specdev/common/schemas/prototype-record.schema.json +0 -24
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
statSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const CHANGE_NAME = /^[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
13
|
+
const DOMAIN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
14
|
+
const WORK_ID = /^learning\/[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
15
|
+
const CHANGE_STATUS = new Set(["active", "blocked", "awaiting_retention", "completed", "archived"]);
|
|
16
|
+
const PHASE = new Set(["intake", "assessment", "teaching", "practice", "immediate_quiz", "retention", "ready_to_archive", "archived"]);
|
|
17
|
+
const DOMAIN_TYPE = new Set(["project", "product", "subject", "language", "skill"]);
|
|
18
|
+
const RESULT = new Set(["not_attempted", "failed", "passed", "needs_review"]);
|
|
19
|
+
const KNOWLEDGE_STATUS = new Set(["mastered", "review_due", "needs_refresh", "superseded"]);
|
|
20
|
+
const EXPECTED_WORKS = new Set([
|
|
21
|
+
"A-archive-and-consolidate",
|
|
22
|
+
"A-assess-and-plan",
|
|
23
|
+
"E-eli5",
|
|
24
|
+
"I-init-setup",
|
|
25
|
+
"P-practice",
|
|
26
|
+
"Q-quiz",
|
|
27
|
+
"R-review",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
const result = { workflowRoot: null, stateRoot: null, stage: null, change: null, selfCheck: false };
|
|
32
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
33
|
+
const arg = argv[index];
|
|
34
|
+
if (arg === "--workflow-root") result.workflowRoot = resolve(argv[++index] ?? "");
|
|
35
|
+
else if (arg === "--state-root") result.stateRoot = resolve(argv[++index] ?? "");
|
|
36
|
+
else if (arg === "--stage") result.stage = argv[++index] ?? null;
|
|
37
|
+
else if (arg === "--change") result.change = argv[++index] ?? null;
|
|
38
|
+
else if (arg === "--self-check") result.selfCheck = true;
|
|
39
|
+
else throw new Error(`unknown option: ${arg}`);
|
|
40
|
+
}
|
|
41
|
+
if (!result.workflowRoot && !result.stateRoot && !result.selfCheck) {
|
|
42
|
+
throw new Error("use --workflow-root <path>, --state-root <path>, or --self-check");
|
|
43
|
+
}
|
|
44
|
+
if (result.stage && !new Set(["pre-archive", "complete"]).has(result.stage)) {
|
|
45
|
+
throw new Error("--stage must be pre-archive or complete");
|
|
46
|
+
}
|
|
47
|
+
if (result.stage && !result.change) throw new Error("--stage requires --change");
|
|
48
|
+
if (result.change && !CHANGE_NAME.test(result.change)) throw new Error("--change has an invalid name");
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isFile(path) {
|
|
53
|
+
return existsSync(path) && statSync(path).isFile();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isDirectory(path) {
|
|
57
|
+
return existsSync(path) && statSync(path).isDirectory();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readJson(path, label, errors) {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
errors.push(`${label}: invalid JSON (${error.message})`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function walk(root) {
|
|
70
|
+
if (!isDirectory(root)) return [];
|
|
71
|
+
const files = [];
|
|
72
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
73
|
+
const path = join(root, entry.name);
|
|
74
|
+
if (entry.isDirectory()) files.push(...walk(path));
|
|
75
|
+
else if (entry.isFile()) files.push(path);
|
|
76
|
+
}
|
|
77
|
+
return files;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sameMembers(actual, expected) {
|
|
81
|
+
return actual.size === expected.size && [...actual].every((item) => expected.has(item));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function duplicates(values) {
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
return values.filter((value) => seen.has(value) || !seen.add(value));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function validDateTime(value) {
|
|
90
|
+
return typeof value === "string" && !Number.isNaN(Date.parse(value));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validateWorkflowRoot(root, errors) {
|
|
94
|
+
const indexPath = join(root, "INDEX.md");
|
|
95
|
+
const readmePath = join(root, "README.md");
|
|
96
|
+
const seedPath = join(root, "_state", "status.json");
|
|
97
|
+
const runtimePath = join(root, "runtime-contract.json");
|
|
98
|
+
for (const path of [indexPath, readmePath, seedPath, runtimePath]) {
|
|
99
|
+
if (!isFile(path)) errors.push(`${path}: required workflow file is missing`);
|
|
100
|
+
}
|
|
101
|
+
if (errors.length) return;
|
|
102
|
+
|
|
103
|
+
const index = readFileSync(indexPath, "utf8");
|
|
104
|
+
const readme = readFileSync(readmePath, "utf8");
|
|
105
|
+
if (!/^id: learning$/m.test(index) || !/^type: workflow$/m.test(index) || !/^workflow: learning$/m.test(index)) {
|
|
106
|
+
errors.push("INDEX.md: identity must be learning/type workflow");
|
|
107
|
+
}
|
|
108
|
+
for (const heading of ["## 永久知识", "## Work 激活"]) {
|
|
109
|
+
if (!index.includes(heading)) errors.push(`INDEX.md: missing ${heading}`);
|
|
110
|
+
}
|
|
111
|
+
if (!index.includes("<Path>{roots.workflows}/learning/README.md</Path>")) errors.push("INDEX.md: missing activation pointer");
|
|
112
|
+
if ((readme.match(/AUTO-INDEX-START/g) ?? []).length !== 1 || (readme.match(/AUTO-INDEX-END/g) ?? []).length !== 1) {
|
|
113
|
+
errors.push("README.md: requires one AUTO-INDEX marker pair");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const works = new Set(readdirSync(root, { withFileTypes: true })
|
|
117
|
+
.filter((entry) => entry.isDirectory() && /^[A-Z]-/.test(entry.name))
|
|
118
|
+
.map((entry) => entry.name));
|
|
119
|
+
if (!sameMembers(works, EXPECTED_WORKS)) {
|
|
120
|
+
errors.push(`workflow works mismatch: expected ${[...EXPECTED_WORKS].sort().join(", ")}; found ${[...works].sort().join(", ")}`);
|
|
121
|
+
}
|
|
122
|
+
for (const work of works) {
|
|
123
|
+
const path = join(root, work, `${work}.md`);
|
|
124
|
+
if (!isFile(path)) {
|
|
125
|
+
errors.push(`${work}: missing same-named entry`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const text = readFileSync(path, "utf8");
|
|
129
|
+
if (!text.includes("<Path>{roots.workflows}/learning/README.md</Path>")) errors.push(`${work}: missing activation contract reference`);
|
|
130
|
+
if (!/^workflow: learning$/m.test(text) || !/^type: workflow-entry$/m.test(text)) errors.push(`${work}: invalid frontmatter identity`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const seed = readJson(seedPath, "_state/status.json", errors);
|
|
134
|
+
if (seed && (seed.schema_version !== 1 || seed.workflow !== "learning" || !Array.isArray(seed.active) || !Array.isArray(seed.archived))) {
|
|
135
|
+
errors.push("_state/status.json: expected empty learning schema v1 status");
|
|
136
|
+
}
|
|
137
|
+
const runtime = readJson(runtimePath, "runtime-contract.json", errors);
|
|
138
|
+
if (runtime && (runtime.schema_version !== 1 || runtime.workflow !== "learning" || runtime.config !== null || runtime.opaque_default !== "preserve-byte-for-byte")) {
|
|
139
|
+
errors.push("runtime-contract.json: invalid learning contract");
|
|
140
|
+
}
|
|
141
|
+
for (const schema of ["status.schema.json", "change-status.schema.json"]) {
|
|
142
|
+
readJson(join(root, "common", "schemas", schema), `common/schemas/${schema}`, errors);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const path of walk(root).filter((candidate) => candidate.toLowerCase().endsWith(".md"))) {
|
|
146
|
+
if (/```mermaid\b/i.test(readFileSync(path, "utf8"))) {
|
|
147
|
+
errors.push(`${relative(root, path)}: Learning diagrams must use fenced ASCII text, not Mermaid`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const teachingContracts = [
|
|
152
|
+
[join(root, "E-eli5", "E-eli5.md"), ["5 岁的小孩", "大一新生", "ASCII", "不得生成 Mermaid"]],
|
|
153
|
+
[join(root, "E-eli5", "lesson-template.md"), ["## 教学表达基线", "5 岁的小孩", "大一新生", "```text"]],
|
|
154
|
+
[join(root, "common", "rules", "teaching-policy.md"), ["5 岁的小孩", "大一新生", "默认使用", "纯文本 ASCII"]],
|
|
155
|
+
[join(root, "I-init-setup", "learner-profile-template.md"), ["教学表达基线", "5 岁的小孩", "仅纯文本 ASCII"]],
|
|
156
|
+
];
|
|
157
|
+
for (const [path, requiredMarkers] of teachingContracts) {
|
|
158
|
+
if (!isFile(path)) {
|
|
159
|
+
errors.push(`${relative(root, path)}: required teaching contract is missing`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const text = readFileSync(path, "utf8");
|
|
163
|
+
for (const marker of requiredMarkers) {
|
|
164
|
+
if (!text.includes(marker)) errors.push(`${relative(root, path)}: missing '${marker}'`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function validateGlobalStatus(status, errors) {
|
|
170
|
+
if (!status) return;
|
|
171
|
+
if (status.schema_version !== 1 || status.workflow !== "learning" || !Array.isArray(status.active) || !Array.isArray(status.archived)) {
|
|
172
|
+
errors.push("status.json: expected learning schema v1 with active and archived arrays");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const activeNames = [];
|
|
176
|
+
for (const [index, entry] of status.active.entries()) {
|
|
177
|
+
const label = `status.json active[${index}]`;
|
|
178
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
179
|
+
errors.push(`${label}: must be an object`);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (!CHANGE_NAME.test(entry.change ?? "")) errors.push(`${label}: invalid change`);
|
|
183
|
+
if (!DOMAIN.test(entry.domain ?? "")) errors.push(`${label}: invalid domain`);
|
|
184
|
+
if (typeof entry.topic !== "string" || !entry.topic.trim()) errors.push(`${label}: topic is required`);
|
|
185
|
+
if (!(entry.current_work === null || WORK_ID.test(entry.current_work ?? ""))) errors.push(`${label}: invalid current_work`);
|
|
186
|
+
if (!Array.isArray(entry.works_run) || entry.works_run.some((item) => !WORK_ID.test(item)) || duplicates(entry.works_run ?? []).length) {
|
|
187
|
+
errors.push(`${label}: works_run must contain unique learning work ids`);
|
|
188
|
+
}
|
|
189
|
+
activeNames.push(entry.change);
|
|
190
|
+
}
|
|
191
|
+
if (duplicates(activeNames).length || duplicates(status.archived).length) errors.push("status.json: duplicate change names");
|
|
192
|
+
for (const name of status.archived) {
|
|
193
|
+
if (!CHANGE_NAME.test(name)) errors.push(`status.json: invalid archived change ${name}`);
|
|
194
|
+
if (activeNames.includes(name)) errors.push(`status.json: ${name} is both active and archived`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function validateMasteryGate(changeStatus, label, errors) {
|
|
199
|
+
const mastery = changeStatus.mastery;
|
|
200
|
+
if (!mastery || typeof mastery !== "object" || Array.isArray(mastery)) {
|
|
201
|
+
errors.push(`${label}: mastery is required`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!RESULT.has(mastery.immediate) || !RESULT.has(mastery.retention)) errors.push(`${label}: invalid mastery result`);
|
|
205
|
+
if (mastery.score !== null && (typeof mastery.score !== "number" || mastery.score < 0 || mastery.score > 100)) errors.push(`${label}: invalid mastery score`);
|
|
206
|
+
if (typeof mastery.critical_objectives_passed !== "boolean" || typeof mastery.transfer_passed !== "boolean") errors.push(`${label}: mastery booleans are required`);
|
|
207
|
+
if (!Array.isArray(mastery.blocking_misconceptions) || !Array.isArray(mastery.evidence)) errors.push(`${label}: mastery arrays are required`);
|
|
208
|
+
if (!(mastery.next_review_at === null || validDateTime(mastery.next_review_at))) errors.push(`${label}: invalid next_review_at`);
|
|
209
|
+
|
|
210
|
+
if (changeStatus.change_status === "awaiting_retention" && mastery.immediate !== "passed") {
|
|
211
|
+
errors.push(`${label}: awaiting_retention requires immediate passed`);
|
|
212
|
+
}
|
|
213
|
+
if (["completed", "archived"].includes(changeStatus.change_status)) {
|
|
214
|
+
if (mastery.immediate !== "passed" || mastery.retention !== "passed") errors.push(`${label}: completed knowledge requires both mastery gates passed`);
|
|
215
|
+
if (typeof mastery.score !== "number" || mastery.score < 80) errors.push(`${label}: completed knowledge requires score >= 80`);
|
|
216
|
+
if (!mastery.critical_objectives_passed) errors.push(`${label}: completed knowledge requires all critical objectives`);
|
|
217
|
+
if (!mastery.transfer_passed) errors.push(`${label}: completed knowledge requires transfer evidence`);
|
|
218
|
+
if (mastery.blocking_misconceptions.length) errors.push(`${label}: completed knowledge has blocking misconceptions`);
|
|
219
|
+
if (mastery.evidence.length < 2 || !mastery.evidence.some((item) => /\/quiz\/immediate-/.test(item)) || !mastery.evidence.some((item) => /\/quiz\/retention-/.test(item))) {
|
|
220
|
+
errors.push(`${label}: completed knowledge requires immediate and retention result evidence`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function validateChangeStatus(value, expectedName, expectedArchived, errors) {
|
|
226
|
+
const label = `${expectedName}/.status.json`;
|
|
227
|
+
if (!value) return;
|
|
228
|
+
if (value.schema_version !== 1 || value.artifact !== "learning-change-status") errors.push(`${label}: invalid identity/schema`);
|
|
229
|
+
if (value.change !== expectedName) errors.push(`${label}: change must match directory`);
|
|
230
|
+
if (!DOMAIN.test(value.domain ?? "") || !DOMAIN_TYPE.has(value.domain_type)) errors.push(`${label}: invalid domain or domain_type`);
|
|
231
|
+
if (typeof value.topic !== "string" || !value.topic.trim()) errors.push(`${label}: topic is required`);
|
|
232
|
+
if (!CHANGE_STATUS.has(value.change_status) || !PHASE.has(value.phase)) errors.push(`${label}: invalid status or phase`);
|
|
233
|
+
if (!(value.current_work === null || WORK_ID.test(value.current_work ?? ""))) errors.push(`${label}: invalid current_work`);
|
|
234
|
+
if (!Array.isArray(value.works_run) || value.works_run.some((item) => !WORK_ID.test(item)) || duplicates(value.works_run ?? []).length) errors.push(`${label}: invalid works_run`);
|
|
235
|
+
if (!validDateTime(value.created_at) || !validDateTime(value.updated_at)) errors.push(`${label}: created_at and updated_at must be date-times`);
|
|
236
|
+
if (!Array.isArray(value.blockers)) errors.push(`${label}: blockers must be an array`);
|
|
237
|
+
if (expectedArchived && (value.change_status !== "archived" || value.phase !== "archived" || !validDateTime(value.archived_at) || typeof value.archive_path !== "string")) {
|
|
238
|
+
errors.push(`${label}: archived index requires archived status, phase, time, and path`);
|
|
239
|
+
}
|
|
240
|
+
if (expectedArchived && value.archive_path !== `<Path>{roots.state}/learning/archive/${expectedName.slice(0, 7)}/${expectedName}</Path>`) {
|
|
241
|
+
errors.push(`${label}: archive_path does not match the indexed archive location`);
|
|
242
|
+
}
|
|
243
|
+
if (!expectedArchived && value.change_status === "archived") errors.push(`${label}: active index cannot point to archived change`);
|
|
244
|
+
if (value.change_status === "completed" && (!validDateTime(value.completed_at) || value.phase !== "ready_to_archive")) errors.push(`${label}: completed requires completed_at and ready_to_archive phase`);
|
|
245
|
+
validateMasteryGate(value, label, errors);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function statePathFromTag(stateRoot, taggedPath) {
|
|
249
|
+
const match = /^<Path>\{roots\.state\}\/learning\/(.+)<\/Path>$/.exec(taggedPath);
|
|
250
|
+
return match ? join(stateRoot, ...match[1].split("/")) : null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function validateContext(stateRoot, errors) {
|
|
254
|
+
const contextRoot = join(stateRoot, "context");
|
|
255
|
+
if (!isDirectory(contextRoot)) return;
|
|
256
|
+
for (const required of ["INDEX.md", "REVIEW.md"]) {
|
|
257
|
+
if (!isFile(join(contextRoot, required))) errors.push(`context/${required}: missing`);
|
|
258
|
+
}
|
|
259
|
+
const ids = new Map();
|
|
260
|
+
for (const file of walk(contextRoot).filter((path) => path.endsWith(".md"))) {
|
|
261
|
+
const text = readFileSync(file, "utf8");
|
|
262
|
+
const fileLabel = relative(stateRoot, file).split(sep).join("/");
|
|
263
|
+
for (const match of text.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) {
|
|
264
|
+
const target = match[1].split("#")[0].trim();
|
|
265
|
+
if (!target || /^(?:https?:|mailto:|#)/.test(target) || target.includes("<")) continue;
|
|
266
|
+
const resolved = resolve(dirname(file), decodeURIComponent(target));
|
|
267
|
+
if (!resolved.startsWith(resolve(contextRoot) + sep) || !existsSync(resolved)) errors.push(`${fileLabel}: broken or escaping Markdown link ${target}`);
|
|
268
|
+
}
|
|
269
|
+
const id = /^\|\s*Knowledge ID\s*\|\s*([^|]+?)\s*\|\s*$/m.exec(text)?.[1];
|
|
270
|
+
if (!id) continue;
|
|
271
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*\/[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id)) errors.push(`${fileLabel}: invalid Knowledge ID ${id}`);
|
|
272
|
+
if (ids.has(id)) errors.push(`${fileLabel}: duplicate Knowledge ID ${id} also in ${ids.get(id)}`);
|
|
273
|
+
else ids.set(id, fileLabel);
|
|
274
|
+
const state = /^\|\s*状态\s*\|\s*([^|]+?)\s*\|\s*$/m.exec(text)?.[1];
|
|
275
|
+
if (!KNOWLEDGE_STATUS.has(state)) errors.push(`${fileLabel}: invalid or missing knowledge 状态`);
|
|
276
|
+
for (const heading of ["## 当前理解", "## 心智模型", "## 示例与应用", "## 常见误区", "## 来源与证据"]) {
|
|
277
|
+
if (!text.includes(heading)) errors.push(`${fileLabel}: missing ${heading}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function validateStateRoot(root, options, errors) {
|
|
283
|
+
const statusPath = join(root, "status.json");
|
|
284
|
+
if (!isFile(statusPath)) {
|
|
285
|
+
errors.push("status.json: missing");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const status = readJson(statusPath, "status.json", errors);
|
|
289
|
+
validateGlobalStatus(status, errors);
|
|
290
|
+
if (!status || !Array.isArray(status.active) || !Array.isArray(status.archived)) return;
|
|
291
|
+
|
|
292
|
+
for (const entry of status.active) {
|
|
293
|
+
if (!entry || typeof entry.change !== "string") continue;
|
|
294
|
+
const path = join(root, "changes", entry.change, ".status.json");
|
|
295
|
+
if (!isFile(path)) {
|
|
296
|
+
errors.push(`${entry.change}: indexed active change is missing .status.json`);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const changeStatus = readJson(path, `${entry.change}/.status.json`, errors);
|
|
300
|
+
validateChangeStatus(changeStatus, entry.change, false, errors);
|
|
301
|
+
if (changeStatus && (changeStatus.domain !== entry.domain || changeStatus.topic !== entry.topic || changeStatus.current_work !== entry.current_work || JSON.stringify(changeStatus.works_run) !== JSON.stringify(entry.works_run))) {
|
|
302
|
+
errors.push(`${entry.change}: global and change status projections differ`);
|
|
303
|
+
}
|
|
304
|
+
if (options.stage === "pre-archive" && (!options.change || options.change === entry.change) && changeStatus?.change_status !== "completed") {
|
|
305
|
+
errors.push(`${entry.change}: pre-archive requires completed status`);
|
|
306
|
+
}
|
|
307
|
+
if (options.stage === "complete" && (!options.change || options.change === entry.change)) {
|
|
308
|
+
errors.push(`${entry.change}: complete stage cannot leave the change active`);
|
|
309
|
+
}
|
|
310
|
+
for (const evidence of changeStatus?.mastery?.evidence ?? []) {
|
|
311
|
+
const resolved = statePathFromTag(root, evidence);
|
|
312
|
+
if (!resolved || !isFile(resolved)) errors.push(`${entry.change}: mastery evidence does not exist ${evidence}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const name of status.archived) {
|
|
317
|
+
const archiveRoot = join(root, "archive", name.slice(0, 7), name);
|
|
318
|
+
const path = join(archiveRoot, ".status.json");
|
|
319
|
+
if (!isFile(path)) {
|
|
320
|
+
errors.push(`${name}: indexed archive is missing .status.json`);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
validateChangeStatus(readJson(path, `${name}/.status.json`, errors), name, true, errors);
|
|
324
|
+
if (isDirectory(join(root, "changes", name))) errors.push(`${name}: archived change still exists under changes`);
|
|
325
|
+
if (options.stage === "complete" && (!options.change || options.change === name) && !isFile(join(archiveRoot, "promotion-plan.md"))) {
|
|
326
|
+
errors.push(`${name}: complete stage requires archived promotion-plan.md`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (options.change && !status.active.some((entry) => entry.change === options.change) && !status.archived.includes(options.change)) errors.push(`${options.change}: change is not indexed`);
|
|
330
|
+
validateContext(root, errors);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function main() {
|
|
334
|
+
const options = parseArgs(process.argv.slice(2));
|
|
335
|
+
const errors = [];
|
|
336
|
+
if (options.selfCheck) {
|
|
337
|
+
const ownRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
338
|
+
validateWorkflowRoot(ownRoot, errors);
|
|
339
|
+
}
|
|
340
|
+
if (options.workflowRoot) validateWorkflowRoot(options.workflowRoot, errors);
|
|
341
|
+
if (options.stateRoot) validateStateRoot(options.stateRoot, options, errors);
|
|
342
|
+
if (errors.length) {
|
|
343
|
+
console.error(`Learning validation failed (${errors.length})`);
|
|
344
|
+
for (const error of errors) console.error(` - ${error}`);
|
|
345
|
+
process.exitCode = 1;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
console.log("Learning validation: OK");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
main();
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error(`Learning validation failed: ${error.message}`);
|
|
355
|
+
process.exitCode = 1;
|
|
356
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"workflow": "learning",
|
|
4
|
+
"config": null,
|
|
5
|
+
"structured_state": [
|
|
6
|
+
".speculo/learning/status.json",
|
|
7
|
+
".speculo/learning/changes/*/.status.json",
|
|
8
|
+
".speculo/learning/archive/*/*/.status.json"
|
|
9
|
+
],
|
|
10
|
+
"opaque_default": "preserve-byte-for-byte"
|
|
11
|
+
}
|
|
@@ -55,7 +55,7 @@ keywords: [初始化, 配置, status, tracking, 验证命令]
|
|
|
55
55
|
仅在上下文未提供时询问:
|
|
56
56
|
|
|
57
57
|
- 交互语言与持久化工件语言;
|
|
58
|
-
- implementation subagent
|
|
58
|
+
- implementation subagent、集成尝试次数和 UI 设计候选上限(初始化时写入 config,Lead 不计入);
|
|
59
59
|
- Deep Ticket 的迁移、发布和不可逆操作是否必须人工批准;
|
|
60
60
|
|
|
61
61
|
不询问可由仓库事实回答的文件位置、脚本名或默认分支。
|
|
@@ -3,8 +3,8 @@ id: specdev
|
|
|
3
3
|
type: workflow
|
|
4
4
|
workflow: specdev
|
|
5
5
|
name: SpecDev Workflow
|
|
6
|
-
description:
|
|
7
|
-
keywords: [specdev, local-first, 规格驱动开发, decision-complete,
|
|
6
|
+
description: 以本地工件为唯一开发权威,从来源冻结、诊断、设计、原型、规格、Ticket、编排和审查推进到证据驱动实现、远程 reconcile 与知识归档。
|
|
7
|
+
keywords: [specdev, local-first, 规格驱动开发, decision-complete, prototype, code-review, TDD, 证据]
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# SpecDev Index
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
- Tickets Map 与全部 Ticket 存在、Ready、DAG 无环;
|
|
11
11
|
- 每个验收合同被 Ticket 覆盖;
|
|
12
12
|
- writable/shared path 有唯一 owner,Wave 候选无写冲突;
|
|
13
|
-
- config schema v5,`max_implementation_agents` 与 `max_integration_attempts`
|
|
13
|
+
- config schema v5,`max_implementation_agents` 与 `max_integration_attempts` 为正整数;UI 设计候选范围读取 planning 配置;
|
|
14
14
|
- 父分支可定位,implementation commit 与本地 integration 已获授权;
|
|
15
15
|
- Deep Ticket 的迁移、兼容、监控、恢复和不可逆批准点完整。
|
|
16
16
|
- Ticket 与 `<Path>{roots.state}/specdev/changes/{change}/spec.md</Path>`、`<Path>{roots.state}/specdev/changes/{change}/ADR.md</Path>`、`<Path>{roots.state}/specdev/adr/</Path>`、`<Path>{roots.state}/specdev/context/</Path>` 和当前代码事实不存在未处理冲突;
|
|
@@ -2,47 +2,51 @@
|
|
|
2
2
|
id: specdev/prototype
|
|
3
3
|
type: workflow-entry
|
|
4
4
|
workflow: specdev
|
|
5
|
-
name:
|
|
6
|
-
description:
|
|
7
|
-
keywords: [prototype, 原型,
|
|
5
|
+
name: UI 设计原型
|
|
6
|
+
description: 检测现有项目的 UI 事实,按产品任务推荐并逐步选择设计风格,生成持久化设计系统文档、多风格 HTML 对照和可运行 HTML/CSS/JS 原型。
|
|
7
|
+
keywords: [prototype, UI 原型, 风格检测, 设计系统, HTML, CSS, 交互, design tokens]
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
#
|
|
10
|
+
# UI 设计原型
|
|
11
11
|
|
|
12
12
|
> 激活本 Work 后,先读取 `<Path>{roots.workflows}/specdev/README.md</Path>`,再执行本入口。
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
本 Work 的主导词是**设计定向**:先从项目事实识别现状,再让用户看到基于功能而非流行色推荐的候选,最后把选择固化为既可阅读又可运行的 UI 设计包。P 不修改生产 UI;后续 S、Tickets 和 I 以设计包为视觉与交互权威。
|
|
15
15
|
|
|
16
16
|
## 输入与所有权
|
|
17
17
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
18
|
+
- 必需输入:目标项目或新产品设想、核心用户任务,以及当前 SpecDev change。
|
|
19
|
+
- 可选输入:现有代码、页面、截图、设计 token、品牌约束、ADR、Spec、调查和用户明确给出的参考产品。
|
|
20
|
+
- 项目事实权威:依赖清单、样式源码、组件实现、路由、真实页面、测试和可复现截图;仅凭依赖名称不得断言实际风格。
|
|
21
|
+
- 用户决定权威:产品方向、候选风格、密度、色调、字体气质、圆角、交互反馈和跨端优先级。
|
|
21
22
|
|
|
22
|
-
P 拥有 `<Path>{roots.state}/specdev/changes/{change}/prototypes/{
|
|
23
|
+
P 拥有 `<Path>{roots.state}/specdev/changes/{change}/prototypes/{design-id}/</Path>`。其中 `design-id` 使用当前 change 内最小未占用的 `UI-NNN`;目录中的设计系统主文档是唯一设计权威,comparison 和 final 文件均由它或其中已记录的候选决定派生。P 不写永久 research namespace,不创建 branch/worktree,不提交生产代码。
|
|
23
24
|
|
|
24
25
|
## 流程
|
|
25
26
|
|
|
26
|
-
1.
|
|
27
|
-
2.
|
|
28
|
-
3.
|
|
29
|
-
4.
|
|
30
|
-
5.
|
|
31
|
-
6.
|
|
27
|
+
1. **创建或恢复设计包**:读取 change 与 `<Path>{roots.state}/specdev/changes/{change}/prototypes/{design-id}/design-system.md</Path>`。不存在时从 `<Path>{roots.workflows}/specdev/P-prototype/design-system-template.md</Path>` 创建,写入 `status: detecting` 并原子重读;存在时只询问尚未确认的高影响选择。
|
|
28
|
+
2. **检测现有风格**:加载 `<Path>{roots.workflows}/specdev/P-prototype/detect-existing-style.md</Path>`,穷尽技术栈、布局、视觉 token、组件状态、响应式与可访问性证据。新项目明确记录 `project_kind: new`,不伪造检测结果。
|
|
29
|
+
3. **建立功能候选**:读取 `<Path>{roots.workflows}/specdev/P-prototype/design-library/INDEX.md</Path>`,依据产品任务、平台和信息密度从八种风格及六种布局中推荐 2-4 个候选。每个候选写明适合原因、需要保留/调整/替换的现有事实、参考项目和迁移成本。
|
|
30
|
+
4. **逐项选择并持久化**:加载 `<Path>{roots.workflows}/specdev/P-prototype/style-selection-protocol.md</Path>`。每轮只处理一个决策层;每个答案立即写回设计系统主文档并重读。高影响偏好未确认时保持 `status: selecting`,不得自行标记 Ready。
|
|
31
|
+
5. **生成可视对照与最终原型**:加载 `<Path>{roots.workflows}/specdev/P-prototype/generate-design-package.md</Path>`。为每个候选生成独立 comparison variant,再生成 comparison 索引。用户确认或明确组合后,把最终 HTML/CSS/JS 作为具名代码块写入设计系统主文档,再物化到 final 目录。
|
|
32
|
+
6. **验证并封板**:运行 `<Path>{roots.workflows}/specdev/P-prototype/tools/materialize-prototype.mjs</Path> --check` 和 `<Path>{roots.workflows}/specdev/P-prototype/tools/validate-design-package.mjs</Path>`;随后运行 SpecDev `--stage prototype`。全部通过且选择闭合后设置 `status: ready`,更新 `works_run` 并清空 `current_work`。缺少项目事实或用户决定时设置 `blocked` 并保留恢复位置。
|
|
33
|
+
7. **返回路由**:返回设计包、comparison 和 final 的完整路径、验证命令与结果、未决问题和下一 Work。需要补足产品决定时返回 G;需要写外部行为合同返回 S;需要拆实施工作返回 Tickets;已有 Ready Ticket 才返回 I。Wayfinder 调用时只把设计包与 comparison locator 写入对应 solution comment。
|
|
32
34
|
|
|
33
35
|
## 完成标准
|
|
34
36
|
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
37
|
+
- 现有项目的检测结论逐项有项目相对证据;新项目明确没有既有风格。
|
|
38
|
+
- 候选由功能、平台和密度推导,不只更换颜色;用户已看到每个候选的独立 HTML。
|
|
39
|
+
- 设计系统主文档完整包含 UI 结构、CSS token、交互 JS 和最终 HTML 源码,并记录研究出处与拒绝理由。
|
|
40
|
+
- comparison 索引可导航到 2-4 个候选,final HTML 可离线运行;桌面与移动宽度不存在空白、遮挡或无意横向溢出。
|
|
41
|
+
- 字体、间距、圆角、色彩、层级、控件状态、异步反馈、响应式、键盘、focus 和 reduced motion 均有明确合同。
|
|
42
|
+
- Markdown 代码块与 `final/` 文件逐字一致,schema、局部校验和 SpecDev stage 校验全部通过。
|
|
43
|
+
- 设计包不包含机器绝对路径、敏感值、生产数据写入、发布动作或未授权的生产实现。
|
|
42
44
|
|
|
43
45
|
## 子文件引用
|
|
44
46
|
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
47
|
+
- 检测:`<Path>{roots.workflows}/specdev/P-prototype/detect-existing-style.md</Path>`,仅在步骤 2 加载。
|
|
48
|
+
- 选择:`<Path>{roots.workflows}/specdev/P-prototype/style-selection-protocol.md</Path>`,仅在步骤 4 加载。
|
|
49
|
+
- 生成:`<Path>{roots.workflows}/specdev/P-prototype/generate-design-package.md</Path>`,仅在步骤 5 加载。
|
|
50
|
+
- 模板:`<Path>{roots.workflows}/specdev/P-prototype/design-system-template.md</Path>`,创建设计包时加载。
|
|
51
|
+
- 设计库:`<Path>{roots.workflows}/specdev/P-prototype/design-library/INDEX.md</Path>`,建立候选或查具体规则时渐进读取。
|
|
52
|
+
- Schema:`<Path>{roots.workflows}/specdev/P-prototype/design-package.schema.json</Path>`。
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# P-prototype 设计库
|
|
2
|
+
|
|
3
|
+
本目录是 P-prototype 的静态设计参考层。它帮助 UI 原型先按任务、对象、密度和平台选择结构,再选择视觉表达;它不拥有运行时状态,不替代项目现有设计系统,也不授权修改生产代码。
|
|
4
|
+
|
|
5
|
+
研究快照固定在 commit `49b565913c52f4867456e609696625157f19b930`。快照是证据与可运行样板,不在原型过程中原地修改;需要复用时,把所需 HTML/CSS/JS/图片和 attribution 复制到当前 change 的设计包。
|
|
6
|
+
|
|
7
|
+
## 渐进披露入口
|
|
8
|
+
|
|
9
|
+
| 当前问题 | 读取 | 读完应得到 |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| 先决定信息架构、密度和组件边界 | `<Path>{roots.workflows}/specdev/P-prototype/design-library/foundations.md</Path>` | 一个主布局范式、密度档和设计系统边界 |
|
|
12
|
+
| 在八种可运行风格中选择起点 | `<Path>{roots.workflows}/specdev/P-prototype/design-library/style-index.md</Path>` | 一个主风格、必要时一个受控对照风格 |
|
|
13
|
+
| 寻找相似产品及其真实模式 | `<Path>{roots.workflows}/specdev/P-prototype/design-library/product-pattern-index.md</Path>` | 对应项目、可借鉴模式和一手研究路径 |
|
|
14
|
+
| 定义浅色/深色、语义 token、字号与层级 | `<Path>{roots.workflows}/specdev/P-prototype/design-library/color-and-theme.md</Path>` | 可实现的主题与密度假设 |
|
|
15
|
+
| 设计导航、表格、表单、反馈或危险操作 | `<Path>{roots.workflows}/specdev/P-prototype/design-library/interaction-patterns.md</Path>` | 完整状态和验证动作 |
|
|
16
|
+
| 需要窄屏、触屏、桌面/Web/移动重排 | `<Path>{roots.workflows}/specdev/P-prototype/design-library/responsive-and-platforms.md</Path>` | 每个平台的任务职责和断点行为 |
|
|
17
|
+
| 需要引用、复制资产或判断证据强度/许可 | `<Path>{roots.workflows}/specdev/P-prototype/design-library/research-provenance.md</Path>` | 可追溯来源、复用边界和 attribution |
|
|
18
|
+
|
|
19
|
+
## 使用顺序
|
|
20
|
+
|
|
21
|
+
1. 检测现有项目或明确新产品设想,并列出核心对象、最常见的 3-5 个任务、目标输入设备和最坏数据状态。
|
|
22
|
+
2. 读取 foundations,选择一个主布局范式。不要先按品牌色、圆角或流行风格选择页面。
|
|
23
|
+
3. 读取 style-index,按功能推荐 2-4 个结构不同的候选,并为每个候选生成独立 comparison HTML。
|
|
24
|
+
4. 读取 product-pattern-index,只深入与当前对象、任务或平台相似的项目研究;星标不构成设计质量或许可结论。
|
|
25
|
+
5. 只加载当前问题涉及的 theme、interaction 或 responsive 文件。精确值必须能回到 snapshot 中的 token、CSS 或研究证据。
|
|
26
|
+
6. 复制样板时保留相对目录关系和所需 attribution;把 demo 名称、权限、Git、同步与审批状态替换为原型自己的假数据,不把演示逻辑当作真实业务逻辑。
|
|
27
|
+
7. 用真实极端数据、键盘、目标宽度和浅/深主题验证。选择、拒绝理由和最终源码进入设计系统主文档,设计库本身保持不变。
|
|
28
|
+
|
|
29
|
+
## 快照入口
|
|
30
|
+
|
|
31
|
+
- 总报告:`<Path>{roots.workflows}/specdev/P-prototype/design-library/research-snapshot/report/OPEN_SOURCE_UI_RESEARCH_2026.md</Path>`
|
|
32
|
+
- 八种风格总览:`<Path>{roots.workflows}/specdev/P-prototype/design-library/research-snapshot/ui-gallery/index.html</Path>`
|
|
33
|
+
- 样板使用说明:`<Path>{roots.workflows}/specdev/P-prototype/design-library/research-snapshot/ui-gallery/README.md</Path>`
|
|
34
|
+
- 结构化项目数据:`<Path>{roots.workflows}/specdev/P-prototype/design-library/research-snapshot/data/projects.json</Path>` 与 `<Path>{roots.workflows}/specdev/P-prototype/design-library/research-snapshot/data/extended-projects.json</Path>`
|
|
35
|
+
- 综合 token:`<Path>{roots.workflows}/specdev/P-prototype/design-library/research-snapshot/reference/design-tokens.css</Path>`
|
|
36
|
+
|
|
37
|
+
## 完成门
|
|
38
|
+
|
|
39
|
+
- 选择理由落在任务模型、信息密度、输入设备和状态复杂度,而不是“看起来像”;
|
|
40
|
+
- 一个布局范式和一个主风格已明确,所有新增 token 都有语义角色;
|
|
41
|
+
- loading、empty、no-results、error、offline、read-only、permission 与长文本按适用性逐项处理;
|
|
42
|
+
- 窄屏改变任务流而非压缩桌面多栏;
|
|
43
|
+
- 引用与复制均可回到固定 commit、研究文件和 attribution。
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# 色彩、主题与视觉层级
|
|
2
|
+
|
|
3
|
+
视觉系统先定义角色,再选择色值。固定快照中的 `<Path>{roots.workflows}/specdev/P-prototype/design-library/research-snapshot/reference/design-tokens.css</Path>` 是跨项目综合起点,不是任何单一产品的 token 复制品。
|
|
4
|
+
|
|
5
|
+
## 最小语义集合
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
canvas
|
|
9
|
+
surface-1 / surface-2 / surface-3 / overlay
|
|
10
|
+
text-primary / text-secondary / text-tertiary / text-disabled
|
|
11
|
+
border-subtle / border-default / border-strong
|
|
12
|
+
accent / accent-hover / accent-subtle / accent-contrast
|
|
13
|
+
success / warning / danger / info (+ subtle variants)
|
|
14
|
+
hover / selected / pressed / focus-ring / scrim
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
业务组件消费语义角色,不直接写 `#fff`、`black` 或任意透明度。品牌 accent 只服务主要动作、选中和链接;状态色必须同时配图标、文本或形状。数据可视化色板、Git diff、syntax highlight 和业务状态分别建组,不复用 UI accent 充当所有含义。
|
|
18
|
+
|
|
19
|
+
## 浅色与深色
|
|
20
|
+
|
|
21
|
+
- 两种主题一起设计,不把深色简单反相。深色 overlay 往往比基础表面更亮,边框和语义色需要重新分配明度与对比。
|
|
22
|
+
- 分别检查正文、次要文字、图标、边框、focus ring 和状态,而不只检查按钮。
|
|
23
|
+
- 中性色保持低 chroma,避免所有表面都带强品牌倾向。
|
|
24
|
+
- 用户切换主题时保持布局、选择、输入和面板尺寸;主题改变不应触发内容重排。
|
|
25
|
+
- 高对比需求不能只靠增加饱和度,应重新检查边界、文字与焦点。
|
|
26
|
+
|
|
27
|
+
## 字体和密度
|
|
28
|
+
|
|
29
|
+
| 角色 | 字号/行高起点 |
|
|
30
|
+
|---|---|
|
|
31
|
+
| 坐标、元数据、辅助标签 | 11-12px / 16px;不承载长文本、错误原因或主要操作 |
|
|
32
|
+
| 表格、侧栏、紧凑控件 | 13px / 18-20px |
|
|
33
|
+
| 默认 UI 正文与表单 | 14px / 20px |
|
|
34
|
+
| 内容正文 | 15-16px / 24-26px |
|
|
35
|
+
| 小节/弹窗标题 | 16-18px / 22-26px |
|
|
36
|
+
| 页面标题 | 20-24px / 28-32px |
|
|
37
|
+
|
|
38
|
+
工具界面不使用营销页级大标题。数字表格可启用 tabular numerals,代码/SQL/ID 使用 mono;中英文混排单独验证 CJK 回退和行高。不使用负字距挤压 UI,长名称通过布局、换行或中间截断处理。
|
|
39
|
+
|
|
40
|
+
| 密度 | 控件高度 | 表格行高 | 典型任务 |
|
|
41
|
+
|---|---:|---:|---|
|
|
42
|
+
| Compact | 28px | 32px | SQL、日志、开发工具 |
|
|
43
|
+
| Default | 32-36px | 36-40px | CRM、项目管理、后台 |
|
|
44
|
+
| Comfortable | 40px | 44-48px | 文档、协作、低频表单 |
|
|
45
|
+
| Touch | 44-48px | 48-52px | 移动与触屏 |
|
|
46
|
+
|
|
47
|
+
## 圆角、边框与阴影
|
|
48
|
+
|
|
49
|
+
- 2-4px:代码块、表格小元素、密集工具按钮。
|
|
50
|
+
- 4-6px:输入、按钮、菜单项、紧凑面板。
|
|
51
|
+
- 6-8px:普通卡片、popover、较大控件。
|
|
52
|
+
- 8-12px:dialog、sheet、移动大容器;移动样板可因触屏语言使用更大值。
|
|
53
|
+
- pill:状态徽标、分段选择滑块或真正胶囊语义,不用于所有按钮。
|
|
54
|
+
|
|
55
|
+
同一视觉层只保留 2-3 个主圆角。常驻页面分区、侧栏、表格和检查器用 1px 边框或背景差建立层级,不用浮卡阴影;popover/menu 使用清晰边框和中等阴影;dialog 使用 scrim 与较大阴影;focus ring 独立于层级阴影。hover、selected、pressed 必须是不同 token。
|
|
56
|
+
|
|
57
|
+
## 原型验证
|
|
58
|
+
|
|
59
|
+
- 浅色、深色、200% 缩放和常见色觉差异下,文本与状态仍可区分;
|
|
60
|
+
- 页面没有绕过 token 的随机颜色、圆角和阴影;
|
|
61
|
+
- 状态不只依赖颜色;
|
|
62
|
+
- 最长名称、中文、数字和错误文案不会撑坏控件;
|
|
63
|
+
- reduced motion 时 token 中的 UI duration 降为零或对应交互停止非必要动画。
|
|
64
|
+
|
|
65
|
+
八种具体 `.theme-*` 实现在 `<Path>{roots.workflows}/specdev/P-prototype/design-library/research-snapshot/ui-gallery/styles/pages.css</Path>`,综合 token 的使用约束在 `<Path>{roots.workflows}/specdev/P-prototype/design-library/research-snapshot/reference/USAGE.md</Path>`。
|