@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,240 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
buildReviewPresentationFeedback,
|
|
7
|
+
canonicalReviewPath,
|
|
8
|
+
defaultReviewArtifactPath,
|
|
9
|
+
renderReviewArtifact,
|
|
10
|
+
renderReviewEntryHtml,
|
|
11
|
+
writeHtmlArtifact,
|
|
12
|
+
} from './html-artifacts.js';
|
|
13
|
+
|
|
14
|
+
export const REVIEW_PRESENTATION_TEMPLATE = {
|
|
15
|
+
diagram: {
|
|
16
|
+
type: 'map',
|
|
17
|
+
note: '默认用关系图;只有确认为线性流程时改为 flow,并用 flowEdges 明确哪些节点有箭头。',
|
|
18
|
+
},
|
|
19
|
+
mapNodes: {
|
|
20
|
+
problem: { title: '问题定义', text: '30字内说明问题' },
|
|
21
|
+
goal: { title: '目标', text: '30字内说明目标' },
|
|
22
|
+
scope: { title: '范围', text: '30字内说明范围' },
|
|
23
|
+
flow: { title: '流程', text: '30字内说明主流程' },
|
|
24
|
+
risk: { title: '风险', text: '30字内说明风险' },
|
|
25
|
+
},
|
|
26
|
+
flowNodes: [
|
|
27
|
+
{ id: 'step1', text: '30字内说明第1步' },
|
|
28
|
+
{ id: 'step2', text: '30字内说明第2步' },
|
|
29
|
+
{ id: 'step3', text: '30字内说明第3步' },
|
|
30
|
+
],
|
|
31
|
+
flowEdges: [
|
|
32
|
+
{ from: 'step1', to: 'step2' },
|
|
33
|
+
{ from: 'step2', to: 'step3' },
|
|
34
|
+
],
|
|
35
|
+
panels: {
|
|
36
|
+
flow: [
|
|
37
|
+
{ summary: '15字内标签', detail: '一句话说明主流程' },
|
|
38
|
+
],
|
|
39
|
+
function: [
|
|
40
|
+
{ summary: '15字内标签', detail: '一句话说明功能约束' },
|
|
41
|
+
],
|
|
42
|
+
guardrail: [
|
|
43
|
+
{ summary: '15字内标签', detail: '一句话说明业务护栏' },
|
|
44
|
+
],
|
|
45
|
+
risk: [
|
|
46
|
+
{ summary: '15字内标签', detail: '一句话说明风险问题' },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function buildReviewPresentationTemplatePayload() {
|
|
52
|
+
return {
|
|
53
|
+
intent: 'Agent 先按这个模板写 reviewPresentation,再用本脚本校验;不要让 HTML 截断文案。',
|
|
54
|
+
presentationTemplate: REVIEW_PRESENTATION_TEMPLATE,
|
|
55
|
+
presentationContract: buildReviewPresentationFeedback({ sections: {} }).contract,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function reviewPresentationWorkspace(projectRoot, options = {}) {
|
|
60
|
+
const resolvedProjectRoot = path.resolve(projectRoot);
|
|
61
|
+
const snapshotPath = await resolveReviewPresentationSnapshotPath(resolvedProjectRoot, options.version);
|
|
62
|
+
const snapshot = await readJson(snapshotPath);
|
|
63
|
+
let presentationSource = snapshot.reviewPresentation ? 'snapshot' : null;
|
|
64
|
+
|
|
65
|
+
if (options.presentationPath) {
|
|
66
|
+
const presentationPath = path.resolve(options.presentationPath);
|
|
67
|
+
snapshot.reviewPresentation = normalizeReviewPresentationInput(await readJson(presentationPath));
|
|
68
|
+
delete snapshot.reviewPresentationMeta;
|
|
69
|
+
presentationSource = presentationPath;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (options.write && !options.presentationPath) {
|
|
73
|
+
throw new Error('--write 需要配合 --presentation,避免误写没有更新的展示文案。');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const feedback = buildReviewPresentationFeedback(snapshot);
|
|
77
|
+
if (options.write && options.presentationPath && feedback.violations.length === 0) {
|
|
78
|
+
snapshot.reviewPresentationMeta = buildReviewPresentationMeta({
|
|
79
|
+
presentation: snapshot.reviewPresentation,
|
|
80
|
+
feedback,
|
|
81
|
+
source: presentationSource,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const gate = getReviewPresentationGate(snapshot, feedback);
|
|
85
|
+
const result = {
|
|
86
|
+
ok: gate.ok,
|
|
87
|
+
versionId: snapshot.versionId,
|
|
88
|
+
title: snapshot.title,
|
|
89
|
+
snapshotPath,
|
|
90
|
+
presentationSource,
|
|
91
|
+
presentationContract: feedback.contract,
|
|
92
|
+
presentationFeedback: feedback.violations,
|
|
93
|
+
reviewPresentationGate: gate,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (options.write) {
|
|
97
|
+
if (!gate.ok) {
|
|
98
|
+
result.writeBlocked = true;
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
await fs.writeFile(snapshotPath, `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
102
|
+
result.written = snapshotPath;
|
|
103
|
+
const reviewFiles = await renderValidatedReviewPresentation(resolvedProjectRoot, snapshot);
|
|
104
|
+
result.reviewPath = reviewFiles.canonicalReview;
|
|
105
|
+
result.reviewEntryPath = reviewFiles.activeReviewEntry;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getReviewPresentationGate(snapshot, feedback = buildReviewPresentationFeedback(snapshot)) {
|
|
112
|
+
const errors = [];
|
|
113
|
+
const presentation = snapshot?.reviewPresentation;
|
|
114
|
+
const meta = snapshot?.reviewPresentationMeta;
|
|
115
|
+
if (!presentation || typeof presentation !== 'object' || Array.isArray(presentation)) {
|
|
116
|
+
errors.push('缺少已由脚本写入的 reviewPresentation。先运行 openprd review-presentation . --template,再填写 presentation JSON。');
|
|
117
|
+
}
|
|
118
|
+
if (feedback.violations.length > 0) {
|
|
119
|
+
errors.push(`reviewPresentation 仍有 ${feedback.violations.length} 个超限或格式问题。请按 presentationFeedback 中的 jsonPath 重写后再写入。`);
|
|
120
|
+
}
|
|
121
|
+
if (!meta || typeof meta !== 'object' || Array.isArray(meta)) {
|
|
122
|
+
errors.push('缺少 reviewPresentationMeta。必须通过 openprd review-presentation --presentation <json> --write --fail-on-violation 写入,不能手工改快照。');
|
|
123
|
+
} else {
|
|
124
|
+
const expectedPresentationHash = hashStableJson(presentation ?? null);
|
|
125
|
+
const expectedViolationsHash = hashStableJson(feedback.violations);
|
|
126
|
+
if (meta.presentationHash !== expectedPresentationHash) {
|
|
127
|
+
errors.push('reviewPresentationMeta.presentationHash 与当前 reviewPresentation 不一致,请重新运行 review-presentation 写入。');
|
|
128
|
+
}
|
|
129
|
+
if (meta.violationsHash !== expectedViolationsHash) {
|
|
130
|
+
errors.push('reviewPresentationMeta.violationsHash 与当前校验结果不一致,请重新运行 review-presentation 写入。');
|
|
131
|
+
}
|
|
132
|
+
if (!meta.validatedAt) {
|
|
133
|
+
errors.push('reviewPresentationMeta.validatedAt 缺失,请重新运行 review-presentation 写入。');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
ok: errors.length === 0,
|
|
138
|
+
errors,
|
|
139
|
+
violations: feedback.violations,
|
|
140
|
+
requiredCommand: 'openprd review-presentation . --presentation <review-presentation.json> --write --fail-on-violation',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function assertReviewPresentationReady(snapshot) {
|
|
145
|
+
const gate = getReviewPresentationGate(snapshot);
|
|
146
|
+
if (gate.ok) return gate;
|
|
147
|
+
const details = gate.violations.slice(0, 6).map((item) => {
|
|
148
|
+
const pathHint = item.jsonPath ? `${item.jsonPath}: ` : '';
|
|
149
|
+
const sizeHint = item.maxChars ? ` 当前 ${item.currentChars} 字,限制 ${item.maxChars} 字。` : '';
|
|
150
|
+
return `- ${pathHint}${item.action}${sizeHint}`;
|
|
151
|
+
});
|
|
152
|
+
throw new Error([
|
|
153
|
+
'OpenPrd 已阻止生成可确认 review.html:评审展示文案必须先通过 review-presentation 脚本写入。',
|
|
154
|
+
...gate.errors.map((error) => `- ${error}`),
|
|
155
|
+
...details,
|
|
156
|
+
].join('\n'));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildReviewPresentationMeta({ presentation, feedback, source }) {
|
|
160
|
+
return {
|
|
161
|
+
validatedAt: new Date().toISOString(),
|
|
162
|
+
source: source ?? 'snapshot',
|
|
163
|
+
presentationHash: hashStableJson(presentation ?? null),
|
|
164
|
+
violationsHash: hashStableJson(feedback.violations),
|
|
165
|
+
validator: 'openprd review-presentation',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function hashStableJson(value) {
|
|
170
|
+
return crypto.createHash('sha256').update(stableStringify(value)).digest('hex');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function stableStringify(value) {
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
176
|
+
}
|
|
177
|
+
if (value && typeof value === 'object') {
|
|
178
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
|
|
179
|
+
}
|
|
180
|
+
return JSON.stringify(value);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function renderValidatedReviewPresentation(projectRoot, snapshot) {
|
|
184
|
+
const workspaceRoot = path.join(projectRoot, '.openprd');
|
|
185
|
+
const canonicalReview = canonicalReviewPath({ workspaceRoot }, snapshot.versionId);
|
|
186
|
+
const activeReviewEntry = defaultReviewArtifactPath({ workspaceRoot });
|
|
187
|
+
await writeHtmlArtifact(canonicalReview, renderReviewArtifact({ snapshot }));
|
|
188
|
+
|
|
189
|
+
const versionIndexPath = path.join(workspaceRoot, 'state', 'version-index.json');
|
|
190
|
+
const versionIndex = await readJson(versionIndexPath);
|
|
191
|
+
const latestVersionId = versionIndex.at(-1)?.versionId;
|
|
192
|
+
if (latestVersionId === snapshot.versionId) {
|
|
193
|
+
await writeHtmlArtifact(activeReviewEntry, renderReviewEntryHtml({
|
|
194
|
+
entryPath: activeReviewEntry,
|
|
195
|
+
reviewPath: canonicalReview,
|
|
196
|
+
title: `${snapshot.title} / 评审入口`,
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
canonicalReview,
|
|
201
|
+
activeReviewEntry: latestVersionId === snapshot.versionId ? activeReviewEntry : null,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function normalizeReviewPresentationVersionId(version) {
|
|
206
|
+
if (!version) return null;
|
|
207
|
+
const text = `${version}`.trim().toLowerCase();
|
|
208
|
+
const digits = text.replace(/^v/u, '');
|
|
209
|
+
return /^\d+$/u.test(digits) ? `v${digits.padStart(4, '0')}` : text;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function readJson(filePath) {
|
|
213
|
+
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function resolveReviewPresentationSnapshotPath(projectRoot, version) {
|
|
217
|
+
const workspaceRoot = path.join(projectRoot, '.openprd');
|
|
218
|
+
const versionId = normalizeReviewPresentationVersionId(version);
|
|
219
|
+
if (versionId) {
|
|
220
|
+
return path.join(workspaceRoot, 'state', 'versions', `${versionId}.json`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const versionIndexPath = path.join(workspaceRoot, 'state', 'version-index.json');
|
|
224
|
+
const versionIndex = await readJson(versionIndexPath);
|
|
225
|
+
const latestVersionId = versionIndex.at(-1)?.versionId;
|
|
226
|
+
if (!latestVersionId) {
|
|
227
|
+
throw new Error(`未找到 PRD 版本索引: ${versionIndexPath}`);
|
|
228
|
+
}
|
|
229
|
+
return path.join(workspaceRoot, 'state', 'versions', `${latestVersionId}.json`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function normalizeReviewPresentationInput(value) {
|
|
233
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
234
|
+
throw new Error('presentation JSON 必须是对象。');
|
|
235
|
+
}
|
|
236
|
+
if (value.reviewPresentation && typeof value.reviewPresentation === 'object' && !Array.isArray(value.reviewPresentation)) {
|
|
237
|
+
return value.reviewPresentation;
|
|
238
|
+
}
|
|
239
|
+
return value;
|
|
240
|
+
}
|