@openprd/cli 0.1.1 → 0.1.8
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 +344 -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 +399 -438
- package/README_CN.md +4 -578
- package/README_EN.md +850 -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 +46 -24
- package/skills/openprd-harness/references/workflow-gates.md +15 -0
- package/skills/openprd-quality/SKILL.md +10 -4
- 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 +265 -65
- 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 +639 -117
- 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 +1176 -75
- 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
|
Binary file
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="980" viewBox="0 0 1600 980" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">OpenPrd requirement routing</title>
|
|
3
|
+
<desc id="desc">A diagram showing how OpenPrd routes incoming requests into quick fixes, existing-flow improvements, or new feature and workflow work.</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
6
|
+
<stop offset="0" stop-color="#fff8f1"/>
|
|
7
|
+
<stop offset="0.45" stop-color="#f8fbff"/>
|
|
8
|
+
<stop offset="1" stop-color="#f6f2ff"/>
|
|
9
|
+
</linearGradient>
|
|
10
|
+
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
|
11
|
+
<feDropShadow dx="0" dy="16" stdDeviation="18" flood-color="#0f172a" flood-opacity="0.12"/>
|
|
12
|
+
</filter>
|
|
13
|
+
<marker id="arrow" viewBox="0 0 12 12" refX="11" refY="6" markerWidth="10" markerHeight="10" orient="auto">
|
|
14
|
+
<path d="M2 2 L10 6 L2 10 Z" fill="#475569"/>
|
|
15
|
+
</marker>
|
|
16
|
+
<style>
|
|
17
|
+
.eyebrow { font: 700 20px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; letter-spacing: 2px; fill: #2563eb; }
|
|
18
|
+
.title { font: 800 56px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #0f172a; }
|
|
19
|
+
.subtitle { font: 500 26px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #334155; }
|
|
20
|
+
.card-title { font: 800 28px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
|
21
|
+
.card-body { font: 500 20px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #475569; }
|
|
22
|
+
.pill { font: 700 18px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #ffffff; }
|
|
23
|
+
.footer-title { font: 700 22px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #0f172a; }
|
|
24
|
+
.footer-body { font: 500 18px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #475569; }
|
|
25
|
+
.line { fill: none; stroke: #64748b; stroke-width: 4; marker-end: url(#arrow); }
|
|
26
|
+
.dash { fill: none; stroke: #94a3b8; stroke-width: 3; stroke-dasharray: 10 10; }
|
|
27
|
+
</style>
|
|
28
|
+
</defs>
|
|
29
|
+
|
|
30
|
+
<rect width="1600" height="980" fill="url(#bg)"/>
|
|
31
|
+
<path d="M0 126 H1600 M0 292 H1600 M0 458 H1600 M0 624 H1600 M0 790 H1600 M190 0 V980 M390 0 V980 M590 0 V980 M790 0 V980 M990 0 V980 M1190 0 V980 M1390 0 V980" stroke="#dbeafe" stroke-width="1" opacity="0.65"/>
|
|
32
|
+
|
|
33
|
+
<g transform="translate(92 78)">
|
|
34
|
+
<text class="eyebrow" x="0" y="0">OPENPRD REQUIREMENT INTAKE</text>
|
|
35
|
+
<text class="title" x="0" y="78">How OpenPrd routes a request</text>
|
|
36
|
+
<text class="subtitle" x="2" y="126">First clarify the goal, situation, and risks, then decide whether to act directly, share a mini-plan, or move into a full change flow.</text>
|
|
37
|
+
</g>
|
|
38
|
+
|
|
39
|
+
<g transform="translate(96 220)" filter="url(#shadow)">
|
|
40
|
+
<rect width="420" height="280" rx="22" fill="#ffffff" stroke="#fdba74" stroke-width="2"/>
|
|
41
|
+
<rect x="30" y="28" width="104" height="38" rx="19" fill="#f97316"/>
|
|
42
|
+
<text class="pill" x="62" y="53">01</text>
|
|
43
|
+
<text class="card-title" x="30" y="108">Quick fix</text>
|
|
44
|
+
<text class="card-body" x="30" y="152">The issue is already clear</text>
|
|
45
|
+
<text class="card-body" x="30" y="184">The blast radius is small</text>
|
|
46
|
+
<text class="card-body" x="30" y="216">Success is easy to verify</text>
|
|
47
|
+
<text class="card-body" x="30" y="248">Usually handled directly</text>
|
|
48
|
+
<text class="card-body" x="30" y="280">Then reported back clearly</text>
|
|
49
|
+
</g>
|
|
50
|
+
|
|
51
|
+
<g transform="translate(590 220)" filter="url(#shadow)">
|
|
52
|
+
<rect width="420" height="280" rx="22" fill="#ffffff" stroke="#c4b5fd" stroke-width="2"/>
|
|
53
|
+
<rect x="30" y="28" width="104" height="38" rx="19" fill="#7c3aed"/>
|
|
54
|
+
<text class="pill" x="62" y="53">02</text>
|
|
55
|
+
<text class="card-title" x="30" y="108">Existing-flow improvement</text>
|
|
56
|
+
<text class="card-body" x="30" y="152">The goal is clear</text>
|
|
57
|
+
<text class="card-body" x="30" y="184">But several screens, states,</text>
|
|
58
|
+
<text class="card-body" x="30" y="216">or user actions are involved</text>
|
|
59
|
+
<text class="card-body" x="30" y="248">Start with a plain-language</text>
|
|
60
|
+
<text class="card-body" x="30" y="280">mini-plan, then continue</text>
|
|
61
|
+
</g>
|
|
62
|
+
|
|
63
|
+
<g transform="translate(1084 220)" filter="url(#shadow)">
|
|
64
|
+
<rect width="420" height="280" rx="22" fill="#ffffff" stroke="#86efac" stroke-width="2"/>
|
|
65
|
+
<rect x="30" y="28" width="104" height="38" rx="19" fill="#16a34a"/>
|
|
66
|
+
<text class="pill" x="62" y="53">03</text>
|
|
67
|
+
<text class="card-title" x="30" y="108">New feature / workflow</text>
|
|
68
|
+
<text class="card-body" x="30" y="152">The surface area is bigger</text>
|
|
69
|
+
<text class="card-body" x="30" y="184">More roles are involved</text>
|
|
70
|
+
<text class="card-body" x="30" y="216">Or business risk is unclear</text>
|
|
71
|
+
<text class="card-body" x="30" y="248">Clarify the first version</text>
|
|
72
|
+
<text class="card-body" x="30" y="280">before the full plan path</text>
|
|
73
|
+
</g>
|
|
74
|
+
|
|
75
|
+
<path class="line" d="M516 360 H578"/>
|
|
76
|
+
<path class="line" d="M1010 360 H1072"/>
|
|
77
|
+
|
|
78
|
+
<g transform="translate(220 566)" filter="url(#shadow)">
|
|
79
|
+
<rect width="1160" height="156" rx="24" fill="#ffffff" stroke="#dbeafe" stroke-width="2"/>
|
|
80
|
+
<text class="footer-title" x="40" y="54">Start with the business problem</text>
|
|
81
|
+
<text class="footer-body" x="40" y="90">Who is stuck, in what situation, and what should be solved first.</text>
|
|
82
|
+
<path class="dash" d="M438 78 H538"/>
|
|
83
|
+
<text class="footer-title" x="576" y="54">OpenPrd summarizes first</text>
|
|
84
|
+
<text class="footer-body" x="576" y="90">It brings the goal, scope, non-goals, and risks into one view.</text>
|
|
85
|
+
<path class="dash" d="M952 78 H1052"/>
|
|
86
|
+
<text class="footer-title" x="1088" y="54">Then it picks the right rhythm</text>
|
|
87
|
+
<text class="footer-body" x="1088" y="90">This avoids both heavy process too early and premature implementation.</text>
|
|
88
|
+
</g>
|
|
89
|
+
|
|
90
|
+
<g transform="translate(120 792)" filter="url(#shadow)">
|
|
91
|
+
<rect width="1360" height="110" rx="24" fill="#ffffff" stroke="#dbeafe" stroke-width="2"/>
|
|
92
|
+
<circle cx="72" cy="55" r="28" fill="#fb923c"/>
|
|
93
|
+
<circle cx="460" cy="55" r="28" fill="#38bdf8"/>
|
|
94
|
+
<circle cx="864" cy="55" r="28" fill="#34d399"/>
|
|
95
|
+
<text class="footer-title" x="118" y="44">Consumer requests</text>
|
|
96
|
+
<text class="footer-body" x="118" y="74">Focus on the user moment, first felt value, and return motivation.</text>
|
|
97
|
+
<text class="footer-title" x="506" y="44">B2B requests</text>
|
|
98
|
+
<text class="footer-body" x="506" y="74">Focus on who approves, who uses, and who must drive rollout.</text>
|
|
99
|
+
<text class="footer-title" x="910" y="44">Agent requests</text>
|
|
100
|
+
<text class="footer-body" x="910" y="74">Focus on what can be automated and where human backup is required.</text>
|
|
101
|
+
</g>
|
|
102
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="980" viewBox="0 0 1600 980" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">OpenPrd 需求分流节奏</title>
|
|
3
|
+
<desc id="desc">一张说明 OpenPrd 如何把用户需求分成快速修正、现有功能优化和新功能新流程方案的流程图。</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
6
|
+
<stop offset="0" stop-color="#fff8f1"/>
|
|
7
|
+
<stop offset="0.45" stop-color="#f8fbff"/>
|
|
8
|
+
<stop offset="1" stop-color="#f6f2ff"/>
|
|
9
|
+
</linearGradient>
|
|
10
|
+
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
|
11
|
+
<feDropShadow dx="0" dy="16" stdDeviation="18" flood-color="#0f172a" flood-opacity="0.12"/>
|
|
12
|
+
</filter>
|
|
13
|
+
<marker id="arrow" viewBox="0 0 12 12" refX="11" refY="6" markerWidth="10" markerHeight="10" orient="auto">
|
|
14
|
+
<path d="M2 2 L10 6 L2 10 Z" fill="#475569"/>
|
|
15
|
+
</marker>
|
|
16
|
+
<style>
|
|
17
|
+
.eyebrow { font: 700 20px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; letter-spacing: 2px; fill: #2563eb; }
|
|
18
|
+
.title { font: 800 56px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #0f172a; }
|
|
19
|
+
.subtitle { font: 500 26px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #334155; }
|
|
20
|
+
.card-title { font: 800 28px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
|
21
|
+
.card-body { font: 500 20px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #475569; }
|
|
22
|
+
.pill { font: 700 18px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #ffffff; }
|
|
23
|
+
.footer-title { font: 700 22px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #0f172a; }
|
|
24
|
+
.footer-body { font: 500 18px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #475569; }
|
|
25
|
+
.line { fill: none; stroke: #64748b; stroke-width: 4; marker-end: url(#arrow); }
|
|
26
|
+
.dash { fill: none; stroke: #94a3b8; stroke-width: 3; stroke-dasharray: 10 10; }
|
|
27
|
+
</style>
|
|
28
|
+
</defs>
|
|
29
|
+
|
|
30
|
+
<rect width="1600" height="980" fill="url(#bg)"/>
|
|
31
|
+
<path d="M0 126 H1600 M0 292 H1600 M0 458 H1600 M0 624 H1600 M0 790 H1600 M190 0 V980 M390 0 V980 M590 0 V980 M790 0 V980 M990 0 V980 M1190 0 V980 M1390 0 V980" stroke="#dbeafe" stroke-width="1" opacity="0.65"/>
|
|
32
|
+
|
|
33
|
+
<g transform="translate(92 78)">
|
|
34
|
+
<text class="eyebrow" x="0" y="0">OPENPRD REQUIREMENT INTAKE</text>
|
|
35
|
+
<text class="title" x="0" y="78">一句需求进来,OpenPrd 怎么接住</text>
|
|
36
|
+
<text class="subtitle" x="2" y="126">先帮你把目标、场景和风险说清楚,再决定是直接改、先给 mini-plan,还是进入完整方案。</text>
|
|
37
|
+
</g>
|
|
38
|
+
|
|
39
|
+
<g transform="translate(96 220)" filter="url(#shadow)">
|
|
40
|
+
<rect width="420" height="280" rx="22" fill="#ffffff" stroke="#fdba74" stroke-width="2"/>
|
|
41
|
+
<rect x="30" y="28" width="104" height="38" rx="19" fill="#f97316"/>
|
|
42
|
+
<text class="pill" x="62" y="53">01</text>
|
|
43
|
+
<text class="card-title" x="30" y="108">快速修正</text>
|
|
44
|
+
<text class="card-body" x="30" y="152">问题已经很明确</text>
|
|
45
|
+
<text class="card-body" x="30" y="184">影响小,验收也清楚</text>
|
|
46
|
+
<text class="card-body" x="30" y="216">通常直接处理</text>
|
|
47
|
+
<text class="card-body" x="30" y="248">做完告诉你改了什么</text>
|
|
48
|
+
<text class="card-body" x="30" y="280">以及怎么验证</text>
|
|
49
|
+
</g>
|
|
50
|
+
|
|
51
|
+
<g transform="translate(590 220)" filter="url(#shadow)">
|
|
52
|
+
<rect width="420" height="280" rx="22" fill="#ffffff" stroke="#c4b5fd" stroke-width="2"/>
|
|
53
|
+
<rect x="30" y="28" width="104" height="38" rx="19" fill="#7c3aed"/>
|
|
54
|
+
<text class="pill" x="62" y="53">02</text>
|
|
55
|
+
<text class="card-title" x="30" y="108">现有功能优化</text>
|
|
56
|
+
<text class="card-body" x="30" y="152">目标明确</text>
|
|
57
|
+
<text class="card-body" x="30" y="184">但会牵动几个页面</text>
|
|
58
|
+
<text class="card-body" x="30" y="216">状态或用户动作</text>
|
|
59
|
+
<text class="card-body" x="30" y="248">先用人话给 mini-plan</text>
|
|
60
|
+
<text class="card-body" x="30" y="280">方向对了再继续</text>
|
|
61
|
+
</g>
|
|
62
|
+
|
|
63
|
+
<g transform="translate(1084 220)" filter="url(#shadow)">
|
|
64
|
+
<rect width="420" height="280" rx="22" fill="#ffffff" stroke="#86efac" stroke-width="2"/>
|
|
65
|
+
<rect x="30" y="28" width="104" height="38" rx="19" fill="#16a34a"/>
|
|
66
|
+
<text class="pill" x="62" y="53">03</text>
|
|
67
|
+
<text class="card-title" x="30" y="108">新功能 / 新流程方案</text>
|
|
68
|
+
<text class="card-body" x="30" y="152">范围更大,角色更多</text>
|
|
69
|
+
<text class="card-body" x="30" y="184">或业务风险还不清楚</text>
|
|
70
|
+
<text class="card-body" x="30" y="216">先讲清用户场景</text>
|
|
71
|
+
<text class="card-body" x="30" y="248">第一版、不做什么</text>
|
|
72
|
+
<text class="card-body" x="30" y="280">再进入完整方案</text>
|
|
73
|
+
</g>
|
|
74
|
+
|
|
75
|
+
<path class="line" d="M516 360 H578"/>
|
|
76
|
+
<path class="line" d="M1010 360 H1072"/>
|
|
77
|
+
|
|
78
|
+
<g transform="translate(220 566)" filter="url(#shadow)">
|
|
79
|
+
<rect width="1160" height="156" rx="24" fill="#ffffff" stroke="#dbeafe" stroke-width="2"/>
|
|
80
|
+
<text class="footer-title" x="40" y="54">你只需要先说明业务问题</text>
|
|
81
|
+
<text class="footer-body" x="40" y="90">谁在什么场景下遇到什么问题,想先解决哪一块。</text>
|
|
82
|
+
<path class="dash" d="M420 78 H520"/>
|
|
83
|
+
<text class="footer-title" x="560" y="54">OpenPrd 先帮你归纳</text>
|
|
84
|
+
<text class="footer-body" x="560" y="90">先讲目标、范围、先不做什么,以及主要风险。</text>
|
|
85
|
+
<path class="dash" d="M936 78 H1036"/>
|
|
86
|
+
<text class="footer-title" x="1074" y="54">再选合适的推进节奏</text>
|
|
87
|
+
<text class="footer-body" x="1074" y="90">避免一上来就重流程,也避免没说清就直接开做。</text>
|
|
88
|
+
</g>
|
|
89
|
+
|
|
90
|
+
<g transform="translate(120 792)" filter="url(#shadow)">
|
|
91
|
+
<rect width="1360" height="110" rx="24" fill="#ffffff" stroke="#dbeafe" stroke-width="2"/>
|
|
92
|
+
<circle cx="72" cy="55" r="28" fill="#fb923c"/>
|
|
93
|
+
<circle cx="460" cy="55" r="28" fill="#38bdf8"/>
|
|
94
|
+
<circle cx="864" cy="55" r="28" fill="#34d399"/>
|
|
95
|
+
<text class="footer-title" x="118" y="44">个人用户场景</text>
|
|
96
|
+
<text class="footer-body" x="118" y="74">更关注什么时候会用、第一下有没有感受到价值。</text>
|
|
97
|
+
<text class="footer-title" x="506" y="44">团队 / 企业流程</text>
|
|
98
|
+
<text class="footer-body" x="506" y="74">更关注谁拍板、谁使用、谁要推进上线。</text>
|
|
99
|
+
<text class="footer-title" x="910" y="44">Agent 协作</text>
|
|
100
|
+
<text class="footer-body" x="910" y="74">更关注哪些能自动做、哪里必须人工兜底。</text>
|
|
101
|
+
</g>
|
|
102
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openprd/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "AI-native PRD workspace and lifecycle CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"AGENTS.md",
|
|
12
12
|
"CONTRIBUTING.md",
|
|
13
13
|
"README_CN.md",
|
|
14
|
+
"README_EN.md",
|
|
14
15
|
"SECURITY.md",
|
|
15
16
|
"bin",
|
|
16
17
|
"docs/assets",
|
|
@@ -49,8 +50,11 @@
|
|
|
49
50
|
},
|
|
50
51
|
"scripts": {
|
|
51
52
|
"start": "node ./bin/openprd.js",
|
|
53
|
+
"release:notes": "node scripts/openprd-github-release-notes.mjs",
|
|
52
54
|
"test": "node --test",
|
|
53
|
-
"test:smoke": "node --test --test-name-pattern \"quality requires current smoke evidence|run exposes hook-stable\""
|
|
55
|
+
"test:smoke": "node --test --test-name-pattern \"quality requires current smoke evidence|run exposes hook-stable\"",
|
|
56
|
+
"test:perf": "node scripts/quality-perf-check.mjs --mode normal",
|
|
57
|
+
"test:perf:extreme": "node scripts/quality-perf-check.mjs --mode extreme --fixture test/fixtures/release-ledger-extreme.json"
|
|
54
58
|
},
|
|
55
59
|
"dependencies": {
|
|
56
60
|
"sharp": "^0.34.5",
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
export const DEV_CHECK_WRAPUP_COPY_LIMIT = 20;
|
|
4
|
+
const WRAPUP_FIELDS = ['规模信号', '预警原因', '本次处理结果', '后续建议'];
|
|
5
|
+
const VALIDATE_USAGE = 'Usage: echo \'{"rows":[{"影响对象":"src/example.js","规模信号":"701 行(> 700 行/文件)","预警原因":"文件偏大,维护成本升","本次处理结果":"本轮小改,未扩职责","后续建议":"继续改前先拆小"}]}\' | node scripts/dev-check-wrapup-copy.mjs --validate';
|
|
6
|
+
|
|
7
|
+
function countChars(value) {
|
|
8
|
+
return Array.from(String(value ?? ''));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function validateCompactRows(rows = [], limit = DEV_CHECK_WRAPUP_COPY_LIMIT) {
|
|
12
|
+
for (const [index, row] of rows.entries()) {
|
|
13
|
+
const rowLabel = row?.影响对象 ? String(row.影响对象) : (row?.影响位置 ? String(row.影响位置) : `第 ${index + 1} 行`);
|
|
14
|
+
for (const field of WRAPUP_FIELDS) {
|
|
15
|
+
const value = row?.[field];
|
|
16
|
+
if (typeof value !== 'string') {
|
|
17
|
+
throw new Error(`后续建议校验失败:${rowLabel} 缺少字段“${field}”。${VALIDATE_USAGE}`);
|
|
18
|
+
}
|
|
19
|
+
const actual = countChars(value).length;
|
|
20
|
+
if (actual > limit) {
|
|
21
|
+
throw new Error(`后续建议校验失败:${rowLabel} 的“${field}”有 ${actual} 个字,超过 ${limit} 字上限。当前内容:“${value}”。请缩短后重试。`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatLineCount(value) {
|
|
28
|
+
return value === null || value === undefined ? '未知行数' : `${value} 行`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function thresholdText(file) {
|
|
32
|
+
const thresholds = file.thresholds ?? {};
|
|
33
|
+
const okMax = thresholds.okMax ?? 700;
|
|
34
|
+
const attentionMax = thresholds.attentionMax ?? 1500;
|
|
35
|
+
const lineText = formatLineCount(file.lineCount);
|
|
36
|
+
if (file.status === 'warning') {
|
|
37
|
+
return `${lineText}(> ${attentionMax} 行/文件)`;
|
|
38
|
+
}
|
|
39
|
+
if (file.status === 'attention') {
|
|
40
|
+
return `${lineText}(> ${okMax} 行/文件)`;
|
|
41
|
+
}
|
|
42
|
+
if (file.status === 'error') {
|
|
43
|
+
return `${lineText}(检查失败)`;
|
|
44
|
+
}
|
|
45
|
+
if (file.status === 'ok') {
|
|
46
|
+
return `${lineText}(范围内)`;
|
|
47
|
+
}
|
|
48
|
+
return lineText;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function reasonText(file) {
|
|
52
|
+
if (file.status === 'warning') return '文件太大,后续改动风险高';
|
|
53
|
+
if (file.status === 'attention') return '文件偏大,维护成本升';
|
|
54
|
+
if (file.status === 'error') return '检查失败,需先修复';
|
|
55
|
+
if (file.status === 'ok') return '暂无额外预警';
|
|
56
|
+
return '待补充';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function currentResultText(file) {
|
|
60
|
+
if (file.status === 'warning') return '这次先完成需求,暂不拆分';
|
|
61
|
+
if (file.status === 'attention') return '本轮小改,未扩职责';
|
|
62
|
+
if (file.status === 'error') return '本轮失败,未收口';
|
|
63
|
+
if (file.status === 'ok') return '本轮已回顾';
|
|
64
|
+
return '待确认';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function followUpText(file) {
|
|
68
|
+
if (file.status === 'warning') return '优先拆出独立职责';
|
|
69
|
+
if (file.status === 'attention') return '继续改前先拆小';
|
|
70
|
+
if (file.status === 'error') return '先修复后重跑';
|
|
71
|
+
if (file.status === 'ok') return '无需额外动作';
|
|
72
|
+
return '待确认';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildCompactDevCheckWrapUpRows(files = []) {
|
|
76
|
+
return files.map((file) => ({
|
|
77
|
+
规模信号: thresholdText(file),
|
|
78
|
+
预警原因: reasonText(file),
|
|
79
|
+
本次处理结果: currentResultText(file),
|
|
80
|
+
后续建议: followUpText(file),
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function readStdin() {
|
|
85
|
+
const chunks = [];
|
|
86
|
+
for await (const chunk of process.stdin) {
|
|
87
|
+
chunks.push(chunk);
|
|
88
|
+
}
|
|
89
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function main() {
|
|
93
|
+
const validateOnly = process.argv.includes('--validate');
|
|
94
|
+
const raw = await readStdin();
|
|
95
|
+
const payload = raw.trim() ? JSON.parse(raw) : {};
|
|
96
|
+
const rows = validateOnly
|
|
97
|
+
? (Array.isArray(payload.rows) ? payload.rows : [])
|
|
98
|
+
: buildCompactDevCheckWrapUpRows(Array.isArray(payload.files) ? payload.files : []);
|
|
99
|
+
validateCompactRows(rows);
|
|
100
|
+
process.stdout.write(JSON.stringify({
|
|
101
|
+
ok: true,
|
|
102
|
+
limit: DEV_CHECK_WRAPUP_COPY_LIMIT,
|
|
103
|
+
rows,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
main().catch((error) => {
|
|
108
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { buildGitHubReleasePayload, writeGitHubReleaseNotes } from '../src/github-release.js';
|
|
4
|
+
|
|
5
|
+
function printHelp() {
|
|
6
|
+
console.log([
|
|
7
|
+
'Usage: node scripts/openprd-github-release-notes.mjs [projectRoot] [options]',
|
|
8
|
+
'',
|
|
9
|
+
'Options:',
|
|
10
|
+
' --version <x.y.z> Expected project version. Defaults to package.json version.',
|
|
11
|
+
' --tag <tag> Git tag to publish, for example v0.1.23.',
|
|
12
|
+
' --title <text> Override the GitHub Release title.',
|
|
13
|
+
' --out <file> Write the rendered markdown to a file instead of stdout.',
|
|
14
|
+
' --json Print the structured payload as JSON.',
|
|
15
|
+
' --help Show this help message.',
|
|
16
|
+
].join('\n'));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const args = [...argv];
|
|
21
|
+
const result = {
|
|
22
|
+
projectRoot: '.',
|
|
23
|
+
version: '',
|
|
24
|
+
tag: '',
|
|
25
|
+
title: '',
|
|
26
|
+
out: '',
|
|
27
|
+
json: false,
|
|
28
|
+
help: false,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (args[0] && !args[0].startsWith('--')) {
|
|
32
|
+
result.projectRoot = args.shift();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
while (args.length > 0) {
|
|
36
|
+
const arg = args.shift();
|
|
37
|
+
if (arg === '--help') {
|
|
38
|
+
result.help = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (arg === '--json') {
|
|
42
|
+
result.json = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (['--version', '--tag', '--title', '--out'].includes(arg)) {
|
|
46
|
+
const value = args.shift();
|
|
47
|
+
if (!value) {
|
|
48
|
+
throw new Error(`Missing value for ${arg}.`);
|
|
49
|
+
}
|
|
50
|
+
result[arg.slice(2)] = value;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
result.projectRoot = path.resolve(result.projectRoot);
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
const args = parseArgs(process.argv.slice(2));
|
|
62
|
+
if (args.help) {
|
|
63
|
+
printHelp();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const options = {
|
|
68
|
+
version: args.version || undefined,
|
|
69
|
+
tag: args.tag || undefined,
|
|
70
|
+
title: args.title || undefined,
|
|
71
|
+
out: args.out || undefined,
|
|
72
|
+
};
|
|
73
|
+
const result = args.out
|
|
74
|
+
? await writeGitHubReleaseNotes(args.projectRoot, options)
|
|
75
|
+
: await buildGitHubReleasePayload(args.projectRoot, options);
|
|
76
|
+
|
|
77
|
+
if (!result.ok) {
|
|
78
|
+
console.error(result.error);
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (args.json) {
|
|
84
|
+
console.log(JSON.stringify(result, null, 2));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (args.out) {
|
|
89
|
+
console.log(result.out);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(result.markdown);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
main().catch((error) => {
|
|
97
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
import { performance } from 'node:perf_hooks';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
appendReleaseEntry,
|
|
10
|
+
buildReleaseChangeSummary,
|
|
11
|
+
buildReleaseLedgerSummary,
|
|
12
|
+
defaultReleaseLedger,
|
|
13
|
+
normalizeReleaseLedger,
|
|
14
|
+
setCurrentReleaseVersion,
|
|
15
|
+
} from '../src/release-ledger.js';
|
|
16
|
+
import { buildChangeSummaryFromEntries } from '../src/change-summary.js';
|
|
17
|
+
|
|
18
|
+
const MB = 1024 * 1024;
|
|
19
|
+
|
|
20
|
+
function parseArgs(argv) {
|
|
21
|
+
const options = {
|
|
22
|
+
mode: 'normal',
|
|
23
|
+
fixture: null,
|
|
24
|
+
};
|
|
25
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
26
|
+
const arg = argv[index];
|
|
27
|
+
if (arg === '--mode' && argv[index + 1]) {
|
|
28
|
+
options.mode = argv[index + 1];
|
|
29
|
+
index += 1;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (arg === '--fixture' && argv[index + 1]) {
|
|
33
|
+
options.fixture = argv[index + 1];
|
|
34
|
+
index += 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return options;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function percentile(values, ratio) {
|
|
41
|
+
if (!values.length) return 0;
|
|
42
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
43
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * ratio) - 1));
|
|
44
|
+
return sorted[index];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatNumber(value) {
|
|
48
|
+
return Number(value).toFixed(2);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isoTimestamp() {
|
|
52
|
+
return new Date().toISOString();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function readQualityBaseline(mode) {
|
|
56
|
+
const configPath = path.resolve('.openprd/quality/config.json');
|
|
57
|
+
const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
|
|
58
|
+
return mode === 'extreme'
|
|
59
|
+
? config.evalHarness?.projectBaseline?.extreme
|
|
60
|
+
: config.evalHarness?.projectBaseline?.normal;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readFixtureConfig(mode, fixturePath) {
|
|
64
|
+
if (mode !== 'extreme' || !fixturePath) {
|
|
65
|
+
return {
|
|
66
|
+
versionCount: 6,
|
|
67
|
+
itemsPerVersion: 18,
|
|
68
|
+
detailRepeats: 2,
|
|
69
|
+
iterations: 180,
|
|
70
|
+
limit: 12,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const fullPath = path.resolve(fixturePath);
|
|
74
|
+
const raw = JSON.parse(await fs.readFile(fullPath, 'utf8'));
|
|
75
|
+
return {
|
|
76
|
+
versionCount: Number(raw.versionCount ?? 24),
|
|
77
|
+
itemsPerVersion: Number(raw.itemsPerVersion ?? 36),
|
|
78
|
+
detailRepeats: Number(raw.detailRepeats ?? 6),
|
|
79
|
+
iterations: Number(raw.iterations ?? 140),
|
|
80
|
+
limit: Number(raw.limit ?? 16),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildSentence(versionIndex, itemIndex, detailRepeats) {
|
|
85
|
+
const verbs = ['新增', '修复', '优化', '调整', '移除'];
|
|
86
|
+
const verb = verbs[(versionIndex + itemIndex) % verbs.length];
|
|
87
|
+
const topic = `版本轨道变更 ${versionIndex + 1}-${itemIndex + 1}`;
|
|
88
|
+
const detail = Array.from({ length: detailRepeats }, (_, index) => `${topic} 说明片段 ${index + 1}`).join(',');
|
|
89
|
+
return `${verb}${topic},${detail}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildVersion(versionIndex) {
|
|
93
|
+
const major = 0;
|
|
94
|
+
const minor = Math.floor(versionIndex / 10) + 1;
|
|
95
|
+
const patch = (versionIndex % 10) + 1;
|
|
96
|
+
return `${major}.${minor}.${patch}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function buildLedger(config) {
|
|
100
|
+
let ledger = {
|
|
101
|
+
...defaultReleaseLedger(),
|
|
102
|
+
enabled: true,
|
|
103
|
+
};
|
|
104
|
+
for (let versionIndex = 0; versionIndex < config.versionCount; versionIndex += 1) {
|
|
105
|
+
const version = buildVersion(versionIndex);
|
|
106
|
+
({ ledger } = setCurrentReleaseVersion(ledger, version));
|
|
107
|
+
for (let itemIndex = 0; itemIndex < config.itemsPerVersion; itemIndex += 1) {
|
|
108
|
+
({ ledger } = appendReleaseEntry(ledger, buildSentence(versionIndex, itemIndex, config.detailRepeats), {
|
|
109
|
+
version,
|
|
110
|
+
source: {
|
|
111
|
+
kind: 'perf-fixture',
|
|
112
|
+
versionIndex,
|
|
113
|
+
itemIndex,
|
|
114
|
+
},
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return normalizeReleaseLedger(ledger);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function runBenchmark(ledger, config) {
|
|
122
|
+
const durations = [];
|
|
123
|
+
const heapSamples = [];
|
|
124
|
+
const startHeap = process.memoryUsage().heapUsed / MB;
|
|
125
|
+
for (let index = 0; index < config.iterations; index += 1) {
|
|
126
|
+
const startedAt = performance.now();
|
|
127
|
+
const summary = buildReleaseLedgerSummary(ledger);
|
|
128
|
+
const changeSummary = buildReleaseChangeSummary(ledger, {
|
|
129
|
+
version: summary.currentVersion,
|
|
130
|
+
limit: config.limit,
|
|
131
|
+
});
|
|
132
|
+
buildChangeSummaryFromEntries(changeSummary.items, { limit: config.limit });
|
|
133
|
+
durations.push(performance.now() - startedAt);
|
|
134
|
+
heapSamples.push(process.memoryUsage().heapUsed / MB);
|
|
135
|
+
}
|
|
136
|
+
const endHeap = process.memoryUsage().heapUsed / MB;
|
|
137
|
+
return {
|
|
138
|
+
iterations: durations.length,
|
|
139
|
+
p50Ms: percentile(durations, 0.5),
|
|
140
|
+
p95Ms: percentile(durations, 0.95),
|
|
141
|
+
maxMs: Math.max(...durations),
|
|
142
|
+
avgMs: durations.reduce((sum, value) => sum + value, 0) / durations.length,
|
|
143
|
+
startHeapMB: startHeap,
|
|
144
|
+
endHeapMB: endHeap,
|
|
145
|
+
peakHeapMB: Math.max(...heapSamples, endHeap),
|
|
146
|
+
heapDeltaMB: endHeap - startHeap,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function writeReport(mode, fixtureConfig, baseline, metrics) {
|
|
151
|
+
const reportDir = path.resolve('.openprd/harness/test-reports');
|
|
152
|
+
await fs.mkdir(reportDir, { recursive: true });
|
|
153
|
+
const reportPath = path.join(reportDir, `quality-${mode}-performance.md`);
|
|
154
|
+
const title = mode === 'extreme' ? 'EVO Extreme Performance Report' : 'EVO Normal Performance Report';
|
|
155
|
+
const lines = [
|
|
156
|
+
`# ${title}`,
|
|
157
|
+
'',
|
|
158
|
+
`- generatedAt: ${isoTimestamp()}`,
|
|
159
|
+
`- mode: ${mode}`,
|
|
160
|
+
`- performance: p50 ${formatNumber(metrics.p50Ms)}ms, p95 ${formatNumber(metrics.p95Ms)}ms, max ${formatNumber(metrics.maxMs)}ms`,
|
|
161
|
+
`- latency baseline: p95 <= ${baseline.apiLatencyMsP95Max}ms`,
|
|
162
|
+
`- memory baseline: peak <= ${baseline.memoryMBP95Max}MB`,
|
|
163
|
+
`- memory: start ${formatNumber(metrics.startHeapMB)}MB, peak ${formatNumber(metrics.peakHeapMB)}MB, delta ${formatNumber(metrics.heapDeltaMB)}MB`,
|
|
164
|
+
`- dataset: ${fixtureConfig.versionCount} versions x ${fixtureConfig.itemsPerVersion} items`,
|
|
165
|
+
mode === 'extreme'
|
|
166
|
+
? `- extreme: fixture-driven load passed with ${fixtureConfig.detailRepeats} repeated detail fragments per item`
|
|
167
|
+
: '- performance: baseline ledger summary path remained within project limits',
|
|
168
|
+
'',
|
|
169
|
+
];
|
|
170
|
+
await fs.writeFile(reportPath, `${lines.join('\n')}\n`, 'utf8');
|
|
171
|
+
return reportPath;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function main() {
|
|
175
|
+
const options = parseArgs(process.argv.slice(2));
|
|
176
|
+
const [baseline, fixtureConfig] = await Promise.all([
|
|
177
|
+
readQualityBaseline(options.mode),
|
|
178
|
+
readFixtureConfig(options.mode, options.fixture),
|
|
179
|
+
]);
|
|
180
|
+
const ledger = await buildLedger(fixtureConfig);
|
|
181
|
+
const metrics = runBenchmark(ledger, fixtureConfig);
|
|
182
|
+
const withinLatency = metrics.p95Ms <= Number(baseline.apiLatencyMsP95Max ?? 500);
|
|
183
|
+
const withinMemory = metrics.peakHeapMB <= Number(baseline.memoryMBP95Max ?? 512);
|
|
184
|
+
const reportPath = await writeReport(options.mode, fixtureConfig, baseline, metrics);
|
|
185
|
+
const result = {
|
|
186
|
+
ok: withinLatency && withinMemory,
|
|
187
|
+
mode: options.mode,
|
|
188
|
+
reportPath,
|
|
189
|
+
dataset: {
|
|
190
|
+
versionCount: fixtureConfig.versionCount,
|
|
191
|
+
itemsPerVersion: fixtureConfig.itemsPerVersion,
|
|
192
|
+
detailRepeats: fixtureConfig.detailRepeats,
|
|
193
|
+
},
|
|
194
|
+
metrics,
|
|
195
|
+
baseline,
|
|
196
|
+
};
|
|
197
|
+
console.log(JSON.stringify(result, null, 2));
|
|
198
|
+
if (!result.ok) {
|
|
199
|
+
process.exitCode = 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
await main();
|
|
@@ -40,6 +40,7 @@ description: 为 OpenPrd 产品、CLI、Agent harness、AI code review / PR revi
|
|
|
40
40
|
- 项目自己的 `.openprd/benchmarks/` 优先于 OpenPrd 内置 Source Map。
|
|
41
41
|
- `sources.yaml` 里的 approved source 是长期可复用参考;`inbox/` 里的 candidate 只表示待确认线索。
|
|
42
42
|
- 用 `openprd benchmark add <url|repo|file>` 写入 candidate,用 `openprd benchmark approve <id>` 纳入 approved registry。
|
|
43
|
+
- 执行或复盘中发现被用户采纳的优质信源时,用 `openprd benchmark observe <url|repo|file> --notes <text>` 累计 evidence;达到阈值后只推荐 approve,不自动晋级。
|
|
43
44
|
- 用 `openprd benchmark verify` 检查重复来源、失效链接、缺失本地文件和过宽触发规则。
|
|
44
45
|
|
|
45
46
|
## Source Policy
|