@peterxiaoyang/superspec 0.1.46 → 0.1.48

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.
Files changed (39) hide show
  1. package/dist/cli.js +54 -9
  2. package/dist/code_review.d.ts +11 -1
  3. package/dist/code_review.js +40 -0
  4. package/dist/explore_round.d.ts +25 -0
  5. package/dist/explore_round.js +139 -0
  6. package/dist/format.d.ts +78 -2
  7. package/dist/format.js +227 -16
  8. package/dist/next.d.ts +1 -1
  9. package/dist/next.js +97 -12
  10. package/dist/openspec.js +26 -5
  11. package/dist/phase_confirmation.js +73 -2
  12. package/dist/phase_plan.d.ts +24 -1
  13. package/dist/phase_plan.js +194 -34
  14. package/dist/propose_round.d.ts +15 -0
  15. package/dist/propose_round.js +137 -0
  16. package/dist/record.js +313 -57
  17. package/dist/review.js +2 -0
  18. package/dist/review_job_gates.d.ts +1 -1
  19. package/dist/review_job_gates.js +12 -4
  20. package/dist/skill_loop.js +20 -0
  21. package/dist/task_evidence.js +5 -3
  22. package/dist/transition.d.ts +1 -0
  23. package/dist/transition.js +248 -31
  24. package/dist/types.d.ts +77 -1
  25. package/dist/workflow_profile.js +1 -1
  26. package/package.json +7 -1
  27. package/templates/workflow/AGENTS.md +17 -5
  28. package/templates/workflow/prompts/architect.md +4 -2
  29. package/templates/workflow/prompts/code-reviewer.md +3 -3
  30. package/templates/workflow/prompts/critic.md +13 -4
  31. package/templates/workflow/prompts/executor.md +5 -5
  32. package/templates/workflow/prompts/explore.md +2 -2
  33. package/templates/workflow/prompts/test-engineer.md +6 -5
  34. package/templates/workflow/prompts/test-runner.md +5 -5
  35. package/templates/workflow/prompts/verifier.md +4 -4
  36. package/templates/workflow/skills/superspec-apply/SKILL.md +26 -12
  37. package/templates/workflow/skills/superspec-explore/SKILL.md +25 -7
  38. package/templates/workflow/skills/superspec-propose/SKILL.md +33 -16
  39. package/templates/workflow/skills/superspec-review/SKILL.md +9 -9
