@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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
function pathWithin(parentPath, childPath) {
|
|
5
|
+
const relativePath = path.relative(parentPath, childPath);
|
|
6
|
+
return relativePath !== '' && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function analyzeWorkspaceRegistryHygiene(entries = [], options = {}) {
|
|
10
|
+
const homeDir = path.resolve(options.homeDir ?? os.homedir());
|
|
11
|
+
const issues = [];
|
|
12
|
+
const normalized = entries
|
|
13
|
+
.filter((entry) => entry?.workspaceRoot)
|
|
14
|
+
.map((entry) => ({
|
|
15
|
+
workspaceRoot: path.resolve(entry.workspaceRoot),
|
|
16
|
+
realpath: path.resolve(entry.realpath ?? entry.workspaceRoot),
|
|
17
|
+
}))
|
|
18
|
+
.sort((left, right) => left.workspaceRoot.localeCompare(right.workspaceRoot));
|
|
19
|
+
|
|
20
|
+
for (const entry of normalized) {
|
|
21
|
+
if (entry.workspaceRoot === homeDir || entry.realpath === homeDir) {
|
|
22
|
+
issues.push({
|
|
23
|
+
kind: 'overbroad-root',
|
|
24
|
+
severity: 'warning',
|
|
25
|
+
workspaceRoot: entry.workspaceRoot,
|
|
26
|
+
message: `Workspace root ${entry.workspaceRoot} 过宽,容易把多个不相关项目混进同一 registry 视野。`,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
32
|
+
for (let nextIndex = index + 1; nextIndex < normalized.length; nextIndex += 1) {
|
|
33
|
+
const current = normalized[index];
|
|
34
|
+
const next = normalized[nextIndex];
|
|
35
|
+
if (!pathWithin(current.realpath, next.realpath) && !pathWithin(current.workspaceRoot, next.workspaceRoot)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
issues.push({
|
|
39
|
+
kind: 'ambiguous-nesting',
|
|
40
|
+
severity: 'warning',
|
|
41
|
+
workspaceRoot: current.workspaceRoot,
|
|
42
|
+
relatedWorkspaceRoot: next.workspaceRoot,
|
|
43
|
+
message: `Workspace root ${current.workspaceRoot} 与 ${next.workspaceRoot} 存在父子嵌套,恢复会话时需要额外消歧。`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
ok: issues.length === 0,
|
|
50
|
+
issues,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { analyzeWorkspaceRegistryHygiene };
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 核心功能
|
|
3
|
+
* 维护可选的项目级 release/version ledger,统一记录当前版本、版本状态、版本内变化项与本地 tag 元数据。
|
|
4
|
+
*
|
|
5
|
+
* 输入
|
|
6
|
+
* 接收项目根目录、版本号、变化说明、tag 同步结果等项目级 release 信息。
|
|
7
|
+
*
|
|
8
|
+
* 输出
|
|
9
|
+
* 读写 `.openprd/state/release-ledger.json`,并返回 handoff、status、commit 可复用的结构化版本摘要。
|
|
10
|
+
*
|
|
11
|
+
* 定位
|
|
12
|
+
* 位于 OpenPrd 的项目发布事实层,刻意与内部 PRD `v000x` 快照版本分离。
|
|
13
|
+
*
|
|
14
|
+
* 依赖
|
|
15
|
+
* 依赖 fs-utils、change-summary 和基础时间工具,不依赖 workspace workflow。
|
|
16
|
+
*
|
|
17
|
+
* 维护规则
|
|
18
|
+
* 版本号默认按 semver 提示,但必须允许项目保留自己的版本体系;不可把远端 tag 风险静默吞掉。
|
|
19
|
+
*/
|
|
20
|
+
import { buildChangeEntries, buildChangeEntry, buildChangeSummaryFromEntries } from './change-summary.js';
|
|
21
|
+
import { cjoin, readJson, writeJson } from './fs-utils.js';
|
|
22
|
+
import { timestamp } from './time.js';
|
|
23
|
+
|
|
24
|
+
export const RELEASE_LEDGER_STATUSES = ['draft', 'current', 'released'];
|
|
25
|
+
export const RELEASE_LEDGER_STRATEGY = 'semver';
|
|
26
|
+
export const RELEASE_SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u;
|
|
27
|
+
|
|
28
|
+
function normalizeReleaseStatus(status, fallback = 'current') {
|
|
29
|
+
return RELEASE_LEDGER_STATUSES.includes(status) ? status : fallback;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeReleaseVersionInput(value) {
|
|
33
|
+
const text = String(value ?? '').trim();
|
|
34
|
+
if (!text) return '';
|
|
35
|
+
if (/^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/iu.test(text)) {
|
|
36
|
+
return text.slice(1);
|
|
37
|
+
}
|
|
38
|
+
return text;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildSemverHint(version) {
|
|
42
|
+
const normalized = normalizeReleaseVersionInput(version);
|
|
43
|
+
if (!normalized) {
|
|
44
|
+
return { matchesSemver: false, normalizedVersion: normalized, warning: '未设置项目版本号。' };
|
|
45
|
+
}
|
|
46
|
+
if (RELEASE_SEMVER_PATTERN.test(normalized)) {
|
|
47
|
+
return { matchesSemver: true, normalizedVersion: normalized, warning: null };
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
matchesSemver: false,
|
|
51
|
+
normalizedVersion: normalized,
|
|
52
|
+
warning: `当前项目版本 ${normalized} 未匹配 x.y.z 形式;OpenPrd 仍会保留它,但不会把 semver 当成强校验。`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeReleaseTag(tag) {
|
|
57
|
+
if (!tag || typeof tag !== 'object' || Array.isArray(tag)) return null;
|
|
58
|
+
return {
|
|
59
|
+
name: typeof tag.name === 'string' ? tag.name.trim() : null,
|
|
60
|
+
localSha: typeof tag.localSha === 'string' && tag.localSha.trim() ? tag.localSha.trim() : null,
|
|
61
|
+
remoteSha: typeof tag.remoteSha === 'string' && tag.remoteSha.trim() ? tag.remoteSha.trim() : null,
|
|
62
|
+
remoteStatus: typeof tag.remoteStatus === 'string' && tag.remoteStatus.trim() ? tag.remoteStatus.trim() : null,
|
|
63
|
+
updatedAt: typeof tag.updatedAt === 'string' && tag.updatedAt.trim() ? tag.updatedAt.trim() : null,
|
|
64
|
+
warning: typeof tag.warning === 'string' && tag.warning.trim() ? tag.warning.trim() : null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeReleaseSource(source) {
|
|
69
|
+
if (!source || typeof source !== 'object' || Array.isArray(source)) return null;
|
|
70
|
+
const next = {};
|
|
71
|
+
for (const [key, value] of Object.entries(source)) {
|
|
72
|
+
if (value === null || value === undefined) continue;
|
|
73
|
+
if (typeof value === 'string') {
|
|
74
|
+
const trimmed = value.trim();
|
|
75
|
+
if (!trimmed) continue;
|
|
76
|
+
next[key] = trimmed;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
next[key] = value;
|
|
80
|
+
}
|
|
81
|
+
return Object.keys(next).length > 0 ? next : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeReleaseItem(raw) {
|
|
85
|
+
if (!raw) return null;
|
|
86
|
+
|
|
87
|
+
const entry = buildChangeEntry(
|
|
88
|
+
typeof raw === 'string'
|
|
89
|
+
? raw
|
|
90
|
+
: raw.sentence ?? raw.detail ?? raw.summary ?? raw.title ?? '',
|
|
91
|
+
{ fallbackType: raw.type ?? '调整', summaryMaxLength: 15 },
|
|
92
|
+
);
|
|
93
|
+
if (!entry) return null;
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
type: entry.type,
|
|
97
|
+
summary: entry.summary,
|
|
98
|
+
detail: entry.detail,
|
|
99
|
+
sentence: entry.sentence,
|
|
100
|
+
recordedAt: typeof raw.recordedAt === 'string' && raw.recordedAt.trim() ? raw.recordedAt.trim() : null,
|
|
101
|
+
source: normalizeReleaseSource(raw.source),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeReleaseVersionEntry(raw) {
|
|
106
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
107
|
+
const version = normalizeReleaseVersionInput(raw.version);
|
|
108
|
+
if (!version) return null;
|
|
109
|
+
const items = Array.isArray(raw.items)
|
|
110
|
+
? raw.items.map((item) => normalizeReleaseItem(item)).filter(Boolean)
|
|
111
|
+
: [];
|
|
112
|
+
return {
|
|
113
|
+
version,
|
|
114
|
+
status: normalizeReleaseStatus(raw.status),
|
|
115
|
+
createdAt: typeof raw.createdAt === 'string' && raw.createdAt.trim() ? raw.createdAt.trim() : null,
|
|
116
|
+
updatedAt: typeof raw.updatedAt === 'string' && raw.updatedAt.trim() ? raw.updatedAt.trim() : null,
|
|
117
|
+
notes: typeof raw.notes === 'string' && raw.notes.trim() ? raw.notes.trim() : null,
|
|
118
|
+
items,
|
|
119
|
+
tag: normalizeReleaseTag(raw.tag),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeReleaseVersions(values) {
|
|
124
|
+
const items = Array.isArray(values) ? values : [];
|
|
125
|
+
const versions = [];
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
for (const item of items) {
|
|
128
|
+
const entry = normalizeReleaseVersionEntry(item);
|
|
129
|
+
if (!entry) continue;
|
|
130
|
+
if (seen.has(entry.version)) continue;
|
|
131
|
+
seen.add(entry.version);
|
|
132
|
+
versions.push(entry);
|
|
133
|
+
}
|
|
134
|
+
return versions;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function defaultReleaseLedger() {
|
|
138
|
+
return {
|
|
139
|
+
version: 1,
|
|
140
|
+
enabled: false,
|
|
141
|
+
strategy: RELEASE_LEDGER_STRATEGY,
|
|
142
|
+
currentVersion: null,
|
|
143
|
+
versions: [],
|
|
144
|
+
createdAt: null,
|
|
145
|
+
updatedAt: null,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function normalizeReleaseLedger(raw) {
|
|
150
|
+
const defaults = defaultReleaseLedger();
|
|
151
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
152
|
+
return defaults;
|
|
153
|
+
}
|
|
154
|
+
const versions = normalizeReleaseVersions(raw.versions);
|
|
155
|
+
const currentVersion = normalizeReleaseVersionInput(raw.currentVersion);
|
|
156
|
+
return {
|
|
157
|
+
version: 1,
|
|
158
|
+
enabled: raw.enabled === true,
|
|
159
|
+
strategy: typeof raw.strategy === 'string' && raw.strategy.trim() ? raw.strategy.trim() : RELEASE_LEDGER_STRATEGY,
|
|
160
|
+
currentVersion: versions.some((entry) => entry.version === currentVersion) ? currentVersion : (currentVersion || null),
|
|
161
|
+
versions,
|
|
162
|
+
createdAt: typeof raw.createdAt === 'string' && raw.createdAt.trim() ? raw.createdAt.trim() : null,
|
|
163
|
+
updatedAt: typeof raw.updatedAt === 'string' && raw.updatedAt.trim() ? raw.updatedAt.trim() : null,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function releaseLedgerPath(projectRoot) {
|
|
168
|
+
return cjoin(projectRoot, '.openprd', 'state', 'release-ledger.json');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function loadReleaseLedger(projectRoot) {
|
|
172
|
+
const filePath = releaseLedgerPath(projectRoot);
|
|
173
|
+
const raw = await readJson(filePath).catch(() => null);
|
|
174
|
+
return {
|
|
175
|
+
filePath,
|
|
176
|
+
exists: raw !== null,
|
|
177
|
+
ledger: normalizeReleaseLedger(raw),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function saveReleaseLedger(projectRoot, ledger) {
|
|
182
|
+
const filePath = releaseLedgerPath(projectRoot);
|
|
183
|
+
const normalized = normalizeReleaseLedger({
|
|
184
|
+
...ledger,
|
|
185
|
+
updatedAt: timestamp(),
|
|
186
|
+
createdAt: ledger?.createdAt ?? timestamp(),
|
|
187
|
+
});
|
|
188
|
+
await writeJson(filePath, normalized);
|
|
189
|
+
return { filePath, ledger: normalized };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function findReleaseVersionEntry(ledger, version) {
|
|
193
|
+
const normalized = normalizeReleaseVersionInput(version);
|
|
194
|
+
return normalizeReleaseLedger(ledger).versions.find((entry) => entry.version === normalized) ?? null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function getCurrentReleaseEntry(ledger) {
|
|
198
|
+
const normalized = normalizeReleaseLedger(ledger);
|
|
199
|
+
return normalized.currentVersion
|
|
200
|
+
? normalized.versions.find((entry) => entry.version === normalized.currentVersion) ?? null
|
|
201
|
+
: null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function ensureReleaseLedgerEntry(ledger, version, options = {}) {
|
|
205
|
+
const normalizedLedger = normalizeReleaseLedger(ledger);
|
|
206
|
+
const normalizedVersion = normalizeReleaseVersionInput(version);
|
|
207
|
+
if (!normalizedVersion) {
|
|
208
|
+
throw new Error('项目版本号不能为空。');
|
|
209
|
+
}
|
|
210
|
+
let entry = normalizedLedger.versions.find((item) => item.version === normalizedVersion) ?? null;
|
|
211
|
+
const now = timestamp();
|
|
212
|
+
if (!entry) {
|
|
213
|
+
entry = {
|
|
214
|
+
version: normalizedVersion,
|
|
215
|
+
status: normalizeReleaseStatus(options.status),
|
|
216
|
+
createdAt: now,
|
|
217
|
+
updatedAt: now,
|
|
218
|
+
notes: options.notes ?? null,
|
|
219
|
+
items: [],
|
|
220
|
+
tag: null,
|
|
221
|
+
};
|
|
222
|
+
normalizedLedger.versions.push(entry);
|
|
223
|
+
}
|
|
224
|
+
if (options.status) {
|
|
225
|
+
entry.status = normalizeReleaseStatus(options.status, entry.status);
|
|
226
|
+
}
|
|
227
|
+
entry.updatedAt = now;
|
|
228
|
+
if (!entry.createdAt) entry.createdAt = now;
|
|
229
|
+
return { ledger: normalizedLedger, entry };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function setReleaseLedgerEnabled(ledger, enabled) {
|
|
233
|
+
const normalized = normalizeReleaseLedger(ledger);
|
|
234
|
+
const next = {
|
|
235
|
+
...normalized,
|
|
236
|
+
enabled: Boolean(enabled),
|
|
237
|
+
createdAt: normalized.createdAt ?? timestamp(),
|
|
238
|
+
updatedAt: timestamp(),
|
|
239
|
+
};
|
|
240
|
+
return { ledger: next };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function setCurrentReleaseVersion(ledger, version, options = {}) {
|
|
244
|
+
const normalized = normalizeReleaseLedger(ledger);
|
|
245
|
+
const nextVersion = normalizeReleaseVersionInput(version);
|
|
246
|
+
if (!nextVersion) {
|
|
247
|
+
throw new Error('项目版本号不能为空。');
|
|
248
|
+
}
|
|
249
|
+
const previousCurrent = getCurrentReleaseEntry(normalized);
|
|
250
|
+
const ensured = ensureReleaseLedgerEntry(normalized, nextVersion, {
|
|
251
|
+
status: options.status ?? 'current',
|
|
252
|
+
notes: options.notes ?? null,
|
|
253
|
+
});
|
|
254
|
+
const next = ensured.ledger;
|
|
255
|
+
for (const item of next.versions) {
|
|
256
|
+
if (item.version === nextVersion) {
|
|
257
|
+
item.status = normalizeReleaseStatus(options.status ?? 'current');
|
|
258
|
+
item.updatedAt = timestamp();
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (item.status === 'current') {
|
|
262
|
+
item.status = normalizeReleaseStatus(options.previousStatus ?? 'released', 'released');
|
|
263
|
+
item.updatedAt = timestamp();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
next.enabled = true;
|
|
267
|
+
next.currentVersion = nextVersion;
|
|
268
|
+
next.createdAt = next.createdAt ?? timestamp();
|
|
269
|
+
next.updatedAt = timestamp();
|
|
270
|
+
return {
|
|
271
|
+
ledger: next,
|
|
272
|
+
entry: ensured.entry,
|
|
273
|
+
previousVersion: previousCurrent && previousCurrent.version !== nextVersion ? previousCurrent.version : null,
|
|
274
|
+
semver: buildSemverHint(nextVersion),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function setReleaseVersionStatus(ledger, status, options = {}) {
|
|
279
|
+
const normalized = normalizeReleaseLedger(ledger);
|
|
280
|
+
const targetVersion = normalizeReleaseVersionInput(options.version ?? normalized.currentVersion);
|
|
281
|
+
if (!targetVersion) {
|
|
282
|
+
throw new Error('还没有可更新状态的项目版本;请先设置当前版本号。');
|
|
283
|
+
}
|
|
284
|
+
const ensured = ensureReleaseLedgerEntry(normalized, targetVersion, { status });
|
|
285
|
+
const next = ensured.ledger;
|
|
286
|
+
ensured.entry.status = normalizeReleaseStatus(status);
|
|
287
|
+
ensured.entry.updatedAt = timestamp();
|
|
288
|
+
if (ensured.entry.status === 'current') {
|
|
289
|
+
next.currentVersion = targetVersion;
|
|
290
|
+
for (const entry of next.versions) {
|
|
291
|
+
if (entry.version !== targetVersion && entry.status === 'current') {
|
|
292
|
+
entry.status = 'released';
|
|
293
|
+
entry.updatedAt = timestamp();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
next.enabled = true;
|
|
298
|
+
next.createdAt = next.createdAt ?? timestamp();
|
|
299
|
+
next.updatedAt = timestamp();
|
|
300
|
+
return { ledger: next, entry: ensured.entry };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function releaseItemKey(item) {
|
|
304
|
+
return [
|
|
305
|
+
item.type,
|
|
306
|
+
item.detail,
|
|
307
|
+
item.source?.kind ?? '',
|
|
308
|
+
item.source?.taskId ?? '',
|
|
309
|
+
item.source?.manualId ?? '',
|
|
310
|
+
].join('::');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function appendReleaseEntry(ledger, rawValue, options = {}) {
|
|
314
|
+
const normalized = normalizeReleaseLedger(ledger);
|
|
315
|
+
const targetVersion = normalizeReleaseVersionInput(options.version ?? normalized.currentVersion);
|
|
316
|
+
if (!targetVersion) {
|
|
317
|
+
throw new Error('还没有当前项目版本;请先设置版本号再累计变化条目。');
|
|
318
|
+
}
|
|
319
|
+
const ensured = ensureReleaseLedgerEntry(normalized, targetVersion, { status: options.status ?? undefined });
|
|
320
|
+
const next = ensured.ledger;
|
|
321
|
+
next.enabled = true;
|
|
322
|
+
if (!next.currentVersion) {
|
|
323
|
+
next.currentVersion = targetVersion;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const entries = buildChangeEntries(Array.isArray(rawValue) ? rawValue : [rawValue], {
|
|
327
|
+
fallbackType: options.fallbackType ?? '调整',
|
|
328
|
+
summaryMaxLength: 15,
|
|
329
|
+
});
|
|
330
|
+
const now = timestamp();
|
|
331
|
+
const added = [];
|
|
332
|
+
for (const item of entries) {
|
|
333
|
+
const candidate = normalizeReleaseItem({
|
|
334
|
+
...item,
|
|
335
|
+
recordedAt: now,
|
|
336
|
+
source: options.source ?? null,
|
|
337
|
+
});
|
|
338
|
+
if (!candidate) continue;
|
|
339
|
+
const existing = ensured.entry.items.find((entry) => releaseItemKey(entry) === releaseItemKey(candidate));
|
|
340
|
+
if (existing) {
|
|
341
|
+
existing.recordedAt = now;
|
|
342
|
+
existing.source = normalizeReleaseSource({ ...(existing.source ?? {}), ...(options.source ?? {}) });
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
ensured.entry.items.push(candidate);
|
|
346
|
+
added.push(candidate);
|
|
347
|
+
}
|
|
348
|
+
ensured.entry.updatedAt = now;
|
|
349
|
+
next.updatedAt = now;
|
|
350
|
+
next.createdAt = next.createdAt ?? now;
|
|
351
|
+
return { ledger: next, entry: ensured.entry, added };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function updateReleaseTag(ledger, options = {}) {
|
|
355
|
+
const normalized = normalizeReleaseLedger(ledger);
|
|
356
|
+
const targetVersion = normalizeReleaseVersionInput(options.version ?? normalized.currentVersion);
|
|
357
|
+
if (!targetVersion) {
|
|
358
|
+
throw new Error('还没有当前项目版本;无法记录 tag 状态。');
|
|
359
|
+
}
|
|
360
|
+
const ensured = ensureReleaseLedgerEntry(normalized, targetVersion, {});
|
|
361
|
+
ensured.entry.tag = normalizeReleaseTag({
|
|
362
|
+
...(ensured.entry.tag ?? {}),
|
|
363
|
+
name: options.name ?? targetVersion,
|
|
364
|
+
localSha: options.localSha ?? ensured.entry.tag?.localSha ?? null,
|
|
365
|
+
remoteSha: options.remoteSha ?? ensured.entry.tag?.remoteSha ?? null,
|
|
366
|
+
remoteStatus: options.remoteStatus ?? ensured.entry.tag?.remoteStatus ?? null,
|
|
367
|
+
updatedAt: options.updatedAt ?? timestamp(),
|
|
368
|
+
warning: options.warning ?? null,
|
|
369
|
+
});
|
|
370
|
+
ensured.entry.updatedAt = timestamp();
|
|
371
|
+
ensured.ledger.updatedAt = timestamp();
|
|
372
|
+
ensured.ledger.createdAt = ensured.ledger.createdAt ?? timestamp();
|
|
373
|
+
return { ledger: ensured.ledger, entry: ensured.entry };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function buildReleaseLedgerSummary(ledger, options = {}) {
|
|
377
|
+
const normalized = normalizeReleaseLedger(ledger);
|
|
378
|
+
const current = options.version
|
|
379
|
+
? findReleaseVersionEntry(normalized, options.version)
|
|
380
|
+
: getCurrentReleaseEntry(normalized);
|
|
381
|
+
const semver = buildSemverHint(current?.version ?? normalized.currentVersion);
|
|
382
|
+
return {
|
|
383
|
+
enabled: normalized.enabled,
|
|
384
|
+
strategy: normalized.strategy,
|
|
385
|
+
currentVersion: current?.version ?? normalized.currentVersion ?? null,
|
|
386
|
+
currentStatus: current?.status ?? null,
|
|
387
|
+
versionCount: normalized.versions.length,
|
|
388
|
+
itemCount: current?.items?.length ?? 0,
|
|
389
|
+
items: current?.items ?? [],
|
|
390
|
+
semver,
|
|
391
|
+
tag: current?.tag ?? null,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function buildReleaseChangeSummary(ledger, options = {}) {
|
|
396
|
+
const normalized = normalizeReleaseLedger(ledger);
|
|
397
|
+
const entry = options.version
|
|
398
|
+
? findReleaseVersionEntry(normalized, options.version)
|
|
399
|
+
: getCurrentReleaseEntry(normalized);
|
|
400
|
+
if (!entry || entry.items.length === 0) {
|
|
401
|
+
return {
|
|
402
|
+
title: `${options.version ?? normalized.currentVersion ?? '当前版本'}变化摘要`,
|
|
403
|
+
perspective: null,
|
|
404
|
+
preferredVerbs: [],
|
|
405
|
+
items: [],
|
|
406
|
+
markdown: '',
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
return buildChangeSummaryFromEntries(entry.items, {
|
|
410
|
+
title: `${entry.version} 变化摘要`,
|
|
411
|
+
limit: options.limit,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
|
+
import { USER_CHANGE_SUMMARY_GUIDE } from './change-summary.js';
|
|
5
|
+
import { buildReleaseLedgerSummary, loadReleaseLedger } from './release-ledger.js';
|
|
4
6
|
|
|
5
7
|
import {
|
|
6
8
|
buildReviewPresentationFeedback,
|
|
@@ -34,23 +36,23 @@ export const REVIEW_PRESENTATION_TEMPLATE = {
|
|
|
34
36
|
],
|
|
35
37
|
panels: {
|
|
36
38
|
flow: [
|
|
37
|
-
|
|
39
|
+
USER_CHANGE_SUMMARY_GUIDE.panelExamples.flow,
|
|
38
40
|
],
|
|
39
41
|
function: [
|
|
40
|
-
|
|
42
|
+
USER_CHANGE_SUMMARY_GUIDE.panelExamples.function,
|
|
41
43
|
],
|
|
42
44
|
guardrail: [
|
|
43
|
-
|
|
45
|
+
USER_CHANGE_SUMMARY_GUIDE.panelExamples.guardrail,
|
|
44
46
|
],
|
|
45
47
|
risk: [
|
|
46
|
-
|
|
48
|
+
USER_CHANGE_SUMMARY_GUIDE.panelExamples.risk,
|
|
47
49
|
],
|
|
48
50
|
},
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
export function buildReviewPresentationTemplatePayload() {
|
|
52
54
|
return {
|
|
53
|
-
intent: 'Agent 先按这个模板写 reviewPresentation
|
|
55
|
+
intent: 'Agent 先按这个模板写 reviewPresentation,再用本脚本校验;短标签优先使用新增、修复、优化、调整、移除这类用户可感知变化。',
|
|
54
56
|
presentationTemplate: REVIEW_PRESENTATION_TEMPLATE,
|
|
55
57
|
presentationContract: buildReviewPresentationFeedback({ sections: {} }).contract,
|
|
56
58
|
};
|
|
@@ -184,7 +186,11 @@ async function renderValidatedReviewPresentation(projectRoot, snapshot) {
|
|
|
184
186
|
const workspaceRoot = path.join(projectRoot, '.openprd');
|
|
185
187
|
const canonicalReview = canonicalReviewPath({ workspaceRoot }, snapshot.versionId);
|
|
186
188
|
const activeReviewEntry = defaultReviewArtifactPath({ workspaceRoot });
|
|
187
|
-
|
|
189
|
+
const releaseLedger = await loadReleaseLedger(projectRoot);
|
|
190
|
+
await writeHtmlArtifact(canonicalReview, renderReviewArtifact({
|
|
191
|
+
snapshot,
|
|
192
|
+
projectRelease: buildReleaseLedgerSummary(releaseLedger.ledger),
|
|
193
|
+
}));
|
|
188
194
|
|
|
189
195
|
const versionIndexPath = path.join(workspaceRoot, 'state', 'version-index.json');
|
|
190
196
|
const versionIndex = await readJson(versionIndexPath);
|