@openprd/cli 0.1.0

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 (154) hide show
  1. package/.openprd/README.md +82 -0
  2. package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +14 -0
  3. package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +14 -0
  4. package/.openprd/benchmarks/index.md +37 -0
  5. package/.openprd/benchmarks/sources.yaml +56 -0
  6. package/.openprd/config.yaml +50 -0
  7. package/.openprd/discovery/config.json +21 -0
  8. package/.openprd/engagements/active/flows.md +30 -0
  9. package/.openprd/engagements/active/handoff.md +9 -0
  10. package/.openprd/engagements/active/intake.md +15 -0
  11. package/.openprd/engagements/active/prd.md +161 -0
  12. package/.openprd/engagements/active/review.html +61 -0
  13. package/.openprd/engagements/active/roles.md +21 -0
  14. package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +23 -0
  15. package/.openprd/exports/.gitkeep +0 -0
  16. package/.openprd/knowledge/index.json +7 -0
  17. package/.openprd/quality/config.json +229 -0
  18. package/.openprd/reviews/v0001.html +1256 -0
  19. package/.openprd/schema/diagram-architecture.schema.yaml +49 -0
  20. package/.openprd/schema/diagram-product-flow.schema.yaml +52 -0
  21. package/.openprd/schema/prd.schema.yaml +121 -0
  22. package/.openprd/sessions/.gitkeep +0 -0
  23. package/.openprd/standards/config.json +88 -0
  24. package/.openprd/standards/file-manual-template.md +28 -0
  25. package/.openprd/standards/folder-readme-template.md +28 -0
  26. package/.openprd/state/.gitkeep +0 -0
  27. package/.openprd/state/changes.json +12 -0
  28. package/.openprd/state/current.json +169 -0
  29. package/.openprd/state/version-index.json +15 -0
  30. package/.openprd/state/versions/.gitkeep +0 -0
  31. package/.openprd/state/versions/v0001.json +121 -0
  32. package/.openprd/state/versions/v0001.md +161 -0
  33. package/.openprd/templates/agent/intake.md +6 -0
  34. package/.openprd/templates/agent/prd.md +21 -0
  35. package/.openprd/templates/b2b/intake.md +6 -0
  36. package/.openprd/templates/b2b/prd.md +24 -0
  37. package/.openprd/templates/base/intake.md +18 -0
  38. package/.openprd/templates/base/prd.md +67 -0
  39. package/.openprd/templates/company/README.md +10 -0
  40. package/.openprd/templates/consumer/intake.md +6 -0
  41. package/.openprd/templates/consumer/prd.md +19 -0
  42. package/.openprd/templates/diagram/architecture.contract.json +53 -0
  43. package/.openprd/templates/diagram/product-flow.contract.json +76 -0
  44. package/.openprd/templates/industry/README.md +16 -0
  45. package/.openprd/templates/manifest.yaml +27 -0
  46. package/.openprd/templates/project/README.md +14 -0
  47. package/.openprd/templates/session/README.md +14 -0
  48. package/AGENTS.md +44 -0
  49. package/CONTRIBUTING.md +30 -0
  50. package/LICENSE +21 -0
  51. package/README.md +727 -0
  52. package/README_CN.md +583 -0
  53. package/SECURITY.md +23 -0
  54. package/bin/openprd.js +5 -0
  55. package/docs/assets/openprd-capability-overview-en.png +0 -0
  56. package/docs/assets/openprd-capability-overview-zh.png +0 -0
  57. package/docs/assets/openprd-learning-html.png +0 -0
  58. package/docs/assets/openprd-quality-html.png +0 -0
  59. package/docs/assets/openprd-review-html.png +0 -0
  60. package/docs/assets/openprd-scenario-overview.png +0 -0
  61. package/docs/assets/openprd-scenario-overview.svg +114 -0
  62. package/docs/assets/openprd-self-evolving-mechanisms-en.png +0 -0
  63. package/docs/assets/openprd-self-evolving-mechanisms-zh.png +0 -0
  64. package/docs/assets/openprd-visual-compare-case-study-en.png +0 -0
  65. package/docs/assets/openprd-visual-compare-case-study-zh.png +0 -0
  66. package/package.json +59 -0
  67. package/scripts/openprd-dev-check.mjs +5 -0
  68. package/scripts/openprd-review-presentation.mjs +82 -0
  69. package/skills/openprd-benchmark-router/SKILL.md +92 -0
  70. package/skills/openprd-benchmark-router/agents/openai.yaml +4 -0
  71. package/skills/openprd-benchmark-router/references/benchmark-sources.md +74 -0
  72. package/skills/openprd-benchmark-router/references/evaluation-lenses.md +66 -0
  73. package/skills/openprd-benchmark-router/references/source-policy.md +35 -0
  74. package/skills/openprd-diagram-review/SKILL.md +91 -0
  75. package/skills/openprd-diagram-review/agents/openai.yaml +4 -0
  76. package/skills/openprd-diagram-review/examples/architecture-zh.md +8 -0
  77. package/skills/openprd-diagram-review/examples/product-flow-zh.md +7 -0
  78. package/skills/openprd-diagram-review/references/cocoon-patterns.md +17 -0
  79. package/skills/openprd-diagram-review/references/diagram-contracts.md +126 -0
  80. package/skills/openprd-diagram-review/references/review-checklist.md +10 -0
  81. package/skills/openprd-discovery-loop/SKILL.md +196 -0
  82. package/skills/openprd-discovery-loop/agents/openai.yaml +3 -0
  83. package/skills/openprd-harness/SKILL.md +179 -0
  84. package/skills/openprd-harness/agents/openai.yaml +4 -0
  85. package/skills/openprd-harness/examples/full-workflow-zh.md +9 -0
  86. package/skills/openprd-harness/references/command-map.md +71 -0
  87. package/skills/openprd-harness/references/examples.md +26 -0
  88. package/skills/openprd-harness/references/usage-guide.md +335 -0
  89. package/skills/openprd-harness/references/workflow-gates.md +51 -0
  90. package/skills/openprd-learning-review/SKILL.md +75 -0
  91. package/skills/openprd-learning-review/agents/openai.yaml +4 -0
  92. package/skills/openprd-learning-review/references/content-contract.md +125 -0
  93. package/skills/openprd-learning-review/references/ebook-reader.md +46 -0
  94. package/skills/openprd-learning-review/references/evidence-manifest.md +55 -0
  95. package/skills/openprd-learning-review/references/genre-library.md +43 -0
  96. package/skills/openprd-learning-review/references/prompt-engineering.md +71 -0
  97. package/skills/openprd-learning-review/references/quality-rubric.md +28 -0
  98. package/skills/openprd-learning-review/references/retrieval-worked-example.md +40 -0
  99. package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +67 -0
  100. package/skills/openprd-quality/SKILL.md +101 -0
  101. package/skills/openprd-requirement-intake/SKILL.md +76 -0
  102. package/skills/openprd-requirement-intake/agents/openai.yaml +4 -0
  103. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +105 -0
  104. package/skills/openprd-requirement-intake/references/routing-rubric.md +64 -0
  105. package/skills/openprd-router/SKILL.md +40 -0
  106. package/skills/openprd-shared/SKILL.md +142 -0
  107. package/skills/openprd-shared/agents/openai.yaml +4 -0
  108. package/skills/openprd-shared/references/language-and-review.md +50 -0
  109. package/skills/openprd-shared/references/operating-rules.md +65 -0
  110. package/skills/openprd-shared/references/skill-architecture.md +70 -0
  111. package/skills/openprd-standards/SKILL.md +79 -0
  112. package/skills/openprd-standards/agents/openai.yaml +4 -0
  113. package/src/agent-integration.js +1717 -0
  114. package/src/benchmark.js +873 -0
  115. package/src/cli/args.js +460 -0
  116. package/src/cli/print.js +1423 -0
  117. package/src/codex-hook-runner-template.mjs +2422 -0
  118. package/src/dev-standards.js +372 -0
  119. package/src/diagram-core.js +1047 -0
  120. package/src/diagram-workspace.js +262 -0
  121. package/src/discovery.js +709 -0
  122. package/src/fleet.js +531 -0
  123. package/src/fs-utils.js +83 -0
  124. package/src/growth.js +545 -0
  125. package/src/html-artifacts.js +3803 -0
  126. package/src/knowledge.js +668 -0
  127. package/src/language-policy.js +142 -0
  128. package/src/learning-review.js +1655 -0
  129. package/src/loop.js +1290 -0
  130. package/src/openprd.js +1136 -0
  131. package/src/openspec/change-lifecycle.js +359 -0
  132. package/src/openspec/change-validate.js +248 -0
  133. package/src/openspec/constants.js +12 -0
  134. package/src/openspec/execute.js +300 -0
  135. package/src/openspec/generate.js +692 -0
  136. package/src/openspec/paths.js +111 -0
  137. package/src/openspec/tasks.js +352 -0
  138. package/src/prd-core.js +656 -0
  139. package/src/quality-html-artifact.js +1414 -0
  140. package/src/quality-learning.js +658 -0
  141. package/src/quality.js +1262 -0
  142. package/src/review-presentation.js +240 -0
  143. package/src/run-harness.js +1470 -0
  144. package/src/self-update.js +329 -0
  145. package/src/session-binding.js +140 -0
  146. package/src/source-inventory.js +224 -0
  147. package/src/standards.js +914 -0
  148. package/src/time.js +33 -0
  149. package/src/visual-compare.js +216 -0
  150. package/src/work-unit-migration.js +232 -0
  151. package/src/work-unit.js +88 -0
  152. package/src/workspace-core.js +1706 -0
  153. package/src/workspace-registry.js +162 -0
  154. package/src/workspace-workflow.js +1797 -0
