@openprd/cli 0.1.1 → 0.1.9
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 +43 -69
- package/.openprd/README_EN.md +84 -0
- package/.openprd/benchmarks/index.md +7 -0
- package/.openprd/benchmarks/sources.yaml +25 -3
- package/.openprd/discovery/config.json +16 -2
- package/.openprd/engagements/active/flows.md +19 -14
- package/.openprd/engagements/active/handoff.md +11 -4
- package/.openprd/engagements/active/prd.md +99 -71
- package/.openprd/engagements/active/review.html +4 -4
- package/.openprd/engagements/active/roles.md +9 -8
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +4 -4
- package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +18 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +78 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +129 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +41 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +14 -0
- package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +49 -0
- package/.openprd/knowledge/index.json +44 -4
- package/.openprd/reviews/v0001.html +195 -129
- package/.openprd/reviews/v0002.html +1150 -0
- package/.openprd/reviews/v0003.html +1150 -0
- package/.openprd/reviews/v0004.html +1150 -0
- package/.openprd/reviews/v0005.html +1150 -0
- package/.openprd/standards/config.json +12 -9
- package/.openprd/state/changes.json +17 -2
- package/.openprd/state/current.json +399 -63
- package/.openprd/state/release-ledger.json +387 -0
- package/.openprd/state/version-index.json +52 -0
- package/.openprd/state/versions/v0002.json +264 -0
- package/.openprd/state/versions/v0002.md +183 -0
- package/.openprd/state/versions/v0003.json +269 -0
- package/.openprd/state/versions/v0003.md +188 -0
- package/.openprd/state/versions/v0004.json +274 -0
- package/.openprd/state/versions/v0004.md +193 -0
- package/.openprd/state/versions/v0005.json +299 -0
- package/.openprd/state/versions/v0005.md +189 -0
- package/.openprd/templates/agent/intake.md +5 -4
- package/.openprd/templates/b2b/intake.md +5 -4
- package/.openprd/templates/base/intake.md +10 -4
- package/.openprd/templates/company/README.md +9 -7
- package/.openprd/templates/company/README_EN.md +12 -0
- package/.openprd/templates/consumer/intake.md +5 -4
- package/.openprd/templates/industry/README.md +12 -10
- package/.openprd/templates/industry/README_EN.md +18 -0
- package/.openprd/templates/project/README.md +11 -9
- package/.openprd/templates/project/README_EN.md +16 -0
- package/.openprd/templates/session/README.md +11 -9
- package/.openprd/templates/session/README_EN.md +16 -0
- package/AGENTS.md +12 -8
- package/README.md +419 -438
- package/README_CN.md +4 -578
- package/README_EN.md +870 -0
- package/docs/assets/openprd-requirement-routing-en.png +0 -0
- package/docs/assets/openprd-requirement-routing-en.svg +102 -0
- package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.svg +102 -0
- package/package.json +6 -2
- package/scripts/dev-check-wrapup-copy.mjs +110 -0
- package/scripts/openprd-github-release-notes.mjs +99 -0
- package/scripts/quality-perf-check.mjs +203 -0
- package/skills/openprd-benchmark-router/SKILL.md +1 -0
- package/skills/openprd-benchmark-router/references/benchmark-sources.md +1 -0
- package/skills/openprd-benchmark-router/references/source-policy.md +2 -0
- package/skills/openprd-discovery-loop/SKILL.md +2 -2
- package/skills/openprd-harness/SKILL.md +47 -25
- package/skills/openprd-harness/references/workflow-gates.md +15 -0
- package/skills/openprd-quality/SKILL.md +11 -5
- package/skills/openprd-requirement-intake/SKILL.md +31 -20
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -6
- package/skills/openprd-requirement-intake/references/routing-rubric.md +10 -2
- package/skills/openprd-router/SKILL.md +2 -2
- package/skills/openprd-shared/SKILL.md +51 -23
- package/skills/openprd-standards/SKILL.md +2 -1
- package/src/agent-integration.js +271 -71
- package/src/benchmark/constants.js +107 -0
- package/src/benchmark/operations.js +235 -0
- package/src/benchmark/registry.js +64 -0
- package/src/benchmark/render.js +115 -0
- package/src/benchmark/source.js +617 -0
- package/src/benchmark/storage.js +121 -0
- package/src/benchmark/verify.js +235 -0
- package/src/benchmark.js +50 -851
- package/src/change-summary.js +339 -0
- package/src/cli/args.js +67 -6
- package/src/cli/basic-print.js +365 -0
- package/src/cli/benchmark-print.js +91 -0
- package/src/cli/change-print.js +221 -0
- package/src/cli/doctor-print.js +268 -0
- package/src/cli/growth-print.js +176 -0
- package/src/cli/print.js +73 -1384
- package/src/cli/quality-print.js +284 -0
- package/src/cli/run-print.js +297 -0
- package/src/cli/shared-print.js +127 -0
- package/src/cli/workflow-print.js +195 -0
- package/src/codex-hook-runner-template.mjs +659 -124
- package/src/codex-runtime.js +324 -0
- package/src/dev-standards.js +178 -5
- package/src/diagram-core.js +5 -5
- package/src/discovery.js +2 -1
- package/src/execution-strategy.js +369 -0
- package/src/fleet.js +4 -0
- package/src/github-release.js +156 -0
- package/src/growth.js +311 -13
- package/src/html-artifact-utils.js +25 -0
- package/src/html-artifacts.js +157 -1596
- package/src/knowledge.js +1321 -76
- package/src/language-policy.js +2 -112
- package/src/learning-html-artifact.js +1031 -0
- package/src/learning-review.js +3 -2
- package/src/loop.js +280 -9
- package/src/openprd.js +341 -38
- package/src/openspec/change-validate.js +0 -9
- package/src/openspec/execute.js +79 -3
- package/src/openspec/generate.js +33 -20
- package/src/openspec/tasks.js +33 -2
- package/src/prd-core.js +10 -9
- package/src/product-type-copy.js +69 -0
- package/src/quality-html-artifact.js +108 -9
- package/src/quality-learning.js +30 -0
- package/src/quality-visual-review.js +237 -0
- package/src/quality.js +329 -43
- package/src/registry-hygiene.js +54 -0
- package/src/release-ledger.js +413 -0
- package/src/review-presentation.js +12 -6
- package/src/run-harness.js +722 -48
- package/src/session-binding.js +40 -3
- package/src/session-registry.js +159 -0
- package/src/standards.js +5 -3
- package/src/test-strategy.js +386 -0
- package/src/visual-compare.js +915 -34
- package/src/work-unit-migration.js +5 -1
- package/src/workspace-core.js +343 -19
- package/src/workspace-workflow.js +538 -134
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import { canonicalReviewPath, defaultReviewArtifactPath, renderReviewArtifact, renderReviewEntryHtml, writeHtmlArtifact } from './html-artifacts.js';
|
|
5
5
|
import { summarizeSnapshot } from './prd-core.js';
|
|
6
|
+
import { buildReleaseLedgerSummary } from './release-ledger.js';
|
|
6
7
|
import { appendWorkflowEvent, buildWorkflowTaskGraph, loadWorkspace, readVersionIndex, readVersionSnapshot, writeVersionIndex, writeVersionSnapshot } from './workspace-core.js';
|
|
7
8
|
import { exists, readText, writeJson } from './fs-utils.js';
|
|
8
9
|
import { writeWorkUnitBinding } from './work-unit.js';
|
|
@@ -82,7 +83,10 @@ function buildBackfilledSnapshot(ws, snapshot, isLatest) {
|
|
|
82
83
|
async function writeReviewBundle(ws, snapshot, isLatest) {
|
|
83
84
|
const activeReviewArtifact = defaultReviewArtifactPath(ws);
|
|
84
85
|
const reviewPath = canonicalReviewPath(ws, snapshot.versionId);
|
|
85
|
-
await writeHtmlArtifact(reviewPath, renderReviewArtifact({
|
|
86
|
+
await writeHtmlArtifact(reviewPath, renderReviewArtifact({
|
|
87
|
+
snapshot,
|
|
88
|
+
projectRelease: buildReleaseLedgerSummary(ws.data.releaseLedger),
|
|
89
|
+
}));
|
|
86
90
|
|
|
87
91
|
if (isLatest) {
|
|
88
92
|
await writeHtmlArtifact(activeReviewArtifact, renderReviewEntryHtml({
|
package/src/workspace-core.js
CHANGED
|
@@ -2,11 +2,14 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { buildSnapshotChangeSummary } from './change-summary.js';
|
|
6
|
+
import { formatProductTypeDisplay, formatTemplatePackDisplay } from './product-type-copy.js';
|
|
5
7
|
import { buildDiagramArtifact, renderDiagramMermaidFromModel } from './diagram-core.js';
|
|
6
|
-
import { analyzePrdSnapshot, buildPrdSnapshot, getRequiredFieldDescriptors, renderPrdMarkdown, summarizeSnapshot } from './prd-core.js';
|
|
8
|
+
import { analyzePrdSnapshot, buildPrdSnapshot, formatVersionId, getRequiredFieldDescriptors, renderPrdMarkdown, summarizeSnapshot } from './prd-core.js';
|
|
9
|
+
import { normalizeReleaseLedger } from './release-ledger.js';
|
|
7
10
|
import { appendJsonl, appendText, cjoin, exists, readJson, readText, readYaml, stringifyYaml, writeJson, writeText, writeYaml } from './fs-utils.js';
|
|
8
11
|
import { OPENSPEC_TASK_MAX_ITEMS_PER_FILE } from './openspec/constants.js';
|
|
9
|
-
import { checkStandardsWorkspace } from './standards.js';
|
|
12
|
+
import { checkStandardsWorkspace, STANDARD_DOCS } from './standards.js';
|
|
10
13
|
import { timestamp } from './time.js';
|
|
11
14
|
|
|
12
15
|
const PACKAGE_ROOT = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
@@ -15,6 +18,7 @@ const REQUIRED_PRODUCT_TYPES = ['consumer', 'b2b', 'agent'];
|
|
|
15
18
|
const REQUIRED_SECTIONS = ['meta', 'problem', 'users', 'goals', 'scope', 'scenarios', 'requirements', 'constraints', 'risks', 'handoff'];
|
|
16
19
|
const CORE_TEMPLATE_FILES = [
|
|
17
20
|
'README.md',
|
|
21
|
+
'README_EN.md',
|
|
18
22
|
'config.yaml',
|
|
19
23
|
'schema/prd.schema.yaml',
|
|
20
24
|
'schema/diagram-architecture.schema.yaml',
|
|
@@ -31,9 +35,13 @@ const CORE_TEMPLATE_FILES = [
|
|
|
31
35
|
'templates/agent/prd.md',
|
|
32
36
|
'templates/agent/intake.md',
|
|
33
37
|
'templates/company/README.md',
|
|
38
|
+
'templates/company/README_EN.md',
|
|
34
39
|
'templates/industry/README.md',
|
|
40
|
+
'templates/industry/README_EN.md',
|
|
35
41
|
'templates/project/README.md',
|
|
42
|
+
'templates/project/README_EN.md',
|
|
36
43
|
'templates/session/README.md',
|
|
44
|
+
'templates/session/README_EN.md',
|
|
37
45
|
'standards/config.json',
|
|
38
46
|
'standards/file-manual-template.md',
|
|
39
47
|
'standards/folder-readme-template.md',
|
|
@@ -51,6 +59,7 @@ const CORE_TEMPLATE_FILES = [
|
|
|
51
59
|
];
|
|
52
60
|
const WORKSPACE_SEED_REFRESH_FILES = [
|
|
53
61
|
'README.md',
|
|
62
|
+
'README_EN.md',
|
|
54
63
|
'schema/prd.schema.yaml',
|
|
55
64
|
'schema/diagram-architecture.schema.yaml',
|
|
56
65
|
'schema/diagram-product-flow.schema.yaml',
|
|
@@ -66,9 +75,13 @@ const WORKSPACE_SEED_REFRESH_FILES = [
|
|
|
66
75
|
'templates/agent/prd.md',
|
|
67
76
|
'templates/agent/intake.md',
|
|
68
77
|
'templates/company/README.md',
|
|
78
|
+
'templates/company/README_EN.md',
|
|
69
79
|
'templates/industry/README.md',
|
|
80
|
+
'templates/industry/README_EN.md',
|
|
70
81
|
'templates/project/README.md',
|
|
82
|
+
'templates/project/README_EN.md',
|
|
71
83
|
'templates/session/README.md',
|
|
84
|
+
'templates/session/README_EN.md',
|
|
72
85
|
'standards/file-manual-template.md',
|
|
73
86
|
'standards/folder-readme-template.md',
|
|
74
87
|
];
|
|
@@ -112,6 +125,162 @@ const DEFAULT_DISCOVERY_CONFIG = {
|
|
|
112
125
|
},
|
|
113
126
|
};
|
|
114
127
|
|
|
128
|
+
const WORKSPACE_SCENARIO_IGNORED_ENTRIES = new Set(['.openprd', '.DS_Store', '.git', '.omx']);
|
|
129
|
+
const WORKSPACE_SCENARIO_BOOTSTRAP_FILE_MARKERS = new Map([
|
|
130
|
+
['AGENTS.md', 'OPENPRD:AGENTS:START'],
|
|
131
|
+
['CLAUDE.md', 'OPENPRD:CLAUDE:START'],
|
|
132
|
+
]);
|
|
133
|
+
const OPENPRD_INSTALL_MANIFEST_PATH = path.join('.openprd', 'harness', 'install-manifest.json');
|
|
134
|
+
const OPENPRD_HARNESS_DIR = cjoin('.openprd', 'harness');
|
|
135
|
+
const OPENPRD_HARNESS_REQUIREMENT_GATE = cjoin(OPENPRD_HARNESS_DIR, 'requirement-gate.json');
|
|
136
|
+
const OPENPRD_HARNESS_REQUIREMENT_GATES_DIR = cjoin(OPENPRD_HARNESS_DIR, 'requirement-gates');
|
|
137
|
+
const OPENPRD_HARNESS_SESSION_BINDINGS_DIR = cjoin(OPENPRD_HARNESS_DIR, 'session-bindings');
|
|
138
|
+
const OPENPRD_HARNESS_SESSION_STATES_DIR = cjoin(OPENPRD_HARNESS_DIR, 'session-states');
|
|
139
|
+
|
|
140
|
+
function normalizeLaneSessionId(sessionId) {
|
|
141
|
+
const text = String(sessionId ?? '').trim();
|
|
142
|
+
return text || null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sessionLaneFileName(sessionId) {
|
|
146
|
+
const normalized = normalizeLaneSessionId(sessionId);
|
|
147
|
+
if (!normalized) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
return `${normalized.replace(/[^A-Za-z0-9._-]/g, '_')}.json`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function sessionCurrentStatePath(projectRoot, sessionId) {
|
|
154
|
+
const fileName = sessionLaneFileName(sessionId);
|
|
155
|
+
if (!fileName) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return cjoin(projectRoot, OPENPRD_HARNESS_SESSION_STATES_DIR, fileName);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function sessionRequirementGatePath(projectRoot, sessionId) {
|
|
162
|
+
const fileName = sessionLaneFileName(sessionId);
|
|
163
|
+
if (!fileName) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
return cjoin(projectRoot, OPENPRD_HARNESS_REQUIREMENT_GATES_DIR, fileName);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function sessionBindingPath(projectRoot, sessionId) {
|
|
170
|
+
const fileName = sessionLaneFileName(sessionId);
|
|
171
|
+
if (!fileName) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return cjoin(projectRoot, OPENPRD_HARNESS_SESSION_BINDINGS_DIR, fileName);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function readActiveRequirementLane(projectRoot, workspaceCurrentState = null, options = {}) {
|
|
178
|
+
const legacyGate = options.legacyGate ?? await readJson(cjoin(projectRoot, OPENPRD_HARNESS_REQUIREMENT_GATE)).catch(() => null);
|
|
179
|
+
const explicitSessionId = normalizeLaneSessionId(options.sessionId);
|
|
180
|
+
const activeSessionId = explicitSessionId
|
|
181
|
+
?? normalizeLaneSessionId(legacyGate?.active ? legacyGate.sessionId : null)
|
|
182
|
+
?? normalizeLaneSessionId(workspaceCurrentState?.laneSessionId);
|
|
183
|
+
if (!activeSessionId) {
|
|
184
|
+
return {
|
|
185
|
+
sessionId: null,
|
|
186
|
+
scope: 'workspace',
|
|
187
|
+
gate: legacyGate?.active ? legacyGate : null,
|
|
188
|
+
binding: null,
|
|
189
|
+
sessionState: null,
|
|
190
|
+
currentStatePath: null,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const [scopedGate, binding, sessionState] = await Promise.all([
|
|
194
|
+
readJson(sessionRequirementGatePath(projectRoot, activeSessionId)).catch(() => null),
|
|
195
|
+
readJson(sessionBindingPath(projectRoot, activeSessionId)).catch(() => null),
|
|
196
|
+
readJson(sessionCurrentStatePath(projectRoot, activeSessionId)).catch(() => null),
|
|
197
|
+
]);
|
|
198
|
+
return {
|
|
199
|
+
sessionId: activeSessionId,
|
|
200
|
+
scope: 'session',
|
|
201
|
+
gate: scopedGate ?? (legacyGate?.active ? legacyGate : null),
|
|
202
|
+
binding,
|
|
203
|
+
sessionState,
|
|
204
|
+
currentStatePath: sessionCurrentStatePath(projectRoot, activeSessionId),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function materializePersistedCurrentState(currentState, sessionId = null) {
|
|
209
|
+
const normalized = normalizeLaneSessionId(sessionId);
|
|
210
|
+
return {
|
|
211
|
+
...(currentState ?? {}),
|
|
212
|
+
laneScope: normalized ? 'session' : 'workspace',
|
|
213
|
+
laneSessionId: normalized,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function persistWorkspaceCurrentState(ws, currentState, options = {}) {
|
|
218
|
+
const sessionId = normalizeLaneSessionId(options.sessionId ?? ws.data.currentSessionId);
|
|
219
|
+
const stored = materializePersistedCurrentState(currentState, sessionId);
|
|
220
|
+
if (sessionId) {
|
|
221
|
+
const scopedPath = ws.paths.sessionCurrentState ?? sessionCurrentStatePath(ws.projectRoot, sessionId);
|
|
222
|
+
await writeJson(scopedPath, stored);
|
|
223
|
+
}
|
|
224
|
+
if (!sessionId || options.writeWorkspaceMirror !== false) {
|
|
225
|
+
await writeJson(ws.paths.workspaceCurrentState ?? ws.paths.currentState, stored);
|
|
226
|
+
}
|
|
227
|
+
return stored;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function buildCurrentStateSnapshot(ws, currentState, versionIndex = []) {
|
|
231
|
+
const state = currentState ?? {};
|
|
232
|
+
return buildPrdSnapshot(ws, {
|
|
233
|
+
...state,
|
|
234
|
+
versionNumber: state.prdVersion ?? (versionIndex.at(-1)?.versionNumber ?? 0),
|
|
235
|
+
versionId: state.prdVersion > 0
|
|
236
|
+
? formatVersionId(state.prdVersion)
|
|
237
|
+
: (versionIndex.at(-1)?.versionId ?? 'v0000'),
|
|
238
|
+
productType: state.productType ?? resolveCurrentProductType(ws),
|
|
239
|
+
templatePack: state.templatePack ?? resolveActiveTemplatePack(ws),
|
|
240
|
+
status: state.status ?? 'draft',
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function loadCurrentLaneSnapshot(ws, options = {}) {
|
|
245
|
+
const versionIndex = await readVersionIndex(ws);
|
|
246
|
+
const currentState = ws.data.currentState ?? {};
|
|
247
|
+
const fallbackToLatest = options.fallbackToLatest !== false;
|
|
248
|
+
const candidateVersionIds = [
|
|
249
|
+
currentState.reviewStatus?.versionId,
|
|
250
|
+
currentState.latestVersionId,
|
|
251
|
+
currentState.versionId,
|
|
252
|
+
ws.data.sessionBinding?.versionId,
|
|
253
|
+
].map((value) => normalizeVersionId(value)).filter(Boolean);
|
|
254
|
+
for (const versionId of candidateVersionIds) {
|
|
255
|
+
const snapshot = await readVersionSnapshot(ws, versionId);
|
|
256
|
+
if (snapshot) {
|
|
257
|
+
return {
|
|
258
|
+
snapshot,
|
|
259
|
+
indexEntry: versionIndex.find((entry) => normalizeVersionId(entry.versionId) === versionId) ?? null,
|
|
260
|
+
source: ws.data.currentSessionId ? 'session-version' : 'workspace-version',
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (Object.keys(currentState).length > 0) {
|
|
265
|
+
return {
|
|
266
|
+
snapshot: buildCurrentStateSnapshot(ws, currentState, versionIndex),
|
|
267
|
+
indexEntry: null,
|
|
268
|
+
source: ws.data.currentSessionId ? 'session-draft' : 'workspace-draft',
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (!fallbackToLatest) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const latest = await loadLatestVersionSnapshot(ws);
|
|
275
|
+
if (latest) {
|
|
276
|
+
return {
|
|
277
|
+
...latest,
|
|
278
|
+
source: ws.data.currentSessionId ? 'workspace-latest-fallback' : 'workspace-latest',
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
115
284
|
function formatMarkdownLines(lines) {
|
|
116
285
|
return lines.filter(Boolean).map((line) => `- ${line}`).join('\n');
|
|
117
286
|
}
|
|
@@ -770,6 +939,9 @@ async function migrateWorkspaceSkeleton(projectRoot, options = {}) {
|
|
|
770
939
|
const typeSpecificBlock = extractMarkdownSection(seedPrd, '## 类型专项模块') || seedPrd;
|
|
771
940
|
await ensureActiveFileContains(projectRoot, 'engagements/active/intake.md', '我们要解决什么问题?', seedIntake, changes);
|
|
772
941
|
await ensureActiveFileContains(projectRoot, 'engagements/active/prd.md', '类型专项模块', typeSpecificBlock, changes);
|
|
942
|
+
await ensureHeadingFile(projectRoot, 'engagements/active/flows.md', '# 流程', '# 流程\n\n- 已初始化 OpenPrd 流程说明。\n', changes);
|
|
943
|
+
await ensureHeadingFile(projectRoot, 'engagements/active/roles.md', '# 角色', '# 角色\n\n- 已初始化 OpenPrd 角色说明。\n', changes);
|
|
944
|
+
await ensureHeadingFile(projectRoot, 'engagements/active/handoff.md', '# 交接', '# 交接\n\n- 已初始化 OpenPrd 交接摘要。\n', changes);
|
|
773
945
|
await ensureHeadingFile(projectRoot, 'engagements/active/decision-log.md', '# 决策记录', '# 决策记录\n\n- 已初始化 OpenPrd 决策跟踪。\n', changes);
|
|
774
946
|
await ensureHeadingFile(projectRoot, 'engagements/active/open-questions.md', '# 开放问题', '# 开放问题\n\n- 已初始化 OpenPrd 问题跟踪。\n', changes);
|
|
775
947
|
await ensureHeadingFile(projectRoot, 'engagements/active/progress.md', '# 进度', '# 进度\n\n- 已初始化 OpenPrd 进度跟踪。\n', changes);
|
|
@@ -824,7 +996,7 @@ async function migrateWorkspaceSkeleton(projectRoot, options = {}) {
|
|
|
824
996
|
};
|
|
825
997
|
}
|
|
826
998
|
|
|
827
|
-
async function loadWorkspace(projectRoot) {
|
|
999
|
+
async function loadWorkspace(projectRoot, options = {}) {
|
|
828
1000
|
const workspaceRoot = cjoin(projectRoot, '.openprd');
|
|
829
1001
|
const paths = {
|
|
830
1002
|
workspaceRoot,
|
|
@@ -886,7 +1058,13 @@ async function loadWorkspace(projectRoot) {
|
|
|
886
1058
|
stateDir: cjoin(workspaceRoot, 'state'),
|
|
887
1059
|
versionsDir: cjoin(workspaceRoot, 'state', 'versions'),
|
|
888
1060
|
versionIndex: cjoin(workspaceRoot, 'state', 'version-index.json'),
|
|
1061
|
+
releaseLedger: cjoin(workspaceRoot, 'state', 'release-ledger.json'),
|
|
889
1062
|
currentState: cjoin(workspaceRoot, 'state', 'current.json'),
|
|
1063
|
+
workspaceCurrentState: cjoin(workspaceRoot, 'state', 'current.json'),
|
|
1064
|
+
sessionStatesDir: cjoin(workspaceRoot, 'harness', 'session-states'),
|
|
1065
|
+
sessionBindingsDir: cjoin(workspaceRoot, 'harness', 'session-bindings'),
|
|
1066
|
+
requirementGate: cjoin(workspaceRoot, 'harness', 'requirement-gate.json'),
|
|
1067
|
+
requirementGatesDir: cjoin(workspaceRoot, 'harness', 'requirement-gates'),
|
|
890
1068
|
freezeState: cjoin(workspaceRoot, 'state', 'freeze.json'),
|
|
891
1069
|
taskGraph: cjoin(workspaceRoot, 'state', 'task-graph.json'),
|
|
892
1070
|
eventsLog: cjoin(workspaceRoot, 'state', 'events.jsonl'),
|
|
@@ -899,15 +1077,27 @@ async function loadWorkspace(projectRoot) {
|
|
|
899
1077
|
openspecHandoffMd: cjoin(workspaceRoot, 'exports', 'openspec', 'handoff.md'),
|
|
900
1078
|
};
|
|
901
1079
|
|
|
1080
|
+
const workspaceCurrentState = await readJson(paths.workspaceCurrentState).catch(() => null);
|
|
1081
|
+
const lane = await readActiveRequirementLane(projectRoot, workspaceCurrentState, { sessionId: options.sessionId });
|
|
1082
|
+
if (lane.currentStatePath) {
|
|
1083
|
+
paths.sessionCurrentState = lane.currentStatePath;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
902
1086
|
const data = {
|
|
903
1087
|
config: await readYaml(paths.config).catch(() => null),
|
|
904
1088
|
schema: await readYaml(paths.schema).catch(() => null),
|
|
905
1089
|
diagramArchitectureSchema: await readYaml(paths.diagramArchitectureSchema).catch(() => null),
|
|
906
1090
|
diagramProductFlowSchema: await readYaml(paths.diagramProductFlowSchema).catch(() => null),
|
|
907
1091
|
manifest: await readYaml(paths.manifest).catch(() => null),
|
|
908
|
-
currentState:
|
|
1092
|
+
currentState: lane.sessionId ? (lane.sessionState ?? {}) : workspaceCurrentState,
|
|
1093
|
+
workspaceCurrentState,
|
|
1094
|
+
currentSessionId: lane.sessionId,
|
|
1095
|
+
currentStateScope: lane.scope,
|
|
1096
|
+
sessionBinding: lane.binding,
|
|
1097
|
+
activeRequirementGate: lane.gate,
|
|
909
1098
|
freezeState: await readJson(paths.freezeState).catch(() => null),
|
|
910
1099
|
versionIndex: await readJson(paths.versionIndex).catch(() => []),
|
|
1100
|
+
releaseLedger: normalizeReleaseLedger(await readJson(paths.releaseLedger).catch(() => null)),
|
|
911
1101
|
learningIndex: await readJson(paths.learningIndex).catch(() => null),
|
|
912
1102
|
learningCurrent: await readJson(paths.learningCurrent).catch(() => null),
|
|
913
1103
|
};
|
|
@@ -1031,7 +1221,7 @@ function listMissing(actual, expected) {
|
|
|
1031
1221
|
return expected.filter((item) => !actualSet.has(item));
|
|
1032
1222
|
}
|
|
1033
1223
|
|
|
1034
|
-
async function validateWorkspace(projectRoot) {
|
|
1224
|
+
async function validateWorkspace(projectRoot, options = {}) {
|
|
1035
1225
|
const report = {
|
|
1036
1226
|
valid: true,
|
|
1037
1227
|
errors: [],
|
|
@@ -1268,7 +1458,11 @@ async function validateWorkspace(projectRoot) {
|
|
|
1268
1458
|
report.errors.push('state/events.jsonl is missing');
|
|
1269
1459
|
}
|
|
1270
1460
|
|
|
1271
|
-
const standards = await checkStandardsWorkspace(projectRoot, {
|
|
1461
|
+
const standards = await checkStandardsWorkspace(projectRoot, {
|
|
1462
|
+
optional: true,
|
|
1463
|
+
sourceManuals: options.sourceManuals,
|
|
1464
|
+
docsContent: options.docsContent,
|
|
1465
|
+
});
|
|
1272
1466
|
if (!standards.skipped) {
|
|
1273
1467
|
if (standards.errors.length > 0) {
|
|
1274
1468
|
report.valid = false;
|
|
@@ -1300,6 +1494,18 @@ async function validateWorkspace(projectRoot) {
|
|
|
1300
1494
|
}
|
|
1301
1495
|
}
|
|
1302
1496
|
|
|
1497
|
+
const releaseLedger = ws.data.releaseLedger ?? normalizeReleaseLedger(null);
|
|
1498
|
+
const currentRelease = releaseLedger.currentVersion
|
|
1499
|
+
? releaseLedger.versions.find((entry) => entry.version === releaseLedger.currentVersion) ?? null
|
|
1500
|
+
: null;
|
|
1501
|
+
if (releaseLedger.enabled && !releaseLedger.currentVersion) {
|
|
1502
|
+
report.warnings.push('state/release-ledger.json 已启用,但还没有设置当前项目版本号');
|
|
1503
|
+
}
|
|
1504
|
+
if (releaseLedger.currentVersion && !currentRelease) {
|
|
1505
|
+
report.valid = false;
|
|
1506
|
+
report.errors.push(`state/release-ledger.json 当前版本 ${releaseLedger.currentVersion} 缺少对应条目`);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1303
1509
|
report.checks.push({ name: 'workspace', ok: true });
|
|
1304
1510
|
report.checks.push({ name: 'schema', ok: true });
|
|
1305
1511
|
report.checks.push({ name: 'manifest', ok: true });
|
|
@@ -1424,6 +1630,114 @@ function coerceCapturedValue(pathString, rawValue, append = false) {
|
|
|
1424
1630
|
return text;
|
|
1425
1631
|
}
|
|
1426
1632
|
|
|
1633
|
+
function normalizeScenarioRelativePath(relativePath) {
|
|
1634
|
+
return String(relativePath ?? '').replaceAll('\\', '/');
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
async function readScenarioManagedPaths(projectRoot) {
|
|
1638
|
+
const manifestPath = path.join(projectRoot, OPENPRD_INSTALL_MANIFEST_PATH);
|
|
1639
|
+
const manifest = await readJson(manifestPath).catch(() => null);
|
|
1640
|
+
const managedPaths = new Set();
|
|
1641
|
+
for (const entry of manifest?.managedFiles ?? []) {
|
|
1642
|
+
if (typeof entry?.path !== 'string') {
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
const normalized = normalizeScenarioRelativePath(entry.path.trim());
|
|
1646
|
+
if (normalized) {
|
|
1647
|
+
managedPaths.add(normalized);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return managedPaths;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
async function listScenarioDirectoryFiles(projectRoot, relativeDir) {
|
|
1654
|
+
const normalizedDir = normalizeScenarioRelativePath(relativeDir);
|
|
1655
|
+
const absoluteDir = path.join(projectRoot, normalizedDir);
|
|
1656
|
+
const entries = await fs.readdir(absoluteDir, { withFileTypes: true }).catch(() => []);
|
|
1657
|
+
const files = [];
|
|
1658
|
+
for (const entry of entries) {
|
|
1659
|
+
if (entry.name === '.DS_Store') {
|
|
1660
|
+
continue;
|
|
1661
|
+
}
|
|
1662
|
+
const childRelative = normalizeScenarioRelativePath(path.posix.join(normalizedDir, entry.name));
|
|
1663
|
+
if (entry.isDirectory()) {
|
|
1664
|
+
files.push(...await listScenarioDirectoryFiles(projectRoot, childRelative));
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
files.push(childRelative);
|
|
1668
|
+
}
|
|
1669
|
+
return files;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
async function directoryContainsOnlyManagedBootstrap(projectRoot, relativeDir, managedPaths) {
|
|
1673
|
+
const files = await listScenarioDirectoryFiles(projectRoot, relativeDir);
|
|
1674
|
+
return files.length > 0 && files.every((file) => managedPaths.has(file));
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
async function docsDirectoryIsOpenPrdBootstrap(projectRoot) {
|
|
1678
|
+
const docsRoot = path.join(projectRoot, 'docs');
|
|
1679
|
+
const docsEntries = await fs.readdir(docsRoot, { withFileTypes: true }).catch(() => null);
|
|
1680
|
+
if (!docsEntries) {
|
|
1681
|
+
return false;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
const visibleDocsEntries = docsEntries.filter((entry) => entry.name !== '.DS_Store');
|
|
1685
|
+
if (visibleDocsEntries.length !== 1 || visibleDocsEntries[0].name !== 'basic' || !visibleDocsEntries[0].isDirectory()) {
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
const basicRoot = path.join(docsRoot, 'basic');
|
|
1690
|
+
const basicEntries = await fs.readdir(basicRoot, { withFileTypes: true }).catch(() => null);
|
|
1691
|
+
if (!basicEntries) {
|
|
1692
|
+
return false;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
const visibleBasicEntries = basicEntries.filter((entry) => entry.name !== '.DS_Store');
|
|
1696
|
+
if (visibleBasicEntries.length !== STANDARD_DOCS.length || visibleBasicEntries.some((entry) => !entry.isFile())) {
|
|
1697
|
+
return false;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
const expectedDocs = new Map(STANDARD_DOCS.map((doc) => [doc.fileName, doc.body.trim()]));
|
|
1701
|
+
for (const entry of visibleBasicEntries) {
|
|
1702
|
+
const expectedBody = expectedDocs.get(entry.name);
|
|
1703
|
+
if (!expectedBody) {
|
|
1704
|
+
return false;
|
|
1705
|
+
}
|
|
1706
|
+
const actualBody = await readText(path.join(basicRoot, entry.name)).catch(() => null);
|
|
1707
|
+
if (actualBody?.trim() !== expectedBody) {
|
|
1708
|
+
return false;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
return true;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
async function shouldIgnoreWorkspaceScenarioEntry(projectRoot, entry, managedPaths) {
|
|
1716
|
+
if (WORKSPACE_SCENARIO_IGNORED_ENTRIES.has(entry.name)) {
|
|
1717
|
+
return true;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
if (entry.isFile() && WORKSPACE_SCENARIO_BOOTSTRAP_FILE_MARKERS.has(entry.name)) {
|
|
1721
|
+
const marker = WORKSPACE_SCENARIO_BOOTSTRAP_FILE_MARKERS.get(entry.name);
|
|
1722
|
+
const content = await readText(path.join(projectRoot, entry.name)).catch(() => null);
|
|
1723
|
+
return Boolean(content?.includes(marker));
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
if (!entry.isDirectory()) {
|
|
1727
|
+
return false;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
if (entry.name === 'docs') {
|
|
1731
|
+
return docsDirectoryIsOpenPrdBootstrap(projectRoot);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
if (['.codex', '.claude', '.cursor'].includes(entry.name)) {
|
|
1735
|
+
return directoryContainsOnlyManagedBootstrap(projectRoot, entry.name, managedPaths);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
return false;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1427
1741
|
async function detectWorkspaceScenario(projectRoot, ws, versionIndex = []) {
|
|
1428
1742
|
const currentStatus = ws.data.currentState?.status ?? 'unknown';
|
|
1429
1743
|
if (versionIndex.length > 0 || ['synthesized', 'frozen', 'handed_off'].includes(currentStatus)) {
|
|
@@ -1436,13 +1750,14 @@ async function detectWorkspaceScenario(projectRoot, ws, versionIndex = []) {
|
|
|
1436
1750
|
}
|
|
1437
1751
|
|
|
1438
1752
|
const entries = await fs.readdir(projectRoot, { withFileTypes: true }).catch(() => []);
|
|
1439
|
-
const
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
if (entry
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1753
|
+
const managedPaths = await readScenarioManagedPaths(projectRoot);
|
|
1754
|
+
const meaningfulEntries = [];
|
|
1755
|
+
for (const entry of entries) {
|
|
1756
|
+
if (await shouldIgnoreWorkspaceScenarioEntry(projectRoot, entry, managedPaths)) {
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
meaningfulEntries.push(entry);
|
|
1760
|
+
}
|
|
1446
1761
|
|
|
1447
1762
|
if (meaningfulEntries.length === 0) {
|
|
1448
1763
|
return {
|
|
@@ -1503,13 +1818,13 @@ function buildClarificationState({ snapshot, analysis, basePlan, scenario, captu
|
|
|
1503
1818
|
{
|
|
1504
1819
|
id: 'existing-project-goal',
|
|
1505
1820
|
label: '已有项目范围',
|
|
1506
|
-
prompt: '
|
|
1821
|
+
prompt: '基于当前已有项目,这一轮主要是为谁改善什么问题,第一版准备先落哪一块最有价值?',
|
|
1507
1822
|
reason: 'kickoff',
|
|
1508
1823
|
},
|
|
1509
1824
|
{
|
|
1510
1825
|
id: 'reuse-boundary',
|
|
1511
1826
|
label: '复用边界',
|
|
1512
|
-
prompt: '
|
|
1827
|
+
prompt: '哪些既有数据、流程、体验或业务结果不能被破坏,哪些区域仍可调整?',
|
|
1513
1828
|
reason: 'kickoff',
|
|
1514
1829
|
},
|
|
1515
1830
|
...missingQuestions,
|
|
@@ -1552,9 +1867,9 @@ function buildClarificationPlan(snapshot, analysis) {
|
|
|
1552
1867
|
const mustAsk = analysis.missingFields.filter((field) => USER_CLARIFICATION_PATHS.has(field.path));
|
|
1553
1868
|
const derived = analysis.missingFields.filter((field) => !USER_CLARIFICATION_PATHS.has(field.path));
|
|
1554
1869
|
const kickoffQuestions = [
|
|
1555
|
-
{ id: 'project-overview', label: '
|
|
1556
|
-
{ id: '
|
|
1557
|
-
{ id: '
|
|
1870
|
+
{ id: 'project-overview', label: '项目轮廓', prompt: '这件事主要是给谁用的,他们会在什么场景下最先感受到价值?' },
|
|
1871
|
+
{ id: 'first-slice', label: '首版切片', prompt: '如果先做第一版,最值得先让用户完成什么关键动作?' },
|
|
1872
|
+
{ id: 'guardrails', label: '保护项', prompt: '这轮明确先不做什么,或者哪些既有体验、流程和业务结果不能被影响?' },
|
|
1558
1873
|
];
|
|
1559
1874
|
return {
|
|
1560
1875
|
totalRequiredFields: analysis.totalRequiredFields,
|
|
@@ -1666,7 +1981,11 @@ function renderRolesDoc(snapshot) {
|
|
|
1666
1981
|
|
|
1667
1982
|
function renderHandoffDoc(snapshot) {
|
|
1668
1983
|
const { handoff } = snapshot.sections;
|
|
1669
|
-
|
|
1984
|
+
const changeSummary = buildSnapshotChangeSummary(snapshot, { limit: 4 });
|
|
1985
|
+
const summarySection = changeSummary.markdown
|
|
1986
|
+
? `\n## 变化摘要\n\n${changeSummary.markdown}\n`
|
|
1987
|
+
: '';
|
|
1988
|
+
return `# 交接\n\n- 本次内容: ${snapshot.title}\n- 产品场景: ${formatProductTypeDisplay(snapshot.productType, { fallback: '待确认' })}\n- 场景模板: ${formatTemplatePackDisplay(snapshot.templatePack, { fallback: '待确认' })}\n- 负责人: ${handoff.owner}\n- 下一步: ${handoff.nextStep}\n- 交接去向: ${handoff.targetSystem}\n${summarySection}`;
|
|
1670
1989
|
}
|
|
1671
1990
|
|
|
1672
1991
|
|
|
@@ -1675,6 +1994,7 @@ export {
|
|
|
1675
1994
|
appendOpenQuestions,
|
|
1676
1995
|
appendProgress,
|
|
1677
1996
|
appendVerification,
|
|
1997
|
+
buildCurrentStateSnapshot,
|
|
1678
1998
|
appendWorkflowEvent,
|
|
1679
1999
|
buildClarificationPlan,
|
|
1680
2000
|
buildClarificationState,
|
|
@@ -1689,11 +2009,15 @@ export {
|
|
|
1689
2009
|
extractMarkdownSection,
|
|
1690
2010
|
FIELD_PATH_TO_STATE_KEY,
|
|
1691
2011
|
isSupportedProductType,
|
|
2012
|
+
loadCurrentLaneSnapshot,
|
|
1692
2013
|
loadLatestVersionSnapshot,
|
|
1693
2014
|
loadWorkspace,
|
|
1694
2015
|
migrateWorkspaceSkeleton,
|
|
2016
|
+
normalizeLaneSessionId,
|
|
1695
2017
|
normalizeVersionId,
|
|
2018
|
+
persistWorkspaceCurrentState,
|
|
1696
2019
|
readVersionIndex,
|
|
2020
|
+
readActiveRequirementLane,
|
|
1697
2021
|
readVersionSnapshot,
|
|
1698
2022
|
renderFlowDoc,
|
|
1699
2023
|
renderHandoffDoc,
|