@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.
- package/.openprd/README.md +82 -0
- package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +14 -0
- package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +14 -0
- package/.openprd/benchmarks/index.md +37 -0
- package/.openprd/benchmarks/sources.yaml +56 -0
- package/.openprd/config.yaml +50 -0
- package/.openprd/discovery/config.json +21 -0
- package/.openprd/engagements/active/flows.md +30 -0
- package/.openprd/engagements/active/handoff.md +9 -0
- package/.openprd/engagements/active/intake.md +15 -0
- package/.openprd/engagements/active/prd.md +161 -0
- package/.openprd/engagements/active/review.html +61 -0
- package/.openprd/engagements/active/roles.md +21 -0
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +23 -0
- package/.openprd/exports/.gitkeep +0 -0
- package/.openprd/knowledge/index.json +7 -0
- package/.openprd/quality/config.json +229 -0
- package/.openprd/reviews/v0001.html +1256 -0
- package/.openprd/schema/diagram-architecture.schema.yaml +49 -0
- package/.openprd/schema/diagram-product-flow.schema.yaml +52 -0
- package/.openprd/schema/prd.schema.yaml +121 -0
- package/.openprd/sessions/.gitkeep +0 -0
- package/.openprd/standards/config.json +88 -0
- package/.openprd/standards/file-manual-template.md +28 -0
- package/.openprd/standards/folder-readme-template.md +28 -0
- package/.openprd/state/.gitkeep +0 -0
- package/.openprd/state/changes.json +12 -0
- package/.openprd/state/current.json +169 -0
- package/.openprd/state/version-index.json +15 -0
- package/.openprd/state/versions/.gitkeep +0 -0
- package/.openprd/state/versions/v0001.json +121 -0
- package/.openprd/state/versions/v0001.md +161 -0
- package/.openprd/templates/agent/intake.md +6 -0
- package/.openprd/templates/agent/prd.md +21 -0
- package/.openprd/templates/b2b/intake.md +6 -0
- package/.openprd/templates/b2b/prd.md +24 -0
- package/.openprd/templates/base/intake.md +18 -0
- package/.openprd/templates/base/prd.md +67 -0
- package/.openprd/templates/company/README.md +10 -0
- package/.openprd/templates/consumer/intake.md +6 -0
- package/.openprd/templates/consumer/prd.md +19 -0
- package/.openprd/templates/diagram/architecture.contract.json +53 -0
- package/.openprd/templates/diagram/product-flow.contract.json +76 -0
- package/.openprd/templates/industry/README.md +16 -0
- package/.openprd/templates/manifest.yaml +27 -0
- package/.openprd/templates/project/README.md +14 -0
- package/.openprd/templates/session/README.md +14 -0
- package/AGENTS.md +44 -0
- package/CONTRIBUTING.md +30 -0
- package/LICENSE +21 -0
- package/README.md +727 -0
- package/README_CN.md +583 -0
- package/SECURITY.md +23 -0
- package/bin/openprd.js +5 -0
- package/docs/assets/openprd-capability-overview-en.png +0 -0
- package/docs/assets/openprd-capability-overview-zh.png +0 -0
- package/docs/assets/openprd-learning-html.png +0 -0
- package/docs/assets/openprd-quality-html.png +0 -0
- package/docs/assets/openprd-review-html.png +0 -0
- package/docs/assets/openprd-scenario-overview.png +0 -0
- package/docs/assets/openprd-scenario-overview.svg +114 -0
- package/docs/assets/openprd-self-evolving-mechanisms-en.png +0 -0
- package/docs/assets/openprd-self-evolving-mechanisms-zh.png +0 -0
- package/docs/assets/openprd-visual-compare-case-study-en.png +0 -0
- package/docs/assets/openprd-visual-compare-case-study-zh.png +0 -0
- package/package.json +59 -0
- package/scripts/openprd-dev-check.mjs +5 -0
- package/scripts/openprd-review-presentation.mjs +82 -0
- package/skills/openprd-benchmark-router/SKILL.md +92 -0
- package/skills/openprd-benchmark-router/agents/openai.yaml +4 -0
- package/skills/openprd-benchmark-router/references/benchmark-sources.md +74 -0
- package/skills/openprd-benchmark-router/references/evaluation-lenses.md +66 -0
- package/skills/openprd-benchmark-router/references/source-policy.md +35 -0
- package/skills/openprd-diagram-review/SKILL.md +91 -0
- package/skills/openprd-diagram-review/agents/openai.yaml +4 -0
- package/skills/openprd-diagram-review/examples/architecture-zh.md +8 -0
- package/skills/openprd-diagram-review/examples/product-flow-zh.md +7 -0
- package/skills/openprd-diagram-review/references/cocoon-patterns.md +17 -0
- package/skills/openprd-diagram-review/references/diagram-contracts.md +126 -0
- package/skills/openprd-diagram-review/references/review-checklist.md +10 -0
- package/skills/openprd-discovery-loop/SKILL.md +196 -0
- package/skills/openprd-discovery-loop/agents/openai.yaml +3 -0
- package/skills/openprd-harness/SKILL.md +179 -0
- package/skills/openprd-harness/agents/openai.yaml +4 -0
- package/skills/openprd-harness/examples/full-workflow-zh.md +9 -0
- package/skills/openprd-harness/references/command-map.md +71 -0
- package/skills/openprd-harness/references/examples.md +26 -0
- package/skills/openprd-harness/references/usage-guide.md +335 -0
- package/skills/openprd-harness/references/workflow-gates.md +51 -0
- package/skills/openprd-learning-review/SKILL.md +75 -0
- package/skills/openprd-learning-review/agents/openai.yaml +4 -0
- package/skills/openprd-learning-review/references/content-contract.md +125 -0
- package/skills/openprd-learning-review/references/ebook-reader.md +46 -0
- package/skills/openprd-learning-review/references/evidence-manifest.md +55 -0
- package/skills/openprd-learning-review/references/genre-library.md +43 -0
- package/skills/openprd-learning-review/references/prompt-engineering.md +71 -0
- package/skills/openprd-learning-review/references/quality-rubric.md +28 -0
- package/skills/openprd-learning-review/references/retrieval-worked-example.md +40 -0
- package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +67 -0
- package/skills/openprd-quality/SKILL.md +101 -0
- package/skills/openprd-requirement-intake/SKILL.md +76 -0
- package/skills/openprd-requirement-intake/agents/openai.yaml +4 -0
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +105 -0
- package/skills/openprd-requirement-intake/references/routing-rubric.md +64 -0
- package/skills/openprd-router/SKILL.md +40 -0
- package/skills/openprd-shared/SKILL.md +142 -0
- package/skills/openprd-shared/agents/openai.yaml +4 -0
- package/skills/openprd-shared/references/language-and-review.md +50 -0
- package/skills/openprd-shared/references/operating-rules.md +65 -0
- package/skills/openprd-shared/references/skill-architecture.md +70 -0
- package/skills/openprd-standards/SKILL.md +79 -0
- package/skills/openprd-standards/agents/openai.yaml +4 -0
- package/src/agent-integration.js +1717 -0
- package/src/benchmark.js +873 -0
- package/src/cli/args.js +460 -0
- package/src/cli/print.js +1423 -0
- package/src/codex-hook-runner-template.mjs +2422 -0
- package/src/dev-standards.js +372 -0
- package/src/diagram-core.js +1047 -0
- package/src/diagram-workspace.js +262 -0
- package/src/discovery.js +709 -0
- package/src/fleet.js +531 -0
- package/src/fs-utils.js +83 -0
- package/src/growth.js +545 -0
- package/src/html-artifacts.js +3803 -0
- package/src/knowledge.js +668 -0
- package/src/language-policy.js +142 -0
- package/src/learning-review.js +1655 -0
- package/src/loop.js +1290 -0
- package/src/openprd.js +1136 -0
- package/src/openspec/change-lifecycle.js +359 -0
- package/src/openspec/change-validate.js +248 -0
- package/src/openspec/constants.js +12 -0
- package/src/openspec/execute.js +300 -0
- package/src/openspec/generate.js +692 -0
- package/src/openspec/paths.js +111 -0
- package/src/openspec/tasks.js +352 -0
- package/src/prd-core.js +656 -0
- package/src/quality-html-artifact.js +1414 -0
- package/src/quality-learning.js +658 -0
- package/src/quality.js +1262 -0
- package/src/review-presentation.js +240 -0
- package/src/run-harness.js +1470 -0
- package/src/self-update.js +329 -0
- package/src/session-binding.js +140 -0
- package/src/source-inventory.js +224 -0
- package/src/standards.js +914 -0
- package/src/time.js +33 -0
- package/src/visual-compare.js +216 -0
- package/src/work-unit-migration.js +232 -0
- package/src/work-unit.js +88 -0
- package/src/workspace-core.js +1706 -0
- package/src/workspace-registry.js +162 -0
- package/src/workspace-workflow.js +1797 -0
|
@@ -0,0 +1,1706 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { buildDiagramArtifact, renderDiagramMermaidFromModel } from './diagram-core.js';
|
|
6
|
+
import { analyzePrdSnapshot, buildPrdSnapshot, getRequiredFieldDescriptors, renderPrdMarkdown, summarizeSnapshot } from './prd-core.js';
|
|
7
|
+
import { appendJsonl, appendText, cjoin, exists, readJson, readText, readYaml, stringifyYaml, writeJson, writeText, writeYaml } from './fs-utils.js';
|
|
8
|
+
import { OPENSPEC_TASK_MAX_ITEMS_PER_FILE } from './openspec/constants.js';
|
|
9
|
+
import { checkStandardsWorkspace } from './standards.js';
|
|
10
|
+
import { timestamp } from './time.js';
|
|
11
|
+
|
|
12
|
+
const PACKAGE_ROOT = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
13
|
+
const SEED_WORKSPACE = path.join(PACKAGE_ROOT, '.openprd');
|
|
14
|
+
const REQUIRED_PRODUCT_TYPES = ['consumer', 'b2b', 'agent'];
|
|
15
|
+
const REQUIRED_SECTIONS = ['meta', 'problem', 'users', 'goals', 'scope', 'scenarios', 'requirements', 'constraints', 'risks', 'handoff'];
|
|
16
|
+
const CORE_TEMPLATE_FILES = [
|
|
17
|
+
'README.md',
|
|
18
|
+
'config.yaml',
|
|
19
|
+
'schema/prd.schema.yaml',
|
|
20
|
+
'schema/diagram-architecture.schema.yaml',
|
|
21
|
+
'schema/diagram-product-flow.schema.yaml',
|
|
22
|
+
'templates/manifest.yaml',
|
|
23
|
+
'templates/base/prd.md',
|
|
24
|
+
'templates/base/intake.md',
|
|
25
|
+
'templates/diagram/architecture.contract.json',
|
|
26
|
+
'templates/diagram/product-flow.contract.json',
|
|
27
|
+
'templates/consumer/prd.md',
|
|
28
|
+
'templates/consumer/intake.md',
|
|
29
|
+
'templates/b2b/prd.md',
|
|
30
|
+
'templates/b2b/intake.md',
|
|
31
|
+
'templates/agent/prd.md',
|
|
32
|
+
'templates/agent/intake.md',
|
|
33
|
+
'templates/company/README.md',
|
|
34
|
+
'templates/industry/README.md',
|
|
35
|
+
'templates/project/README.md',
|
|
36
|
+
'templates/session/README.md',
|
|
37
|
+
'standards/config.json',
|
|
38
|
+
'standards/file-manual-template.md',
|
|
39
|
+
'standards/folder-readme-template.md',
|
|
40
|
+
'engagements/active/intake.md',
|
|
41
|
+
'engagements/active/prd.md',
|
|
42
|
+
'engagements/active/flows.md',
|
|
43
|
+
'engagements/active/roles.md',
|
|
44
|
+
'engagements/active/handoff.md',
|
|
45
|
+
'engagements/active/decision-log.md',
|
|
46
|
+
'engagements/active/open-questions.md',
|
|
47
|
+
'engagements/active/progress.md',
|
|
48
|
+
'engagements/active/verification.md',
|
|
49
|
+
'state/task-graph.json',
|
|
50
|
+
'state/events.jsonl',
|
|
51
|
+
];
|
|
52
|
+
const WORKSPACE_SEED_REFRESH_FILES = [
|
|
53
|
+
'README.md',
|
|
54
|
+
'schema/prd.schema.yaml',
|
|
55
|
+
'schema/diagram-architecture.schema.yaml',
|
|
56
|
+
'schema/diagram-product-flow.schema.yaml',
|
|
57
|
+
'templates/manifest.yaml',
|
|
58
|
+
'templates/base/prd.md',
|
|
59
|
+
'templates/base/intake.md',
|
|
60
|
+
'templates/diagram/architecture.contract.json',
|
|
61
|
+
'templates/diagram/product-flow.contract.json',
|
|
62
|
+
'templates/consumer/prd.md',
|
|
63
|
+
'templates/consumer/intake.md',
|
|
64
|
+
'templates/b2b/prd.md',
|
|
65
|
+
'templates/b2b/intake.md',
|
|
66
|
+
'templates/agent/prd.md',
|
|
67
|
+
'templates/agent/intake.md',
|
|
68
|
+
'templates/company/README.md',
|
|
69
|
+
'templates/industry/README.md',
|
|
70
|
+
'templates/project/README.md',
|
|
71
|
+
'templates/session/README.md',
|
|
72
|
+
'standards/file-manual-template.md',
|
|
73
|
+
'standards/folder-readme-template.md',
|
|
74
|
+
];
|
|
75
|
+
const WORKSPACE_SEED_COPY_IGNORE = new Set([
|
|
76
|
+
'artifacts',
|
|
77
|
+
'discovery',
|
|
78
|
+
'growth',
|
|
79
|
+
'harness',
|
|
80
|
+
'knowledge',
|
|
81
|
+
'learning',
|
|
82
|
+
'quality',
|
|
83
|
+
'state',
|
|
84
|
+
'sessions',
|
|
85
|
+
'exports',
|
|
86
|
+
'engagements/active/architecture-diagram.html',
|
|
87
|
+
'engagements/active/architecture-diagram.json',
|
|
88
|
+
'engagements/active/architecture-diagram.mmd',
|
|
89
|
+
'engagements/active/decision-log.md',
|
|
90
|
+
'engagements/active/product-flow-diagram.html',
|
|
91
|
+
'engagements/active/product-flow-diagram.json',
|
|
92
|
+
'engagements/active/product-flow-diagram.mmd',
|
|
93
|
+
'engagements/active/open-questions.md',
|
|
94
|
+
'engagements/active/progress.md',
|
|
95
|
+
'engagements/active/review.html',
|
|
96
|
+
'engagements/active/verification.md',
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
const DEFAULT_DISCOVERY_CONFIG = {
|
|
100
|
+
activeChange: null,
|
|
101
|
+
taskSharding: {
|
|
102
|
+
maxItemsPerFile: OPENSPEC_TASK_MAX_ITEMS_PER_FILE,
|
|
103
|
+
handoffRequired: true,
|
|
104
|
+
firstFile: 'tasks.md',
|
|
105
|
+
nextFilePattern: 'tasks-###.md',
|
|
106
|
+
},
|
|
107
|
+
taskMetadata: {
|
|
108
|
+
stableIdPattern: 'T###.##',
|
|
109
|
+
required: ['done', 'verify'],
|
|
110
|
+
optional: ['deps', 'type'],
|
|
111
|
+
dependencyOrder: 'dependencies must appear before dependents',
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function formatMarkdownLines(lines) {
|
|
116
|
+
return lines.filter(Boolean).map((line) => `- ${line}`).join('\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function renderLogEntry(title, lines) {
|
|
120
|
+
return `\n## ${title}\n\n${formatMarkdownLines(lines)}\n`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeMarkdownTimestampHeadings(text) {
|
|
124
|
+
return text.replace(/^##\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z)\s*$/gm, (_match, isoValue) => {
|
|
125
|
+
const parsed = new Date(isoValue);
|
|
126
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
127
|
+
return `## ${isoValue}`;
|
|
128
|
+
}
|
|
129
|
+
return `## ${timestamp(parsed)}`;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function normalizeWorkspaceMarkdownTimestamps(projectRoot, changes) {
|
|
134
|
+
const relativePaths = [
|
|
135
|
+
'engagements/active/decision-log.md',
|
|
136
|
+
'engagements/active/open-questions.md',
|
|
137
|
+
'engagements/active/progress.md',
|
|
138
|
+
'engagements/active/verification.md',
|
|
139
|
+
];
|
|
140
|
+
for (const relativePath of relativePaths) {
|
|
141
|
+
const targetPath = cjoin(projectRoot, '.openprd', relativePath);
|
|
142
|
+
const current = await readText(targetPath).catch(() => null);
|
|
143
|
+
if (current === null) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const next = normalizeMarkdownTimestampHeadings(current);
|
|
147
|
+
if (next !== current) {
|
|
148
|
+
await writeText(targetPath, next);
|
|
149
|
+
changes.push({ path: cjoin('.openprd', relativePath), status: 'updated-timestamp-format' });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildWorkflowTaskGraph(snapshot = null, analysis = null, options = {}) {
|
|
155
|
+
const productType = snapshot?.productType ?? null;
|
|
156
|
+
const prdVersion = Number(snapshot?.versionNumber ?? snapshot?.prdVersion ?? 0);
|
|
157
|
+
const isSynthesized = Boolean(snapshot?.digest || prdVersion > 0);
|
|
158
|
+
const isFrozen = snapshot?.status === 'frozen';
|
|
159
|
+
const isHandedOff = snapshot?.status === 'handed_off';
|
|
160
|
+
const isInterviewComplete = Boolean(
|
|
161
|
+
snapshot?.sections?.problem?.problemStatement
|
|
162
|
+
&& snapshot?.sections?.users?.primaryUsers?.length
|
|
163
|
+
&& snapshot?.sections?.goals?.goals?.length
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const blockers = analysis?.missingFields?.map((field) => ({
|
|
167
|
+
id: field.path,
|
|
168
|
+
label: field.label,
|
|
169
|
+
question: field.prompt,
|
|
170
|
+
section: field.section,
|
|
171
|
+
status: 'blocked',
|
|
172
|
+
})) ?? [];
|
|
173
|
+
const diagramState = options.diagramState ?? null;
|
|
174
|
+
const diagramGateActive = Boolean(diagramState?.shouldGateFreeze);
|
|
175
|
+
const prdReviewState = options.prdReviewState ?? null;
|
|
176
|
+
const reviewGateActive = Boolean(prdReviewState?.shouldGateFreeze);
|
|
177
|
+
const clarificationState = options.clarificationState ?? null;
|
|
178
|
+
const clarifyGateActive = Boolean(clarificationState?.shouldAskUser);
|
|
179
|
+
|
|
180
|
+
const workflow = [
|
|
181
|
+
{
|
|
182
|
+
id: 'clarify',
|
|
183
|
+
label: 'clarify',
|
|
184
|
+
kind: 'workflow-step',
|
|
185
|
+
status: clarifyGateActive ? 'ready' : 'done',
|
|
186
|
+
dependsOn: [],
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 'classify',
|
|
190
|
+
label: 'classify',
|
|
191
|
+
kind: 'workflow-step',
|
|
192
|
+
status: productType ? 'done' : (clarifyGateActive ? 'blocked' : 'ready'),
|
|
193
|
+
dependsOn: ['clarify'],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: 'interview',
|
|
197
|
+
label: 'interview',
|
|
198
|
+
kind: 'workflow-step',
|
|
199
|
+
status: isInterviewComplete || isSynthesized ? 'done' : (productType ? 'ready' : 'blocked'),
|
|
200
|
+
dependsOn: ['classify'],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: 'synthesize',
|
|
204
|
+
label: 'synthesize',
|
|
205
|
+
kind: 'workflow-step',
|
|
206
|
+
status: isSynthesized ? 'done' : (productType ? 'ready' : 'blocked'),
|
|
207
|
+
dependsOn: ['classify', 'interview'],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: 'validate',
|
|
211
|
+
label: 'validate',
|
|
212
|
+
kind: 'workflow-step',
|
|
213
|
+
status: isFrozen || isHandedOff ? 'done' : (isSynthesized ? 'ready' : 'blocked'),
|
|
214
|
+
dependsOn: ['synthesize'],
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: 'diagram',
|
|
218
|
+
label: 'diagram',
|
|
219
|
+
kind: 'workflow-step',
|
|
220
|
+
status: !isSynthesized
|
|
221
|
+
? 'blocked'
|
|
222
|
+
: (diagramGateActive ? 'ready' : 'done'),
|
|
223
|
+
dependsOn: ['synthesize'],
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: 'review',
|
|
227
|
+
label: 'review',
|
|
228
|
+
kind: 'workflow-step',
|
|
229
|
+
status: !isSynthesized
|
|
230
|
+
? 'blocked'
|
|
231
|
+
: (reviewGateActive ? 'ready' : 'done'),
|
|
232
|
+
dependsOn: ['synthesize'],
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: 'freeze',
|
|
236
|
+
label: 'freeze',
|
|
237
|
+
kind: 'workflow-step',
|
|
238
|
+
status: isFrozen || isHandedOff ? 'done' : (snapshot?.digest && !diagramGateActive && !reviewGateActive ? 'ready' : 'blocked'),
|
|
239
|
+
dependsOn: [
|
|
240
|
+
'validate',
|
|
241
|
+
...(diagramGateActive ? ['diagram'] : []),
|
|
242
|
+
...(reviewGateActive ? ['review'] : []),
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: 'handoff',
|
|
247
|
+
label: 'handoff',
|
|
248
|
+
kind: 'workflow-step',
|
|
249
|
+
status: isHandedOff ? 'done' : (isFrozen ? 'ready' : 'blocked'),
|
|
250
|
+
dependsOn: ['freeze'],
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
id: 'archive',
|
|
254
|
+
label: 'archive',
|
|
255
|
+
kind: 'workflow-step',
|
|
256
|
+
status: isHandedOff ? 'done' : 'blocked',
|
|
257
|
+
dependsOn: ['handoff'],
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
const artifacts = [
|
|
262
|
+
{
|
|
263
|
+
id: 'decision-log',
|
|
264
|
+
label: 'decision-log.md',
|
|
265
|
+
kind: 'record',
|
|
266
|
+
status: 'ready',
|
|
267
|
+
dependsOn: [],
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: 'open-questions',
|
|
271
|
+
label: 'open-questions.md',
|
|
272
|
+
kind: 'record',
|
|
273
|
+
status: productType ? (analysis?.missingRequiredFields > 0 ? 'ready' : 'done') : 'ready',
|
|
274
|
+
dependsOn: ['interview'],
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
id: 'progress',
|
|
278
|
+
label: 'progress.md',
|
|
279
|
+
kind: 'record',
|
|
280
|
+
status: isSynthesized || isFrozen || isHandedOff ? 'done' : 'ready',
|
|
281
|
+
dependsOn: ['classify'],
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: 'verification',
|
|
285
|
+
label: 'verification.md',
|
|
286
|
+
kind: 'record',
|
|
287
|
+
status: isFrozen || isHandedOff ? 'done' : 'ready',
|
|
288
|
+
dependsOn: ['freeze'],
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
id: 'architecture-diagram',
|
|
292
|
+
label: 'architecture-diagram.html',
|
|
293
|
+
kind: 'artifact',
|
|
294
|
+
status: diagramState?.architecture?.exists ? 'done' : (isSynthesized ? 'ready' : 'blocked'),
|
|
295
|
+
dependsOn: ['diagram'],
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: 'product-flow-diagram',
|
|
299
|
+
label: 'product-flow-diagram.html',
|
|
300
|
+
kind: 'artifact',
|
|
301
|
+
status: diagramState?.productFlow?.exists ? 'done' : (isSynthesized ? 'ready' : 'blocked'),
|
|
302
|
+
dependsOn: ['diagram'],
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
id: 'prd',
|
|
306
|
+
label: 'prd.md',
|
|
307
|
+
kind: 'artifact',
|
|
308
|
+
status: isSynthesized || isFrozen || isHandedOff ? 'done' : 'ready',
|
|
309
|
+
dependsOn: ['synthesize'],
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: 'intake-reflection',
|
|
313
|
+
label: 'intake-reflection.md',
|
|
314
|
+
kind: 'record',
|
|
315
|
+
status: clarifyGateActive ? 'ready' : 'done',
|
|
316
|
+
dependsOn: ['clarify'],
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
id: 'review-file',
|
|
320
|
+
label: 'review.html',
|
|
321
|
+
kind: 'artifact',
|
|
322
|
+
status: isFrozen || isHandedOff
|
|
323
|
+
? 'done'
|
|
324
|
+
: (isSynthesized ? (reviewGateActive ? 'ready' : 'done') : 'blocked'),
|
|
325
|
+
dependsOn: ['synthesize', 'review'],
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
id: 'flows',
|
|
329
|
+
label: 'flows.md',
|
|
330
|
+
kind: 'artifact',
|
|
331
|
+
status: isSynthesized || isFrozen || isHandedOff ? 'done' : 'ready',
|
|
332
|
+
dependsOn: ['synthesize'],
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
id: 'roles',
|
|
336
|
+
label: 'roles.md',
|
|
337
|
+
kind: 'artifact',
|
|
338
|
+
status: isSynthesized || isFrozen || isHandedOff ? 'done' : 'ready',
|
|
339
|
+
dependsOn: ['synthesize'],
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
id: 'handoff',
|
|
343
|
+
label: 'handoff.md',
|
|
344
|
+
kind: 'artifact',
|
|
345
|
+
status: isHandedOff ? 'done' : (isFrozen ? 'ready' : 'blocked'),
|
|
346
|
+
dependsOn: ['freeze'],
|
|
347
|
+
},
|
|
348
|
+
];
|
|
349
|
+
|
|
350
|
+
let nextReadyNode = 'classify';
|
|
351
|
+
if (clarifyGateActive) {
|
|
352
|
+
nextReadyNode = 'clarify';
|
|
353
|
+
} else if (!productType) {
|
|
354
|
+
nextReadyNode = 'classify';
|
|
355
|
+
} else if (analysis?.missingRequiredFields > 0) {
|
|
356
|
+
nextReadyNode = 'interview';
|
|
357
|
+
} else if (diagramGateActive) {
|
|
358
|
+
nextReadyNode = 'diagram';
|
|
359
|
+
} else if (reviewGateActive) {
|
|
360
|
+
nextReadyNode = 'review';
|
|
361
|
+
} else if (isFrozen) {
|
|
362
|
+
nextReadyNode = 'handoff';
|
|
363
|
+
} else if (isHandedOff) {
|
|
364
|
+
nextReadyNode = 'archive';
|
|
365
|
+
} else if (isSynthesized) {
|
|
366
|
+
nextReadyNode = 'freeze';
|
|
367
|
+
} else {
|
|
368
|
+
nextReadyNode = 'synthesize';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
version: 1,
|
|
373
|
+
generatedAt: timestamp(),
|
|
374
|
+
nextReadyNode,
|
|
375
|
+
workflow,
|
|
376
|
+
artifacts,
|
|
377
|
+
nodes: [
|
|
378
|
+
...workflow.map((step) => ({
|
|
379
|
+
id: step.id,
|
|
380
|
+
label: step.label,
|
|
381
|
+
kind: step.kind,
|
|
382
|
+
status: step.status,
|
|
383
|
+
dependsOn: step.dependsOn,
|
|
384
|
+
})),
|
|
385
|
+
...artifacts.map((artifact) => ({
|
|
386
|
+
id: artifact.id,
|
|
387
|
+
label: artifact.label,
|
|
388
|
+
kind: artifact.kind,
|
|
389
|
+
status: artifact.status,
|
|
390
|
+
dependsOn: artifact.dependsOn,
|
|
391
|
+
})),
|
|
392
|
+
],
|
|
393
|
+
edges: [
|
|
394
|
+
{ from: 'clarify', to: 'classify', relation: 'unblocks' },
|
|
395
|
+
{ from: 'classify', to: 'interview', relation: 'enables' },
|
|
396
|
+
{ from: 'interview', to: 'synthesize', relation: 'enables' },
|
|
397
|
+
{ from: 'synthesize', to: 'validate', relation: 'enables' },
|
|
398
|
+
{ from: 'synthesize', to: 'diagram', relation: 'enables' },
|
|
399
|
+
{ from: 'synthesize', to: 'review', relation: 'enables' },
|
|
400
|
+
{ from: 'diagram', to: 'freeze', relation: 'confirms' },
|
|
401
|
+
{ from: 'review', to: 'freeze', relation: 'confirms' },
|
|
402
|
+
{ from: 'validate', to: 'freeze', relation: 'guards' },
|
|
403
|
+
{ from: 'freeze', to: 'handoff', relation: 'enables' },
|
|
404
|
+
{ from: 'handoff', to: 'archive', relation: 'enables' },
|
|
405
|
+
{ from: 'interview', to: 'open-questions', relation: 'updates' },
|
|
406
|
+
{ from: 'synthesize', to: 'prd', relation: 'produces' },
|
|
407
|
+
{ from: 'clarify', to: 'intake-reflection', relation: 'produces' },
|
|
408
|
+
{ from: 'synthesize', to: 'review-file', relation: 'produces' },
|
|
409
|
+
{ from: 'synthesize', to: 'flows', relation: 'produces' },
|
|
410
|
+
{ from: 'synthesize', to: 'roles', relation: 'produces' },
|
|
411
|
+
{ from: 'diagram', to: 'architecture-diagram', relation: 'produces' },
|
|
412
|
+
{ from: 'diagram', to: 'product-flow-diagram', relation: 'produces' },
|
|
413
|
+
{ from: 'freeze', to: 'verification', relation: 'produces' },
|
|
414
|
+
{ from: 'freeze', to: 'handoff', relation: 'produces' },
|
|
415
|
+
],
|
|
416
|
+
blockers,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function appendWorkflowEvent(ws, type, payload = {}) {
|
|
421
|
+
await appendJsonl(ws.paths.eventsLog, {
|
|
422
|
+
type,
|
|
423
|
+
at: timestamp(),
|
|
424
|
+
...payload,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function appendDecision(ws, lines) {
|
|
429
|
+
await appendText(ws.paths.decisionLog, renderLogEntry(timestamp(), lines));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function appendProgress(ws, lines) {
|
|
433
|
+
await appendText(ws.paths.progressLog, renderLogEntry(timestamp(), lines));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function appendOpenQuestions(ws, lines) {
|
|
437
|
+
await appendText(ws.paths.openQuestionsLog, renderLogEntry(timestamp(), lines));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function appendVerification(ws, lines) {
|
|
441
|
+
await appendText(ws.paths.verificationLog, renderLogEntry(timestamp(), lines));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function normalizeRelativePath(filePath) {
|
|
445
|
+
return filePath.split(path.sep).join('/');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function shouldIgnoreSeedCopy(relativePath, ignorePaths) {
|
|
449
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
450
|
+
return ignorePaths.has(normalized)
|
|
451
|
+
|| [...ignorePaths].some((ignored) => normalized.startsWith(`${ignored}/`));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function copyTree(sourceDir, targetDir, { overwrite = false, ignorePaths = new Set(), rootDir = sourceDir } = {}) {
|
|
455
|
+
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
456
|
+
let created = 0;
|
|
457
|
+
|
|
458
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
459
|
+
|
|
460
|
+
for (const entry of entries) {
|
|
461
|
+
const sourcePath = cjoin(sourceDir, entry.name);
|
|
462
|
+
const targetPath = cjoin(targetDir, entry.name);
|
|
463
|
+
const relativePath = path.relative(rootDir, sourcePath);
|
|
464
|
+
if (shouldIgnoreSeedCopy(relativePath, ignorePaths)) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (entry.isDirectory()) {
|
|
469
|
+
created += await copyTree(sourcePath, targetPath, { overwrite, ignorePaths, rootDir });
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (!overwrite && await exists(targetPath)) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
478
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
479
|
+
created += 1;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return created;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function ensureWorkspaceSkeleton(projectRoot, options = {}) {
|
|
486
|
+
const workspaceRoot = cjoin(projectRoot, '.openprd');
|
|
487
|
+
const created = await copyTree(SEED_WORKSPACE, workspaceRoot, {
|
|
488
|
+
overwrite: Boolean(options.force),
|
|
489
|
+
ignorePaths: WORKSPACE_SEED_COPY_IGNORE,
|
|
490
|
+
rootDir: SEED_WORKSPACE,
|
|
491
|
+
});
|
|
492
|
+
await fs.mkdir(cjoin(workspaceRoot, 'state'), { recursive: true });
|
|
493
|
+
await fs.mkdir(cjoin(workspaceRoot, 'state', 'versions'), { recursive: true });
|
|
494
|
+
await fs.mkdir(cjoin(workspaceRoot, 'reviews'), { recursive: true });
|
|
495
|
+
await fs.mkdir(cjoin(workspaceRoot, 'sessions'), { recursive: true });
|
|
496
|
+
await fs.mkdir(cjoin(workspaceRoot, 'exports'), { recursive: true });
|
|
497
|
+
await fs.mkdir(cjoin(workspaceRoot, 'artifacts', 'active'), { recursive: true });
|
|
498
|
+
await fs.mkdir(cjoin(workspaceRoot, 'artifacts', 'archive'), { recursive: true });
|
|
499
|
+
await fs.mkdir(cjoin(workspaceRoot, 'learning', 'archive'), { recursive: true });
|
|
500
|
+
await fs.mkdir(cjoin(workspaceRoot, 'quality', 'reports'), { recursive: true });
|
|
501
|
+
await fs.mkdir(cjoin(workspaceRoot, 'growth'), { recursive: true });
|
|
502
|
+
await fs.mkdir(cjoin(workspaceRoot, 'knowledge', 'incidents'), { recursive: true });
|
|
503
|
+
await fs.mkdir(cjoin(workspaceRoot, 'knowledge', 'patterns'), { recursive: true });
|
|
504
|
+
await fs.mkdir(cjoin(workspaceRoot, 'knowledge', 'skills'), { recursive: true });
|
|
505
|
+
await fs.mkdir(cjoin(workspaceRoot, 'knowledge', 'candidates'), { recursive: true });
|
|
506
|
+
await fs.mkdir(cjoin(workspaceRoot, 'knowledge', 'drafts'), { recursive: true });
|
|
507
|
+
await fs.mkdir(cjoin(workspaceRoot, 'benchmarks', 'inbox'), { recursive: true });
|
|
508
|
+
await fs.mkdir(cjoin(workspaceRoot, 'benchmarks', 'evidence'), { recursive: true });
|
|
509
|
+
await fs.mkdir(cjoin(workspaceRoot, 'discovery'), { recursive: true });
|
|
510
|
+
await fs.mkdir(cjoin(workspaceRoot, 'engagements', 'active'), { recursive: true });
|
|
511
|
+
|
|
512
|
+
const defaults = [
|
|
513
|
+
[cjoin(workspaceRoot, 'engagements', 'active', 'decision-log.md'), '# 决策记录\n\n- 已初始化 OpenPrd 决策跟踪。\n'],
|
|
514
|
+
[cjoin(workspaceRoot, 'engagements', 'active', 'open-questions.md'), '# 开放问题\n\n- 已初始化 OpenPrd 问题跟踪。\n'],
|
|
515
|
+
[cjoin(workspaceRoot, 'engagements', 'active', 'progress.md'), '# 进度\n\n- 已初始化 OpenPrd 进度跟踪。\n'],
|
|
516
|
+
[cjoin(workspaceRoot, 'engagements', 'active', 'verification.md'), '# 验证\n\n- 已初始化 OpenPrd 验证跟踪。\n'],
|
|
517
|
+
[cjoin(workspaceRoot, 'state', 'task-graph.json'), JSON.stringify(buildWorkflowTaskGraph(), null, 2) + '\n'],
|
|
518
|
+
[cjoin(workspaceRoot, 'state', 'events.jsonl'), ''],
|
|
519
|
+
[cjoin(workspaceRoot, 'benchmarks', 'sources.yaml'), 'version: 1\nschema: openprd.benchmarks.v1\nsources: []\n'],
|
|
520
|
+
[cjoin(workspaceRoot, 'benchmarks', 'index.md'), '# OpenPrd Benchmark Registry\n\n## 规则\n\n- 项目级 approved benchmark 优先于 OpenPrd 内置 Source Map。\n- `inbox/` 里的 candidate 只表示待确认线索,不表示长期最佳实践。\n- 每次只挑 1-3 个高相关来源;来源目录不是事实来源。\n\n## Approved Sources\n\n- 暂无已批准来源。\n\n## Candidate Sources\n\n- 暂无待确认来源。\n'],
|
|
521
|
+
[cjoin(workspaceRoot, 'discovery', 'config.json'), JSON.stringify(DEFAULT_DISCOVERY_CONFIG, null, 2) + '\n'],
|
|
522
|
+
];
|
|
523
|
+
|
|
524
|
+
for (const [filePath, content] of defaults) {
|
|
525
|
+
if (!(await exists(filePath))) {
|
|
526
|
+
await writeText(filePath, content);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return { workspaceRoot, created };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function copySeedFileIfChanged(projectRoot, relativePath, changes) {
|
|
534
|
+
const sourcePath = cjoin(SEED_WORKSPACE, relativePath);
|
|
535
|
+
const targetPath = cjoin(projectRoot, '.openprd', relativePath);
|
|
536
|
+
if (!(await exists(sourcePath))) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const next = await readText(sourcePath);
|
|
540
|
+
const current = await readText(targetPath).catch(() => null);
|
|
541
|
+
if (current === next) {
|
|
542
|
+
changes.push({ path: cjoin('.openprd', relativePath), status: 'unchanged' });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
await writeText(targetPath, next);
|
|
546
|
+
changes.push({ path: cjoin('.openprd', relativePath), status: current === null ? 'created' : 'updated' });
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function migrateWorkspaceConfig(projectRoot, changes) {
|
|
550
|
+
const sourcePath = cjoin(SEED_WORKSPACE, 'config.yaml');
|
|
551
|
+
const targetPath = cjoin(projectRoot, '.openprd', 'config.yaml');
|
|
552
|
+
const seed = await readYaml(sourcePath);
|
|
553
|
+
const current = await readYaml(targetPath).catch(() => ({}));
|
|
554
|
+
const next = {
|
|
555
|
+
...seed,
|
|
556
|
+
...current,
|
|
557
|
+
learningReview: {
|
|
558
|
+
...(seed.learningReview ?? {}),
|
|
559
|
+
...(current.learningReview ?? {}),
|
|
560
|
+
enabled: current.learningReview?.enabled ?? seed.learningReview?.enabled ?? true,
|
|
561
|
+
autoOpen: current.learningReview?.autoOpen ?? seed.learningReview?.autoOpen ?? true,
|
|
562
|
+
defaultGenre: current.learningReview?.defaultGenre ?? seed.learningReview?.defaultGenre ?? 'internet-product',
|
|
563
|
+
sourceScope: current.learningReview?.sourceScope ?? seed.learningReview?.sourceScope ?? 'workspace',
|
|
564
|
+
},
|
|
565
|
+
quality: {
|
|
566
|
+
...(seed.quality ?? {}),
|
|
567
|
+
...(current.quality ?? {}),
|
|
568
|
+
},
|
|
569
|
+
supportedProductTypes: seed.supportedProductTypes,
|
|
570
|
+
templateInheritance: seed.templateInheritance,
|
|
571
|
+
workflow: seed.workflow,
|
|
572
|
+
qualityGates: {
|
|
573
|
+
...(seed.qualityGates ?? {}),
|
|
574
|
+
...(current.qualityGates ?? {}),
|
|
575
|
+
standards: seed.qualityGates?.standards,
|
|
576
|
+
freezeRequires: seed.qualityGates?.freezeRequires,
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
const currentText = await readText(targetPath).catch(() => null);
|
|
580
|
+
const nextText = stringifyYaml(next);
|
|
581
|
+
if (currentText !== nextText) {
|
|
582
|
+
await writeText(targetPath, nextText);
|
|
583
|
+
changes.push({ path: cjoin('.openprd', 'config.yaml'), status: currentText === null ? 'created' : 'updated' });
|
|
584
|
+
} else {
|
|
585
|
+
changes.push({ path: cjoin('.openprd', 'config.yaml'), status: 'unchanged' });
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function mergeStringLists(...lists) {
|
|
590
|
+
const merged = [];
|
|
591
|
+
const seen = new Set();
|
|
592
|
+
for (const list of lists) {
|
|
593
|
+
if (!Array.isArray(list)) {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
for (const item of list) {
|
|
597
|
+
if (typeof item !== 'string') {
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
const normalized = item.trim();
|
|
601
|
+
if (!normalized || seen.has(normalized)) {
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
seen.add(normalized);
|
|
605
|
+
merged.push(normalized);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return merged;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async function migrateStandardsConfig(projectRoot, changes) {
|
|
612
|
+
const sourcePath = cjoin(SEED_WORKSPACE, 'standards', 'config.json');
|
|
613
|
+
const targetPath = cjoin(projectRoot, '.openprd', 'standards', 'config.json');
|
|
614
|
+
const seed = await readJson(sourcePath).catch(() => null);
|
|
615
|
+
if (!seed) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const current = await readJson(targetPath).catch(() => null);
|
|
619
|
+
if (!current) {
|
|
620
|
+
await copySeedFileIfChanged(projectRoot, 'standards/config.json', changes);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const next = {
|
|
625
|
+
...seed,
|
|
626
|
+
...current,
|
|
627
|
+
version: seed.version ?? current.version ?? 1,
|
|
628
|
+
docsRoot: seed.docsRoot ?? current.docsRoot ?? 'docs/basic',
|
|
629
|
+
requiredDocs: Array.isArray(seed.requiredDocs) && seed.requiredDocs.length > 0
|
|
630
|
+
? seed.requiredDocs
|
|
631
|
+
: (current.requiredDocs ?? []),
|
|
632
|
+
fileManual: {
|
|
633
|
+
...(seed.fileManual ?? {}),
|
|
634
|
+
...(current.fileManual ?? {}),
|
|
635
|
+
ignorePaths: mergeStringLists(seed.fileManual?.ignorePaths, current.fileManual?.ignorePaths),
|
|
636
|
+
},
|
|
637
|
+
folderManual: {
|
|
638
|
+
...(seed.folderManual ?? {}),
|
|
639
|
+
...(current.folderManual ?? {}),
|
|
640
|
+
ignorePaths: mergeStringLists(seed.folderManual?.ignorePaths, current.folderManual?.ignorePaths),
|
|
641
|
+
},
|
|
642
|
+
sourceManual: {
|
|
643
|
+
...(seed.sourceManual ?? {}),
|
|
644
|
+
...(current.sourceManual ?? {}),
|
|
645
|
+
ignorePaths: mergeStringLists(seed.sourceManual?.ignorePaths, current.sourceManual?.ignorePaths),
|
|
646
|
+
},
|
|
647
|
+
developmentStandards: {
|
|
648
|
+
...(seed.developmentStandards ?? {}),
|
|
649
|
+
...(current.developmentStandards ?? {}),
|
|
650
|
+
codeFileLines: {
|
|
651
|
+
...(seed.developmentStandards?.codeFileLines ?? {}),
|
|
652
|
+
...(current.developmentStandards?.codeFileLines ?? {}),
|
|
653
|
+
codeFileExtensions: mergeStringLists(
|
|
654
|
+
seed.developmentStandards?.codeFileLines?.codeFileExtensions,
|
|
655
|
+
current.developmentStandards?.codeFileLines?.codeFileExtensions
|
|
656
|
+
),
|
|
657
|
+
exemptPathSegments: mergeStringLists(
|
|
658
|
+
seed.developmentStandards?.codeFileLines?.exemptPathSegments,
|
|
659
|
+
current.developmentStandards?.codeFileLines?.exemptPathSegments
|
|
660
|
+
),
|
|
661
|
+
exemptFilePatterns: mergeStringLists(
|
|
662
|
+
seed.developmentStandards?.codeFileLines?.exemptFilePatterns,
|
|
663
|
+
current.developmentStandards?.codeFileLines?.exemptFilePatterns
|
|
664
|
+
),
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
growth: {
|
|
668
|
+
...(seed.growth ?? {}),
|
|
669
|
+
...(current.growth ?? {}),
|
|
670
|
+
autoApply: {
|
|
671
|
+
...(seed.growth?.autoApply ?? {}),
|
|
672
|
+
...(current.growth?.autoApply ?? {}),
|
|
673
|
+
safeTypes: mergeStringLists(seed.growth?.autoApply?.safeTypes, current.growth?.autoApply?.safeTypes),
|
|
674
|
+
},
|
|
675
|
+
scopes: mergeStringLists(seed.growth?.scopes, current.growth?.scopes),
|
|
676
|
+
supportedCandidateTypes: mergeStringLists(seed.growth?.supportedCandidateTypes, current.growth?.supportedCandidateTypes),
|
|
677
|
+
},
|
|
678
|
+
externalReferences: {
|
|
679
|
+
...(seed.externalReferences ?? {}),
|
|
680
|
+
...(current.externalReferences ?? {}),
|
|
681
|
+
paths: mergeStringLists(seed.externalReferences?.paths, current.externalReferences?.paths),
|
|
682
|
+
},
|
|
683
|
+
qualityGates: {
|
|
684
|
+
...(seed.qualityGates ?? {}),
|
|
685
|
+
...(current.qualityGates ?? {}),
|
|
686
|
+
},
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
const currentText = await readText(targetPath).catch(() => null);
|
|
690
|
+
const nextText = `${JSON.stringify(next, null, 2)}\n`;
|
|
691
|
+
if (currentText !== nextText) {
|
|
692
|
+
await writeText(targetPath, nextText);
|
|
693
|
+
changes.push({ path: cjoin('.openprd', 'standards', 'config.json'), status: 'updated' });
|
|
694
|
+
} else {
|
|
695
|
+
changes.push({ path: cjoin('.openprd', 'standards', 'config.json'), status: 'unchanged' });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function extractMarkdownSection(text, heading) {
|
|
700
|
+
const start = text.indexOf(heading);
|
|
701
|
+
if (start < 0) {
|
|
702
|
+
return '';
|
|
703
|
+
}
|
|
704
|
+
const rest = text.slice(start);
|
|
705
|
+
const nextHeading = rest.slice(heading.length).search(/\n##\s+/);
|
|
706
|
+
if (nextHeading < 0) {
|
|
707
|
+
return rest.trim();
|
|
708
|
+
}
|
|
709
|
+
return rest.slice(0, heading.length + nextHeading).trim();
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function ensureActiveFileContains(projectRoot, relativePath, requiredToken, seedFallback, changes) {
|
|
713
|
+
const targetPath = cjoin(projectRoot, '.openprd', relativePath);
|
|
714
|
+
const current = await readText(targetPath).catch(() => null);
|
|
715
|
+
if (current?.includes(requiredToken)) {
|
|
716
|
+
changes.push({ path: cjoin('.openprd', relativePath), status: 'unchanged' });
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const next = current
|
|
720
|
+
? `${current.trimEnd()}\n\n---\n\n${seedFallback.trim()}\n`
|
|
721
|
+
: `${seedFallback.trim()}\n`;
|
|
722
|
+
await writeText(targetPath, next);
|
|
723
|
+
changes.push({ path: cjoin('.openprd', relativePath), status: current === null ? 'created' : 'updated-append' });
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async function ensureHeadingFile(projectRoot, relativePath, heading, fallbackBody, changes) {
|
|
727
|
+
const targetPath = cjoin(projectRoot, '.openprd', relativePath);
|
|
728
|
+
const current = await readText(targetPath).catch(() => null);
|
|
729
|
+
if (current?.includes(heading)) {
|
|
730
|
+
changes.push({ path: cjoin('.openprd', relativePath), status: 'unchanged' });
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const next = current
|
|
734
|
+
? `${heading}\n\n${current.trim()}\n`
|
|
735
|
+
: fallbackBody;
|
|
736
|
+
await writeText(targetPath, next);
|
|
737
|
+
changes.push({ path: cjoin('.openprd', relativePath), status: current === null ? 'created' : 'updated-prepend' });
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async function migrateWorkspaceSkeleton(projectRoot, options = {}) {
|
|
741
|
+
const changes = [];
|
|
742
|
+
const workspaceRoot = cjoin(projectRoot, '.openprd');
|
|
743
|
+
await fs.mkdir(workspaceRoot, { recursive: true });
|
|
744
|
+
await fs.mkdir(cjoin(workspaceRoot, 'state'), { recursive: true });
|
|
745
|
+
await fs.mkdir(cjoin(workspaceRoot, 'state', 'versions'), { recursive: true });
|
|
746
|
+
await fs.mkdir(cjoin(workspaceRoot, 'sessions'), { recursive: true });
|
|
747
|
+
await fs.mkdir(cjoin(workspaceRoot, 'exports'), { recursive: true });
|
|
748
|
+
await fs.mkdir(cjoin(workspaceRoot, 'artifacts', 'active'), { recursive: true });
|
|
749
|
+
await fs.mkdir(cjoin(workspaceRoot, 'artifacts', 'archive'), { recursive: true });
|
|
750
|
+
await fs.mkdir(cjoin(workspaceRoot, 'learning', 'archive'), { recursive: true });
|
|
751
|
+
await fs.mkdir(cjoin(workspaceRoot, 'quality', 'reports'), { recursive: true });
|
|
752
|
+
await fs.mkdir(cjoin(workspaceRoot, 'growth'), { recursive: true });
|
|
753
|
+
await fs.mkdir(cjoin(workspaceRoot, 'knowledge', 'incidents'), { recursive: true });
|
|
754
|
+
await fs.mkdir(cjoin(workspaceRoot, 'knowledge', 'patterns'), { recursive: true });
|
|
755
|
+
await fs.mkdir(cjoin(workspaceRoot, 'knowledge', 'skills'), { recursive: true });
|
|
756
|
+
await fs.mkdir(cjoin(workspaceRoot, 'knowledge', 'candidates'), { recursive: true });
|
|
757
|
+
await fs.mkdir(cjoin(workspaceRoot, 'knowledge', 'drafts'), { recursive: true });
|
|
758
|
+
await fs.mkdir(cjoin(workspaceRoot, 'benchmarks', 'inbox'), { recursive: true });
|
|
759
|
+
await fs.mkdir(cjoin(workspaceRoot, 'benchmarks', 'evidence'), { recursive: true });
|
|
760
|
+
await fs.mkdir(cjoin(workspaceRoot, 'engagements', 'active'), { recursive: true });
|
|
761
|
+
|
|
762
|
+
await migrateWorkspaceConfig(projectRoot, changes);
|
|
763
|
+
await migrateStandardsConfig(projectRoot, changes);
|
|
764
|
+
for (const relativePath of WORKSPACE_SEED_REFRESH_FILES) {
|
|
765
|
+
await copySeedFileIfChanged(projectRoot, relativePath, changes);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const seedIntake = await readText(cjoin(SEED_WORKSPACE, 'engagements', 'active', 'intake.md'));
|
|
769
|
+
const seedPrd = await readText(cjoin(SEED_WORKSPACE, 'engagements', 'active', 'prd.md'));
|
|
770
|
+
const typeSpecificBlock = extractMarkdownSection(seedPrd, '## 类型专项模块') || seedPrd;
|
|
771
|
+
await ensureActiveFileContains(projectRoot, 'engagements/active/intake.md', '我们要解决什么问题?', seedIntake, changes);
|
|
772
|
+
await ensureActiveFileContains(projectRoot, 'engagements/active/prd.md', '类型专项模块', typeSpecificBlock, changes);
|
|
773
|
+
await ensureHeadingFile(projectRoot, 'engagements/active/decision-log.md', '# 决策记录', '# 决策记录\n\n- 已初始化 OpenPrd 决策跟踪。\n', changes);
|
|
774
|
+
await ensureHeadingFile(projectRoot, 'engagements/active/open-questions.md', '# 开放问题', '# 开放问题\n\n- 已初始化 OpenPrd 问题跟踪。\n', changes);
|
|
775
|
+
await ensureHeadingFile(projectRoot, 'engagements/active/progress.md', '# 进度', '# 进度\n\n- 已初始化 OpenPrd 进度跟踪。\n', changes);
|
|
776
|
+
await ensureHeadingFile(projectRoot, 'engagements/active/verification.md', '# 验证', '# 验证\n\n- 已初始化 OpenPrd 验证跟踪。\n', changes);
|
|
777
|
+
await normalizeWorkspaceMarkdownTimestamps(projectRoot, changes);
|
|
778
|
+
|
|
779
|
+
const ws = await loadWorkspace(projectRoot);
|
|
780
|
+
const taskGraphPath = cjoin(projectRoot, '.openprd', 'state', 'task-graph.json');
|
|
781
|
+
const currentTaskGraph = await readText(taskGraphPath).catch(() => null);
|
|
782
|
+
let shouldRewriteTaskGraph = currentTaskGraph === null;
|
|
783
|
+
if (currentTaskGraph !== null) {
|
|
784
|
+
try {
|
|
785
|
+
const parsed = JSON.parse(currentTaskGraph);
|
|
786
|
+
shouldRewriteTaskGraph = !Array.isArray(parsed.workflow) || !Array.isArray(parsed.nodes);
|
|
787
|
+
} catch {
|
|
788
|
+
shouldRewriteTaskGraph = true;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if (shouldRewriteTaskGraph) {
|
|
792
|
+
await writeJson(taskGraphPath, buildWorkflowTaskGraph(ws.data.currentState));
|
|
793
|
+
changes.push({ path: cjoin('.openprd', 'state', 'task-graph.json'), status: currentTaskGraph === null ? 'created' : 'updated' });
|
|
794
|
+
} else {
|
|
795
|
+
changes.push({ path: cjoin('.openprd', 'state', 'task-graph.json'), status: 'unchanged' });
|
|
796
|
+
}
|
|
797
|
+
if (!(await exists(cjoin(projectRoot, '.openprd', 'state', 'events.jsonl')))) {
|
|
798
|
+
await writeText(cjoin(projectRoot, '.openprd', 'state', 'events.jsonl'), '');
|
|
799
|
+
changes.push({ path: cjoin('.openprd', 'state', 'events.jsonl'), status: 'created' });
|
|
800
|
+
}
|
|
801
|
+
if (!(await exists(cjoin(projectRoot, '.openprd', 'benchmarks', 'sources.yaml')))) {
|
|
802
|
+
await writeText(cjoin(projectRoot, '.openprd', 'benchmarks', 'sources.yaml'), 'version: 1\nschema: openprd.benchmarks.v1\nsources: []\n');
|
|
803
|
+
changes.push({ path: cjoin('.openprd', 'benchmarks', 'sources.yaml'), status: 'created' });
|
|
804
|
+
}
|
|
805
|
+
if (!(await exists(cjoin(projectRoot, '.openprd', 'benchmarks', 'index.md')))) {
|
|
806
|
+
await writeText(cjoin(projectRoot, '.openprd', 'benchmarks', 'index.md'), '# OpenPrd Benchmark Registry\n\n## 规则\n\n- 项目级 approved benchmark 优先于 OpenPrd 内置 Source Map。\n- `inbox/` 里的 candidate 只表示待确认线索,不表示长期最佳实践。\n- 每次只挑 1-3 个高相关来源;来源目录不是事实来源。\n\n## Approved Sources\n\n- 暂无已批准来源。\n\n## Candidate Sources\n\n- 暂无待确认来源。\n');
|
|
807
|
+
changes.push({ path: cjoin('.openprd', 'benchmarks', 'index.md'), status: 'created' });
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const changedCount = changes.filter((change) => change.status !== 'unchanged').length;
|
|
811
|
+
if (options.recordEvent && changedCount > 0) {
|
|
812
|
+
const nextWs = await loadWorkspace(projectRoot);
|
|
813
|
+
await appendWorkflowEvent(nextWs, 'workspace_migrated', {
|
|
814
|
+
changed: changedCount,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return {
|
|
819
|
+
ok: true,
|
|
820
|
+
action: 'migrate',
|
|
821
|
+
projectRoot,
|
|
822
|
+
workspaceRoot,
|
|
823
|
+
changes,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
async function loadWorkspace(projectRoot) {
|
|
828
|
+
const workspaceRoot = cjoin(projectRoot, '.openprd');
|
|
829
|
+
const paths = {
|
|
830
|
+
workspaceRoot,
|
|
831
|
+
config: cjoin(workspaceRoot, 'config.yaml'),
|
|
832
|
+
schema: cjoin(workspaceRoot, 'schema', 'prd.schema.yaml'),
|
|
833
|
+
diagramArchitectureSchema: cjoin(workspaceRoot, 'schema', 'diagram-architecture.schema.yaml'),
|
|
834
|
+
diagramProductFlowSchema: cjoin(workspaceRoot, 'schema', 'diagram-product-flow.schema.yaml'),
|
|
835
|
+
manifest: cjoin(workspaceRoot, 'templates', 'manifest.yaml'),
|
|
836
|
+
basePrd: cjoin(workspaceRoot, 'templates', 'base', 'prd.md'),
|
|
837
|
+
baseIntake: cjoin(workspaceRoot, 'templates', 'base', 'intake.md'),
|
|
838
|
+
diagramArchitectureTemplate: cjoin(workspaceRoot, 'templates', 'diagram', 'architecture.contract.json'),
|
|
839
|
+
diagramProductFlowTemplate: cjoin(workspaceRoot, 'templates', 'diagram', 'product-flow.contract.json'),
|
|
840
|
+
consumerPrd: cjoin(workspaceRoot, 'templates', 'consumer', 'prd.md'),
|
|
841
|
+
consumerIntake: cjoin(workspaceRoot, 'templates', 'consumer', 'intake.md'),
|
|
842
|
+
b2bPrd: cjoin(workspaceRoot, 'templates', 'b2b', 'prd.md'),
|
|
843
|
+
b2bIntake: cjoin(workspaceRoot, 'templates', 'b2b', 'intake.md'),
|
|
844
|
+
agentPrd: cjoin(workspaceRoot, 'templates', 'agent', 'prd.md'),
|
|
845
|
+
agentIntake: cjoin(workspaceRoot, 'templates', 'agent', 'intake.md'),
|
|
846
|
+
activeIntake: cjoin(workspaceRoot, 'engagements', 'active', 'intake.md'),
|
|
847
|
+
activePrd: cjoin(workspaceRoot, 'engagements', 'active', 'prd.md'),
|
|
848
|
+
activeFlows: cjoin(workspaceRoot, 'engagements', 'active', 'flows.md'),
|
|
849
|
+
activeRoles: cjoin(workspaceRoot, 'engagements', 'active', 'roles.md'),
|
|
850
|
+
activeHandoff: cjoin(workspaceRoot, 'engagements', 'active', 'handoff.md'),
|
|
851
|
+
activeReviewHtml: cjoin(workspaceRoot, 'engagements', 'active', 'review.html'),
|
|
852
|
+
reviewsDir: cjoin(workspaceRoot, 'reviews'),
|
|
853
|
+
artifactsActiveDir: cjoin(workspaceRoot, 'artifacts', 'active'),
|
|
854
|
+
artifactsArchiveDir: cjoin(workspaceRoot, 'artifacts', 'archive'),
|
|
855
|
+
learningDir: cjoin(workspaceRoot, 'learning'),
|
|
856
|
+
learningArchiveDir: cjoin(workspaceRoot, 'learning', 'archive'),
|
|
857
|
+
learningIndex: cjoin(workspaceRoot, 'learning', 'index.json'),
|
|
858
|
+
learningCurrent: cjoin(workspaceRoot, 'learning', 'current.json'),
|
|
859
|
+
qualityDir: cjoin(workspaceRoot, 'quality'),
|
|
860
|
+
qualityConfig: cjoin(workspaceRoot, 'quality', 'config.json'),
|
|
861
|
+
qualityReportsDir: cjoin(workspaceRoot, 'quality', 'reports'),
|
|
862
|
+
qualityReportIndex: cjoin(workspaceRoot, 'quality', 'reports', 'index.json'),
|
|
863
|
+
qualityReportLatest: cjoin(workspaceRoot, 'quality', 'reports', 'latest.json'),
|
|
864
|
+
growthDir: cjoin(workspaceRoot, 'growth'),
|
|
865
|
+
growthCandidates: cjoin(workspaceRoot, 'growth', 'candidates.jsonl'),
|
|
866
|
+
growthAccepted: cjoin(workspaceRoot, 'growth', 'accepted.json'),
|
|
867
|
+
growthRejected: cjoin(workspaceRoot, 'growth', 'rejected.json'),
|
|
868
|
+
growthLocalPreferences: cjoin(workspaceRoot, 'growth', 'preferences.local.json'),
|
|
869
|
+
knowledgeDir: cjoin(workspaceRoot, 'knowledge'),
|
|
870
|
+
knowledgeIndex: cjoin(workspaceRoot, 'knowledge', 'index.json'),
|
|
871
|
+
benchmarkDir: cjoin(workspaceRoot, 'benchmarks'),
|
|
872
|
+
benchmarkInboxDir: cjoin(workspaceRoot, 'benchmarks', 'inbox'),
|
|
873
|
+
benchmarkEvidenceDir: cjoin(workspaceRoot, 'benchmarks', 'evidence'),
|
|
874
|
+
benchmarkSources: cjoin(workspaceRoot, 'benchmarks', 'sources.yaml'),
|
|
875
|
+
benchmarkIndex: cjoin(workspaceRoot, 'benchmarks', 'index.md'),
|
|
876
|
+
activeArchitectureDiagramHtml: cjoin(workspaceRoot, 'engagements', 'active', 'architecture-diagram.html'),
|
|
877
|
+
activeArchitectureDiagramJson: cjoin(workspaceRoot, 'engagements', 'active', 'architecture-diagram.json'),
|
|
878
|
+
activeArchitectureDiagramMermaid: cjoin(workspaceRoot, 'engagements', 'active', 'architecture-diagram.mmd'),
|
|
879
|
+
activeProductFlowDiagramHtml: cjoin(workspaceRoot, 'engagements', 'active', 'product-flow-diagram.html'),
|
|
880
|
+
activeProductFlowDiagramJson: cjoin(workspaceRoot, 'engagements', 'active', 'product-flow-diagram.json'),
|
|
881
|
+
activeProductFlowDiagramMermaid: cjoin(workspaceRoot, 'engagements', 'active', 'product-flow-diagram.mmd'),
|
|
882
|
+
decisionLog: cjoin(workspaceRoot, 'engagements', 'active', 'decision-log.md'),
|
|
883
|
+
openQuestionsLog: cjoin(workspaceRoot, 'engagements', 'active', 'open-questions.md'),
|
|
884
|
+
progressLog: cjoin(workspaceRoot, 'engagements', 'active', 'progress.md'),
|
|
885
|
+
verificationLog: cjoin(workspaceRoot, 'engagements', 'active', 'verification.md'),
|
|
886
|
+
stateDir: cjoin(workspaceRoot, 'state'),
|
|
887
|
+
versionsDir: cjoin(workspaceRoot, 'state', 'versions'),
|
|
888
|
+
versionIndex: cjoin(workspaceRoot, 'state', 'version-index.json'),
|
|
889
|
+
currentState: cjoin(workspaceRoot, 'state', 'current.json'),
|
|
890
|
+
freezeState: cjoin(workspaceRoot, 'state', 'freeze.json'),
|
|
891
|
+
taskGraph: cjoin(workspaceRoot, 'state', 'task-graph.json'),
|
|
892
|
+
eventsLog: cjoin(workspaceRoot, 'state', 'events.jsonl'),
|
|
893
|
+
standardsConfig: cjoin(workspaceRoot, 'standards', 'config.json'),
|
|
894
|
+
standardsFileManualTemplate: cjoin(workspaceRoot, 'standards', 'file-manual-template.md'),
|
|
895
|
+
standardsFolderReadmeTemplate: cjoin(workspaceRoot, 'standards', 'folder-readme-template.md'),
|
|
896
|
+
exportsDir: cjoin(workspaceRoot, 'exports'),
|
|
897
|
+
openspecExportDir: cjoin(workspaceRoot, 'exports', 'openspec'),
|
|
898
|
+
openspecHandoffJson: cjoin(workspaceRoot, 'exports', 'openspec', 'handoff.json'),
|
|
899
|
+
openspecHandoffMd: cjoin(workspaceRoot, 'exports', 'openspec', 'handoff.md'),
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
const data = {
|
|
903
|
+
config: await readYaml(paths.config).catch(() => null),
|
|
904
|
+
schema: await readYaml(paths.schema).catch(() => null),
|
|
905
|
+
diagramArchitectureSchema: await readYaml(paths.diagramArchitectureSchema).catch(() => null),
|
|
906
|
+
diagramProductFlowSchema: await readYaml(paths.diagramProductFlowSchema).catch(() => null),
|
|
907
|
+
manifest: await readYaml(paths.manifest).catch(() => null),
|
|
908
|
+
currentState: await readJson(paths.currentState).catch(() => null),
|
|
909
|
+
freezeState: await readJson(paths.freezeState).catch(() => null),
|
|
910
|
+
versionIndex: await readJson(paths.versionIndex).catch(() => []),
|
|
911
|
+
learningIndex: await readJson(paths.learningIndex).catch(() => null),
|
|
912
|
+
learningCurrent: await readJson(paths.learningCurrent).catch(() => null),
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
return { projectRoot, workspaceRoot, paths, data };
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function isSupportedProductType(value) {
|
|
919
|
+
return REQUIRED_PRODUCT_TYPES.includes(value);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function resolveActiveTemplatePack(ws) {
|
|
923
|
+
return ws.data.currentState?.templatePack ?? ws.data.config?.activeTemplatePack ?? 'base';
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function resolveCurrentProductType(ws) {
|
|
927
|
+
return ws.data.currentState?.productType ?? null;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const USER_CLARIFICATION_PATHS = new Set([
|
|
931
|
+
'meta.productType',
|
|
932
|
+
'problem.problemStatement',
|
|
933
|
+
'problem.whyNow',
|
|
934
|
+
'users.primaryUsers',
|
|
935
|
+
'goals.goals',
|
|
936
|
+
'goals.successMetrics',
|
|
937
|
+
'scope.inScope',
|
|
938
|
+
'scope.outOfScope',
|
|
939
|
+
'scenarios.primaryFlows',
|
|
940
|
+
'requirements.functional',
|
|
941
|
+
'businessGuardrails.costDrivers',
|
|
942
|
+
'businessGuardrails.usageLimits',
|
|
943
|
+
'businessGuardrails.abusePrevention',
|
|
944
|
+
'businessGuardrails.monitoringSignals',
|
|
945
|
+
'businessGuardrails.alertThresholds',
|
|
946
|
+
'businessGuardrails.stopLossActions',
|
|
947
|
+
'risks.openQuestions',
|
|
948
|
+
'handoff.owner',
|
|
949
|
+
'handoff.nextStep',
|
|
950
|
+
'handoff.targetSystem',
|
|
951
|
+
'typeSpecific.fields.humanAgentContract',
|
|
952
|
+
'typeSpecific.fields.autonomyBoundary',
|
|
953
|
+
'typeSpecific.fields.toolBoundary',
|
|
954
|
+
'typeSpecific.fields.evalPlan',
|
|
955
|
+
'typeSpecific.fields.persona',
|
|
956
|
+
'typeSpecific.fields.journey',
|
|
957
|
+
'typeSpecific.fields.roles',
|
|
958
|
+
'typeSpecific.fields.asIs',
|
|
959
|
+
'typeSpecific.fields.toBe',
|
|
960
|
+
]);
|
|
961
|
+
|
|
962
|
+
const FIELD_PATH_TO_STATE_KEY = {
|
|
963
|
+
'meta.title': 'title',
|
|
964
|
+
'meta.owner': 'owner',
|
|
965
|
+
'meta.status': 'status',
|
|
966
|
+
'meta.version': 'versionLabel',
|
|
967
|
+
'meta.productType': 'productType',
|
|
968
|
+
reviewPresentation: 'reviewPresentation',
|
|
969
|
+
'problem.problemStatement': 'problemStatement',
|
|
970
|
+
'problem.whyNow': 'whyNow',
|
|
971
|
+
'problem.evidence': 'evidence',
|
|
972
|
+
'users.primaryUsers': 'primaryUsers',
|
|
973
|
+
'users.secondaryUsers': 'secondaryUsers',
|
|
974
|
+
'users.stakeholders': 'stakeholders',
|
|
975
|
+
'goals.goals': 'goals',
|
|
976
|
+
'goals.successMetrics': 'successMetrics',
|
|
977
|
+
'goals.acceptanceGoals': 'acceptanceGoals',
|
|
978
|
+
'scope.inScope': 'inScope',
|
|
979
|
+
'scope.outOfScope': 'outOfScope',
|
|
980
|
+
'scenarios.primaryFlows': 'primaryFlows',
|
|
981
|
+
'scenarios.edgeCases': 'edgeCases',
|
|
982
|
+
'scenarios.failureModes': 'failureModes',
|
|
983
|
+
'requirements.functional': 'functional',
|
|
984
|
+
'requirements.nonFunctional': 'nonFunctional',
|
|
985
|
+
'requirements.businessRules': 'businessRules',
|
|
986
|
+
'businessGuardrails.costDrivers': 'costDrivers',
|
|
987
|
+
'businessGuardrails.usageLimits': 'usageLimits',
|
|
988
|
+
'businessGuardrails.abusePrevention': 'abusePrevention',
|
|
989
|
+
'businessGuardrails.monitoringSignals': 'monitoringSignals',
|
|
990
|
+
'businessGuardrails.alertThresholds': 'alertThresholds',
|
|
991
|
+
'businessGuardrails.stopLossActions': 'stopLossActions',
|
|
992
|
+
'constraints.technical': 'technical',
|
|
993
|
+
'constraints.compliance': 'compliance',
|
|
994
|
+
'constraints.dependencies': 'dependencies',
|
|
995
|
+
'risks.assumptions': 'assumptions',
|
|
996
|
+
'risks.risks': 'risks',
|
|
997
|
+
'risks.openQuestions': 'openQuestions',
|
|
998
|
+
'handoff.owner': 'handoffOwner',
|
|
999
|
+
'handoff.nextStep': 'nextStep',
|
|
1000
|
+
'handoff.targetSystem': 'targetSystem',
|
|
1001
|
+
'typeSpecific.fields.persona': 'persona',
|
|
1002
|
+
'typeSpecific.fields.segment': 'segment',
|
|
1003
|
+
'typeSpecific.fields.journey': 'journey',
|
|
1004
|
+
'typeSpecific.fields.activationMetric': 'activationMetric',
|
|
1005
|
+
'typeSpecific.fields.retentionMetric': 'retentionMetric',
|
|
1006
|
+
'typeSpecific.fields.buyer': 'buyer',
|
|
1007
|
+
'typeSpecific.fields.user': 'user',
|
|
1008
|
+
'typeSpecific.fields.admin': 'admin',
|
|
1009
|
+
'typeSpecific.fields.operator': 'operator',
|
|
1010
|
+
'typeSpecific.fields.roles': 'roles',
|
|
1011
|
+
'typeSpecific.fields.asIs': 'asIs',
|
|
1012
|
+
'typeSpecific.fields.toBe': 'toBe',
|
|
1013
|
+
'typeSpecific.fields.permissionMatrix': 'permissionMatrix',
|
|
1014
|
+
'typeSpecific.fields.approvalFlow': 'approvalFlow',
|
|
1015
|
+
'typeSpecific.fields.humanAgentContract': 'humanAgentContract',
|
|
1016
|
+
'typeSpecific.fields.autonomyBoundary': 'autonomyBoundary',
|
|
1017
|
+
'typeSpecific.fields.toolBoundary': 'toolBoundary',
|
|
1018
|
+
'typeSpecific.fields.stateModel': 'stateModel',
|
|
1019
|
+
'typeSpecific.fields.evalPlan': 'evalPlan',
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
const NON_SEMANTIC_CAPTURE_SOURCES = new Set(['agent-normalized']);
|
|
1023
|
+
const CAPTURE_SOURCES = ['user-confirmed', 'project-derived', 'agent-inferred', 'agent-normalized'];
|
|
1024
|
+
|
|
1025
|
+
function captureSourceRequiresUserConfirmation(source) {
|
|
1026
|
+
return Boolean(source) && source !== 'user-confirmed' && !NON_SEMANTIC_CAPTURE_SOURCES.has(source);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function listMissing(actual, expected) {
|
|
1030
|
+
const actualSet = new Set(actual);
|
|
1031
|
+
return expected.filter((item) => !actualSet.has(item));
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
async function validateWorkspace(projectRoot) {
|
|
1035
|
+
const report = {
|
|
1036
|
+
valid: true,
|
|
1037
|
+
errors: [],
|
|
1038
|
+
warnings: [],
|
|
1039
|
+
checks: [],
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
const ws = await loadWorkspace(projectRoot);
|
|
1043
|
+
|
|
1044
|
+
if (!(await exists(ws.workspaceRoot))) {
|
|
1045
|
+
report.valid = false;
|
|
1046
|
+
report.errors.push(`Missing workspace: ${ws.workspaceRoot}`);
|
|
1047
|
+
return { report, ws };
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const requiredFiles = [
|
|
1051
|
+
ws.paths.config,
|
|
1052
|
+
ws.paths.schema,
|
|
1053
|
+
ws.paths.diagramArchitectureSchema,
|
|
1054
|
+
ws.paths.diagramProductFlowSchema,
|
|
1055
|
+
ws.paths.manifest,
|
|
1056
|
+
ws.paths.basePrd,
|
|
1057
|
+
ws.paths.baseIntake,
|
|
1058
|
+
ws.paths.diagramArchitectureTemplate,
|
|
1059
|
+
ws.paths.diagramProductFlowTemplate,
|
|
1060
|
+
ws.paths.consumerPrd,
|
|
1061
|
+
ws.paths.consumerIntake,
|
|
1062
|
+
ws.paths.b2bPrd,
|
|
1063
|
+
ws.paths.b2bIntake,
|
|
1064
|
+
ws.paths.agentPrd,
|
|
1065
|
+
ws.paths.agentIntake,
|
|
1066
|
+
ws.paths.activeIntake,
|
|
1067
|
+
ws.paths.activePrd,
|
|
1068
|
+
ws.paths.activeFlows,
|
|
1069
|
+
ws.paths.activeRoles,
|
|
1070
|
+
ws.paths.activeHandoff,
|
|
1071
|
+
ws.paths.decisionLog,
|
|
1072
|
+
ws.paths.openQuestionsLog,
|
|
1073
|
+
ws.paths.progressLog,
|
|
1074
|
+
ws.paths.verificationLog,
|
|
1075
|
+
ws.paths.taskGraph,
|
|
1076
|
+
ws.paths.eventsLog,
|
|
1077
|
+
ws.paths.standardsConfig,
|
|
1078
|
+
ws.paths.standardsFileManualTemplate,
|
|
1079
|
+
ws.paths.standardsFolderReadmeTemplate,
|
|
1080
|
+
];
|
|
1081
|
+
|
|
1082
|
+
const missingFiles = [];
|
|
1083
|
+
for (const filePath of requiredFiles) {
|
|
1084
|
+
if (!(await exists(filePath))) {
|
|
1085
|
+
missingFiles.push(path.relative(ws.workspaceRoot, filePath));
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (missingFiles.length > 0) {
|
|
1089
|
+
report.valid = false;
|
|
1090
|
+
report.errors.push(`Missing required files: ${missingFiles.join(', ')}`);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (!ws.data.config) {
|
|
1094
|
+
report.valid = false;
|
|
1095
|
+
report.errors.push('Failed to parse config.yaml');
|
|
1096
|
+
return { report, ws };
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
if (!ws.data.schema) {
|
|
1100
|
+
report.valid = false;
|
|
1101
|
+
report.errors.push('Failed to parse prd.schema.yaml');
|
|
1102
|
+
return { report, ws };
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if (!ws.data.diagramArchitectureSchema) {
|
|
1106
|
+
report.valid = false;
|
|
1107
|
+
report.errors.push('Failed to parse diagram-architecture.schema.yaml');
|
|
1108
|
+
return { report, ws };
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
if (!ws.data.diagramProductFlowSchema) {
|
|
1112
|
+
report.valid = false;
|
|
1113
|
+
report.errors.push('Failed to parse diagram-product-flow.schema.yaml');
|
|
1114
|
+
return { report, ws };
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (!ws.data.manifest) {
|
|
1118
|
+
report.valid = false;
|
|
1119
|
+
report.errors.push('Failed to parse templates/manifest.yaml');
|
|
1120
|
+
return { report, ws };
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const config = ws.data.config;
|
|
1124
|
+
const schema = ws.data.schema;
|
|
1125
|
+
const manifest = ws.data.manifest;
|
|
1126
|
+
|
|
1127
|
+
if (config.schema !== schema.name) {
|
|
1128
|
+
report.valid = false;
|
|
1129
|
+
report.errors.push(`config.schema (${config.schema}) must match schema.name (${schema.name})`);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const missingTypes = listMissing(config.supportedProductTypes ?? [], REQUIRED_PRODUCT_TYPES);
|
|
1133
|
+
if (missingTypes.length > 0) {
|
|
1134
|
+
report.valid = false;
|
|
1135
|
+
report.errors.push(`config.supportedProductTypes is missing: ${missingTypes.join(', ')}`);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const inheritance = JSON.stringify(config.templateInheritance ?? []);
|
|
1139
|
+
const expectedInheritance = JSON.stringify(['base', 'industry', 'company', 'project', 'session']);
|
|
1140
|
+
if (inheritance !== expectedInheritance) {
|
|
1141
|
+
report.valid = false;
|
|
1142
|
+
report.errors.push('config.templateInheritance must equal base -> industry -> company -> project -> session');
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const sections = Object.keys(schema.sections ?? {});
|
|
1146
|
+
const missingSections = listMissing(sections, REQUIRED_SECTIONS);
|
|
1147
|
+
if (missingSections.length > 0) {
|
|
1148
|
+
report.valid = false;
|
|
1149
|
+
report.errors.push(`schema.sections is missing: ${missingSections.join(', ')}`);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const extensions = Object.keys(schema.extensions ?? {});
|
|
1153
|
+
const missingExtensions = listMissing(extensions, REQUIRED_PRODUCT_TYPES);
|
|
1154
|
+
if (missingExtensions.length > 0) {
|
|
1155
|
+
report.valid = false;
|
|
1156
|
+
report.errors.push(`schema.extensions is missing: ${missingExtensions.join(', ')}`);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const registry = manifest.registry ?? {};
|
|
1160
|
+
const missingRegistry = listMissing(Object.keys(registry), ['base', 'consumer', 'b2b', 'agent']);
|
|
1161
|
+
if (missingRegistry.length > 0) {
|
|
1162
|
+
report.valid = false;
|
|
1163
|
+
report.errors.push(`manifest.registry is missing: ${missingRegistry.join(', ')}`);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
if (registry.base?.path !== 'base/prd.md') {
|
|
1167
|
+
report.valid = false;
|
|
1168
|
+
report.errors.push('manifest.registry.base.path must be base/prd.md');
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
for (const key of ['consumer', 'b2b', 'agent']) {
|
|
1172
|
+
const entry = registry[key];
|
|
1173
|
+
if (!entry || !Array.isArray(entry.extends) || entry.extends.length !== 1 || entry.extends[0] !== 'base') {
|
|
1174
|
+
report.valid = false;
|
|
1175
|
+
report.errors.push(`manifest.registry.${key}.extends must be ["base"]`);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const basePrd = await readText(ws.paths.basePrd);
|
|
1180
|
+
for (const section of ['元信息', '问题', '用户与相关方', '目标与成功标准', '范围与非目标', '场景与流程', '需求', '约束、依赖与风险', '交接']) {
|
|
1181
|
+
if (!basePrd.includes(section)) {
|
|
1182
|
+
report.valid = false;
|
|
1183
|
+
report.errors.push(`templates/base/prd.md is missing section heading: ${section}`);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const consumerPrd = await readText(ws.paths.consumerPrd);
|
|
1188
|
+
for (const token of ['用户画像', '用户分层', '用户旅程', '激活指标', '留存指标']) {
|
|
1189
|
+
if (!consumerPrd.includes(token)) {
|
|
1190
|
+
report.valid = false;
|
|
1191
|
+
report.errors.push(`templates/consumer/prd.md is missing field: ${token}`);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const b2bPrd = await readText(ws.paths.b2bPrd);
|
|
1196
|
+
for (const token of ['采购方', '使用者', '管理员', '运营者', '权限矩阵', '审批流程']) {
|
|
1197
|
+
if (!b2bPrd.includes(token)) {
|
|
1198
|
+
report.valid = false;
|
|
1199
|
+
report.errors.push(`templates/b2b/prd.md is missing field: ${token}`);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const agentPrd = await readText(ws.paths.agentPrd);
|
|
1204
|
+
for (const token of ['Human-Agent contract', '自主边界', '工具边界', '状态模型', '评估计划']) {
|
|
1205
|
+
if (!agentPrd.includes(token)) {
|
|
1206
|
+
report.valid = false;
|
|
1207
|
+
report.errors.push(`templates/agent/prd.md is missing field: ${token}`);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const architectureTemplate = await readJson(ws.paths.diagramArchitectureTemplate).catch(() => null);
|
|
1212
|
+
if (!architectureTemplate || architectureTemplate.type !== 'architecture') {
|
|
1213
|
+
report.valid = false;
|
|
1214
|
+
report.errors.push('templates/diagram/architecture.contract.json is missing or invalid');
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const productFlowTemplate = await readJson(ws.paths.diagramProductFlowTemplate).catch(() => null);
|
|
1218
|
+
if (!productFlowTemplate || productFlowTemplate.type !== 'product-flow') {
|
|
1219
|
+
report.valid = false;
|
|
1220
|
+
report.errors.push('templates/diagram/product-flow.contract.json is missing or invalid');
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const activeIntake = await readText(ws.paths.activeIntake);
|
|
1224
|
+
if (!activeIntake.includes('我们要解决什么问题?')) {
|
|
1225
|
+
report.valid = false;
|
|
1226
|
+
report.errors.push('engagements/active/intake.md is missing the core discovery prompts');
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const activePrd = await readText(ws.paths.activePrd);
|
|
1230
|
+
if (!activePrd.includes('类型专项模块')) {
|
|
1231
|
+
report.valid = false;
|
|
1232
|
+
report.errors.push('engagements/active/prd.md is missing the type-specific block');
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
const decisionLog = await readText(ws.paths.decisionLog);
|
|
1236
|
+
if (!decisionLog.includes('# 决策记录')) {
|
|
1237
|
+
report.valid = false;
|
|
1238
|
+
report.errors.push('engagements/active/decision-log.md is missing the decision log heading');
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
const openQuestionsLog = await readText(ws.paths.openQuestionsLog);
|
|
1242
|
+
if (!openQuestionsLog.includes('# 开放问题')) {
|
|
1243
|
+
report.valid = false;
|
|
1244
|
+
report.errors.push('engagements/active/open-questions.md is missing the open questions heading');
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const progressLog = await readText(ws.paths.progressLog);
|
|
1248
|
+
if (!progressLog.includes('# 进度')) {
|
|
1249
|
+
report.valid = false;
|
|
1250
|
+
report.errors.push('engagements/active/progress.md is missing the progress heading');
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const verificationLog = await readText(ws.paths.verificationLog);
|
|
1254
|
+
if (!verificationLog.includes('# 验证')) {
|
|
1255
|
+
report.valid = false;
|
|
1256
|
+
report.errors.push('engagements/active/verification.md is missing the verification heading');
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const taskGraph = await readJson(ws.paths.taskGraph).catch(() => null);
|
|
1260
|
+
if (!taskGraph || !Array.isArray(taskGraph.nodes) || !Array.isArray(taskGraph.edges) || !Array.isArray(taskGraph.workflow) || !Array.isArray(taskGraph.artifacts) || typeof taskGraph.nextReadyNode !== 'string') {
|
|
1261
|
+
report.valid = false;
|
|
1262
|
+
report.errors.push('state/task-graph.json is missing a valid graph structure');
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const eventsLog = await readText(ws.paths.eventsLog);
|
|
1266
|
+
if (typeof eventsLog !== 'string') {
|
|
1267
|
+
report.valid = false;
|
|
1268
|
+
report.errors.push('state/events.jsonl is missing');
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const standards = await checkStandardsWorkspace(projectRoot, { optional: true });
|
|
1272
|
+
if (!standards.skipped) {
|
|
1273
|
+
if (standards.errors.length > 0) {
|
|
1274
|
+
report.valid = false;
|
|
1275
|
+
}
|
|
1276
|
+
report.errors.push(...standards.errors);
|
|
1277
|
+
report.warnings.push(...standards.warnings);
|
|
1278
|
+
report.checks.push(...standards.checks.map((check) => ({ name: `standards: ${check}`, ok: standards.ok })));
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
if (ws.data.currentState && ws.data.currentState.templatePack && !['base', 'consumer', 'b2b', 'agent'].includes(ws.data.currentState.templatePack)) {
|
|
1282
|
+
report.warnings.push(`state/current.json has unknown templatePack: ${ws.data.currentState.templatePack}`);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
if (ws.data.currentState && ws.data.currentState.productType && !isSupportedProductType(ws.data.currentState.productType)) {
|
|
1286
|
+
report.valid = false;
|
|
1287
|
+
report.errors.push(`state/current.json has unknown productType: ${ws.data.currentState.productType}`);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
const prdVersion = Number(ws.data.currentState?.prdVersion ?? 0);
|
|
1291
|
+
const versionIndex = Array.isArray(ws.data.versionIndex) ? ws.data.versionIndex : [];
|
|
1292
|
+
if (prdVersion > 0 && versionIndex.length === 0) {
|
|
1293
|
+
report.valid = false;
|
|
1294
|
+
report.errors.push('state/current.json indicates a synthesized PRD, but no version history exists');
|
|
1295
|
+
}
|
|
1296
|
+
if (versionIndex.length > 0) {
|
|
1297
|
+
const latest = versionIndex[versionIndex.length - 1];
|
|
1298
|
+
if (prdVersion > 0 && Number(latest.versionNumber) !== prdVersion) {
|
|
1299
|
+
report.warnings.push(`PRD version history latest (${latest.versionId}) does not match current prdVersion (${prdVersion})`);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
report.checks.push({ name: 'workspace', ok: true });
|
|
1304
|
+
report.checks.push({ name: 'schema', ok: true });
|
|
1305
|
+
report.checks.push({ name: 'manifest', ok: true });
|
|
1306
|
+
|
|
1307
|
+
return { report, ws };
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
async function computeWorkspaceDigest(ws) {
|
|
1311
|
+
const hash = crypto.createHash('sha256');
|
|
1312
|
+
for (const rel of CORE_TEMPLATE_FILES) {
|
|
1313
|
+
const abs = cjoin(ws.workspaceRoot, rel);
|
|
1314
|
+
if (await exists(abs)) {
|
|
1315
|
+
hash.update(rel);
|
|
1316
|
+
hash.update('\n');
|
|
1317
|
+
hash.update(await readText(abs));
|
|
1318
|
+
hash.update('\n');
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return hash.digest('hex');
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
function sortVersionIndex(index) {
|
|
1326
|
+
return [...index].sort((a, b) => Number(a.versionNumber) - Number(b.versionNumber));
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
async function readVersionIndex(ws) {
|
|
1330
|
+
if (await exists(ws.paths.versionIndex)) {
|
|
1331
|
+
const diskIndex = await readJson(ws.paths.versionIndex).catch(() => []);
|
|
1332
|
+
return sortVersionIndex(Array.isArray(diskIndex) ? diskIndex : []);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const index = Array.isArray(ws.data.versionIndex) ? ws.data.versionIndex : [];
|
|
1336
|
+
return sortVersionIndex(index);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
async function writeVersionIndex(ws, index) {
|
|
1340
|
+
await writeJson(ws.paths.versionIndex, sortVersionIndex(index));
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function normalizeVersionId(value) {
|
|
1344
|
+
if (value === null || value === undefined) {
|
|
1345
|
+
return null;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const text = `${value}`.trim();
|
|
1349
|
+
if (!text) {
|
|
1350
|
+
return null;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
if (/^v\d+$/i.test(text)) {
|
|
1354
|
+
return text.toLowerCase();
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
if (/^\d+$/.test(text)) {
|
|
1358
|
+
return formatVersionId(Number(text));
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
return text;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
function coerceCapturedValue(pathString, rawValue, append = false) {
|
|
1365
|
+
if (rawValue === null || rawValue === undefined) {
|
|
1366
|
+
return rawValue;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
if (Array.isArray(rawValue)) {
|
|
1370
|
+
return rawValue;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
if (typeof rawValue === 'object') {
|
|
1374
|
+
return rawValue;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
const text = `${rawValue}`.trim();
|
|
1378
|
+
if (text === '') {
|
|
1379
|
+
return rawValue;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
if (text.startsWith('[') || text.startsWith('{')) {
|
|
1383
|
+
try {
|
|
1384
|
+
return JSON.parse(text);
|
|
1385
|
+
} catch {
|
|
1386
|
+
// fall through
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
const expectsArray = [
|
|
1391
|
+
'problem.evidence',
|
|
1392
|
+
'users.primaryUsers',
|
|
1393
|
+
'users.secondaryUsers',
|
|
1394
|
+
'users.stakeholders',
|
|
1395
|
+
'goals.goals',
|
|
1396
|
+
'goals.successMetrics',
|
|
1397
|
+
'goals.acceptanceGoals',
|
|
1398
|
+
'scope.inScope',
|
|
1399
|
+
'scope.outOfScope',
|
|
1400
|
+
'scenarios.primaryFlows',
|
|
1401
|
+
'scenarios.edgeCases',
|
|
1402
|
+
'scenarios.failureModes',
|
|
1403
|
+
'requirements.functional',
|
|
1404
|
+
'requirements.nonFunctional',
|
|
1405
|
+
'requirements.businessRules',
|
|
1406
|
+
'businessGuardrails.costDrivers',
|
|
1407
|
+
'businessGuardrails.usageLimits',
|
|
1408
|
+
'businessGuardrails.abusePrevention',
|
|
1409
|
+
'businessGuardrails.monitoringSignals',
|
|
1410
|
+
'businessGuardrails.alertThresholds',
|
|
1411
|
+
'businessGuardrails.stopLossActions',
|
|
1412
|
+
'constraints.technical',
|
|
1413
|
+
'constraints.compliance',
|
|
1414
|
+
'constraints.dependencies',
|
|
1415
|
+
'risks.assumptions',
|
|
1416
|
+
'risks.risks',
|
|
1417
|
+
'risks.openQuestions',
|
|
1418
|
+
].includes(pathString);
|
|
1419
|
+
|
|
1420
|
+
if (expectsArray || append) {
|
|
1421
|
+
return text.split(/[\n,;|]+/).map((item) => item.trim()).filter(Boolean);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
return text;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
async function detectWorkspaceScenario(projectRoot, ws, versionIndex = []) {
|
|
1428
|
+
const currentStatus = ws.data.currentState?.status ?? 'unknown';
|
|
1429
|
+
if (versionIndex.length > 0 || ['synthesized', 'frozen', 'handed_off'].includes(currentStatus)) {
|
|
1430
|
+
return {
|
|
1431
|
+
id: 'continuing-workspace',
|
|
1432
|
+
label: '继续已有工作区',
|
|
1433
|
+
userParticipation: '定向确认',
|
|
1434
|
+
reason: '该工作区已有合成结果或历史记录,只需要补充确认增量信息。',
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
const entries = await fs.readdir(projectRoot, { withFileTypes: true }).catch(() => []);
|
|
1439
|
+
const meaningfulEntries = entries.filter((entry) => {
|
|
1440
|
+
if (entry.name === '.openprd') return false;
|
|
1441
|
+
if (entry.name === '.DS_Store') return false;
|
|
1442
|
+
if (entry.name === '.git') return false;
|
|
1443
|
+
if (entry.name === '.omx') return false;
|
|
1444
|
+
return true;
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
if (meaningfulEntries.length === 0) {
|
|
1448
|
+
return {
|
|
1449
|
+
id: 'cold-start-greenfield',
|
|
1450
|
+
label: '冷启动(全新项目)',
|
|
1451
|
+
userParticipation: '高协作',
|
|
1452
|
+
reason: '项目根目录基本为空,需要 Agent 与用户共同梳理初始需求形态。',
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
return {
|
|
1457
|
+
id: 'cold-start-existing-project',
|
|
1458
|
+
label: '冷启动(已有项目)',
|
|
1459
|
+
userParticipation: '上下文复用加确认',
|
|
1460
|
+
reason: '项目已经包含资料,但 OpenPrd 工作区是新的,需要复用既有上下文并向用户确认。',
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function buildClarificationState({ snapshot, analysis, basePlan, scenario, captureMeta, prdReviewState = null, limit = 8 }) {
|
|
1465
|
+
const captureState = captureMeta ?? {};
|
|
1466
|
+
const reviewConfirmed = prdReviewState?.status === 'confirmed';
|
|
1467
|
+
const confirmDerived = reviewConfirmed
|
|
1468
|
+
? []
|
|
1469
|
+
: analysis.completeFields
|
|
1470
|
+
.filter((field) => USER_CLARIFICATION_PATHS.has(field.path))
|
|
1471
|
+
.filter((field) => {
|
|
1472
|
+
const source = captureState[field.path]?.source;
|
|
1473
|
+
return captureSourceRequiresUserConfirmation(source);
|
|
1474
|
+
})
|
|
1475
|
+
.map((field) => ({
|
|
1476
|
+
id: field.path,
|
|
1477
|
+
label: field.label,
|
|
1478
|
+
prompt: `请确认这个推断输入:${field.label}。当前值:${Array.isArray(field.value) ? field.value.join(', ') : JSON.stringify(field.value)}`,
|
|
1479
|
+
reason: 'confirm-derived',
|
|
1480
|
+
}));
|
|
1481
|
+
|
|
1482
|
+
const missingQuestions = basePlan.mustAsk.map((field) => ({
|
|
1483
|
+
id: field.path,
|
|
1484
|
+
label: field.label,
|
|
1485
|
+
prompt: field.prompt,
|
|
1486
|
+
reason: 'missing',
|
|
1487
|
+
}));
|
|
1488
|
+
|
|
1489
|
+
let questions = [];
|
|
1490
|
+
if (scenario.id === 'cold-start-greenfield') {
|
|
1491
|
+
questions = [
|
|
1492
|
+
...basePlan.kickoffQuestions.map((field) => ({
|
|
1493
|
+
id: field.id,
|
|
1494
|
+
label: field.label,
|
|
1495
|
+
prompt: field.prompt,
|
|
1496
|
+
reason: 'kickoff',
|
|
1497
|
+
})),
|
|
1498
|
+
...missingQuestions,
|
|
1499
|
+
...confirmDerived,
|
|
1500
|
+
];
|
|
1501
|
+
} else if (scenario.id === 'cold-start-existing-project') {
|
|
1502
|
+
questions = [
|
|
1503
|
+
{
|
|
1504
|
+
id: 'existing-project-goal',
|
|
1505
|
+
label: '已有项目范围',
|
|
1506
|
+
prompt: '基于当前已有项目,这个 OpenPrd 工作区现在具体要定义或改进什么?',
|
|
1507
|
+
reason: 'kickoff',
|
|
1508
|
+
},
|
|
1509
|
+
{
|
|
1510
|
+
id: 'reuse-boundary',
|
|
1511
|
+
label: '复用边界',
|
|
1512
|
+
prompt: '哪些既有能力应视为固定输入,哪些区域仍可调整?',
|
|
1513
|
+
reason: 'kickoff',
|
|
1514
|
+
},
|
|
1515
|
+
...missingQuestions,
|
|
1516
|
+
...confirmDerived,
|
|
1517
|
+
];
|
|
1518
|
+
} else {
|
|
1519
|
+
questions = [
|
|
1520
|
+
...missingQuestions,
|
|
1521
|
+
...confirmDerived,
|
|
1522
|
+
];
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
const deduped = [];
|
|
1526
|
+
const seen = new Set();
|
|
1527
|
+
for (const question of questions) {
|
|
1528
|
+
const key = question.id;
|
|
1529
|
+
if (seen.has(key)) continue;
|
|
1530
|
+
seen.add(key);
|
|
1531
|
+
deduped.push(question);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
const mustAskUser = deduped.slice(0, limit);
|
|
1535
|
+
const canInferLater = basePlan.derived.map((field) => ({
|
|
1536
|
+
id: field.path,
|
|
1537
|
+
label: field.label,
|
|
1538
|
+
prompt: field.prompt,
|
|
1539
|
+
}));
|
|
1540
|
+
|
|
1541
|
+
return {
|
|
1542
|
+
scenario,
|
|
1543
|
+
totalRequiredFields: basePlan.totalRequiredFields,
|
|
1544
|
+
missingRequiredFields: analysis.missingRequiredFields,
|
|
1545
|
+
mustAskUser,
|
|
1546
|
+
canInferLater,
|
|
1547
|
+
shouldAskUser: mustAskUser.length > 0,
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
function buildClarificationPlan(snapshot, analysis) {
|
|
1552
|
+
const mustAsk = analysis.missingFields.filter((field) => USER_CLARIFICATION_PATHS.has(field.path));
|
|
1553
|
+
const derived = analysis.missingFields.filter((field) => !USER_CLARIFICATION_PATHS.has(field.path));
|
|
1554
|
+
const kickoffQuestions = [
|
|
1555
|
+
{ id: 'project-overview', label: 'Project overview', prompt: 'What are we building at a high level, and for whom?' },
|
|
1556
|
+
{ id: 'success-definition', label: 'Success definition', prompt: 'What outcome would make this first version successful?' },
|
|
1557
|
+
{ id: 'first-milestone', label: '首个里程碑', prompt: '我们希望 freeze 的第一个里程碑是什么?' },
|
|
1558
|
+
];
|
|
1559
|
+
return {
|
|
1560
|
+
totalRequiredFields: analysis.totalRequiredFields,
|
|
1561
|
+
missingRequiredFields: analysis.missingRequiredFields,
|
|
1562
|
+
mustAsk,
|
|
1563
|
+
derived,
|
|
1564
|
+
kickoffQuestions,
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function deriveGateLabels({ nextAction, diagramState, clarification }) {
|
|
1569
|
+
let currentGate = nextAction;
|
|
1570
|
+
if (nextAction === 'diagram') {
|
|
1571
|
+
currentGate = `${diagramState?.preferredType ?? 'architecture'} diagram review`;
|
|
1572
|
+
} else if (nextAction === 'review') {
|
|
1573
|
+
currentGate = 'prd review';
|
|
1574
|
+
} else if (nextAction === 'freeze') {
|
|
1575
|
+
currentGate = 'freeze review';
|
|
1576
|
+
} else if (nextAction === 'clarify-user') {
|
|
1577
|
+
currentGate = 'clarify-user';
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
let upcomingGate = null;
|
|
1581
|
+
if (nextAction === 'clarify-user' || nextAction === 'classify' || nextAction === 'interview' || nextAction === 'synthesize') {
|
|
1582
|
+
if (diagramState?.needed) {
|
|
1583
|
+
upcomingGate = `${diagramState.preferredType} diagram review`;
|
|
1584
|
+
} else {
|
|
1585
|
+
upcomingGate = 'freeze review';
|
|
1586
|
+
}
|
|
1587
|
+
} else if (nextAction === 'diagram') {
|
|
1588
|
+
upcomingGate = 'freeze review';
|
|
1589
|
+
} else if (nextAction === 'review') {
|
|
1590
|
+
upcomingGate = 'freeze review';
|
|
1591
|
+
} else if (nextAction === 'freeze') {
|
|
1592
|
+
upcomingGate = 'handoff review';
|
|
1593
|
+
} else if (nextAction === 'handoff') {
|
|
1594
|
+
upcomingGate = 'post-handoff review';
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
return {
|
|
1598
|
+
currentGate,
|
|
1599
|
+
upcomingGate,
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
async function writeVersionSnapshot(ws, snapshot) {
|
|
1604
|
+
await fs.mkdir(ws.paths.versionsDir, { recursive: true });
|
|
1605
|
+
const jsonPath = cjoin(ws.paths.versionsDir, `${snapshot.versionId}.json`);
|
|
1606
|
+
const mdPath = cjoin(ws.paths.versionsDir, `${snapshot.versionId}.md`);
|
|
1607
|
+
await writeJson(jsonPath, snapshot);
|
|
1608
|
+
await writeText(mdPath, snapshot.content);
|
|
1609
|
+
return { jsonPath, mdPath };
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
async function readVersionSnapshot(ws, versionId) {
|
|
1613
|
+
const normalized = normalizeVersionId(versionId);
|
|
1614
|
+
if (!normalized) {
|
|
1615
|
+
return null;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
const jsonPath = cjoin(ws.paths.versionsDir, `${normalized}.json`);
|
|
1619
|
+
if (!(await exists(jsonPath))) {
|
|
1620
|
+
return null;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
return readJson(jsonPath);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
async function loadLatestVersionSnapshot(ws) {
|
|
1627
|
+
const index = await readVersionIndex(ws);
|
|
1628
|
+
if (index.length === 0) {
|
|
1629
|
+
return null;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
const latest = index[index.length - 1];
|
|
1633
|
+
const snapshot = await readVersionSnapshot(ws, latest.versionId);
|
|
1634
|
+
if (!snapshot) {
|
|
1635
|
+
return null;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
return { indexEntry: latest, snapshot };
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function renderBulletList(items) {
|
|
1642
|
+
const list = Array.isArray(items) ? items : [];
|
|
1643
|
+
if (list.length === 0) {
|
|
1644
|
+
return ['- 待补充'].join('\n');
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
return list.map((item) => `- ${item}`).join('\n');
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
function renderFlowDoc(snapshot) {
|
|
1651
|
+
const { scenarios } = snapshot.sections;
|
|
1652
|
+
const productFlow = buildDiagramArtifact(snapshot, { type: 'product-flow' });
|
|
1653
|
+
const mermaid = renderDiagramMermaidFromModel('product-flow', productFlow.model);
|
|
1654
|
+
return `# 流程\n\n## 主流程\n\n${renderBulletList(scenarios.primaryFlows)}\n\n## Mermaid 流程图\n\n\`\`\`mermaid\n${mermaid}\n\`\`\`\n\n## 边界情况\n\n${renderBulletList(scenarios.edgeCases)}\n\n## 失败模式\n\n${renderBulletList(scenarios.failureModes)}\n`;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function renderRolesDoc(snapshot) {
|
|
1658
|
+
const { users, typeSpecific } = snapshot.sections;
|
|
1659
|
+
const roleFields = typeSpecific.fields ?? {};
|
|
1660
|
+
const extraLines = Object.entries(roleFields)
|
|
1661
|
+
.map(([key, value]) => `- ${key}: ${Array.isArray(value) ? value.join(', ') : value ?? '待补充'}`)
|
|
1662
|
+
.join('\n') || '- 待补充';
|
|
1663
|
+
|
|
1664
|
+
return `# 角色\n\n## 用户\n\n- 主要用户:\n${renderBulletList(users.primaryUsers)}\n\n- 次要用户:\n${renderBulletList(users.secondaryUsers)}\n\n- 相关方:\n${renderBulletList(users.stakeholders)}\n\n## 类型专项\n\n${extraLines}\n`;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function renderHandoffDoc(snapshot) {
|
|
1668
|
+
const { handoff } = snapshot.sections;
|
|
1669
|
+
return `# 交接\n\n- 版本: ${snapshot.versionId}\n- 产品类型: ${snapshot.productType ?? '未分类'}\n- 模板包: ${snapshot.templatePack}\n- Digest: ${snapshot.digest}\n- 负责人: ${handoff.owner}\n- 下一步: ${handoff.nextStep}\n- 目标系统: ${handoff.targetSystem}\n`;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
|
|
1673
|
+
export {
|
|
1674
|
+
appendDecision,
|
|
1675
|
+
appendOpenQuestions,
|
|
1676
|
+
appendProgress,
|
|
1677
|
+
appendVerification,
|
|
1678
|
+
appendWorkflowEvent,
|
|
1679
|
+
buildClarificationPlan,
|
|
1680
|
+
buildClarificationState,
|
|
1681
|
+
buildWorkflowTaskGraph,
|
|
1682
|
+
CAPTURE_SOURCES,
|
|
1683
|
+
coerceCapturedValue,
|
|
1684
|
+
computeWorkspaceDigest,
|
|
1685
|
+
CORE_TEMPLATE_FILES,
|
|
1686
|
+
deriveGateLabels,
|
|
1687
|
+
detectWorkspaceScenario,
|
|
1688
|
+
ensureWorkspaceSkeleton,
|
|
1689
|
+
extractMarkdownSection,
|
|
1690
|
+
FIELD_PATH_TO_STATE_KEY,
|
|
1691
|
+
isSupportedProductType,
|
|
1692
|
+
loadLatestVersionSnapshot,
|
|
1693
|
+
loadWorkspace,
|
|
1694
|
+
migrateWorkspaceSkeleton,
|
|
1695
|
+
normalizeVersionId,
|
|
1696
|
+
readVersionIndex,
|
|
1697
|
+
readVersionSnapshot,
|
|
1698
|
+
renderFlowDoc,
|
|
1699
|
+
renderHandoffDoc,
|
|
1700
|
+
renderRolesDoc,
|
|
1701
|
+
resolveActiveTemplatePack,
|
|
1702
|
+
resolveCurrentProductType,
|
|
1703
|
+
validateWorkspace,
|
|
1704
|
+
writeVersionIndex,
|
|
1705
|
+
writeVersionSnapshot
|
|
1706
|
+
};
|