@openprd/cli 0.1.1 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.openprd/README.md +43 -69
- package/.openprd/README_EN.md +84 -0
- package/.openprd/benchmarks/index.md +7 -0
- package/.openprd/benchmarks/sources.yaml +25 -3
- package/.openprd/discovery/config.json +16 -2
- package/.openprd/engagements/active/flows.md +19 -14
- package/.openprd/engagements/active/handoff.md +11 -4
- package/.openprd/engagements/active/prd.md +99 -71
- package/.openprd/engagements/active/review.html +4 -4
- package/.openprd/engagements/active/roles.md +9 -8
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +4 -4
- package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +18 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +78 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +129 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +41 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +14 -0
- package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +49 -0
- package/.openprd/knowledge/index.json +44 -4
- package/.openprd/reviews/v0001.html +195 -129
- package/.openprd/reviews/v0002.html +1150 -0
- package/.openprd/reviews/v0003.html +1150 -0
- package/.openprd/reviews/v0004.html +1150 -0
- package/.openprd/reviews/v0005.html +1150 -0
- package/.openprd/standards/config.json +12 -9
- package/.openprd/state/changes.json +17 -2
- package/.openprd/state/current.json +399 -63
- package/.openprd/state/release-ledger.json +387 -0
- package/.openprd/state/version-index.json +52 -0
- package/.openprd/state/versions/v0002.json +264 -0
- package/.openprd/state/versions/v0002.md +183 -0
- package/.openprd/state/versions/v0003.json +269 -0
- package/.openprd/state/versions/v0003.md +188 -0
- package/.openprd/state/versions/v0004.json +274 -0
- package/.openprd/state/versions/v0004.md +193 -0
- package/.openprd/state/versions/v0005.json +299 -0
- package/.openprd/state/versions/v0005.md +189 -0
- package/.openprd/templates/agent/intake.md +5 -4
- package/.openprd/templates/b2b/intake.md +5 -4
- package/.openprd/templates/base/intake.md +10 -4
- package/.openprd/templates/company/README.md +9 -7
- package/.openprd/templates/company/README_EN.md +12 -0
- package/.openprd/templates/consumer/intake.md +5 -4
- package/.openprd/templates/industry/README.md +12 -10
- package/.openprd/templates/industry/README_EN.md +18 -0
- package/.openprd/templates/project/README.md +11 -9
- package/.openprd/templates/project/README_EN.md +16 -0
- package/.openprd/templates/session/README.md +11 -9
- package/.openprd/templates/session/README_EN.md +16 -0
- package/AGENTS.md +12 -8
- package/README.md +419 -438
- package/README_CN.md +4 -578
- package/README_EN.md +870 -0
- package/docs/assets/openprd-requirement-routing-en.png +0 -0
- package/docs/assets/openprd-requirement-routing-en.svg +102 -0
- package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.svg +102 -0
- package/package.json +6 -2
- package/scripts/dev-check-wrapup-copy.mjs +110 -0
- package/scripts/openprd-github-release-notes.mjs +99 -0
- package/scripts/quality-perf-check.mjs +203 -0
- package/skills/openprd-benchmark-router/SKILL.md +1 -0
- package/skills/openprd-benchmark-router/references/benchmark-sources.md +1 -0
- package/skills/openprd-benchmark-router/references/source-policy.md +2 -0
- package/skills/openprd-discovery-loop/SKILL.md +2 -2
- package/skills/openprd-harness/SKILL.md +47 -25
- package/skills/openprd-harness/references/workflow-gates.md +15 -0
- package/skills/openprd-quality/SKILL.md +11 -5
- package/skills/openprd-requirement-intake/SKILL.md +31 -20
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -6
- package/skills/openprd-requirement-intake/references/routing-rubric.md +10 -2
- package/skills/openprd-router/SKILL.md +2 -2
- package/skills/openprd-shared/SKILL.md +51 -23
- package/skills/openprd-standards/SKILL.md +2 -1
- package/src/agent-integration.js +271 -71
- package/src/benchmark/constants.js +107 -0
- package/src/benchmark/operations.js +235 -0
- package/src/benchmark/registry.js +64 -0
- package/src/benchmark/render.js +115 -0
- package/src/benchmark/source.js +617 -0
- package/src/benchmark/storage.js +121 -0
- package/src/benchmark/verify.js +235 -0
- package/src/benchmark.js +50 -851
- package/src/change-summary.js +339 -0
- package/src/cli/args.js +67 -6
- package/src/cli/basic-print.js +365 -0
- package/src/cli/benchmark-print.js +91 -0
- package/src/cli/change-print.js +221 -0
- package/src/cli/doctor-print.js +268 -0
- package/src/cli/growth-print.js +176 -0
- package/src/cli/print.js +73 -1384
- package/src/cli/quality-print.js +284 -0
- package/src/cli/run-print.js +297 -0
- package/src/cli/shared-print.js +127 -0
- package/src/cli/workflow-print.js +195 -0
- package/src/codex-hook-runner-template.mjs +659 -124
- package/src/codex-runtime.js +324 -0
- package/src/dev-standards.js +178 -5
- package/src/diagram-core.js +5 -5
- package/src/discovery.js +2 -1
- package/src/execution-strategy.js +369 -0
- package/src/fleet.js +4 -0
- package/src/github-release.js +156 -0
- package/src/growth.js +311 -13
- package/src/html-artifact-utils.js +25 -0
- package/src/html-artifacts.js +157 -1596
- package/src/knowledge.js +1321 -76
- package/src/language-policy.js +2 -112
- package/src/learning-html-artifact.js +1031 -0
- package/src/learning-review.js +3 -2
- package/src/loop.js +280 -9
- package/src/openprd.js +341 -38
- package/src/openspec/change-validate.js +0 -9
- package/src/openspec/execute.js +79 -3
- package/src/openspec/generate.js +33 -20
- package/src/openspec/tasks.js +33 -2
- package/src/prd-core.js +10 -9
- package/src/product-type-copy.js +69 -0
- package/src/quality-html-artifact.js +108 -9
- package/src/quality-learning.js +30 -0
- package/src/quality-visual-review.js +237 -0
- package/src/quality.js +329 -43
- package/src/registry-hygiene.js +54 -0
- package/src/release-ledger.js +413 -0
- package/src/review-presentation.js +12 -6
- package/src/run-harness.js +722 -48
- package/src/session-binding.js +40 -3
- package/src/session-registry.js +159 -0
- package/src/standards.js +5 -3
- package/src/test-strategy.js +386 -0
- package/src/visual-compare.js +915 -34
- package/src/work-unit-migration.js +5 -1
- package/src/workspace-core.js +343 -19
- package/src/workspace-workflow.js +538 -134
package/src/openspec/execute.js
CHANGED
|
@@ -115,7 +115,57 @@ function assertTaskReady(task, state) {
|
|
|
115
115
|
return dependencyState;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
function isTaskEvidenceRequiredCommand(command) {
|
|
119
|
+
return /^openprd\s+tasks\s+\./i.test(String(command ?? ''))
|
|
120
|
+
&& /\s--evidence-required\b/i.test(String(command ?? ''));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isLegacyPerTaskFullVerifyCommand(command) {
|
|
124
|
+
return /^openprd\s+run\s+\.\s+--verify\s*$/i.test(String(command ?? '').replace(/\s+/g, ' ').trim());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function taskEvidenceState(task, options = {}) {
|
|
128
|
+
const metadata = task?.metadata ?? {};
|
|
129
|
+
const evidence = String(options.evidence ?? metadata.evidence ?? '').trim();
|
|
130
|
+
const waiver = String(metadata.waiver ?? metadata['waiver-reason'] ?? '').trim();
|
|
131
|
+
return {
|
|
132
|
+
evidence,
|
|
133
|
+
waiver,
|
|
134
|
+
ok: Boolean(evidence || waiver),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildTaskEvidenceVerification(command, task, options = {}) {
|
|
139
|
+
const evidenceState = taskEvidenceState(task, options);
|
|
140
|
+
const legacyHint = isLegacyPerTaskFullVerifyCommand(command)
|
|
141
|
+
? '旧任务里的 per-task openprd run . --verify 已保留给阶段或最终门禁,不会在任务推进时触发全局 quality。'
|
|
142
|
+
: null;
|
|
143
|
+
const hint = `${legacyHint ? `${legacyHint} ` : ''}先运行本任务最小足够测试或审查,再通过 --evidence <路径或摘要> 传入证据,或在 tasks.md 写入 evidence:/waiver-reason:。`;
|
|
144
|
+
return {
|
|
145
|
+
ok: evidenceState.ok,
|
|
146
|
+
command,
|
|
147
|
+
exitCode: evidenceState.ok ? 0 : 1,
|
|
148
|
+
stdout: evidenceState.ok
|
|
149
|
+
? [
|
|
150
|
+
'OpenPrd task evidence: passed',
|
|
151
|
+
evidenceState.evidence ? `evidence: ${evidenceState.evidence}` : null,
|
|
152
|
+
evidenceState.waiver ? `waiver: ${evidenceState.waiver}` : null,
|
|
153
|
+
legacyHint,
|
|
154
|
+
'scope: task-only; workspace quality is reserved for phase/final gates',
|
|
155
|
+
'',
|
|
156
|
+
].filter(Boolean).join('\n')
|
|
157
|
+
: '',
|
|
158
|
+
stderr: evidenceState.ok
|
|
159
|
+
? ''
|
|
160
|
+
: `OpenPrd task evidence: missing evidence for ${task?.id ?? 'task'}. ${hint}\n`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function runVerifyCommand(command, cwd, context = {}) {
|
|
165
|
+
if ((isTaskEvidenceRequiredCommand(command) || isLegacyPerTaskFullVerifyCommand(command)) && context.task) {
|
|
166
|
+
return buildTaskEvidenceVerification(command, context.task, context);
|
|
167
|
+
}
|
|
168
|
+
|
|
119
169
|
return new Promise((resolve) => {
|
|
120
170
|
const child = spawn(command, {
|
|
121
171
|
cwd,
|
|
@@ -152,6 +202,22 @@ async function runVerifyCommand(command, cwd) {
|
|
|
152
202
|
});
|
|
153
203
|
}
|
|
154
204
|
|
|
205
|
+
export async function checkOpenSpecTaskEvidenceWorkspace(projectRoot, options = {}) {
|
|
206
|
+
const state = await loadTaskState(projectRoot, options);
|
|
207
|
+
const task = resolveTaskSelection(state, options);
|
|
208
|
+
assertTaskReady(task, state);
|
|
209
|
+
const command = `openprd tasks . --change ${state.changeId} --item ${task.id} --evidence-required`;
|
|
210
|
+
const verification = buildTaskEvidenceVerification(command, task, options);
|
|
211
|
+
return {
|
|
212
|
+
ok: verification.ok,
|
|
213
|
+
action: 'evidence-check',
|
|
214
|
+
projectRoot,
|
|
215
|
+
changeId: state.changeId,
|
|
216
|
+
task,
|
|
217
|
+
verification,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
155
221
|
async function markTaskComplete(task) {
|
|
156
222
|
const text = await readText(task.absolutePath);
|
|
157
223
|
const lines = text.split(/\r?\n/);
|
|
@@ -208,7 +274,12 @@ export async function verifyOpenSpecTaskWorkspace(projectRoot, options = {}) {
|
|
|
208
274
|
if (!task.metadata.verify) {
|
|
209
275
|
throw new Error(`${task.id} is missing verify command.`);
|
|
210
276
|
}
|
|
211
|
-
const verification = await runVerifyCommand(task.metadata.verify, projectRoot
|
|
277
|
+
const verification = await runVerifyCommand(task.metadata.verify, projectRoot, {
|
|
278
|
+
task,
|
|
279
|
+
state,
|
|
280
|
+
evidence: options.evidence,
|
|
281
|
+
notes: options.notes,
|
|
282
|
+
});
|
|
212
283
|
await appendTaskEvent(state, {
|
|
213
284
|
action: 'verify',
|
|
214
285
|
taskId: task.id,
|
|
@@ -239,7 +310,12 @@ export async function advanceOpenSpecTaskWorkspace(projectRoot, options = {}) {
|
|
|
239
310
|
if (!task.metadata.verify) {
|
|
240
311
|
throw new Error(`${task.id} is missing verify command.`);
|
|
241
312
|
}
|
|
242
|
-
verification = await runVerifyCommand(task.metadata.verify, projectRoot
|
|
313
|
+
verification = await runVerifyCommand(task.metadata.verify, projectRoot, {
|
|
314
|
+
task,
|
|
315
|
+
state,
|
|
316
|
+
evidence: options.evidence,
|
|
317
|
+
notes: options.notes,
|
|
318
|
+
});
|
|
243
319
|
if (!verification.ok) {
|
|
244
320
|
await appendTaskEvent(state, {
|
|
245
321
|
action: 'advance_failed',
|
package/src/openspec/generate.js
CHANGED
|
@@ -2,6 +2,8 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { preferSimplifiedChinese } from '../language-policy.js';
|
|
4
4
|
import { needsBusinessGuardrails } from '../prd-core.js';
|
|
5
|
+
import { EXECUTION_STRATEGY_METADATA_KEYS, formatTaskExecutionStrategyMetadata } from '../execution-strategy.js';
|
|
6
|
+
import { TEST_STRATEGY_METADATA_KEYS, formatTaskTestStrategyMetadata } from '../test-strategy.js';
|
|
5
7
|
import { OPENSPEC_TASK_MAX_ITEMS_PER_FILE } from './constants.js';
|
|
6
8
|
import { openPrdChangeRoot, openPrdDiscoveryConfigPath, readDiscoveryConfig } from './paths.js';
|
|
7
9
|
|
|
@@ -143,7 +145,12 @@ function chunkItems(items, maxItemsPerChunk = 2) {
|
|
|
143
145
|
return chunks;
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
|
|
148
|
+
function defaultTaskVerifyCommand(changeId, task) {
|
|
149
|
+
if (task.type === 'documentation') {
|
|
150
|
+
return 'openprd standards . --verify';
|
|
151
|
+
}
|
|
152
|
+
return `openprd tasks . --change ${changeId} --item ${task.id} --evidence-required`;
|
|
153
|
+
}
|
|
147
154
|
|
|
148
155
|
const ARCHITECTURE_TASK_DEFINITIONS = [
|
|
149
156
|
{
|
|
@@ -321,7 +328,6 @@ function inferArchitectureTasks(snapshot) {
|
|
|
321
328
|
type: 'implementation',
|
|
322
329
|
title: definition.title,
|
|
323
330
|
done: `${definition.done} 涉及: ${summarizeTaskItems(matches, 2, 72)}。`,
|
|
324
|
-
verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
|
|
325
331
|
phase: 'architecture',
|
|
326
332
|
}));
|
|
327
333
|
}
|
|
@@ -345,7 +351,6 @@ function buildRequirementImplementationTasks(snapshot) {
|
|
|
345
351
|
type: 'implementation',
|
|
346
352
|
title: cleanImplementationTitle(item),
|
|
347
353
|
done: buildDoneText('已完成:', item),
|
|
348
|
-
verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
|
|
349
354
|
phase: 'implementation',
|
|
350
355
|
});
|
|
351
356
|
}
|
|
@@ -364,7 +369,6 @@ function buildFlowIntegrationTasks(snapshot) {
|
|
|
364
369
|
type: 'implementation',
|
|
365
370
|
title: `打通主流程闭环:${summarizeTaskItems(flows, 2, 56)}`,
|
|
366
371
|
done: `主流程关键节点已经打通,用户可以按预期从入口走到结果收尾。涉及: ${summarizeTaskItems(flows, 2, 72)}。`,
|
|
367
|
-
verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
|
|
368
372
|
phase: 'integration',
|
|
369
373
|
}];
|
|
370
374
|
}
|
|
@@ -376,7 +380,6 @@ function buildAcceptanceVerificationTasks(snapshot) {
|
|
|
376
380
|
type: 'verification',
|
|
377
381
|
title: buildVerificationTitle(item),
|
|
378
382
|
done: buildDoneText('已验证:', item),
|
|
379
|
-
verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
|
|
380
383
|
phase: 'verification',
|
|
381
384
|
}));
|
|
382
385
|
}
|
|
@@ -388,7 +391,6 @@ function buildNonFunctionalVerificationTasks(snapshot) {
|
|
|
388
391
|
type: 'verification',
|
|
389
392
|
title: `回归非功能约束:${summarizeTaskItems(items, 2, 56)}`,
|
|
390
393
|
done: `非功能约束已经回归确认。涉及: ${summarizeTaskItems(items, 2, 72)}。`,
|
|
391
|
-
verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
|
|
392
394
|
phase: 'verification',
|
|
393
395
|
}));
|
|
394
396
|
const edgeAndFailure = [
|
|
@@ -401,7 +403,6 @@ function buildNonFunctionalVerificationTasks(snapshot) {
|
|
|
401
403
|
type: 'verification',
|
|
402
404
|
title: `回归边界条件与失败处理:${summarizeTaskItems(edgeAndFailure, 2, 56)}`,
|
|
403
405
|
done: `边界条件与失败处理已经回归确认。涉及: ${summarizeTaskItems(edgeAndFailure, 2, 72)}。`,
|
|
404
|
-
verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
|
|
405
406
|
phase: 'verification',
|
|
406
407
|
}]
|
|
407
408
|
: [];
|
|
@@ -430,7 +431,6 @@ function buildTaskItems({ changeId, snapshot, capability }) {
|
|
|
430
431
|
type: 'verification',
|
|
431
432
|
title: '验证成本与额度护栏',
|
|
432
433
|
done: '已验证免费、试用或低权限用户不能绕过额度、并发、频率或总量限制',
|
|
433
|
-
verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
|
|
434
434
|
phase: 'verification',
|
|
435
435
|
},
|
|
436
436
|
{
|
|
@@ -438,7 +438,6 @@ function buildTaskItems({ changeId, snapshot, capability }) {
|
|
|
438
438
|
type: 'verification',
|
|
439
439
|
title: '验证滥用与越权路径',
|
|
440
440
|
done: '已覆盖重复请求、并发请求、越权身份和异常恢复等负向场景',
|
|
441
|
-
verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
|
|
442
441
|
phase: 'verification',
|
|
443
442
|
},
|
|
444
443
|
{
|
|
@@ -446,7 +445,6 @@ function buildTaskItems({ changeId, snapshot, capability }) {
|
|
|
446
445
|
type: 'verification',
|
|
447
446
|
title: '验证成本监控、报警和止损',
|
|
448
447
|
done: '已确认用量或成本信号、报警阈值和人工/自动止损动作可执行',
|
|
449
|
-
verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
|
|
450
448
|
phase: 'verification',
|
|
451
449
|
},
|
|
452
450
|
]
|
|
@@ -492,15 +490,18 @@ function buildTaskItems({ changeId, snapshot, capability }) {
|
|
|
492
490
|
deduped.push(item);
|
|
493
491
|
}
|
|
494
492
|
|
|
495
|
-
const tasks = deduped.map((item, index) =>
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
493
|
+
const tasks = deduped.map((item, index) => {
|
|
494
|
+
const task = {
|
|
495
|
+
id: `T001.${String(index + 1).padStart(2, '0')}`,
|
|
496
|
+
title: item.title,
|
|
497
|
+
type: item.type,
|
|
498
|
+
phase: item.phase,
|
|
499
|
+
done: item.done,
|
|
500
|
+
deps: [],
|
|
501
|
+
};
|
|
502
|
+
task.verify = item.verify ?? defaultTaskVerifyCommand(changeId, task);
|
|
503
|
+
return task;
|
|
504
|
+
});
|
|
504
505
|
|
|
505
506
|
const phaseTasks = {
|
|
506
507
|
governanceStart: tasks.filter((task) => task.phase === 'governance-start'),
|
|
@@ -582,6 +583,12 @@ function renderTaskFiles(tasks, maxItemsPerFile) {
|
|
|
582
583
|
}
|
|
583
584
|
lines.push(` - done: ${task.done}`);
|
|
584
585
|
lines.push(` - verify: ${task.verify}`);
|
|
586
|
+
for (const metadata of formatTaskTestStrategyMetadata(task)) {
|
|
587
|
+
lines.push(` - ${metadata}`);
|
|
588
|
+
}
|
|
589
|
+
for (const metadata of formatTaskExecutionStrategyMetadata(task)) {
|
|
590
|
+
lines.push(` - ${metadata}`);
|
|
591
|
+
}
|
|
585
592
|
lines.push('');
|
|
586
593
|
}
|
|
587
594
|
|
|
@@ -606,6 +613,12 @@ async function readTaskMax(projectRoot) {
|
|
|
606
613
|
async function writeDiscoveryConfig(projectRoot, changeId) {
|
|
607
614
|
const configPath = openPrdDiscoveryConfigPath(projectRoot);
|
|
608
615
|
const current = await readJson(configPath).catch(() => ({}));
|
|
616
|
+
const optionalMetadata = [
|
|
617
|
+
'deps',
|
|
618
|
+
'type',
|
|
619
|
+
...TEST_STRATEGY_METADATA_KEYS,
|
|
620
|
+
...EXECUTION_STRATEGY_METADATA_KEYS,
|
|
621
|
+
];
|
|
609
622
|
await writeJson(configPath, {
|
|
610
623
|
...current,
|
|
611
624
|
activeChange: changeId,
|
|
@@ -618,7 +631,7 @@ async function writeDiscoveryConfig(projectRoot, changeId) {
|
|
|
618
631
|
taskMetadata: {
|
|
619
632
|
stableIdPattern: current?.taskMetadata?.stableIdPattern ?? 'T###.##',
|
|
620
633
|
required: current?.taskMetadata?.required ?? ['done', 'verify'],
|
|
621
|
-
optional: current?.taskMetadata?.optional ?? [
|
|
634
|
+
optional: [...new Set([...(current?.taskMetadata?.optional ?? []), ...optionalMetadata])],
|
|
622
635
|
dependencyOrder: current?.taskMetadata?.dependencyOrder ?? 'dependencies must appear before dependents',
|
|
623
636
|
},
|
|
624
637
|
});
|
package/src/openspec/tasks.js
CHANGED
|
@@ -6,6 +6,29 @@ import {
|
|
|
6
6
|
OPENSPEC_TASK_MAX_ITEMS_PER_FILE,
|
|
7
7
|
} from './constants.js';
|
|
8
8
|
import { cjoin, exists, listChangeDirs, readDiscoveryConfig, resolveChangeDir } from './paths.js';
|
|
9
|
+
import {
|
|
10
|
+
TEST_STRATEGY_METADATA_KEYS,
|
|
11
|
+
summarizeTaskTestStrategies,
|
|
12
|
+
validateTaskTestStrategy,
|
|
13
|
+
} from '../test-strategy.js';
|
|
14
|
+
import {
|
|
15
|
+
EXECUTION_STRATEGY_METADATA_KEYS,
|
|
16
|
+
summarizeTaskExecutionStrategies,
|
|
17
|
+
validateTaskExecutionStrategy,
|
|
18
|
+
} from '../execution-strategy.js';
|
|
19
|
+
|
|
20
|
+
const OPENSPEC_TASK_METADATA_KEYS = [
|
|
21
|
+
'deps',
|
|
22
|
+
'done',
|
|
23
|
+
'verify',
|
|
24
|
+
'type',
|
|
25
|
+
'category',
|
|
26
|
+
'kind',
|
|
27
|
+
'oracle',
|
|
28
|
+
...TEST_STRATEGY_METADATA_KEYS,
|
|
29
|
+
...EXECUTION_STRATEGY_METADATA_KEYS,
|
|
30
|
+
];
|
|
31
|
+
const OPENSPEC_TASK_METADATA_PATTERN = new RegExp(`^\\s{2,}-\\s+(${OPENSPEC_TASK_METADATA_KEYS.join('|')}):\\s*(.*)$`, 'i');
|
|
9
32
|
|
|
10
33
|
async function readText(filePath) {
|
|
11
34
|
return fs.readFile(filePath, 'utf8');
|
|
@@ -58,7 +81,7 @@ export function parseOpenSpecTaskFile(text) {
|
|
|
58
81
|
return;
|
|
59
82
|
}
|
|
60
83
|
|
|
61
|
-
const metadataMatch = line.match(
|
|
84
|
+
const metadataMatch = line.match(OPENSPEC_TASK_METADATA_PATTERN);
|
|
62
85
|
if (currentTask && metadataMatch) {
|
|
63
86
|
currentTask.metadata[metadataMatch[1].toLowerCase()] = metadataMatch[2].trim();
|
|
64
87
|
}
|
|
@@ -272,6 +295,12 @@ export function validateOpenSpecStructuredTasks(sortedFiles, errors, checks) {
|
|
|
272
295
|
if (normalizedType !== 'governance' && isSpecOnlyValidateCommand(task.metadata.verify)) {
|
|
273
296
|
errors.push(`${formatOpenSpecTaskLocation(task)} 的 verify 只做了 change 结构校验;${normalizedType} 任务必须提供能证明实际落地的验证命令或审查步骤。`);
|
|
274
297
|
}
|
|
298
|
+
for (const strategyError of validateTaskTestStrategy(task)) {
|
|
299
|
+
errors.push(`${formatOpenSpecTaskLocation(task)} ${strategyError}`);
|
|
300
|
+
}
|
|
301
|
+
for (const strategyError of validateTaskExecutionStrategy(task)) {
|
|
302
|
+
errors.push(`${formatOpenSpecTaskLocation(task)} ${strategyError}`);
|
|
303
|
+
}
|
|
275
304
|
|
|
276
305
|
for (const depId of parseOpenSpecTaskDeps(task.metadata.deps)) {
|
|
277
306
|
if (!OPENSPEC_TASK_ID_PATTERN.test(depId)) {
|
|
@@ -290,7 +319,9 @@ export function validateOpenSpecStructuredTasks(sortedFiles, errors, checks) {
|
|
|
290
319
|
}
|
|
291
320
|
}
|
|
292
321
|
|
|
293
|
-
|
|
322
|
+
const strategySummary = summarizeTaskTestStrategies(tasks);
|
|
323
|
+
const executionSummary = summarizeTaskExecutionStrategies(tasks);
|
|
324
|
+
checks.push(`结构化 OpenPrd 任务: ${tasks.length} 个任务,${dependencyCount} 条依赖;测试策略显式 ${strategySummary.explicit} 个、推导 ${strategySummary.inferred} 个;执行策略显式 ${executionSummary.explicit} 个、推导 ${executionSummary.inferred} 个。`);
|
|
294
325
|
}
|
|
295
326
|
|
|
296
327
|
export async function analyzeOpenSpecTaskVolumes(projectRoot, options = {}) {
|
package/src/prd-core.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { buildArchitectureDiagramModel, buildProductFlowDiagramModel, renderDiagramMermaidFromModel } from './diagram-core.js';
|
|
3
3
|
import { TBD_ZH, languagePolicyLines } from './language-policy.js';
|
|
4
|
+
import { formatProductTypeDisplay, formatProductTypeQuestion, formatTemplatePackDisplay, getProductTypeSectionTitle } from './product-type-copy.js';
|
|
4
5
|
import { timestamp } from './time.js';
|
|
5
6
|
|
|
6
7
|
function isPlainObject(value) {
|
|
@@ -151,7 +152,7 @@ function buildTypeSpecificSection(productType, state, overrides) {
|
|
|
151
152
|
if (productType === 'consumer') {
|
|
152
153
|
return {
|
|
153
154
|
kind: 'consumer',
|
|
154
|
-
title: '
|
|
155
|
+
title: getProductTypeSectionTitle('consumer'),
|
|
155
156
|
fields: {
|
|
156
157
|
persona: pickValue(overrides.persona, state.persona),
|
|
157
158
|
segment: pickValue(overrides.segment, state.segment),
|
|
@@ -165,7 +166,7 @@ function buildTypeSpecificSection(productType, state, overrides) {
|
|
|
165
166
|
if (productType === 'b2b') {
|
|
166
167
|
return {
|
|
167
168
|
kind: 'b2b',
|
|
168
|
-
title: '
|
|
169
|
+
title: getProductTypeSectionTitle('b2b'),
|
|
169
170
|
fields: {
|
|
170
171
|
buyer: pickValue(overrides.buyer, state.buyer),
|
|
171
172
|
user: pickValue(overrides.user, state.user),
|
|
@@ -183,7 +184,7 @@ function buildTypeSpecificSection(productType, state, overrides) {
|
|
|
183
184
|
if (productType === 'agent') {
|
|
184
185
|
return {
|
|
185
186
|
kind: 'agent',
|
|
186
|
-
title: '
|
|
187
|
+
title: getProductTypeSectionTitle('agent'),
|
|
187
188
|
fields: {
|
|
188
189
|
humanAgentContract: pickValue(overrides.humanAgentContract, state.humanAgentContract),
|
|
189
190
|
autonomyBoundary: pickValue(overrides.autonomyBoundary, state.autonomyBoundary),
|
|
@@ -196,9 +197,9 @@ function buildTypeSpecificSection(productType, state, overrides) {
|
|
|
196
197
|
|
|
197
198
|
return {
|
|
198
199
|
kind: 'base',
|
|
199
|
-
title: '
|
|
200
|
+
title: getProductTypeSectionTitle('base'),
|
|
200
201
|
fields: {
|
|
201
|
-
note: '
|
|
202
|
+
note: '请选择产品场景,以启用对应的专项 PRD 模块。',
|
|
202
203
|
},
|
|
203
204
|
};
|
|
204
205
|
}
|
|
@@ -338,8 +339,8 @@ export function renderPrdMarkdown(snapshot) {
|
|
|
338
339
|
...languagePolicyLines(),
|
|
339
340
|
`- 版本: ${snapshot.versionId}`,
|
|
340
341
|
`- 负责人: ${snapshot.owner}`,
|
|
341
|
-
`-
|
|
342
|
-
`-
|
|
342
|
+
`- 产品场景: ${formatProductTypeDisplay(snapshot.productType, { fallback: '待确认' })}`,
|
|
343
|
+
`- 场景模板: ${formatTemplatePackDisplay(snapshot.templatePack, { fallback: '待确认' })}`,
|
|
343
344
|
`- 状态: ${snapshot.status}`,
|
|
344
345
|
`- 生成时间: ${snapshot.createdAt}`,
|
|
345
346
|
'',
|
|
@@ -348,7 +349,7 @@ export function renderPrdMarkdown(snapshot) {
|
|
|
348
349
|
['负责人', sections.meta.owner],
|
|
349
350
|
['状态', sections.meta.status],
|
|
350
351
|
['版本', sections.meta.version],
|
|
351
|
-
['
|
|
352
|
+
['产品场景', formatProductTypeDisplay(snapshot.productType, { fallback: '待确认' })],
|
|
352
353
|
['日期', sections.meta.date],
|
|
353
354
|
]),
|
|
354
355
|
renderSection('问题', [
|
|
@@ -421,7 +422,7 @@ const BASE_REQUIRED_FIELD_DESCRIPTORS = [
|
|
|
421
422
|
{ section: 'meta', path: 'meta.owner', label: '负责人', prompt: '谁负责这份 PRD?' },
|
|
422
423
|
{ section: 'meta', path: 'meta.version', label: '版本', prompt: '这份 PRD 从哪个版本开始?' },
|
|
423
424
|
{ section: 'meta', path: 'meta.status', label: '状态', prompt: '当前 PRD 状态是什么?' },
|
|
424
|
-
{ section: 'meta', path: 'meta.productType', label: '
|
|
425
|
+
{ section: 'meta', path: 'meta.productType', label: '产品场景', prompt: formatProductTypeQuestion() },
|
|
425
426
|
{ section: 'problem', path: 'problem.problemStatement', label: '问题陈述', prompt: '我们要解决什么问题?' },
|
|
426
427
|
{ section: 'problem', path: 'problem.whyNow', label: '为什么是现在', prompt: '为什么现在是解决这个问题的合适时机?' },
|
|
427
428
|
{ section: 'problem', path: 'problem.evidence', label: '证据', prompt: '有哪些证据支持这个问题?' },
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const PRODUCT_TYPE_LABELS = {
|
|
2
|
+
base: '通用产品或工程场景',
|
|
3
|
+
consumer: '面向个人消费者场景',
|
|
4
|
+
b2b: '面向企业服务场景',
|
|
5
|
+
agent: '以 Agent 为主要使用场景',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function withCode(label, code, includeCode) {
|
|
9
|
+
return includeCode ? `${label}(${code})` : label;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatProductTypeDisplay(productType, options = {}) {
|
|
13
|
+
const { includeCode = false, fallback = '待确认' } = options;
|
|
14
|
+
const label = PRODUCT_TYPE_LABELS[productType];
|
|
15
|
+
return label ? withCode(label, productType, includeCode) : fallback;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatTemplatePackDisplay(templatePack, options = {}) {
|
|
19
|
+
const { includeCode = false, fallback = '待确认' } = options;
|
|
20
|
+
const label = PRODUCT_TYPE_LABELS[templatePack];
|
|
21
|
+
if (label) return withCode(label, templatePack, includeCode);
|
|
22
|
+
if (templatePack === null || templatePack === undefined || `${templatePack}`.trim() === "") return fallback;
|
|
23
|
+
return includeCode ? `${templatePack}(custom)` : `${templatePack}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatProductTypeSentence(productType, options = {}) {
|
|
27
|
+
const { inferred = false } = options;
|
|
28
|
+
switch (productType) {
|
|
29
|
+
case 'consumer':
|
|
30
|
+
return inferred ? '从当前描述看,更像面向个人消费者场景的产品。' : '当前更像面向个人消费者场景的产品。';
|
|
31
|
+
case 'b2b':
|
|
32
|
+
return inferred ? '从当前描述看,更像面向企业服务场景的产品。' : '当前更像面向企业服务场景的产品。';
|
|
33
|
+
case 'agent':
|
|
34
|
+
return inferred ? '从当前描述看,更像以 Agent 为主要使用场景的产品。' : '当前更像以 Agent 为主要使用场景的产品。';
|
|
35
|
+
case 'base':
|
|
36
|
+
return inferred ? '从当前描述看,更像通用产品或工程场景。' : '当前更像通用产品或工程场景。';
|
|
37
|
+
default:
|
|
38
|
+
return inferred ? '从当前描述看,产品场景仍待确认。' : '产品场景仍待确认。';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatProductTypeOptions(options = {}) {
|
|
43
|
+
const { includeBase = false, includeCode = false } = options;
|
|
44
|
+
const values = [];
|
|
45
|
+
if (includeBase) values.push(withCode(PRODUCT_TYPE_LABELS.base, 'base', includeCode));
|
|
46
|
+
values.push(withCode(PRODUCT_TYPE_LABELS.consumer, 'consumer', includeCode));
|
|
47
|
+
values.push(withCode(PRODUCT_TYPE_LABELS.b2b, 'b2b', includeCode));
|
|
48
|
+
values.push(withCode(PRODUCT_TYPE_LABELS.agent, 'agent', includeCode));
|
|
49
|
+
return values.join(' / ');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatProductTypeQuestion() {
|
|
53
|
+
return '这是更偏向面向个人消费者场景的产品、面向企业服务场景的产品,还是以 Agent 为主要使用场景的产品?';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getProductTypeSectionTitle(productType) {
|
|
57
|
+
switch (productType) {
|
|
58
|
+
case 'consumer':
|
|
59
|
+
return '个人消费者场景专项';
|
|
60
|
+
case 'b2b':
|
|
61
|
+
return '企业服务场景专项';
|
|
62
|
+
case 'agent':
|
|
63
|
+
return 'Agent 使用场景专项';
|
|
64
|
+
case 'base':
|
|
65
|
+
return '通用场景专项';
|
|
66
|
+
default:
|
|
67
|
+
return '产品场景专项';
|
|
68
|
+
}
|
|
69
|
+
}
|