@openprd/cli 0.1.19 → 0.1.22
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/changes/openprd-control-plane-v020/.openprd.yaml +2 -0
- package/.openprd/changes/openprd-control-plane-v020/design.md +78 -0
- package/.openprd/changes/openprd-control-plane-v020/proposal.md +54 -0
- package/.openprd/changes/openprd-control-plane-v020/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/openprd-control-plane-v020/task-events.jsonl +27 -0
- package/.openprd/changes/openprd-control-plane-v020/tasks-002.md +35 -0
- package/.openprd/changes/openprd-control-plane-v020/tasks.md +427 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/.openprd.yaml +2 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/design.md +52 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/proposal.md +35 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/task-events.jsonl +1 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/tasks.md +170 -0
- package/.openprd/design/active/asset-spec.md +19 -14
- package/.openprd/design/active/direction-plan.md +19 -3
- package/.openprd/design/active/facts-sheet.md +16 -7
- package/.openprd/design/active/image-preflight.md +6 -5
- package/.openprd/design/active/review-studio-v020-directions/compare-plan.json +34 -0
- package/.openprd/design/active/review-studio-v020-directions/contact-sheet.jpg +0 -0
- package/.openprd/design/active/review-studio-v020-directions/crops/01.png +0 -0
- package/.openprd/design/active/review-studio-v020-directions/crops/02.png +0 -0
- package/.openprd/design/active/review-studio-v020-directions/crops/03.png +0 -0
- package/.openprd/design/active/review-studio-v020-directions/focus-board.template.json +69 -0
- package/.openprd/design/active/review-studio-v020-directions/parallel-board.template.json +45 -0
- package/.openprd/design/active/review-studio-v020-directions/reference-set.json +143 -0
- package/.openprd/design/active/review-studio-v020-directions/source.png +0 -0
- package/.openprd/design/active/selected-direction.md +23 -9
- package/.openprd/engagements/active/control-plane-architecture.json +203 -0
- package/.openprd/engagements/active/control-plane-intake.json +418 -0
- package/.openprd/engagements/active/prd.md +183 -119
- package/.openprd/engagements/active/review-presentation-v0018.json +176 -0
- package/.openprd/i18n-config.json +12 -0
- package/.openprd/ledger/events.jsonl +6 -0
- package/.openprd/ledger/heads/ebea1a71a9daa566f1c91b53.json +10 -0
- package/.openprd/manifest.json +21 -0
- package/AGENTS.md +7 -6
- package/README.md +31 -29
- package/README_EN.md +36 -39
- package/package.json +1 -1
- package/skills/openprd-frontend-design/SKILL.md +16 -0
- package/skills/openprd-harness/SKILL.md +7 -7
- package/skills/openprd-quality/SKILL.md +1 -1
- package/skills/openprd-requirement-intake/SKILL.md +1 -1
- package/skills/openprd-shared/SKILL.md +6 -6
- package/src/adapters/adapter-spi.js +193 -0
- package/src/adapters/capability-envelope.js +98 -0
- package/src/adapters/event-normalizer.js +55 -0
- package/src/adapters/index.js +4 -0
- package/src/adapters/install-safety.js +249 -0
- package/src/agent-canonical-content.js +4 -2
- package/src/agent-integration.js +169 -46
- package/src/cli/args.js +63 -4
- package/src/cli/gate-print.js +17 -0
- package/src/cli/quality-commands.js +18 -0
- package/src/cli/quality-print.js +10 -0
- package/src/cli/runtime-print.js +24 -0
- package/src/codex-hook-runner-template.mjs +129 -199
- package/src/codex-runtime.js +48 -5
- package/src/context/cache.js +245 -0
- package/src/context/compiler.js +438 -0
- package/src/context/constants.js +30 -0
- package/src/context/index.js +39 -0
- package/src/context/redaction.js +84 -0
- package/src/context/stable.js +69 -0
- package/src/context/telemetry.js +42 -0
- package/src/dev-standards.js +57 -0
- package/src/fleet.js +112 -95
- package/src/gates/index.js +2 -0
- package/src/gates/scoped-gates.js +256 -0
- package/src/gates/store.js +126 -0
- package/src/gates/workspace.js +41 -0
- package/src/html-artifacts.js +725 -28
- package/src/kernel/atomic-store.js +299 -0
- package/src/kernel/event-envelope.js +166 -0
- package/src/kernel/index.js +4 -0
- package/src/kernel/project-ledger.js +467 -0
- package/src/kernel/project-manifest.js +205 -0
- package/src/knowledge-v3/index.js +1 -0
- package/src/knowledge-v3/lifecycle.js +290 -0
- package/src/knowledge.js +14 -7
- package/src/openprd.js +71 -2
- package/src/review-model.js +413 -0
- package/src/review-presentation.js +1 -1
- package/src/run-harness.js +432 -38
- package/src/runtime/cli_runtime_README.md +28 -0
- package/src/runtime/errors.js +66 -0
- package/src/runtime/index.js +44 -0
- package/src/runtime/lane-schema.js +141 -0
- package/src/runtime/lane-store.js +279 -0
- package/src/runtime/task-runtime.js +449 -0
- package/src/runtime/workspace.js +179 -0
- package/src/runtime/write-set.js +206 -0
- package/src/session-binding.js +16 -3
- package/src/session-registry.js +59 -1
- package/src/upgrade/fleet-mutation.js +166 -0
- package/src/upgrade/fleet-transaction.js +398 -0
- package/src/upgrade/transaction-store.js +416 -0
- package/src/visual-compare-core.js +66 -27
- package/src/visual-compare.js +18 -12
- package/src/workspace-core.js +109 -7
- package/src/workspace-registry.js +39 -1
- package/src/workspace-workflow.js +18 -15
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export const REVIEW_MODEL_SCHEMA = 'openprd.review-model.v1';
|
|
4
|
+
export const REVIEW_PROJECTION_SCHEMA = 'openprd.review-projection.v1';
|
|
5
|
+
export const REVIEW_APPROVAL_SCHEMA = 'openprd.review-approval.v1';
|
|
6
|
+
|
|
7
|
+
function record(value) {
|
|
8
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
9
|
+
}
|
|
10
|
+
function text(value) {
|
|
11
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function optionalText(value) {
|
|
15
|
+
return text(value) || null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function list(value) {
|
|
19
|
+
if (!Array.isArray(value)) return [];
|
|
20
|
+
return value.map((item) => text(item)).filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function unique(values) {
|
|
24
|
+
return [...new Set(values.filter(Boolean))];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stableValue(value) {
|
|
28
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
29
|
+
if (!value || typeof value !== 'object') return value;
|
|
30
|
+
return Object.fromEntries(
|
|
31
|
+
Object.keys(value)
|
|
32
|
+
.sort()
|
|
33
|
+
.filter((key) => value[key] !== undefined)
|
|
34
|
+
.map((key) => [key, stableValue(value[key])]),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function reviewModelDigest(value) {
|
|
39
|
+
return createHash('sha256').update(JSON.stringify(stableValue(value))).digest('hex');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizePanelItem(item) {
|
|
43
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
44
|
+
const summary = optionalText(item.summary ?? item.title ?? item.label);
|
|
45
|
+
const detail = optionalText(item.detail ?? item.text ?? item.value);
|
|
46
|
+
if (!summary && !detail) return null;
|
|
47
|
+
return { summary, detail: detail ?? summary };
|
|
48
|
+
}
|
|
49
|
+
const detail = optionalText(item);
|
|
50
|
+
return detail ? { summary: null, detail } : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function panelItems(presentation, kind, fallback) {
|
|
54
|
+
const source = Array.isArray(presentation.panels?.[kind])
|
|
55
|
+
? presentation.panels[kind]
|
|
56
|
+
: fallback;
|
|
57
|
+
return source.map(normalizePanelItem).filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeDirection(direction, index) {
|
|
61
|
+
const item = record(direction);
|
|
62
|
+
const id = optionalText(item.id) ?? `direction-${index + 1}`;
|
|
63
|
+
return {
|
|
64
|
+
id,
|
|
65
|
+
title: optionalText(item.title) ?? `方向 ${index + 1}`,
|
|
66
|
+
summary: optionalText(item.summary),
|
|
67
|
+
tradeoff: optionalText(item.tradeoff),
|
|
68
|
+
imageRef: optionalText(item.image),
|
|
69
|
+
imageDigest: optionalText(item.imageDigest ?? item.checksum),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizePresentationDiagram(presentation) {
|
|
74
|
+
const diagram = record(presentation.diagram);
|
|
75
|
+
const mapNodes = record(presentation.mapNodes);
|
|
76
|
+
const normalizeMapNode = (key) => {
|
|
77
|
+
const node = record(mapNodes[key]);
|
|
78
|
+
const title = optionalText(node.title ?? node.label);
|
|
79
|
+
const body = optionalText(node.text ?? node.value);
|
|
80
|
+
return title || body ? { title, text: body } : null;
|
|
81
|
+
};
|
|
82
|
+
const normalizedMapNodes = Object.fromEntries(
|
|
83
|
+
['problem', 'goal', 'scope', 'flow', 'risk']
|
|
84
|
+
.map((key) => [key, normalizeMapNode(key)])
|
|
85
|
+
.filter(([, value]) => value),
|
|
86
|
+
);
|
|
87
|
+
const flowNodes = Array.isArray(presentation.flowNodes)
|
|
88
|
+
? presentation.flowNodes.map((node, index) => {
|
|
89
|
+
const item = record(node);
|
|
90
|
+
return {
|
|
91
|
+
id: optionalText(item.id ?? item.key) ?? `step${index + 1}`,
|
|
92
|
+
text: optionalText(item.text ?? item.value),
|
|
93
|
+
};
|
|
94
|
+
}).filter((node) => node.text)
|
|
95
|
+
: [];
|
|
96
|
+
const flowEdges = Array.isArray(presentation.flowEdges)
|
|
97
|
+
? presentation.flowEdges.map((edge) => {
|
|
98
|
+
const item = record(edge);
|
|
99
|
+
return {
|
|
100
|
+
from: optionalText(item.from ?? item.fromId ?? item.source ?? item.sourceId),
|
|
101
|
+
to: optionalText(item.to ?? item.toId ?? item.target ?? item.targetId),
|
|
102
|
+
};
|
|
103
|
+
}).filter((edge) => edge.from && edge.to)
|
|
104
|
+
: [];
|
|
105
|
+
return {
|
|
106
|
+
type: diagram.type === 'flow' ? 'flow' : 'map',
|
|
107
|
+
mapNodes: normalizedMapNodes,
|
|
108
|
+
flowNodes,
|
|
109
|
+
flowEdges,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function reviewStatus(snapshot) {
|
|
114
|
+
const status = snapshot.reviewStatus;
|
|
115
|
+
if (typeof status === 'string') return status || 'pending-confirmation';
|
|
116
|
+
return optionalText(record(status).status) ?? 'pending-confirmation';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function selectedDirectionId(snapshot) {
|
|
120
|
+
const status = record(snapshot.reviewStatus);
|
|
121
|
+
const direction = record(status.designDirection);
|
|
122
|
+
return optionalText(direction.id ?? status.direction ?? snapshot.selectedDirectionId);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildDecisionTimeline({ status, directions, selectedDirection }) {
|
|
126
|
+
const confirmed = status === 'confirmed' || status === 'approved';
|
|
127
|
+
const directionDone = Boolean(selectedDirection);
|
|
128
|
+
return [
|
|
129
|
+
{ id: 'clarify', title: '需求澄清', status: 'done' },
|
|
130
|
+
{ id: 'summary', title: '需求摘要', status: 'done' },
|
|
131
|
+
{
|
|
132
|
+
id: 'review',
|
|
133
|
+
title: '评审确认',
|
|
134
|
+
status: confirmed ? 'done' : 'current',
|
|
135
|
+
},
|
|
136
|
+
...(directions.length > 0
|
|
137
|
+
? [{
|
|
138
|
+
id: 'direction',
|
|
139
|
+
title: '方向选择',
|
|
140
|
+
status: directionDone ? 'done' : confirmed ? 'current' : 'pending',
|
|
141
|
+
}]
|
|
142
|
+
: []),
|
|
143
|
+
{
|
|
144
|
+
id: 'tasks',
|
|
145
|
+
title: '任务拆解',
|
|
146
|
+
status: confirmed && (directions.length === 0 || directionDone) ? 'current' : 'pending',
|
|
147
|
+
},
|
|
148
|
+
{ id: 'implementation', title: '实现执行', status: 'pending' },
|
|
149
|
+
{ id: 'verification', title: '验证收口', status: 'pending' },
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeEvidenceItem(item, kind, index) {
|
|
154
|
+
const candidate = record(item);
|
|
155
|
+
const summary = optionalText(
|
|
156
|
+
typeof item === 'string'
|
|
157
|
+
? item
|
|
158
|
+
: candidate.summary ?? candidate.detail ?? candidate.text ?? candidate.value ?? candidate.description,
|
|
159
|
+
);
|
|
160
|
+
if (!summary) return null;
|
|
161
|
+
const label = optionalText(candidate.label ?? candidate.title) ?? `依据 ${index + 1}`;
|
|
162
|
+
const href = optionalText(candidate.href ?? candidate.path ?? candidate.file ?? candidate.url);
|
|
163
|
+
const source = optionalText(candidate.source ?? candidate.authority);
|
|
164
|
+
const stableId = optionalText(candidate.id)
|
|
165
|
+
?? `${kind}-${reviewModelDigest({ kind, label, summary, href }).slice(0, 10)}`;
|
|
166
|
+
return {
|
|
167
|
+
id: stableId.replace(/[^a-zA-Z0-9._-]+/g, '-'),
|
|
168
|
+
kind,
|
|
169
|
+
label,
|
|
170
|
+
summary,
|
|
171
|
+
href,
|
|
172
|
+
source,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildEvidenceReferences(snapshot, sections) {
|
|
177
|
+
const sources = [
|
|
178
|
+
['problem', sections.problem?.evidence],
|
|
179
|
+
['validation', sections.validation?.painEvidence],
|
|
180
|
+
['artifact', snapshot.evidenceRefs],
|
|
181
|
+
['artifact', snapshot.evidence],
|
|
182
|
+
];
|
|
183
|
+
const references = [];
|
|
184
|
+
for (const [kind, items] of sources) {
|
|
185
|
+
const entries = Array.isArray(items) ? items : items == null ? [] : [items];
|
|
186
|
+
entries.forEach((item) => {
|
|
187
|
+
const normalized = normalizeEvidenceItem(item, kind, references.length);
|
|
188
|
+
if (normalized && !references.some((entry) => entry.id === normalized.id)) {
|
|
189
|
+
references.push(normalized);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return references;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function normalizeTaskReference(item, index) {
|
|
197
|
+
const candidate = record(item);
|
|
198
|
+
if (typeof item === 'string') {
|
|
199
|
+
return { id: `task-${index + 1}`, title: text(item), status: null, files: [] };
|
|
200
|
+
}
|
|
201
|
+
const id = optionalText(candidate.id ?? candidate.taskId) ?? `task-${index + 1}`;
|
|
202
|
+
const title = optionalText(candidate.title ?? candidate.summary ?? candidate.name) ?? id;
|
|
203
|
+
return {
|
|
204
|
+
id,
|
|
205
|
+
title,
|
|
206
|
+
status: optionalText(candidate.status),
|
|
207
|
+
files: unique([
|
|
208
|
+
...list(candidate.files),
|
|
209
|
+
...list(candidate.writeSet),
|
|
210
|
+
...list(candidate.paths),
|
|
211
|
+
]),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function buildTaskReferences(snapshot) {
|
|
216
|
+
const taskGraph = record(snapshot.taskGraph);
|
|
217
|
+
const candidates = Array.isArray(snapshot.tasks)
|
|
218
|
+
? snapshot.tasks
|
|
219
|
+
: Array.isArray(taskGraph.tasks)
|
|
220
|
+
? taskGraph.tasks
|
|
221
|
+
: [];
|
|
222
|
+
return candidates.map(normalizeTaskReference).filter((item) => item.title);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function buildFileReferences(snapshot, taskReferences, evidenceReferences) {
|
|
226
|
+
return unique([
|
|
227
|
+
...taskReferences.flatMap((task) => task.files),
|
|
228
|
+
...evidenceReferences.map((item) => item.href).filter((href) => href && !/^[a-z][a-z0-9+.-]*:/iu.test(href)),
|
|
229
|
+
...list(snapshot.files),
|
|
230
|
+
]);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildReviewCore(snapshot) {
|
|
234
|
+
const safeSnapshot = record(snapshot);
|
|
235
|
+
const sections = record(safeSnapshot.sections);
|
|
236
|
+
const presentation = record(safeSnapshot.reviewPresentation);
|
|
237
|
+
const requirements = record(sections.requirements);
|
|
238
|
+
const constraints = record(sections.constraints);
|
|
239
|
+
const scenarios = record(sections.scenarios);
|
|
240
|
+
const risks = record(sections.risks);
|
|
241
|
+
const guardrails = record(sections.businessGuardrails);
|
|
242
|
+
const goals = record(sections.goals);
|
|
243
|
+
const directionsBlock = record(presentation.designDirections);
|
|
244
|
+
const directions = Array.isArray(directionsBlock.directions)
|
|
245
|
+
? directionsBlock.directions.map(normalizeDirection)
|
|
246
|
+
: [];
|
|
247
|
+
const status = reviewStatus(safeSnapshot);
|
|
248
|
+
const selectedDirection = selectedDirectionId(safeSnapshot);
|
|
249
|
+
const evidenceReferences = buildEvidenceReferences(safeSnapshot, sections);
|
|
250
|
+
const taskReferences = buildTaskReferences(safeSnapshot);
|
|
251
|
+
const fileReferences = buildFileReferences(safeSnapshot, taskReferences, evidenceReferences);
|
|
252
|
+
const primaryFlows = list(scenarios.primaryFlows);
|
|
253
|
+
const edgeCases = list(scenarios.edgeCases);
|
|
254
|
+
const failureModes = list(scenarios.failureModes);
|
|
255
|
+
const functional = list(requirements.functional);
|
|
256
|
+
const nonFunctional = list(requirements.nonFunctional);
|
|
257
|
+
const dependencies = list(constraints.dependencies);
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
schema: REVIEW_MODEL_SCHEMA,
|
|
261
|
+
source: {
|
|
262
|
+
versionId: optionalText(safeSnapshot.versionId),
|
|
263
|
+
snapshotDigest: optionalText(safeSnapshot.digest),
|
|
264
|
+
workUnitId: optionalText(safeSnapshot.workUnitId),
|
|
265
|
+
targetRoot: optionalText(safeSnapshot.targetRoot),
|
|
266
|
+
},
|
|
267
|
+
title: optionalText(safeSnapshot.title) ?? 'PRD 评审',
|
|
268
|
+
status,
|
|
269
|
+
selectedDirectionId: selectedDirection,
|
|
270
|
+
timeline: buildDecisionTimeline({ status, directions, selectedDirection }),
|
|
271
|
+
overview: {
|
|
272
|
+
problem: optionalText(sections.problem?.problemStatement) ?? '尚未形成明确问题定义',
|
|
273
|
+
whyNow: optionalText(sections.problem?.whyNow),
|
|
274
|
+
goals: list(goals.goals),
|
|
275
|
+
successMetrics: list(goals.successMetrics),
|
|
276
|
+
},
|
|
277
|
+
scope: {
|
|
278
|
+
included: list(sections.scope?.inScope),
|
|
279
|
+
excluded: list(sections.scope?.outOfScope),
|
|
280
|
+
},
|
|
281
|
+
journey: {
|
|
282
|
+
primary: primaryFlows,
|
|
283
|
+
boundaries: edgeCases,
|
|
284
|
+
recovery: failureModes,
|
|
285
|
+
},
|
|
286
|
+
diagram: normalizePresentationDiagram(presentation),
|
|
287
|
+
panels: {
|
|
288
|
+
flow: panelItems(presentation, 'flow', [...primaryFlows, ...edgeCases, ...failureModes]),
|
|
289
|
+
function: panelItems(presentation, 'function', [...functional, ...nonFunctional, ...dependencies]),
|
|
290
|
+
guardrail: panelItems(presentation, 'guardrail', [
|
|
291
|
+
...list(guardrails.costDrivers),
|
|
292
|
+
...list(guardrails.usageLimits),
|
|
293
|
+
...list(guardrails.abusePrevention),
|
|
294
|
+
...list(guardrails.monitoringSignals),
|
|
295
|
+
...list(guardrails.alertThresholds),
|
|
296
|
+
...list(guardrails.stopLossActions),
|
|
297
|
+
]),
|
|
298
|
+
risk: panelItems(presentation, 'risk', [
|
|
299
|
+
...list(risks.assumptions),
|
|
300
|
+
...list(risks.risks),
|
|
301
|
+
...list(risks.openQuestions),
|
|
302
|
+
]),
|
|
303
|
+
},
|
|
304
|
+
directions,
|
|
305
|
+
acceptance: unique([
|
|
306
|
+
...list(goals.acceptanceGoals),
|
|
307
|
+
...list(sections.validation?.acceptanceGoals),
|
|
308
|
+
...list(safeSnapshot.tests),
|
|
309
|
+
]),
|
|
310
|
+
engineering: {
|
|
311
|
+
requirements: {
|
|
312
|
+
functional,
|
|
313
|
+
nonFunctional,
|
|
314
|
+
},
|
|
315
|
+
constraints: {
|
|
316
|
+
technical: list(constraints.technical),
|
|
317
|
+
compliance: list(constraints.compliance),
|
|
318
|
+
dependencies,
|
|
319
|
+
},
|
|
320
|
+
tasks: taskReferences,
|
|
321
|
+
files: fileReferences,
|
|
322
|
+
handoff: {
|
|
323
|
+
owner: optionalText(sections.handoff?.owner),
|
|
324
|
+
nextStep: optionalText(sections.handoff?.nextStep),
|
|
325
|
+
targetSystem: optionalText(sections.handoff?.targetSystem),
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
evidenceReferences,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function buildReviewModel(snapshot = {}) {
|
|
333
|
+
const core = buildReviewCore(snapshot);
|
|
334
|
+
const digest = reviewModelDigest(core);
|
|
335
|
+
const approvalContract = {
|
|
336
|
+
schema: REVIEW_APPROVAL_SCHEMA,
|
|
337
|
+
approvalId: `review:${core.source.versionId ?? 'legacy'}:${digest.slice(0, 16)}`,
|
|
338
|
+
decisionScope: 'review-version',
|
|
339
|
+
modelDigest: digest,
|
|
340
|
+
snapshotDigest: core.source.snapshotDigest,
|
|
341
|
+
versionId: core.source.versionId,
|
|
342
|
+
workUnitId: core.source.workUnitId,
|
|
343
|
+
allowedDecisions: ['confirmed', 'needs-revision'],
|
|
344
|
+
};
|
|
345
|
+
return {
|
|
346
|
+
...core,
|
|
347
|
+
digest,
|
|
348
|
+
approvalContract,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function businessEvidenceReferences(references) {
|
|
353
|
+
return references.map((item, index) => ({
|
|
354
|
+
id: item.id,
|
|
355
|
+
label: `依据 ${index + 1}`,
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function businessOverview(model) {
|
|
360
|
+
const problem = model.diagram.mapNodes.problem?.text ?? model.overview.problem;
|
|
361
|
+
const goal = model.diagram.mapNodes.goal?.text ?? model.overview.goals[0] ?? null;
|
|
362
|
+
return {
|
|
363
|
+
problem,
|
|
364
|
+
goal,
|
|
365
|
+
successMetrics: model.overview.successMetrics,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function projectReviewModel(model, view = 'business') {
|
|
370
|
+
if (!model || model.schema !== REVIEW_MODEL_SCHEMA || !model.digest) {
|
|
371
|
+
throw new TypeError('projectReviewModel requires a canonical ReviewModel');
|
|
372
|
+
}
|
|
373
|
+
if (!['business', 'engineering'].includes(view)) {
|
|
374
|
+
throw new TypeError(`unsupported review projection: ${view}`);
|
|
375
|
+
}
|
|
376
|
+
const shared = {
|
|
377
|
+
schema: REVIEW_PROJECTION_SCHEMA,
|
|
378
|
+
view,
|
|
379
|
+
modelDigest: model.digest,
|
|
380
|
+
timeline: model.timeline,
|
|
381
|
+
approvalContract: model.approvalContract,
|
|
382
|
+
};
|
|
383
|
+
if (view === 'business') {
|
|
384
|
+
return {
|
|
385
|
+
...shared,
|
|
386
|
+
title: model.title,
|
|
387
|
+
sections: {
|
|
388
|
+
overview: businessOverview(model),
|
|
389
|
+
scope: model.scope,
|
|
390
|
+
journey: model.journey,
|
|
391
|
+
diagram: model.diagram,
|
|
392
|
+
panels: model.panels,
|
|
393
|
+
directions: model.directions,
|
|
394
|
+
acceptance: model.acceptance,
|
|
395
|
+
},
|
|
396
|
+
evidenceReferences: businessEvidenceReferences(model.evidenceReferences),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
...shared,
|
|
401
|
+
title: model.title,
|
|
402
|
+
trace: model.source,
|
|
403
|
+
sections: {
|
|
404
|
+
delivery: model.engineering.requirements,
|
|
405
|
+
constraints: model.engineering.constraints,
|
|
406
|
+
tasks: model.engineering.tasks,
|
|
407
|
+
files: model.engineering.files,
|
|
408
|
+
acceptance: model.acceptance,
|
|
409
|
+
handoff: model.engineering.handoff,
|
|
410
|
+
},
|
|
411
|
+
evidenceReferences: model.evidenceReferences,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
@@ -49,7 +49,7 @@ export const REVIEW_PRESENTATION_TEMPLATE = {
|
|
|
49
49
|
],
|
|
50
50
|
},
|
|
51
51
|
designDirections: {
|
|
52
|
-
note: '
|
|
52
|
+
note: '可选;仅在大界面改动或需要用户选择视觉方向时提供,轻量 UI 微调不触发。按当前工具面用 Codex imagegen 或 Cursor GenerateImage 生成 3 个差异足够大的交互稿;已有界面默认共享同一截图并保持原视觉风格,冷启动使用 design brief。把图片放到 .openprd/design/active/ 后写入路径;不触发时删除本字段。',
|
|
53
53
|
directions: [
|
|
54
54
|
{
|
|
55
55
|
id: 'direction-1',
|