package/dist/cli.js CHANGED
@@ -66,7 +66,7 @@ function transitionExitCode(result) {
66
66
  function proposeReadyExitCode(result) {
67
67
  if (result.outcome === "blocked")
68
68
  return 0;
69
- return result.events_written === 0 && result.message.includes("不能") ? 1 : 0;
69
+ return result.events_written === 0 && result.outcome === "advanced" && result.from_state === result.to_state ? 1 : 0;
70
70
  }
71
71
  function ensureWorkflowMode(projectRoot, _opts) {
72
72
  try {
@@ -493,10 +493,8 @@ async function askToUpdateSelfIfNeeded(projectRoot, rerunArgs) {
493
493
  // ===== 初始化 transition init =====
494
494
  // ===== init/explore 现在在 transition.ts 中(走统一锁内路径)=====
495
495
  // ===== 主分发 =====
496
- async function main(argv) {
497
- // --help / 无参数 → 打印用法
498
- if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
499
- console.log(`SuperSpec 流程引擎 ${SUPERSPEC_VERSION}
496
+ function topLevelHelp() {
497
+ return `SuperSpec 流程引擎 ${SUPERSPEC_VERSION}
500
498
 
501
499
  用法:superspec <命令> [选项]
502
500
 
@@ -507,14 +505,14 @@ async function main(argv) {
507
505
  jobs <子命令> --change <C> 工作项管理(见下)
508
506
  install 安装项目工作流入口
509
507
  init --scope project install 的兼容别名
510
- update 升级 CLI 到 npm latest 并同步项目工作流模板
511
- version 版本号
508
+ update 升级 CLI 到 npm latest 并同步项目工作流模板
509
+ version 版本号
512
510
 
513
511
  transition 子命令:
514
512
  init / explore / sync / next / propose-ready / start-apply
515
513
  task-start --task <T> / task-complete --task <T> [--input -]
516
514
  reopen --to explore|propose|apply --reason <TEXT>
517
- reopen --to apply --reason <TEXT> [--review-fix <JOB#FINDING>]
515
+ reopen --to apply --reason <TEXT> [--review-fix <JOB#FINDING>|--self-test-fix <TASK>]
518
516
  reopen --to propose --reason <TEXT> [--review-finding <JOB#FINDING>]
519
517
  review-ready / accept
520
518
 
@@ -525,7 +523,49 @@ record 子命令:
525
523
 
526
524
  jobs 子命令:
527
525
  list / packet --job <J>
528
- `);
526
+ `;
527
+ }
528
+ function commandHelp(command, subcommand) {
529
+ if (command === "record" && subcommand === "user-decision") {
530
+ return `用法:superspec record user-decision --change <C> --input <F|->
531
+
532
+ 从 JSON 文件读取用户决定;--input - 表示从 stdin 读取。
533
+ 输入至少包含 scope 和 answer,工作流给出的 question 可一并保留。
534
+
535
+ 示例:
536
+ printf '%s' '{"scope":"<next 返回的 scope>","question":"<原问题>","answer":"<用户答复>"}' | superspec record user-decision --change <C> --input -
537
+ `;
538
+ }
539
+ if (command === "record" && subcommand === "job-submit") {
540
+ return `用法:superspec record job-submit --change <C> --job <J> --report <F|->
541
+
542
+ 提交 reviewer JSON 报告;--report - 表示从 stdin 读取。报告契约以 jobs packet 返回的 report_schema 为准。
543
+
544
+ 示例:
545
+ superspec jobs packet --change <C> --job <J>
546
+ superspec record job-submit --change <C> --job <J> --report report.json
547
+ `;
548
+ }
549
+ if (command === "record" && subcommand === "test-run") {
550
+ return `用法:superspec record test-run --change <C> --input <F|->
551
+
552
+ 登记测试证据;--input - 表示从 stdin 读取。输入需包含 test_id、task_structure_digest、command、cwd、exit_code 和 semantic_status。
553
+
554
+ 示例:
555
+ printf '%s' '{"test_id":"TEST-001","task_structure_digest":"sha256:<digest>","command":"npm test","cwd":"<project>","exit_code":0,"semantic_status":"expected_success"}' | superspec record test-run --change <C> --input -
556
+ `;
557
+ }
558
+ if (command === "transition" && subcommand === "propose-ready") {
559
+ return `用法:superspec transition propose-ready --change <C>
560
+
561
+ 校验当前计划材料和审查门禁;满足条件时推进到 propose_ready。工作流模式来自项目配置,不接受 --risk。
562
+ `;
563
+ }
564
+ return topLevelHelp();
565
+ }
566
+ async function main(argv) {
567
+ if (argv.length === 0) {
568
+ console.log(topLevelHelp());
529
569
  return 0;
530
570
  }
531
571
  // version
@@ -534,6 +574,10 @@ jobs 子命令:
534
574
  return 0;
535
575
  }
536
576
  const { command, subcommand, opts } = parseArgs(argv);
577
+ if (argv.includes("--help") || argv.includes("-h") || command === "--help" || command === "-h") {
578
+ console.log(commandHelp(command, subcommand));
579
+ return 0;
580
+ }
537
581
  const projectRoot = process.cwd();
538
582
  // install / init / update 不需要 --change
539
583
  if (command === "install" || command === "init") {
@@ -735,6 +779,7 @@ jobs 子命令:
735
779
  const result = reopen(projectRoot, change, cr, to, reason, {
736
780
  ...(opts["review-fix"] ? { reviewFix: opts["review-fix"] } : {}),
737
781
  ...(opts["review-finding"] ? { reviewFinding: opts["review-finding"] } : {}),
782
+ ...(opts["self-test-fix"] ? { selfTestFix: opts["self-test-fix"] } : {}),
738
783
  });
739
784
  console.log(JSON.stringify(result, null, 2));
740
785
  return transitionExitCode(result);
@@ -1,4 +1,4 @@
1
- import type { CodeReviewResultKind, CodeReviewScope, CodeStateCheck, CoverageExemptionRef, Event, Job, JobPacketContext, Ref, ReviewPreviousRejection } from "./types.ts";
1
+ import type { CodeReviewGateEvidence, CodeReviewResultKind, CodeReviewScope, CodeStateCheck, CoverageExemptionRef, Event, Job, JobPacketContext, Ref, ReviewPreviousRejection, TaskExecutionIndexEntry } from "./types.ts";
2
2
  export declare const CODE_REVIEW_REPAIR_SCOPE_PREFIX = "code_reviewer_report_repair:";
3
3
  export declare const CODE_REVIEW_DECISION_SCOPE_PREFIX = "code_review_decision:";
4
4
  export declare const TEST_COVERAGE_EXEMPTION_SCOPE_PREFIX = "test_coverage_exemption:";
@@ -68,7 +68,15 @@ export declare function codeReviewPacketDigest(input: {
68
68
  export declare function codeReviewPacketContext(changeRoot: string, projectRoot: string, scope: CodeReviewScope, events: Event[]): JobPacketContext;
69
69
  export declare function effectiveCoverageExemptionRefsFromEvents(events: Event[]): CoverageExemptionRef[];
70
70
  export declare function missingCoverageExemptionTestIds(changeRoot: string, events: Event[]): string[];
71
+ /** Read-only execution evidence projected for code review and final verification. */
72
+ export declare function taskExecutionIndexForReview(projectRoot: string, events: Event[]): TaskExecutionIndexEntry[];
71
73
  export declare function codeReviewDecisionScope(jobId: string, findingId: string): string;
74
+ export interface CodeReviewDecisionScopeRef {
75
+ jobId: string;
76
+ findingId: string;
77
+ }
78
+ /** 只接受状态机生成的 <job>#<finding> 决策范围。 */
79
+ export declare function parseCodeReviewDecisionScope(scope: string): CodeReviewDecisionScopeRef | null;
72
80
  export declare function isCodeReviewDecisionAnswer(value: unknown): value is CodeReviewDecisionAnswer;
73
81
  export declare function codeReviewDecisionAnswerLabel(answer: CodeReviewDecisionAnswer): string;
74
82
  export declare function normalizeCodeReviewDecisionAnswer(value: unknown): CodeReviewDecisionAnswer | null;
@@ -82,5 +90,7 @@ export declare function latestApplyDoneToReviewGate(events: Event[]): {
82
90
  job_id?: string;
83
91
  reason?: string;
84
92
  } | null;
93
+ /** Latest code-review gate fact for the current review cycle, frozen into verifier packets. */
94
+ export declare function latestCodeReviewGateEvidence(events: Event[]): CodeReviewGateEvidence | null;
85
95
  export declare function requiresFinalVerifierForCurrentReview(events: Event[]): boolean;
86
96
  export declare function computeCodeStateCheck(projectRoot: string, events: Event[], ignoredCodePaths?: string[]): CodeStateCheck;
@@ -418,6 +418,7 @@ function taskExecutionIndexFromEvents(projectRoot, events) {
418
418
  entries.push({
419
419
  task_id: payload.task_id,
420
420
  attempt_id: payload.attempt_id,
421
+ ...(attempt?.fix ? { fix: attempt.fix } : {}),
421
422
  execution_policy: attempt?.execution_policy ?? "tdd",
422
423
  changed_paths: changedResult ? changedResult.paths : null,
423
424
  ...(changedResult?.partial_reason ? { changed_paths_partial_reason: changedResult.partial_reason } : {}),
@@ -435,6 +436,10 @@ function taskExecutionIndexFromEvents(projectRoot, events) {
435
436
  entries.sort((a, b) => a.task_id.localeCompare(b.task_id) || a.attempt_id.localeCompare(b.attempt_id));
436
437
  return entries;
437
438
  }
439
+ /** Read-only execution evidence projected for code review and final verification. */
440
+ export function taskExecutionIndexForReview(projectRoot, events) {
441
+ return taskExecutionIndexFromEvents(projectRoot, events);
442
+ }
438
443
  function isCodeReviewerJob(job) {
439
444
  return job.role === "code-reviewer" && REVIEW_CODE_REVIEW_GATE.isJobForGate(job);
440
445
  }
@@ -446,6 +451,18 @@ function codeReviewResultKind(value) {
446
451
  export function codeReviewDecisionScope(jobId, findingId) {
447
452
  return `${CODE_REVIEW_DECISION_SCOPE_PREFIX}${jobId}#${findingId}`;
448
453
  }
454
+ /** 只接受状态机生成的 <job>#<finding> 决策范围。 */
455
+ export function parseCodeReviewDecisionScope(scope) {
456
+ if (!scope.startsWith(CODE_REVIEW_DECISION_SCOPE_PREFIX))
457
+ return null;
458
+ const value = scope.slice(CODE_REVIEW_DECISION_SCOPE_PREFIX.length);
459
+ const separator = value.indexOf("#");
460
+ if (separator <= 0 || separator === value.length - 1)
461
+ return null;
462
+ const jobId = value.slice(0, separator);
463
+ const findingId = value.slice(separator + 1);
464
+ return jobId.trim() !== "" && findingId.trim() !== "" ? { jobId, findingId } : null;
465
+ }
449
466
  export function isCodeReviewDecisionAnswer(value) {
450
467
  return value === "reopen_propose" || value === "reopen_apply" || value === "dismiss";
451
468
  }
@@ -622,6 +639,29 @@ export function latestApplyDoneToReviewGate(events) {
622
639
  }
623
640
  return null;
624
641
  }
642
+ /** Latest code-review gate fact for the current review cycle, frozen into verifier packets. */
643
+ export function latestCodeReviewGateEvidence(events) {
644
+ for (let i = events.length - 1; i >= 0; i--) {
645
+ const event = events[i];
646
+ if (event.event_type !== "transition_commit")
647
+ continue;
648
+ const payload = event.payload;
649
+ if (payload.transition !== "review-ready" || payload.from_state !== "apply_done" || payload.to_state !== "review")
650
+ continue;
651
+ const gate = payload.code_review_gate;
652
+ if (!gate || (gate.decision !== "passed" && gate.decision !== "skipped"))
653
+ return null;
654
+ return {
655
+ decision: gate.decision,
656
+ job_id: typeof gate.job_id === "string" ? gate.job_id : null,
657
+ packet_digest: typeof gate.packet_digest === "string" ? gate.packet_digest : null,
658
+ ...(gate.reason === "no_code_changes" ? { reason: gate.reason } : {}),
659
+ event_id: event.event_id,
660
+ event_digest: event.event_digest,
661
+ };
662
+ }
663
+ return null;
664
+ }
625
665
  export function requiresFinalVerifierForCurrentReview(events) {
626
666
  return latestApplyDoneToReviewGate(events) != null;
627
667
  }
@@ -0,0 +1,25 @@
1
+ import { type DiscoveryQuestion } from "./format.ts";
2
+ import type { Event } from "./types.ts";
3
+ /**
4
+ * 当前 Explore 轮次的稳定标识。
5
+ *
6
+ * 同一轮内补充调查或创建审查工作项不会使当前确认事项失效;只有首次进入
7
+ * Explore 或从后续阶段重新打开 Explore 时,才开始新的确认轮次。
8
+ */
9
+ export declare function currentExploreRoundId(events: readonly Event[]): string;
10
+ /** 在进入新 Explore 轮次时冻结已有已确认事项,避免升级时追溯历史答复。 */
11
+ export declare function exploreAnswerRegistrationPayload(discoveryContent: string | null): {
12
+ explore_answer_registration: {
13
+ version: number;
14
+ baseline_closed_question_keys: string[];
15
+ };
16
+ };
17
+ /** 当前确认事项已通过本轮主流程登记过答复。 */
18
+ export declare function exploreAnswerWasRecorded(events: readonly Event[], roundId: string, question: Pick<DiscoveryQuestion, "id" | "ordinal" | "text">, discoveryContent: string): boolean;
19
+ /**
20
+ * 新协议轮次中,被回写为已确认的事项必须有对应答复记录。
21
+ * 进入本轮前已经关闭的事项作为基线保留,历史 change 因缺少协议标记整体兼容。
22
+ */
23
+ export declare function unregisteredClosedExploreQuestions(events: readonly Event[], discoveryContent: string): DiscoveryQuestion[];
24
+ /** 已正式展示但尚未登记答复的 Explore 问题不能通过删除问题行绕过。 */
25
+ export declare function unresolvedPresentedExploreQuestionScopes(events: readonly Event[]): string[];
@@ -0,0 +1,139 @@
1
+ import { discoveryQuestionContextFingerprint, discoveryQuestionDecisionBasisDigest, discoveryQuestionKey, parseDiscoveryQuestions, } from "./format.js";
2
+ const EXPLORE_ANSWER_REGISTRATION_VERSION = 1;
3
+ function isExploreRoundEntry(event) {
4
+ if (event.event_type !== "transition_commit")
5
+ return false;
6
+ const payload = event.payload;
7
+ return payload.transition === "explore" && payload.from_state === "init" && payload.to_state === "explore" ||
8
+ payload.transition === "reopen" && payload.reopen_target === "explore";
9
+ }
10
+ function currentExploreRoundEntry(events) {
11
+ for (let index = events.length - 1; index >= 0; index--) {
12
+ const event = events[index];
13
+ if (isExploreRoundEntry(event))
14
+ return { event, roundId: event.event_id };
15
+ }
16
+ return null;
17
+ }
18
+ /**
19
+ * 当前 Explore 轮次的稳定标识。
20
+ *
21
+ * 同一轮内补充调查或创建审查工作项不会使当前确认事项失效;只有首次进入
22
+ * Explore 或从后续阶段重新打开 Explore 时,才开始新的确认轮次。
23
+ */
24
+ export function currentExploreRoundId(events) {
25
+ return currentExploreRoundEntry(events)?.roundId ?? "legacy-explore-round";
26
+ }
27
+ /** 在进入新 Explore 轮次时冻结已有已确认事项,避免升级时追溯历史答复。 */
28
+ export function exploreAnswerRegistrationPayload(discoveryContent) {
29
+ const baselineClosedQuestionKeys = discoveryContent == null
30
+ ? []
31
+ : parseDiscoveryQuestions(discoveryContent)
32
+ .filter(question => question.status === "closed")
33
+ .map(discoveryQuestionKey)
34
+ .sort();
35
+ return {
36
+ explore_answer_registration: {
37
+ version: EXPLORE_ANSWER_REGISTRATION_VERSION,
38
+ baseline_closed_question_keys: baselineClosedQuestionKeys,
39
+ },
40
+ };
41
+ }
42
+ function currentExploreAnswerRegistration(events) {
43
+ const entry = currentExploreRoundEntry(events);
44
+ if (!entry) {
45
+ return {
46
+ enabled: false,
47
+ roundId: "legacy-explore-round",
48
+ baselineClosedQuestionKeys: new Set(),
49
+ };
50
+ }
51
+ const payload = entry.event.payload;
52
+ const registration = payload.explore_answer_registration;
53
+ if (registration?.version !== EXPLORE_ANSWER_REGISTRATION_VERSION || !Array.isArray(registration.baseline_closed_question_keys)) {
54
+ return {
55
+ enabled: false,
56
+ roundId: entry.roundId,
57
+ baselineClosedQuestionKeys: new Set(),
58
+ };
59
+ }
60
+ return {
61
+ enabled: true,
62
+ roundId: entry.roundId,
63
+ baselineClosedQuestionKeys: new Set(registration.baseline_closed_question_keys.filter((key) => typeof key === "string")),
64
+ };
65
+ }
66
+ /** 当前确认事项已通过本轮主流程登记过答复。 */
67
+ export function exploreAnswerWasRecorded(events, roundId, question, discoveryContent) {
68
+ const currentContextFingerprint = discoveryQuestionContextFingerprint(discoveryContent, question);
69
+ if (!currentContextFingerprint)
70
+ return false;
71
+ const currentBasisDigest = discoveryQuestionDecisionBasisDigest(question);
72
+ return events.some(event => {
73
+ if (event.event_type !== "user_decision_recorded")
74
+ return false;
75
+ const payload = event.payload;
76
+ const recorded = payload.explore_open_question;
77
+ const revisionMatches = typeof recorded?.decision_basis_digest === "string"
78
+ ? recorded.decision_basis_digest === currentBasisDigest
79
+ : recorded?.context_fingerprint === currentContextFingerprint;
80
+ return payload.accepted === true &&
81
+ recorded?.round_id === roundId &&
82
+ recorded.question_id === question.id &&
83
+ (!question.id.startsWith("item-") || recorded.question_ordinal === question.ordinal) &&
84
+ revisionMatches;
85
+ });
86
+ }
87
+ /**
88
+ * 新协议轮次中,被回写为已确认的事项必须有对应答复记录。
89
+ * 进入本轮前已经关闭的事项作为基线保留,历史 change 因缺少协议标记整体兼容。
90
+ */
91
+ export function unregisteredClosedExploreQuestions(events, discoveryContent) {
92
+ const registration = currentExploreAnswerRegistration(events);
93
+ if (!registration.enabled)
94
+ return [];
95
+ return parseDiscoveryQuestions(discoveryContent).filter(question => question.status === "closed" &&
96
+ !registration.baselineClosedQuestionKeys.has(discoveryQuestionKey(question)) &&
97
+ !exploreAnswerWasRecorded(events, registration.roundId, question, discoveryContent));
98
+ }
99
+ /** 已正式展示但尚未登记答复的 Explore 问题不能通过删除问题行绕过。 */
100
+ export function unresolvedPresentedExploreQuestionScopes(events) {
101
+ const roundId = currentExploreRoundId(events);
102
+ const acceptedDecisions = events.flatMap(event => {
103
+ if (event.event_type !== "user_decision_recorded")
104
+ return [];
105
+ const payload = event.payload;
106
+ return payload.accepted === true ? [payload] : [];
107
+ });
108
+ const latestByQuestion = new Map();
109
+ for (const event of events) {
110
+ if (event.event_type !== "user_question_presented")
111
+ continue;
112
+ const payload = event.payload;
113
+ if (payload.phase !== "explore" || payload.round_id !== roundId || typeof payload.scope !== "string" || typeof payload.question_id !== "string")
114
+ continue;
115
+ const identity = payload.question_id.startsWith("item-")
116
+ ? `${payload.question_id}:${String(payload.question_ordinal)}`
117
+ : payload.question_id;
118
+ latestByQuestion.set(identity, {
119
+ scope: payload.scope,
120
+ legacyScope: typeof payload.legacy_scope === "string" ? payload.legacy_scope : null,
121
+ questionId: payload.question_id,
122
+ questionOrdinal: payload.question_ordinal,
123
+ revisionDigest: payload.decision_basis_digest,
124
+ });
125
+ }
126
+ return [...latestByQuestion.values()].flatMap(presented => {
127
+ const answered = acceptedDecisions.some(decision => {
128
+ if (decision.scope === presented.scope || decision.scope === presented.legacyScope)
129
+ return true;
130
+ const recorded = decision.explore_open_question;
131
+ return recorded?.round_id === roundId &&
132
+ recorded.question_id === presented.questionId &&
133
+ (!presented.questionId.startsWith("item-") || recorded.question_ordinal === presented.questionOrdinal) &&
134
+ typeof presented.revisionDigest === "string" &&
135
+ recorded.decision_basis_digest === presented.revisionDigest;
136
+ });
137
+ return answered ? [] : [presented.scope];
138
+ });
139
+ }
package/dist/format.d.ts CHANGED
@@ -1,5 +1,51 @@
1
1
  import { type ExecutionContract, type ExecutionPolicy } from "./types.ts";
2
- /** discovery.md 提取"待确认问题"段内的未确认项数量 */
2
+ export declare const EXPLORE_OPEN_QUESTION_SCOPE_PREFIX = "explore_open_question:";
3
+ /**
4
+ * Discovery 的未确认项是用户决策的唯一候选来源。这里保留原有的“仅指定段落内
5
+ * checkbox 有效”语义;ID 缺失的历史文档按所有 checklist 在该段落中的稳定顺序
6
+ * 使用 item-N,避免升级时要求迁移历史 change。
7
+ */
8
+ export interface DiscoveryOpenQuestion {
9
+ /** Q-xxx;历史材料没有 ID 时为 item-N。 */
10
+ id: string;
11
+ /** 在“待确认问题”段落中所有 checklist 的 1-based 顺序。 */
12
+ ordinal: number;
13
+ /** 去掉 markdown checkbox 后的原始内容,用于留痕与内部匹配。 */
14
+ text: string;
15
+ /** 包含 checkbox 的原始 Markdown 行,供诊断和测试使用。 */
16
+ raw: string;
17
+ /** 当前完整 discovery.md 的内容指纹,避免只改决策依据时沿用旧答复。 */
18
+ documentFingerprint: string;
19
+ }
20
+ /** Discovery 待确认段中的一项;状态机需要同时识别待答复和已回写的项。 */
21
+ export interface DiscoveryQuestion extends DiscoveryOpenQuestion {
22
+ status: "open" | "closed";
23
+ }
24
+ /**
25
+ * 当前文档中同一确认事项的稳定键。结合 Q-ID、顺序和原文,避免把本轮开始前
26
+ * 已经确认的旧事项误认为新答复。
27
+ */
28
+ export declare function discoveryQuestionKey(question: Pick<DiscoveryQuestion, "id" | "ordinal" | "text">): string;
29
+ /**
30
+ * 当前决定所依据的规范化问题内容。会改变选择、推荐或影响的事实必须写入该问题项;
31
+ * 状态机只绑定这份明确依据,不猜测文档其它段落与决定是否相关。
32
+ */
33
+ export declare function discoveryQuestionDecisionBasisDigest(question: Pick<DiscoveryQuestion, "id" | "ordinal" | "text">): string;
34
+ /**
35
+ * 计算某一确认事项之外的 Discovery 决策上下文。回写时该事项本身会从问题改为
36
+ * 结论,因此只归一化这一行;其余事实、证据和其它待确认项的改动都会使指纹失效。
37
+ */
38
+ export declare function discoveryQuestionContextFingerprint(content: string, question: Pick<DiscoveryQuestion, "id" | "ordinal">): string | null;
39
+ /** 按文档顺序提取 discovery.md 的全部确认事项。 */
40
+ export declare function parseDiscoveryQuestions(content: string): DiscoveryQuestion[];
41
+ /** 按文档顺序提取 discovery.md 中尚未确认的问题。 */
42
+ export declare function parseDiscoveryOpenQuestions(content: string): DiscoveryOpenQuestion[];
43
+ export declare function discoveryOpenQuestionScope(question: Pick<DiscoveryOpenQuestion, "id" | "ordinal" | "text">, exploreRoundId: string): string;
44
+ /** 兼容升级前已经展示给用户、但尚未登记的 scope。 */
45
+ export declare function legacyDiscoveryOpenQuestionScope(question: Pick<DiscoveryOpenQuestion, "id" | "documentFingerprint">, exploreRoundId: string): string;
46
+ /** 面向用户展示时隐藏 Q-xxx 这一内部编号;历史无编号问题保持原文。 */
47
+ export declare function discoveryOpenQuestionDisplayText(question: Pick<DiscoveryOpenQuestion, "id" | "text">): string;
48
+ /** 从 discovery.md 提取“待确认问题”段内的未确认项数量。 */
3
49
  export declare function countDiscoveryOpenQuestions(content: string): number;
4
50
  export interface DiscoveryChainCoverageCheck {
5
51
  ok: boolean;
@@ -9,7 +55,10 @@ export interface DiscoveryChainCoverageCheck {
9
55
  export declare function splitMarkdownTableRow(line: string): string[];
10
56
  /** 轻量校验 discovery.md 的链路五要素段。只校验结构和阻塞未知,不判断业务真假。 */
11
57
  export declare function validateDiscoveryChainCoverage(content: string): DiscoveryChainCoverageCheck;
12
- /** 完整校验 discovery.md:存在 + 非空 + 无未确认问题 */
58
+ /**
59
+ * 校验 discovery.md 的可解析结构。未确认问题不是格式错误:next 会把第一个问题
60
+ * 作为当前用户决策返回;只有缺文档、空文档或已声明链路的结构错误才在此阻断。
61
+ */
13
62
  export declare function validateDiscovery(changeRoot: string): {
14
63
  ok: boolean;
15
64
  message: string;
@@ -19,6 +68,26 @@ export interface ProposeOpenQuestionFile {
19
68
  path: string;
20
69
  openCount: number;
21
70
  }
71
+ export declare const PROPOSE_OPEN_QUESTION_SCOPE_PREFIX = "propose_open_question:";
72
+ export interface ProposeQuestion {
73
+ path: string;
74
+ id: string;
75
+ ordinal: number;
76
+ text: string;
77
+ raw: string;
78
+ documentFingerprint: string;
79
+ status: "open" | "closed";
80
+ }
81
+ export declare function parseProposeQuestions(content: string, path: string): ProposeQuestion[];
82
+ export declare function proposeQuestionKey(question: Pick<ProposeQuestion, "path" | "id" | "ordinal" | "text">): string;
83
+ /** Propose 决定的明确依据;其它计划材料变化不会自动使已展示决定失效。 */
84
+ export declare function proposeQuestionDecisionBasisDigest(question: Pick<ProposeQuestion, "path" | "id" | "ordinal" | "text">): string;
85
+ export declare function proposeQuestionContextFingerprint(content: string, question: Pick<ProposeQuestion, "id" | "ordinal">): string | null;
86
+ export declare function proposeOpenQuestionScope(question: ProposeQuestion, proposeRoundId: string): string;
87
+ /** 兼容升级前已经展示给用户、但尚未登记的 scope。 */
88
+ export declare function legacyProposeOpenQuestionScope(question: ProposeQuestion, proposeRoundId: string): string;
89
+ export declare function proposeOpenQuestionDisplayText(question: Pick<ProposeQuestion, "id" | "text">): string;
90
+ export declare function collectProposeQuestions(changeRoot: string): ProposeQuestion[];
22
91
  export declare function countProposeOpenQuestionsInContent(content: string): number;
23
92
  export declare function collectProposeOpenQuestions(changeRoot: string): {
24
93
  openCount: number;
@@ -66,6 +135,13 @@ export declare function adoptedContractForTask(content: string, taskId: string,
66
135
  parsed: ParsedExecutionRequirement | null;
67
136
  contract: ExecutionContract | null;
68
137
  };
138
+ /**
139
+ * Fix task 由状态机从已批准的实现范围派生;它没有 proposal 阶段执行依据块。
140
+ * REVIEW-FIX-* 是发布前已有的持久化 task ID,FIX-SELFTEST-* 是当前引擎生成的
141
+ * 自测修复 task。不能把通用 FIX-* 前缀保留为内部命名空间。
142
+ */
143
+ export declare function isFixTaskId(taskId: string): boolean;
144
+ /** @deprecated 新代码使用 isFixTaskId;保留给旧扩展和历史调用。 */
69
145
  export declare function isReviewFixTaskId(taskId: string): boolean;
70
146
  export declare function isCharacterizationTask(task: ParsedTask): boolean;
71
147
  export declare function parseTestContractEntries(content: string): TestContractParseResult;