@peterxiaoyang/superspec 0.1.55 → 0.1.57
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/README.md +28 -32
- package/dist/approved_ref.d.ts +26 -0
- package/dist/approved_ref.js +159 -0
- package/dist/cli.js +2 -0
- package/dist/code_review.d.ts +7 -0
- package/dist/code_review.js +92 -1
- package/dist/format.d.ts +25 -1
- package/dist/format.js +235 -1
- package/dist/next.js +4 -1
- package/dist/phase_confirmation.js +7 -2
- package/dist/phase_plan.d.ts +6 -1
- package/dist/phase_plan.js +172 -44
- package/dist/propose_round.d.ts +7 -1
- package/dist/propose_round.js +35 -0
- package/dist/record.js +27 -7
- package/dist/transition.js +22 -7
- package/dist/types.d.ts +28 -1
- package/dist/workflow_config.d.ts +15 -0
- package/dist/workflow_config.js +47 -0
- package/package.json +1 -1
- package/templates/workflow/AGENTS.md +2 -0
- package/templates/workflow/agents-md/code-reviewer.md +1 -1
- package/templates/workflow/agents-md/critic.md +1 -1
- package/templates/workflow/prompts/architect.md +1 -0
- package/templates/workflow/prompts/code-reviewer.md +4 -4
- package/templates/workflow/prompts/critic.md +3 -1
- package/templates/workflow/prompts/executor.md +2 -0
- package/templates/workflow/prompts/explore.md +1 -1
- package/templates/workflow/skills/superspec-explore/SKILL.md +16 -12
- package/templates/workflow/skills/superspec-propose/SKILL.md +27 -4
package/dist/format.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// 所有可机械判定的文档协议、格式定义和解析逻辑都在这里。skills 只指导
|
|
4
4
|
// 生成与语义判断,不得自行充当格式校验器或在其它地方重复解析。
|
|
5
|
-
import { readFileSync, existsSync, realpathSync } from "node:fs";
|
|
5
|
+
import { readFileSync, existsSync, realpathSync, statSync } from "node:fs";
|
|
6
6
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { sha256Text } from "./store.js";
|
|
8
8
|
import { GREEN_ONLY_NO_TDD_REASON } from "./types.js";
|
|
@@ -321,6 +321,240 @@ export function collectProposeOpenQuestions(changeRoot) {
|
|
|
321
321
|
files,
|
|
322
322
|
};
|
|
323
323
|
}
|
|
324
|
+
// ===== design.md 结构变更清单 =====
|
|
325
|
+
//
|
|
326
|
+
// 格式(状态机校验,propose skill 负责生成):
|
|
327
|
+
// ## 结构变更清单
|
|
328
|
+
// 无
|
|
329
|
+
// 或
|
|
330
|
+
// | ID | 类别 | 变更 | 需求依据 | 决定 |
|
|
331
|
+
// |---|---|---|---|---|
|
|
332
|
+
// | SC-001 | 新增持久化结构 | ... | specs/x/spec.md#Requirement: ... | DEC-001 |
|
|
333
|
+
//
|
|
334
|
+
// 需决定类别的依据只接受 specs Requirement 或 TEST;仅展示类别可另接受
|
|
335
|
+
// proposal(Impact 除外)与 discovery 的标题。所有依据都必须能在当前 change 中解析。
|
|
336
|
+
export const STRUCTURE_CHANGE_CATEGORIES = [
|
|
337
|
+
"新增持久化结构",
|
|
338
|
+
"迁移或回填",
|
|
339
|
+
"功能开关",
|
|
340
|
+
"新增公共接口",
|
|
341
|
+
"删除既有路径",
|
|
342
|
+
"改既有公共签名",
|
|
343
|
+
"改变既有数据语义",
|
|
344
|
+
"新增公共类型",
|
|
345
|
+
];
|
|
346
|
+
export const STRUCTURE_DECISION_REQUIRED_CATEGORIES = new Set([
|
|
347
|
+
"新增持久化结构",
|
|
348
|
+
"迁移或回填",
|
|
349
|
+
"功能开关",
|
|
350
|
+
"新增公共接口",
|
|
351
|
+
"删除既有路径",
|
|
352
|
+
"改既有公共签名",
|
|
353
|
+
"改变既有数据语义",
|
|
354
|
+
]);
|
|
355
|
+
const STRUCTURE_LEDGER_HEADINGS = ["结构变更清单"];
|
|
356
|
+
const STRUCTURE_LEDGER_COLUMNS = ["ID", "类别", "变更", "需求依据", "决定"];
|
|
357
|
+
const STRUCTURE_LEDGER_ID_RE = /^SC-[A-Za-z0-9_-]+$/;
|
|
358
|
+
const STRUCTURE_LEDGER_NONE_RE = /^\s*无\s*$/;
|
|
359
|
+
const STRUCTURE_TEST_ID_RE = /^TEST-[A-Za-z0-9_-]+$/;
|
|
360
|
+
const DEC_ID_RE = /^DEC-[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
361
|
+
const DISPLAY_ONLY_DECISION_MARKERS = new Set(["—", "-", "–"]);
|
|
362
|
+
const STRUCTURE_LEDGER_MISSING_MESSAGE = 'design.md 缺少 ## 结构变更清单;没有结构变更时在该标题下写"无"';
|
|
363
|
+
const STRUCTURE_LEDGER_FORMAT_MESSAGE = `## 结构变更清单 一节只能是"无"或一张含 ${STRUCTURE_LEDGER_COLUMNS.join("、")} 列的表格,其他说明写到别的标题下`;
|
|
364
|
+
export function isStructureChangeCategory(value) {
|
|
365
|
+
return STRUCTURE_CHANGE_CATEGORIES.includes(value);
|
|
366
|
+
}
|
|
367
|
+
export function parseStructureChangeLedger(content) {
|
|
368
|
+
const sectionBody = sectionBodyByHeadings(content, STRUCTURE_LEDGER_HEADINGS);
|
|
369
|
+
if (sectionBody == null) {
|
|
370
|
+
return { present: false, none: false, entries: [] };
|
|
371
|
+
}
|
|
372
|
+
if (STRUCTURE_LEDGER_NONE_RE.test(sectionBody)) {
|
|
373
|
+
return { present: true, none: true, entries: [] };
|
|
374
|
+
}
|
|
375
|
+
const tableLines = sectionBody.split("\n").filter(line => line.trim().startsWith("|"));
|
|
376
|
+
if (tableLines.length < 2) {
|
|
377
|
+
return { present: true, none: false, entries: [], format_error: STRUCTURE_LEDGER_FORMAT_MESSAGE };
|
|
378
|
+
}
|
|
379
|
+
const header = splitMarkdownTableRow(tableLines[0]);
|
|
380
|
+
if (header.length === 0 || !isMarkdownTableSeparator(tableLines[1])) {
|
|
381
|
+
return { present: true, none: false, entries: [], format_error: STRUCTURE_LEDGER_FORMAT_MESSAGE };
|
|
382
|
+
}
|
|
383
|
+
const columnIndexes = Object.fromEntries(STRUCTURE_LEDGER_COLUMNS.map(col => [col, header.indexOf(col)]));
|
|
384
|
+
if (STRUCTURE_LEDGER_COLUMNS.some(col => columnIndexes[col] < 0)) {
|
|
385
|
+
return { present: true, none: false, entries: [], format_error: STRUCTURE_LEDGER_FORMAT_MESSAGE };
|
|
386
|
+
}
|
|
387
|
+
const entries = [];
|
|
388
|
+
for (const row of tableLines.slice(2).map(splitMarkdownTableRow).filter(cells => cells.length > 0)) {
|
|
389
|
+
entries.push({
|
|
390
|
+
id: (row[columnIndexes.ID] ?? "").trim(),
|
|
391
|
+
category: (row[columnIndexes["类别"]] ?? "").trim(),
|
|
392
|
+
change: (row[columnIndexes["变更"]] ?? "").trim(),
|
|
393
|
+
basis: (row[columnIndexes["需求依据"]] ?? "").trim(),
|
|
394
|
+
decision: (row[columnIndexes["决定"]] ?? "").trim(),
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
if (entries.length === 0) {
|
|
398
|
+
return { present: true, none: false, entries: [], format_error: STRUCTURE_LEDGER_FORMAT_MESSAGE };
|
|
399
|
+
}
|
|
400
|
+
return { present: true, none: false, entries };
|
|
401
|
+
}
|
|
402
|
+
function readChangeDocument(changeRoot, relPath) {
|
|
403
|
+
const root = resolve(changeRoot);
|
|
404
|
+
const target = resolve(root, relPath);
|
|
405
|
+
if (!isPathInside(root, target))
|
|
406
|
+
return null;
|
|
407
|
+
try {
|
|
408
|
+
if (!existsSync(target) || !statSync(target).isFile())
|
|
409
|
+
return null;
|
|
410
|
+
return readFileSync(target, "utf8");
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/** Markdown 标题是否存在(任意层级,允许闭合 #)。供文档引用解析共用。 */
|
|
417
|
+
export function headingExists(content, title) {
|
|
418
|
+
const heading = new RegExp(`^#{1,6}[\\t ]+${escapeRegex(title)}(?:[\\t ]+#+)?[\\t ]*$`, "m");
|
|
419
|
+
return heading.test(content);
|
|
420
|
+
}
|
|
421
|
+
function splitBasisRef(basis) {
|
|
422
|
+
const separator = basis.indexOf("#");
|
|
423
|
+
if (separator <= 0 || separator === basis.length - 1)
|
|
424
|
+
return null;
|
|
425
|
+
return {
|
|
426
|
+
path: basis.slice(0, separator).replace(/\\/g, "/"),
|
|
427
|
+
anchor: basis.slice(separator + 1).trim(),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* 结构变更清单"需求依据"的唯一解析入口。与 approved_refs 的解析分开:
|
|
432
|
+
* 这里接受 discovery 标题、拒绝 proposal.md#Impact,且按类别收紧允许集。
|
|
433
|
+
*/
|
|
434
|
+
export function resolveStructureBasisRef(changeRoot, raw, category) {
|
|
435
|
+
if (typeof raw !== "string" || raw.trim() === "") {
|
|
436
|
+
return { ok: false, reason: "需求依据必须是非空字符串" };
|
|
437
|
+
}
|
|
438
|
+
const basis = raw.trim();
|
|
439
|
+
if (/[\u0000-\u001f\u007f]/.test(basis)) {
|
|
440
|
+
return { ok: false, reason: `需求依据不能包含换行等控制字符:${JSON.stringify(basis)}` };
|
|
441
|
+
}
|
|
442
|
+
const decisionRequired = STRUCTURE_DECISION_REQUIRED_CATEGORIES.has(category);
|
|
443
|
+
const allowedHint = decisionRequired
|
|
444
|
+
? "需决定类别请引用 specs Requirement 或 TEST"
|
|
445
|
+
: "请引用 specs Requirement、TEST、proposal 标题(Impact 除外)或 discovery 标题";
|
|
446
|
+
if (STRUCTURE_TEST_ID_RE.test(basis)) {
|
|
447
|
+
const content = readChangeDocument(changeRoot, join(".superspec", "artifacts", "test-contract.md"));
|
|
448
|
+
if (content == null)
|
|
449
|
+
return { ok: false, reason: `无法读取 test-contract.md,无法校验 TEST 依据:${basis}` };
|
|
450
|
+
const parsed = parseTestContractEntries(content);
|
|
451
|
+
if (!parsed.ok || !parsed.entries.some(entry => entry.test_id === basis)) {
|
|
452
|
+
return { ok: false, reason: `test-contract.md 中不存在 ${basis}` };
|
|
453
|
+
}
|
|
454
|
+
return { ok: true, value: basis };
|
|
455
|
+
}
|
|
456
|
+
const ref = splitBasisRef(basis);
|
|
457
|
+
if (!ref)
|
|
458
|
+
return { ok: false, reason: `需求依据格式无法解析,应为 文件#标题 或 TEST-ID:${basis};${allowedHint}` };
|
|
459
|
+
if (/^specs\/[^/]+\/spec\.md$/.test(ref.path)) {
|
|
460
|
+
const requirementTitle = ref.anchor.startsWith("Requirement:")
|
|
461
|
+
? ref.anchor.slice("Requirement:".length).trim()
|
|
462
|
+
: "";
|
|
463
|
+
if (!requirementTitle)
|
|
464
|
+
return { ok: false, reason: `spec 依据必须以 Requirement: 开头:${basis}` };
|
|
465
|
+
const content = readChangeDocument(changeRoot, ref.path);
|
|
466
|
+
if (content == null)
|
|
467
|
+
return { ok: false, reason: `${ref.path} 在当前 change 中不存在` };
|
|
468
|
+
if (!headingExists(content, `Requirement: ${requirementTitle}`)) {
|
|
469
|
+
return { ok: false, reason: `${ref.path} 中不存在 Requirement「${requirementTitle}」` };
|
|
470
|
+
}
|
|
471
|
+
return { ok: true, value: basis };
|
|
472
|
+
}
|
|
473
|
+
if (ref.path === "proposal.md" && ref.anchor === "Impact") {
|
|
474
|
+
return { ok: false, reason: `proposal.md#Impact 不能作为结构依据;${allowedHint}` };
|
|
475
|
+
}
|
|
476
|
+
if (decisionRequired) {
|
|
477
|
+
return { ok: false, reason: `需求依据 ${basis} 不满足类别要求;${allowedHint}` };
|
|
478
|
+
}
|
|
479
|
+
if (ref.path === "proposal.md") {
|
|
480
|
+
const content = readChangeDocument(changeRoot, "proposal.md");
|
|
481
|
+
if (content == null)
|
|
482
|
+
return { ok: false, reason: "proposal.md 在当前 change 中不存在" };
|
|
483
|
+
if (!headingExists(content, ref.anchor))
|
|
484
|
+
return { ok: false, reason: `proposal.md 中不存在标题「${ref.anchor}」` };
|
|
485
|
+
return { ok: true, value: basis };
|
|
486
|
+
}
|
|
487
|
+
const discoveryRel = ".superspec/artifacts/discovery.md";
|
|
488
|
+
if (ref.path === "discovery.md" || ref.path === discoveryRel) {
|
|
489
|
+
const content = readChangeDocument(changeRoot, join(".superspec", "artifacts", "discovery.md"));
|
|
490
|
+
if (content == null)
|
|
491
|
+
return { ok: false, reason: "discovery.md 在当前 change 中不存在" };
|
|
492
|
+
if (!headingExists(content, ref.anchor))
|
|
493
|
+
return { ok: false, reason: `discovery.md 中不存在标题「${ref.anchor}」` };
|
|
494
|
+
return { ok: true, value: basis };
|
|
495
|
+
}
|
|
496
|
+
return { ok: false, reason: `需求依据 ${basis} 无法解析;${allowedHint}` };
|
|
497
|
+
}
|
|
498
|
+
function isDisplayOnlyDecisionMarker(decision) {
|
|
499
|
+
return decision.trim() === "" || DISPLAY_ONLY_DECISION_MARKERS.has(decision.trim());
|
|
500
|
+
}
|
|
501
|
+
export function validateStructureChangeLedger(changeRoot, ledger) {
|
|
502
|
+
if (!ledger.present)
|
|
503
|
+
return { ok: false, errors: [STRUCTURE_LEDGER_MISSING_MESSAGE] };
|
|
504
|
+
if (ledger.none)
|
|
505
|
+
return { ok: true, errors: [] };
|
|
506
|
+
if (ledger.format_error)
|
|
507
|
+
return { ok: false, errors: [ledger.format_error] };
|
|
508
|
+
const errors = [];
|
|
509
|
+
const seenIds = new Set();
|
|
510
|
+
const knownDecIds = new Set(collectProposeQuestions(changeRoot).map(question => question.id));
|
|
511
|
+
for (const entry of ledger.entries) {
|
|
512
|
+
if (!STRUCTURE_LEDGER_ID_RE.test(entry.id)) {
|
|
513
|
+
errors.push(`结构变更清单 ${entry.id || "<空>"} 的 ID 格式无效,应为 SC-xxx`);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (seenIds.has(entry.id)) {
|
|
517
|
+
errors.push(`结构变更清单 ID 重复:${entry.id}`);
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
seenIds.add(entry.id);
|
|
521
|
+
if (!isStructureChangeCategory(entry.category)) {
|
|
522
|
+
errors.push(`结构变更清单 ${entry.id} 的类别无效:${entry.category || "<空>"};可用类别:${STRUCTURE_CHANGE_CATEGORIES.join("、")}`);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (!entry.change) {
|
|
526
|
+
errors.push(`结构变更清单 ${entry.id} 的变更不能为空`);
|
|
527
|
+
}
|
|
528
|
+
const basis = resolveStructureBasisRef(changeRoot, entry.basis, entry.category);
|
|
529
|
+
if (!basis.ok)
|
|
530
|
+
errors.push(`结构变更清单 ${entry.id} 的需求依据无效:${basis.reason}`);
|
|
531
|
+
const decisionValid = DEC_ID_RE.test(entry.decision) && knownDecIds.has(entry.decision);
|
|
532
|
+
if (STRUCTURE_DECISION_REQUIRED_CATEGORIES.has(entry.category)) {
|
|
533
|
+
if (!decisionValid) {
|
|
534
|
+
errors.push(`结构变更清单 ${entry.id} 属于 ${entry.category},必须在 决定 列引用一个 ## 待用户确认 中的 DEC`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
else if (!isDisplayOnlyDecisionMarker(entry.decision) && !decisionValid) {
|
|
538
|
+
errors.push(`结构变更清单 ${entry.id} 的决定列引用了无效的 DEC:${entry.decision}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return { ok: errors.length === 0, errors: [...new Set(errors)] };
|
|
542
|
+
}
|
|
543
|
+
export function formatStructureChangeLedgerSummary(ledger) {
|
|
544
|
+
if (!ledger.present || ledger.format_error)
|
|
545
|
+
return null;
|
|
546
|
+
if (ledger.none)
|
|
547
|
+
return "结构变更:无";
|
|
548
|
+
if (ledger.entries.length === 0)
|
|
549
|
+
return null;
|
|
550
|
+
const lines = ledger.entries.map(entry => {
|
|
551
|
+
const ref = splitBasisRef(entry.basis);
|
|
552
|
+
const basisLabel = ref && /^specs\//.test(ref.path) ? ref.anchor : entry.basis;
|
|
553
|
+
const decisionSuffix = DEC_ID_RE.test(entry.decision) ? ` — 决定 ${entry.decision}` : "";
|
|
554
|
+
return `- ${entry.id} [${entry.category}] ${entry.change} — 依据 ${basisLabel}${decisionSuffix}`;
|
|
555
|
+
});
|
|
556
|
+
return ["结构变更清单", "", ...lines].join("\n");
|
|
557
|
+
}
|
|
324
558
|
const TASK_LINE_RE = /^(- \[([ xX])\])\s+(\S+)/;
|
|
325
559
|
// 块头独占一行(允许全角/半角冒号);PREFIX 变体用于识别"块头带尾部内容/误加 bullet"的格式错误
|
|
326
560
|
const EXECUTION_REQUIREMENT_LINE_RE = /^\s*执行依据[::]\s*$/;
|
package/dist/next.js
CHANGED
|
@@ -115,14 +115,17 @@ function toNextOutput(change, plan) {
|
|
|
115
115
|
resume: { argv: ["superspec", "transition", "next", "--change", change] },
|
|
116
116
|
reason: plan.reason,
|
|
117
117
|
};
|
|
118
|
-
case "run_transition":
|
|
118
|
+
case "run_transition": {
|
|
119
|
+
const findingContext = plan.reopen?.reason === "review_fix" ? plan.reopen.findingContext : undefined;
|
|
119
120
|
return {
|
|
120
121
|
state: plan.state,
|
|
121
122
|
path: "next_command",
|
|
122
123
|
next_command: transitionCommand(change, plan.transition, formatTransitionArgs(plan)),
|
|
123
124
|
reason: plan.reason,
|
|
124
125
|
missing_inputs: [],
|
|
126
|
+
...(findingContext ? { finding_context: findingContext } : {}),
|
|
125
127
|
};
|
|
128
|
+
}
|
|
126
129
|
case "done":
|
|
127
130
|
return {
|
|
128
131
|
state: plan.state,
|
|
@@ -5,7 +5,7 @@ import { historicalProposeReadyRoles, reviewEvidenceDigest, reviewGateRoleResolu
|
|
|
5
5
|
import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./review_job_gates.js";
|
|
6
6
|
import { changeRoot as openspecChangeRoot } from "./openspec.js";
|
|
7
7
|
import { findLatestEvent, sha256Text } from "./store.js";
|
|
8
|
-
import { parseExecutionRequirements, parseTasksMd, parseTestContractEntries } from "./format.js";
|
|
8
|
+
import { parseExecutionRequirements, parseTasksMd, parseTestContractEntries, parseStructureChangeLedger, formatStructureChangeLedgerSummary } from "./format.js";
|
|
9
9
|
import { hasFrozenWorkflowModeForProposeRound, workflowRiskForProject, workflowRiskForState, } from "./workflow_config.js";
|
|
10
10
|
export const PHASE_CONFIRMATION_SCOPE_PREFIX = "phase_confirmation:";
|
|
11
11
|
function taskTitle(content, task) {
|
|
@@ -68,7 +68,12 @@ function proposeTaskDeliverySummary(changeRoot) {
|
|
|
68
68
|
const status = completedCount === 0
|
|
69
69
|
? ""
|
|
70
70
|
: `已完成 ${completedCount} 项;${pendingTasks.length === 0 ? "当前没有待实施任务。" : `以下 ${pendingTasks.length} 项仍待实施或调整。`}\n\n`;
|
|
71
|
-
|
|
71
|
+
const taskSummary = `执行计划概览\n\n${status}${items.join("\n\n")}`;
|
|
72
|
+
const designPath = join(changeRoot, "design.md");
|
|
73
|
+
const ledgerSummary = existsSync(designPath)
|
|
74
|
+
? formatStructureChangeLedgerSummary(parseStructureChangeLedger(readFileSync(designPath, "utf8")))
|
|
75
|
+
: null;
|
|
76
|
+
return ledgerSummary ? `${taskSummary}\n\n${ledgerSummary}` : taskSummary;
|
|
72
77
|
}
|
|
73
78
|
function latestTransition(events, predicate) {
|
|
74
79
|
return findLatestEvent(events, "transition_commit", event => predicate(event.payload));
|
package/dist/phase_plan.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { ReviewGateRule } from "./review_job_gates.ts";
|
|
2
2
|
import { exploreAnswerRegistrationPayload } from "./explore_round.ts";
|
|
3
3
|
import { proposeAnswerRegistrationPayload } from "./propose_round.ts";
|
|
4
|
-
import type { AcceptedMaterialFollowupContinuation, AskUser, Event, ExecutionPolicy, Job, JobRole, PlanningValidationProfile, WorkflowArtifactKind, State } from "./types.ts";
|
|
4
|
+
import type { AcceptedMaterialFollowupContinuation, AskUser, Event, ExecutionPolicy, Job, JobRole, PlanningValidationProfile, ReviewFindingContext, WorkflowArtifactKind, State } from "./types.ts";
|
|
5
5
|
import type { Snapshot } from "./types.ts";
|
|
6
6
|
import type { ReviewRisk } from "./review.ts";
|
|
7
|
+
export declare const PLAN_SIZE_BUDGET_SCOPE_PREFIX = "plan_size_budget:";
|
|
8
|
+
export declare const PLAN_SIZE_BUDGET_CONFIRM_ANSWER = "\u786E\u8BA4\u89C4\u6A21\u5408\u7406\uFF0C\u7EE7\u7EED\u5BA1\u67E5";
|
|
9
|
+
export declare const PLAN_SIZE_BUDGET_SHRINK_ANSWER = "\u56DE\u53BB\u6536\u7F29\u8BA1\u5212";
|
|
7
10
|
export type TransitionName = "explore" | "propose-ready" | "start-apply" | "task-start" | "task-complete" | "review-ready" | "reopen" | "accept";
|
|
8
11
|
export type WorkflowMode = {
|
|
9
12
|
kind: "risk";
|
|
@@ -29,6 +32,7 @@ export type ReopenNextStep = {
|
|
|
29
32
|
jobId: string;
|
|
30
33
|
findingId: string;
|
|
31
34
|
reopenReason: string;
|
|
35
|
+
findingContext?: ReviewFindingContext;
|
|
32
36
|
} | {
|
|
33
37
|
to: "propose";
|
|
34
38
|
reason: "review_finding";
|
|
@@ -118,6 +122,7 @@ export declare function latestReopenExploreBaseline(events: Event[]): Record<str
|
|
|
118
122
|
export declare function proposalDocsChangedSinceBaseline(changeRoot: string, baseline: Record<string, string>): boolean;
|
|
119
123
|
export declare function discoveryDocsChangedSinceBaseline(changeRoot: string, baseline: Record<string, string>): boolean;
|
|
120
124
|
export declare function pendingTaskIds(changeRoot: string): string[];
|
|
125
|
+
export declare function planSizeBudgetScope(proposeRoundId: string, taskCount: number, testCount: number): string;
|
|
121
126
|
/** 当前 Apply round 的规则版本;缺失版本的历史 event 保持 v1 回放。 */
|
|
122
127
|
export declare function executionRequirementVersionForCurrentRound(events: Event[]): 1 | 2;
|
|
123
128
|
/** 新 planning round 在进入 propose 时冻结当前 OpenSpec 校验契约。 */
|
package/dist/phase_plan.js
CHANGED
|
@@ -2,16 +2,29 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
3
|
import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./review_job_gates.js";
|
|
4
4
|
import { currentExploreRoundId, exploreAnswerRegistrationPayload, unregisteredClosedExploreQuestions, unresolvedPresentedExploreQuestionScopes, } from "./explore_round.js";
|
|
5
|
-
import { currentProposeOpenQuestion, currentProposeRoundId, proposeAnswerRegistrationPayload, unregisteredClosedProposeQuestions, unresolvedPresentedProposeQuestionScopes, } from "./propose_round.js";
|
|
6
|
-
import { discoveryOpenQuestionDisplayText, discoveryOpenQuestionScope, proposeOpenQuestionDisplayText, proposeOpenQuestionScope, parseExecutionRequirements, parseDiscoveryOpenQuestions, parseTasksMd, pendingTasksInContent, validateDiscovery, validateExecutionRequirements, validateExecutionRequirementDocumentReferences, validateProposalImpact, validateTasksDocument, } from "./format.js";
|
|
5
|
+
import { currentProposeOpenQuestion, currentProposeRoundId, isPlanningValidationProfile, proposeAnswerRegistrationPayload, unregisteredClosedProposeQuestions, unresolvedPresentedProposeQuestionScopes, } from "./propose_round.js";
|
|
6
|
+
import { discoveryOpenQuestionDisplayText, discoveryOpenQuestionScope, proposeOpenQuestionDisplayText, proposeOpenQuestionScope, parseExecutionRequirements, parseDiscoveryOpenQuestions, parseTasksMd, parseTestContractEntries, isFixTaskId, pendingTasksInContent, validateDiscovery, validateExecutionRequirements, validateExecutionRequirementDocumentReferences, validateProposalImpact, validateTasksDocument, parseStructureChangeLedger, validateStructureChangeLedger, } from "./format.js";
|
|
7
7
|
import { currentGitHead } from "./git_state.js";
|
|
8
8
|
import { validateOpenSpecChange } from "./openspec.js";
|
|
9
|
-
import { docRef, sha256File } from "./store.js";
|
|
9
|
+
import { docRef, sha256File, sha256Text, findLatestEvent } from "./store.js";
|
|
10
10
|
import { isReviewReadyVerifier, isFreshReviewVerifier, historicalProposeReadyRoles, readReviewPolicyFromEvents, reviewGateRoleResolution, reviewRejectionOverrideScope, reviewEvidenceDigest, } from "./review.js";
|
|
11
|
-
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, codeReviewDecisionScope, codeReviewJobStaleReason, collectCodeReviewGateFacts, currentCodeReviewWorkingPaths, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
|
|
11
|
+
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, codeReviewDecisionScope, codeReviewFindingNeedsUserDecision, codeReviewJobStaleReason, collectCodeReviewGateFacts, countReviewFixReopensSinceStartApply, currentCodeReviewWorkingPaths, isReviewFixCapReached, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
|
|
12
12
|
import { isPhaseAdvanceAuthorized, latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
|
|
13
13
|
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
14
|
-
import { workflowRiskForProposeRound, workflowRiskForState } from "./workflow_config.js";
|
|
14
|
+
import { workflowRiskForProposeRound, workflowRiskForState, workflowBudgetForRisk } from "./workflow_config.js";
|
|
15
|
+
export const PLAN_SIZE_BUDGET_SCOPE_PREFIX = "plan_size_budget:";
|
|
16
|
+
export const PLAN_SIZE_BUDGET_CONFIRM_ANSWER = "确认规模合理,继续审查";
|
|
17
|
+
export const PLAN_SIZE_BUDGET_SHRINK_ANSWER = "回去收缩计划";
|
|
18
|
+
/** 从失败 finding 提取定位上下文:只回传 evidence(位置事实),不回传 description——那是审查建议叙事,不进执行上下文。 */
|
|
19
|
+
function reviewFindingContext(finding) {
|
|
20
|
+
const evidence = typeof finding?.evidence === "string" ? finding.evidence.trim() : "";
|
|
21
|
+
if (!evidence)
|
|
22
|
+
return undefined;
|
|
23
|
+
return {
|
|
24
|
+
evidence,
|
|
25
|
+
note: "非授权上下文:仅用于定位问题代码;实现范围仍以任务行锚定的已批准行为为准",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
15
28
|
function requiredJobs(state, jobs, reason) {
|
|
16
29
|
return { kind: "required_jobs", state, jobs, reason };
|
|
17
30
|
}
|
|
@@ -127,32 +140,47 @@ const REQUIRED_DESIGN_HEADINGS = [
|
|
|
127
140
|
"## 实现方案",
|
|
128
141
|
];
|
|
129
142
|
function validateDesignPlan(changeRoot, profile) {
|
|
130
|
-
if (profile?.openspec.mode !== "strict" || profile.design?.schema_version !== 1)
|
|
131
|
-
return null;
|
|
132
|
-
const designPath = join(changeRoot, "design.md");
|
|
133
|
-
if (!existsSync(designPath))
|
|
134
|
-
return null;
|
|
135
|
-
const lines = readFileSync(designPath, "utf8").split(/\r?\n/).map(line => line.trimEnd());
|
|
136
143
|
const errors = [];
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
// 稳定标题检查自 design.schema_version 引入起生效;更早的 strict round 没有 design 字段,保持不检查。
|
|
145
|
+
if (profile?.openspec.mode === "strict" && profile.design != null) {
|
|
146
|
+
const designPath = join(changeRoot, "design.md");
|
|
147
|
+
if (existsSync(designPath)) {
|
|
148
|
+
const lines = readFileSync(designPath, "utf8").split(/\r?\n/).map(line => line.trimEnd());
|
|
149
|
+
const headingIndexes = new Map();
|
|
150
|
+
for (const heading of REQUIRED_DESIGN_HEADINGS) {
|
|
151
|
+
const indexes = lines.flatMap((line, index) => line === heading ? [index] : []);
|
|
152
|
+
headingIndexes.set(heading, indexes);
|
|
153
|
+
if (indexes.length === 0)
|
|
154
|
+
errors.push(`design.md 缺少稳定结构标题:${heading}`);
|
|
155
|
+
if (indexes.length > 1)
|
|
156
|
+
errors.push(`design.md 稳定结构标题重复:${heading}`);
|
|
157
|
+
}
|
|
158
|
+
if (errors.length === 0) {
|
|
159
|
+
let previousIndex = -1;
|
|
160
|
+
for (const heading of REQUIRED_DESIGN_HEADINGS) {
|
|
161
|
+
const indexes = headingIndexes.get(heading);
|
|
162
|
+
if (indexes[0] <= previousIndex) {
|
|
163
|
+
errors.push(`design.md 稳定结构标题顺序错误:${heading}`);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
previousIndex = indexes[0];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
145
170
|
}
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
171
|
+
if (profile?.design?.schema_version === 2) {
|
|
172
|
+
const designPath = join(changeRoot, "design.md");
|
|
173
|
+
if (!existsSync(designPath)) {
|
|
174
|
+
errors.push('design.md 缺少 ## 结构变更清单;没有结构变更时在该标题下写"无"');
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const ledger = parseStructureChangeLedger(readFileSync(designPath, "utf8"));
|
|
178
|
+
const validation = validateStructureChangeLedger(changeRoot, ledger);
|
|
179
|
+
if (!validation.ok)
|
|
180
|
+
errors.push(...validation.errors);
|
|
181
|
+
}
|
|
154
182
|
}
|
|
155
|
-
return null;
|
|
183
|
+
return errors.length > 0 ? [...new Set(errors)].join(";") : null;
|
|
156
184
|
}
|
|
157
185
|
export function executionPolicyForRisk(risk) {
|
|
158
186
|
return risk === "strict" ? "tdd" : "green_only";
|
|
@@ -343,6 +371,102 @@ function latestStartApplyIndex(events) {
|
|
|
343
371
|
}
|
|
344
372
|
return -1;
|
|
345
373
|
}
|
|
374
|
+
function planSizeCountDigest(taskCount, testCount) {
|
|
375
|
+
return sha256Text(`${taskCount},${testCount}`).replace(/^sha256:/, "");
|
|
376
|
+
}
|
|
377
|
+
export function planSizeBudgetScope(proposeRoundId, taskCount, testCount) {
|
|
378
|
+
return `${PLAN_SIZE_BUDGET_SCOPE_PREFIX}${proposeRoundId}:${planSizeCountDigest(taskCount, testCount)}`;
|
|
379
|
+
}
|
|
380
|
+
function countNonFixPlanTasks(changeRoot) {
|
|
381
|
+
const tasksContent = readFileSync(join(changeRoot, "tasks.md"), "utf8");
|
|
382
|
+
return parseTasksMd(tasksContent).filter(task => !isFixTaskId(task.taskId)).length;
|
|
383
|
+
}
|
|
384
|
+
function countPlanTestEntries(changeRoot) {
|
|
385
|
+
const testContractPath = join(changeRoot, ".superspec", "artifacts", "test-contract.md");
|
|
386
|
+
if (!existsSync(testContractPath))
|
|
387
|
+
return 0;
|
|
388
|
+
const parsed = parseTestContractEntries(readFileSync(testContractPath, "utf8"));
|
|
389
|
+
return parsed.ok ? parsed.entries.length : 0;
|
|
390
|
+
}
|
|
391
|
+
function isOverPlanSizeBudget(budget, taskCount, testCount) {
|
|
392
|
+
const overTasks = budget.tasks !== null && taskCount > budget.tasks;
|
|
393
|
+
const overTests = budget.tests !== null && testCount > budget.tests;
|
|
394
|
+
return overTasks || overTests;
|
|
395
|
+
}
|
|
396
|
+
function latestPlanSizeBudgetAnswer(events, scope) {
|
|
397
|
+
const event = findLatestEvent(events, "user_decision_recorded", ev => {
|
|
398
|
+
const payload = ev.payload;
|
|
399
|
+
if (payload.scope !== scope)
|
|
400
|
+
return false;
|
|
401
|
+
if (payload.accepted === false)
|
|
402
|
+
return false;
|
|
403
|
+
return typeof payload.answer === "string" && payload.answer.trim() !== "";
|
|
404
|
+
});
|
|
405
|
+
if (!event)
|
|
406
|
+
return null;
|
|
407
|
+
const answer = event.payload.answer;
|
|
408
|
+
return typeof answer === "string" ? answer.trim() : null;
|
|
409
|
+
}
|
|
410
|
+
function planSizeBudgetOverageMessage(budget, taskCount, testCount) {
|
|
411
|
+
const parts = [];
|
|
412
|
+
if (budget.tasks !== null && taskCount > budget.tasks) {
|
|
413
|
+
parts.push(`任务 ${taskCount} 个(预算 ${budget.tasks})`);
|
|
414
|
+
}
|
|
415
|
+
if (budget.tests !== null && testCount > budget.tests) {
|
|
416
|
+
parts.push(`TEST ${testCount} 个(预算 ${budget.tests})`);
|
|
417
|
+
}
|
|
418
|
+
return parts.join(";");
|
|
419
|
+
}
|
|
420
|
+
function planSizeBudgetSkipReason(projectRoot, changeRoot, events, risk) {
|
|
421
|
+
const budget = workflowBudgetForRisk(projectRoot, risk);
|
|
422
|
+
if (!budget)
|
|
423
|
+
return null;
|
|
424
|
+
const taskCount = countNonFixPlanTasks(changeRoot);
|
|
425
|
+
const testCount = countPlanTestEntries(changeRoot);
|
|
426
|
+
if (!isOverPlanSizeBudget(budget, taskCount, testCount))
|
|
427
|
+
return null;
|
|
428
|
+
const scope = planSizeBudgetScope(currentProposeRoundId(events), taskCount, testCount);
|
|
429
|
+
const answer = latestPlanSizeBudgetAnswer(events, scope);
|
|
430
|
+
if (answer === PLAN_SIZE_BUDGET_CONFIRM_ANSWER)
|
|
431
|
+
return null;
|
|
432
|
+
if (answer === PLAN_SIZE_BUDGET_SHRINK_ANSWER) {
|
|
433
|
+
return `计划规模仍超过预算(${planSizeBudgetOverageMessage(budget, taskCount, testCount)}),请收缩 tasks 或 test-contract 后重试`;
|
|
434
|
+
}
|
|
435
|
+
return `计划规模超过预算(${planSizeBudgetOverageMessage(budget, taskCount, testCount)}),需要先确认规模或收缩计划`;
|
|
436
|
+
}
|
|
437
|
+
function planSizeBudgetNextStep(context) {
|
|
438
|
+
const { change, changeRoot, events, mode } = context;
|
|
439
|
+
const budget = workflowBudgetForRisk(context.projectRoot, mode.risk);
|
|
440
|
+
if (!budget)
|
|
441
|
+
return null;
|
|
442
|
+
const taskCount = countNonFixPlanTasks(changeRoot);
|
|
443
|
+
const testCount = countPlanTestEntries(changeRoot);
|
|
444
|
+
if (!isOverPlanSizeBudget(budget, taskCount, testCount))
|
|
445
|
+
return null;
|
|
446
|
+
const scope = planSizeBudgetScope(currentProposeRoundId(events), taskCount, testCount);
|
|
447
|
+
const answer = latestPlanSizeBudgetAnswer(events, scope);
|
|
448
|
+
if (answer === PLAN_SIZE_BUDGET_CONFIRM_ANSWER)
|
|
449
|
+
return null;
|
|
450
|
+
if (answer === PLAN_SIZE_BUDGET_SHRINK_ANSWER) {
|
|
451
|
+
return {
|
|
452
|
+
kind: "material_update_required",
|
|
453
|
+
state: "propose",
|
|
454
|
+
errors: [`计划规模仍超过预算:${planSizeBudgetOverageMessage(budget, taskCount, testCount)}`],
|
|
455
|
+
reason: "已选择收缩计划但规模未变化",
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const overage = planSizeBudgetOverageMessage(budget, taskCount, testCount);
|
|
459
|
+
const question = `当前计划规模为 ${overage}。请确认是否按此规模继续进入审查,或先回去收缩 tasks / test-contract。`;
|
|
460
|
+
const ask = {
|
|
461
|
+
question,
|
|
462
|
+
allowed_answers: [PLAN_SIZE_BUDGET_CONFIRM_ANSWER, PLAN_SIZE_BUDGET_SHRINK_ANSWER],
|
|
463
|
+
scope,
|
|
464
|
+
record_argv: ["superspec", "record", "user-decision", "--change", change, "--input", "-"],
|
|
465
|
+
record_input: { scope, question, answer: null },
|
|
466
|
+
required_fields: ["answer"],
|
|
467
|
+
};
|
|
468
|
+
return { kind: "ask_user", state: "propose", ask, reason: "计划规模超过预算" };
|
|
469
|
+
}
|
|
346
470
|
function executionRequirementVersionFromPayload(payload) {
|
|
347
471
|
return payload.execution_requirement_version === 2 ? 2 : 1;
|
|
348
472
|
}
|
|
@@ -364,22 +488,12 @@ function executionRequirementVersionForProposeRound(events) {
|
|
|
364
488
|
}
|
|
365
489
|
return 1;
|
|
366
490
|
}
|
|
367
|
-
function isPlanningValidationProfile(value) {
|
|
368
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
369
|
-
return false;
|
|
370
|
-
const profile = value;
|
|
371
|
-
if (profile.version !== 2 || !profile.openspec || typeof profile.openspec !== "object")
|
|
372
|
-
return false;
|
|
373
|
-
const designValid = profile.design == null || profile.design.schema_version === 1;
|
|
374
|
-
return designValid && (profile.openspec.mode === "disabled" ||
|
|
375
|
-
profile.openspec.mode === "strict" && typeof profile.openspec.config_digest === "string");
|
|
376
|
-
}
|
|
377
491
|
/** 新 planning round 在进入 propose 时冻结当前 OpenSpec 校验契约。 */
|
|
378
492
|
export function planningValidationProfileForNewRound(projectRoot) {
|
|
379
493
|
const configDigest = sha256File(join(projectRoot, "openspec", "config.yaml"));
|
|
380
494
|
return configDigest == null
|
|
381
|
-
? { version: 2, openspec: { mode: "disabled" }, design: { schema_version:
|
|
382
|
-
: { version: 2, openspec: { mode: "strict", config_digest: configDigest }, design: { schema_version:
|
|
495
|
+
? { version: 2, openspec: { mode: "disabled" }, design: { schema_version: 2 } }
|
|
496
|
+
: { version: 2, openspec: { mode: "strict", config_digest: configDigest }, design: { schema_version: 2 } };
|
|
383
497
|
}
|
|
384
498
|
/** propose 状态尚未 ready 时,从进入本 planning round 的事件读取冻结 profile。 */
|
|
385
499
|
function planningValidationProfileForPendingProposeRound(events) {
|
|
@@ -654,6 +768,9 @@ export function planNextStep(context) {
|
|
|
654
768
|
reason: preflight.error,
|
|
655
769
|
};
|
|
656
770
|
}
|
|
771
|
+
const budgetStep = planSizeBudgetNextStep(context);
|
|
772
|
+
if (budgetStep)
|
|
773
|
+
return budgetStep;
|
|
657
774
|
const proposalReviewJobs = PROPOSE_FINAL_REVIEW_GATE.openJobsForGate(snapshot);
|
|
658
775
|
if (proposalReviewJobs.length > 0) {
|
|
659
776
|
return requiredJobs("propose", proposalReviewJobs, `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`);
|
|
@@ -826,7 +943,8 @@ function planApplyDoneNext(context) {
|
|
|
826
943
|
const findingId = pendingFinding?.id ?? "";
|
|
827
944
|
const type = pendingFinding?.type;
|
|
828
945
|
const decision = pendingFinding?.decision;
|
|
829
|
-
|
|
946
|
+
const reviewFixCapReached = isReviewFixCapReached(context.projectRoot, events);
|
|
947
|
+
if (findingId && type === "implementation" && !reviewFixCapReached) {
|
|
830
948
|
return {
|
|
831
949
|
kind: "run_transition",
|
|
832
950
|
state: "apply_done",
|
|
@@ -837,11 +955,12 @@ function planApplyDoneNext(context) {
|
|
|
837
955
|
jobId: latest.job.job_id,
|
|
838
956
|
findingId,
|
|
839
957
|
reopenReason: `修复代码审查问题 ${findingId}`,
|
|
958
|
+
findingContext: reviewFindingContext(pendingFinding?.finding),
|
|
840
959
|
},
|
|
841
960
|
reason: `代码审查发现纯代码实现问题 ${findingId},回到实现阶段修复`,
|
|
842
961
|
};
|
|
843
962
|
}
|
|
844
|
-
if (findingId && (type
|
|
963
|
+
if (findingId && codeReviewFindingNeedsUserDecision(type, reviewFixCapReached)) {
|
|
845
964
|
if (decision?.answer === "reopen_propose") {
|
|
846
965
|
return {
|
|
847
966
|
kind: "run_transition",
|
|
@@ -862,15 +981,21 @@ function planApplyDoneNext(context) {
|
|
|
862
981
|
jobId: latest.job.job_id,
|
|
863
982
|
findingId,
|
|
864
983
|
reopenReason: `根据代码审查问题 ${findingId} 回到实现阶段修复`,
|
|
984
|
+
findingContext: reviewFindingContext(pendingFinding?.finding),
|
|
865
985
|
},
|
|
866
986
|
reason: `使用者已确认问题 ${findingId} 直接回到实现阶段修复`,
|
|
867
987
|
};
|
|
868
988
|
}
|
|
869
989
|
const problemKind = type === "spec"
|
|
870
990
|
? "方案或需求文档可能需要调整"
|
|
871
|
-
: "
|
|
991
|
+
: type === "mixed"
|
|
992
|
+
? "代码实现和方案文档都可能有关"
|
|
993
|
+
: "纯代码实现问题";
|
|
994
|
+
const capNote = reviewFixCapReached && type === "implementation"
|
|
995
|
+
? `本轮 Apply 已自动修复 ${countReviewFixReopensSinceStartApply(events)} 次,`
|
|
996
|
+
: "";
|
|
872
997
|
const ask = {
|
|
873
|
-
question:
|
|
998
|
+
question: `${capNote}代码审查发现问题 ${findingId}:${problemKind}。请选择回到计划阶段修改文档、确认现有文档方向不变并回到实现阶段修代码,或驳回该问题;无论选择哪一项都必须写明原因。`,
|
|
874
999
|
allowed_answers: [
|
|
875
1000
|
CODE_REVIEW_DECISION_ANSWER_LABELS.reopen_propose,
|
|
876
1001
|
CODE_REVIEW_DECISION_ANSWER_LABELS.reopen_apply,
|
|
@@ -1043,6 +1168,9 @@ function planProposeReadyTransition(context) {
|
|
|
1043
1168
|
if (unresolvedPresentedProposeQuestionScopes(context.events).length > 0) {
|
|
1044
1169
|
return { kind: "skip", message: "此前展示的设计问题缺少答复登记且已从计划材料消失,请恢复原问题并完成登记" };
|
|
1045
1170
|
}
|
|
1171
|
+
const budgetSkip = planSizeBudgetSkipReason(context.projectRoot, changeRoot, context.events, risk);
|
|
1172
|
+
if (budgetSkip)
|
|
1173
|
+
return { kind: "skip", message: budgetSkip };
|
|
1046
1174
|
const requiredRoles = PROPOSE_FINAL_REVIEW_GATE.requiredRolesForRisk(risk);
|
|
1047
1175
|
const gatePlan = reviewGatePlan(snapshot, context.events, changeRoot, PROPOSE_FINAL_REVIEW_GATE, requiredRoles);
|
|
1048
1176
|
if (gatePlan)
|
package/dist/propose_round.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { type ProposeQuestion } from "./format.ts";
|
|
2
|
-
import type { Event } from "./types.ts";
|
|
2
|
+
import type { Event, PlanningValidationProfile } from "./types.ts";
|
|
3
|
+
export declare function isPlanningValidationProfile(value: unknown): value is PlanningValidationProfile;
|
|
4
|
+
/**
|
|
5
|
+
* 当前 planning round 的冻结 profile:优先取最近一次 propose-ready 写入的快照,
|
|
6
|
+
* 否则取进入 propose 的边界事件。两者都没有时是升级前的 v1 change。
|
|
7
|
+
*/
|
|
8
|
+
export declare function planningValidationProfileForCurrentRound(events: readonly Event[]): PlanningValidationProfile | null;
|
|
3
9
|
export declare function currentProposeRoundId(events: readonly Event[]): string;
|
|
4
10
|
export declare function proposeAnswerRegistrationPayload(changeRoot: string): {
|
|
5
11
|
propose_answer_registration: {
|