@@ -0,0 +1,111 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {
4
+ LEGACY_OPENSPEC_CHANGE_ROOT,
5
+ LEGACY_OPENSPEC_DISCOVERY_CONFIG_PATH,
6
+ LEGACY_OPENSPEC_DISCOVERY_DIR,
7
+ OPENPRD_ACCEPTED_SPEC_ROOT,
8
+ OPENPRD_ARCHIVE_CHANGE_ROOT,
9
+ OPENPRD_CHANGE_ROOT,
10
+ OPENPRD_DISCOVERY_CONFIG_PATH,
11
+ OPENPRD_DISCOVERY_DIR,
12
+ } from './constants.js';
13
+
14
+ export function cjoin(...parts) {
15
+ return path.join(...parts);
16
+ }
17
+
18
+ export function exists(filePath) {
19
+ return fs.access(filePath).then(() => true).catch(() => false);
20
+ }
21
+
22
+ export function openPrdDiscoveryDir(projectRoot) {
23
+ return cjoin(projectRoot, ...OPENPRD_DISCOVERY_DIR);
24
+ }
25
+
26
+ export function legacyOpenSpecDiscoveryDir(projectRoot) {
27
+ return cjoin(projectRoot, ...LEGACY_OPENSPEC_DISCOVERY_DIR);
28
+ }
29
+
30
+ export function openPrdDiscoveryConfigPath(projectRoot) {
31
+ return cjoin(projectRoot, ...OPENPRD_DISCOVERY_CONFIG_PATH);
32
+ }
33
+
34
+ export function legacyOpenSpecDiscoveryConfigPath(projectRoot) {
35
+ return cjoin(projectRoot, ...LEGACY_OPENSPEC_DISCOVERY_CONFIG_PATH);
36
+ }
37
+
38
+ export async function readDiscoveryConfig(projectRoot, readJson) {
39
+ const primary = openPrdDiscoveryConfigPath(projectRoot);
40
+ const legacy = legacyOpenSpecDiscoveryConfigPath(projectRoot);
41
+ return (await readJson(primary).catch(() => null))
42
+ ?? (await readJson(legacy).catch(() => null))
43
+ ?? {};
44
+ }
45
+
46
+ export function openPrdChangeRoot(projectRoot) {
47
+ return cjoin(projectRoot, ...OPENPRD_CHANGE_ROOT);
48
+ }
49
+
50
+ export function legacyOpenSpecChangeRoot(projectRoot) {
51
+ return cjoin(projectRoot, ...LEGACY_OPENSPEC_CHANGE_ROOT);
52
+ }
53
+
54
+ export function openPrdAcceptedSpecRoot(projectRoot) {
55
+ return cjoin(projectRoot, ...OPENPRD_ACCEPTED_SPEC_ROOT);
56
+ }
57
+
58
+ export function openPrdArchiveChangeRoot(projectRoot) {
59
+ return cjoin(projectRoot, ...OPENPRD_ARCHIVE_CHANGE_ROOT);
60
+ }
61
+
62
+ export async function resolveChangeDir(projectRoot, changeId) {
63
+ const candidates = [
64
+ cjoin(openPrdChangeRoot(projectRoot), changeId),
65
+ cjoin(legacyOpenSpecChangeRoot(projectRoot), changeId),
66
+ cjoin(openPrdArchiveChangeRoot(projectRoot), changeId),
67
+ ];
68
+ for (const candidate of candidates) {
69
+ if (await exists(candidate)) {
70
+ return candidate;
71
+ }
72
+ }
73
+ return cjoin(openPrdChangeRoot(projectRoot), changeId);
74
+ }
75
+
76
+ export async function listChangeDirs(projectRoot) {
77
+ const roots = [
78
+ { root: openPrdChangeRoot(projectRoot), source: 'openprd', archived: false },
79
+ { root: legacyOpenSpecChangeRoot(projectRoot), source: 'legacy-openspec', archived: false },
80
+ { root: openPrdArchiveChangeRoot(projectRoot), source: 'openprd-archive', archived: true },
81
+ ];
82
+ const changes = [];
83
+ const seen = new Set();
84
+
85
+ for (const entryRoot of roots) {
86
+ let entries = [];
87
+ try {
88
+ entries = await fs.readdir(entryRoot.root, { withFileTypes: true });
89
+ } catch {
90
+ continue;
91
+ }
92
+ for (const entry of entries) {
93
+ if (!entry.isDirectory() || entry.name.startsWith('.')) {
94
+ continue;
95
+ }
96
+ const key = `${entry.name}:${entryRoot.source}`;
97
+ if (seen.has(key)) {
98
+ continue;
99
+ }
100
+ seen.add(key);
101
+ changes.push({
102
+ id: entry.name,
103
+ source: entryRoot.source,
104
+ archived: entryRoot.archived,
105
+ changeDir: cjoin(entryRoot.root, entry.name),
106
+ });
107
+ }
108
+ }
109
+
110
+ return changes.sort((a, b) => a.id.localeCompare(b.id) || a.source.localeCompare(b.source));
111
+ }
@@ -0,0 +1,352 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {
4
+ OPENSPEC_TASK_FILE_PATTERN,
5
+ OPENSPEC_TASK_ID_PATTERN,
6
+ OPENSPEC_TASK_MAX_ITEMS_PER_FILE,
7
+ } from './constants.js';
8
+ import { cjoin, exists, listChangeDirs, readDiscoveryConfig, resolveChangeDir } from './paths.js';
9
+
10
+ async function readText(filePath) {
11
+ return fs.readFile(filePath, 'utf8');
12
+ }
13
+
14
+ async function readJson(filePath) {
15
+ const text = await readText(filePath);
16
+ return JSON.parse(text);
17
+ }
18
+
19
+ export function sortOpenSpecTaskFiles(files) {
20
+ return [...files].sort((a, b) => {
21
+ if (a.fileName === 'tasks.md') return -1;
22
+ if (b.fileName === 'tasks.md') return 1;
23
+ return a.fileName.localeCompare(b.fileName);
24
+ });
25
+ }
26
+
27
+ export function parseOpenSpecTaskFile(text) {
28
+ const checkboxLines = [];
29
+ const substantiveCheckboxLines = [];
30
+ const structuredTasks = [];
31
+ let currentTask = null;
32
+
33
+ text.split(/\r?\n/).forEach((line, index) => {
34
+ const checkboxMatch = line.match(/^- \[([ xX])\] (.*)$/);
35
+ if (checkboxMatch) {
36
+ checkboxLines.push(line);
37
+ if (/`tasks-\d{3}\.md`/.test(line) && /\b(continue|continuing)\b/i.test(line)) {
38
+ currentTask = null;
39
+ return;
40
+ }
41
+
42
+ substantiveCheckboxLines.push(line);
43
+ const title = checkboxMatch[2].trim();
44
+ const structuredMatch = title.match(/^(T\d{3}\.\d+)\s+(.+)$/);
45
+ if (!structuredMatch) {
46
+ currentTask = null;
47
+ return;
48
+ }
49
+
50
+ currentTask = {
51
+ id: structuredMatch[1],
52
+ title: structuredMatch[2].trim(),
53
+ checked: checkboxMatch[1].toLowerCase() === 'x',
54
+ lineNumber: index + 1,
55
+ metadata: {},
56
+ };
57
+ structuredTasks.push(currentTask);
58
+ return;
59
+ }
60
+
61
+ const metadataMatch = line.match(/^\s{2,}-\s+(deps|done|verify|type|category|kind|oracle):\s*(.*)$/i);
62
+ if (currentTask && metadataMatch) {
63
+ currentTask.metadata[metadataMatch[1].toLowerCase()] = metadataMatch[2].trim();
64
+ }
65
+ });
66
+
67
+ return {
68
+ checkboxLines,
69
+ substantiveCheckboxLines,
70
+ structuredTasks,
71
+ };
72
+ }
73
+
74
+ export function parseOpenSpecTaskDeps(rawValue) {
75
+ const value = String(rawValue ?? '').trim();
76
+ if (!value || /^(none|n\/a|na|-)$/.test(value.toLowerCase())) {
77
+ return [];
78
+ }
79
+ return value.split(',').map((dep) => dep.trim()).filter(Boolean);
80
+ }
81
+
82
+ const GENERIC_TASK_TITLE_PATTERNS = [
83
+ /^(实现主流程|实现需求|实现验收目标|实现非功能需求)\s*[::]/i,
84
+ /^(验证验收目标|验证非功能需求)\s*[::]/i,
85
+ /^(Implement primary flow|Implement requirement|Implement acceptance goal|Implement non-functional requirement)\s*[::]/i,
86
+ /^(Validate acceptance goal|Validate non-functional requirement)\s*[::]/i,
87
+ ];
88
+
89
+ function looksLikeGovernanceTitle(title) {
90
+ const value = String(title ?? '').trim();
91
+ return (
92
+ /^(评审|检查|校验|验证|运行|Review|Check|Validate|Verify|Run)/i.test(value)
93
+ && /(OpenPrd\s+spec|OpenPrd\s+change|change\s+structure|change\s+schema|spec\s*(覆盖|校验|语言)?|review(?:\.html|\s+html)?|proposal|design|任务拆解)/i.test(value)
94
+ );
95
+ }
96
+
97
+ function isSpecOnlyValidateCommand(rawValue) {
98
+ const value = String(rawValue ?? '').replace(/\s+/g, ' ').trim();
99
+ if (!value || !/^openprd change \. --validate\b/i.test(value)) {
100
+ return false;
101
+ }
102
+ return !/(?:&&|\|\||;|\n)/.test(value);
103
+ }
104
+
105
+ export function normalizeOpenSpecTaskType(taskOrType) {
106
+ const rawType = typeof taskOrType === 'string'
107
+ ? taskOrType
108
+ : (taskOrType?.metadata?.type ?? taskOrType?.metadata?.category ?? taskOrType?.metadata?.kind ?? '');
109
+ const value = String(rawType ?? '').trim().toLowerCase();
110
+ if (['implementation', 'impl', 'feature', 'code'].includes(value)) {
111
+ return 'implementation';
112
+ }
113
+ if (['verification', 'verify', 'test', 'qa'].includes(value)) {
114
+ return 'verification';
115
+ }
116
+ if (['documentation', 'docs', 'doc'].includes(value)) {
117
+ return 'documentation';
118
+ }
119
+ if (['governance', 'spec', 'review', 'process'].includes(value)) {
120
+ return 'governance';
121
+ }
122
+
123
+ const title = typeof taskOrType === 'string' ? '' : String(taskOrType?.title ?? '');
124
+ if (looksLikeGovernanceTitle(title)) {
125
+ return 'governance';
126
+ }
127
+ if (/^(实现|新增|创建|接入|改造|搭建|Build|Create|Add|Implement|Wire|Prepare)/i.test(title)) {
128
+ return 'implementation';
129
+ }
130
+ if (/^(验证|测试|校验|运行|Verify|Test|Validate)/i.test(title)) {
131
+ return 'verification';
132
+ }
133
+ if (/docs\/basic|README|文档|说明书|Documentation|Docs/i.test(title)) {
134
+ return 'documentation';
135
+ }
136
+ return 'governance';
137
+ }
138
+
139
+ export function summarizeOpenSpecTaskTypes(tasks) {
140
+ const byType = {};
141
+ for (const type of ['implementation', 'verification', 'documentation', 'governance']) {
142
+ byType[type] = {
143
+ total: 0,
144
+ completed: 0,
145
+ pending: 0,
146
+ };
147
+ }
148
+
149
+ for (const task of tasks ?? []) {
150
+ const type = normalizeOpenSpecTaskType(task);
151
+ if (!byType[type]) {
152
+ byType[type] = { total: 0, completed: 0, pending: 0 };
153
+ }
154
+ byType[type].total += 1;
155
+ if (task.checked) {
156
+ byType[type].completed += 1;
157
+ } else {
158
+ byType[type].pending += 1;
159
+ }
160
+ }
161
+
162
+ return byType;
163
+ }
164
+
165
+ export function formatOpenSpecTaskLocation(task) {
166
+ return `${task.relativePath}:${task.lineNumber}`;
167
+ }
168
+
169
+ export async function collectOpenSpecTaskFiles(projectRoot, options = {}) {
170
+ const files = [];
171
+ const roots = options.changeId
172
+ ? [await resolveChangeDir(projectRoot, options.changeId)]
173
+ : (await listChangeDirs(projectRoot)).filter((change) => !change.archived).map((change) => change.changeDir);
174
+ if (roots.length === 0) {
175
+ return files;
176
+ }
177
+
178
+ async function walk(currentDir) {
179
+ let entries = [];
180
+ try {
181
+ entries = await fs.readdir(currentDir, { withFileTypes: true });
182
+ } catch {
183
+ return;
184
+ }
185
+
186
+ entries.sort((a, b) => a.name.localeCompare(b.name));
187
+ for (const entry of entries) {
188
+ const absolutePath = cjoin(currentDir, entry.name);
189
+ if (entry.isDirectory()) {
190
+ await walk(absolutePath);
191
+ continue;
192
+ }
193
+ if (!entry.isFile() || !OPENSPEC_TASK_FILE_PATTERN.test(entry.name)) {
194
+ continue;
195
+ }
196
+ const text = await readText(absolutePath);
197
+ const parsedTasks = parseOpenSpecTaskFile(text);
198
+ files.push({
199
+ absolutePath,
200
+ relativePath: path.relative(projectRoot, absolutePath),
201
+ groupDir: path.dirname(absolutePath),
202
+ fileName: entry.name,
203
+ text,
204
+ checkboxCount: parsedTasks.substantiveCheckboxLines.length,
205
+ lastCheckboxLine: parsedTasks.checkboxLines.at(-1) ?? '',
206
+ structuredTasks: parsedTasks.structuredTasks.map((task) => ({
207
+ ...task,
208
+ absolutePath,
209
+ relativePath: path.relative(projectRoot, absolutePath),
210
+ groupDir: path.dirname(absolutePath),
211
+ fileName: entry.name,
212
+ })),
213
+ });
214
+ }
215
+ }
216
+
217
+ for (const root of roots) {
218
+ if (await exists(root)) {
219
+ await walk(root);
220
+ }
221
+ }
222
+ return files;
223
+ }
224
+
225
+ export function flattenOpenSpecStructuredTasks(files) {
226
+ const sortedFiles = sortOpenSpecTaskFiles(files);
227
+ const tasks = [];
228
+ let order = 0;
229
+
230
+ for (const file of sortedFiles) {
231
+ for (const task of file.structuredTasks ?? []) {
232
+ order += 1;
233
+ tasks.push({
234
+ ...task,
235
+ order,
236
+ });
237
+ }
238
+ }
239
+
240
+ return tasks;
241
+ }
242
+
243
+ export function validateOpenSpecStructuredTasks(sortedFiles, errors, checks) {
244
+ const tasks = flattenOpenSpecStructuredTasks(sortedFiles);
245
+
246
+ if (tasks.length === 0) {
247
+ return;
248
+ }
249
+
250
+ const taskById = new Map();
251
+ const dependencyCount = tasks.reduce((sum, task) => sum + parseOpenSpecTaskDeps(task.metadata.deps).length, 0);
252
+
253
+ for (const task of tasks) {
254
+ if (taskById.has(task.id)) {
255
+ errors.push(`${formatOpenSpecTaskLocation(task)} 重复使用任务 id ${task.id}。`);
256
+ continue;
257
+ }
258
+ taskById.set(task.id, task);
259
+ }
260
+
261
+ for (const task of tasks) {
262
+ const normalizedType = normalizeOpenSpecTaskType(task);
263
+ if (!task.metadata.done) {
264
+ errors.push(`${formatOpenSpecTaskLocation(task)} 缺少 "done:"。`);
265
+ }
266
+ if (!task.metadata.verify) {
267
+ errors.push(`${formatOpenSpecTaskLocation(task)} 缺少 "verify:"。`);
268
+ }
269
+ if (GENERIC_TASK_TITLE_PATTERNS.some((pattern) => pattern.test(task.title))) {
270
+ errors.push(`${formatOpenSpecTaskLocation(task)} 任务标题仍在按 PRD 小节平移(${task.title});请改成可直接执行的实现或验证单元,不要使用“实现主流程:”或“验证验收目标:”这类泛化标题。`);
271
+ }
272
+ if (normalizedType !== 'governance' && isSpecOnlyValidateCommand(task.metadata.verify)) {
273
+ errors.push(`${formatOpenSpecTaskLocation(task)} 的 verify 只做了 change 结构校验;${normalizedType} 任务必须提供能证明实际落地的验证命令或审查步骤。`);
274
+ }
275
+
276
+ for (const depId of parseOpenSpecTaskDeps(task.metadata.deps)) {
277
+ if (!OPENSPEC_TASK_ID_PATTERN.test(depId)) {
278
+ errors.push(`${formatOpenSpecTaskLocation(task)} 存在无效依赖 id ${depId}。`);
279
+ continue;
280
+ }
281
+
282
+ const dependency = taskById.get(depId);
283
+ if (!dependency) {
284
+ errors.push(`${formatOpenSpecTaskLocation(task)} 依赖未知任务 ${depId}。`);
285
+ continue;
286
+ }
287
+ if (dependency.order >= task.order) {
288
+ errors.push(`${formatOpenSpecTaskLocation(task)} 依赖 ${depId},该任务必须出现在 ${task.id} 之前。`);
289
+ }
290
+ }
291
+ }
292
+
293
+ checks.push(`结构化 OpenPrd 任务: ${tasks.length} 个任务,${dependencyCount} 条依赖。`);
294
+ }
295
+
296
+ export async function analyzeOpenSpecTaskVolumes(projectRoot, options = {}) {
297
+ const discoveryConfig = await readDiscoveryConfig(projectRoot, readJson);
298
+ const maxItemsPerFile = Number(discoveryConfig?.taskSharding?.maxItemsPerFile ?? OPENSPEC_TASK_MAX_ITEMS_PER_FILE);
299
+ const taskFiles = await collectOpenSpecTaskFiles(projectRoot, options);
300
+ const errors = [];
301
+ const checks = [];
302
+ const filesByGroup = new Map();
303
+
304
+ if (!Number.isInteger(maxItemsPerFile) || maxItemsPerFile < 1) {
305
+ errors.push(`OpenPrd 任务分片 maxItemsPerFile 无效: ${discoveryConfig?.taskSharding?.maxItemsPerFile}`);
306
+ }
307
+
308
+ for (const file of taskFiles) {
309
+ if (file.checkboxCount > maxItemsPerFile) {
310
+ errors.push(`${file.relativePath} 包含 ${file.checkboxCount} 个 checkbox 任务;请拆分到每个文件不超过 ${maxItemsPerFile} 个。`);
311
+ }
312
+ if (!filesByGroup.has(file.groupDir)) {
313
+ filesByGroup.set(file.groupDir, []);
314
+ }
315
+ filesByGroup.get(file.groupDir).push(file);
316
+ }
317
+
318
+ for (const files of filesByGroup.values()) {
319
+ const sortedFiles = sortOpenSpecTaskFiles(files);
320
+ for (let index = 0; index < sortedFiles.length - 1; index += 1) {
321
+ const file = sortedFiles[index];
322
+ const next = sortedFiles[index + 1];
323
+ if (!file.lastCheckboxLine.includes(next.fileName)) {
324
+ errors.push(`${file.relativePath} 最后必须用 checkbox 任务交接到 ${next.fileName}。`);
325
+ }
326
+ }
327
+ validateOpenSpecStructuredTasks(sortedFiles, errors, checks);
328
+ }
329
+
330
+ if (taskFiles.length > 0) {
331
+ const totalCheckboxes = taskFiles.reduce((sum, file) => sum + file.checkboxCount, 0);
332
+ checks.push(`OpenPrd 任务文件: ${taskFiles.length} 个文件,${totalCheckboxes} 个 checkbox 任务,每个文件最多 ${maxItemsPerFile} 个。`);
333
+ }
334
+
335
+ return {
336
+ maxItemsPerFile,
337
+ files: taskFiles,
338
+ errors,
339
+ checks,
340
+ };
341
+ }
342
+
343
+ export async function listOpenSpecStructuredTasks(projectRoot, options = {}) {
344
+ const files = await collectOpenSpecTaskFiles(projectRoot, options);
345
+ const tasks = flattenOpenSpecStructuredTasks(files);
346
+ const taskById = new Map(tasks.map((task) => [task.id, task]));
347
+ return {
348
+ files,
349
+ tasks,
350
+ taskById,
351
+ };
352
+ }