@peterxiaoyang/superspec 0.1.40 → 0.1.41

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/dist/review.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { docRef, sha256File, sha256Text } from "./store.js";
5
- import { REVIEW_FINAL_VERIFIER_GATE } from "./review_job_gates.js";
5
+ import { EXPLORE_DISCOVERY_REVIEW_GATE_ID, PROPOSE_FINAL_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE, } from "./review_job_gates.js";
6
6
  import { computeCodeStateCheck, effectiveCoverageExemptionRefsFromEvents } from "./code_review.js";
7
7
  export const REVIEW_DOC_PATHS = [
8
8
  "proposal.md",
@@ -54,6 +54,108 @@ export function readReviewPolicyFromEvents(events) {
54
54
  }
55
55
  return null;
56
56
  }
57
+ function reviewResultKind(value) {
58
+ return value === "invalid_report" || value === "non_actionable_report" || value === "review_failed"
59
+ ? value
60
+ : null;
61
+ }
62
+ function reviewCycleState(gate) {
63
+ if (gate.gate_id === EXPLORE_DISCOVERY_REVIEW_GATE_ID)
64
+ return "explore";
65
+ if (gate.gate_id === PROPOSE_FINAL_REVIEW_GATE_ID)
66
+ return "propose";
67
+ return null;
68
+ }
69
+ function currentReviewGateCycleStart(events, gate) {
70
+ const state = reviewCycleState(gate);
71
+ if (!state)
72
+ return null;
73
+ for (let i = events.length - 1; i >= 0; i--) {
74
+ const event = events[i];
75
+ if (event.event_type !== "transition_commit")
76
+ continue;
77
+ const payload = event.payload;
78
+ if (payload.to_state === state && payload.from_state !== state)
79
+ return i;
80
+ }
81
+ return 0;
82
+ }
83
+ function reviewTerminalResultsForGateRole(events, gate, role) {
84
+ const cycleStartIndex = currentReviewGateCycleStart(events, gate);
85
+ if (cycleStartIndex == null)
86
+ return [];
87
+ const jobsById = new Map();
88
+ for (let i = cycleStartIndex; i < events.length; i++) {
89
+ const event = events[i];
90
+ if (event.event_type !== "transition_commit")
91
+ continue;
92
+ const jobs = event.payload.new_jobs ?? [];
93
+ for (const job of jobs) {
94
+ if (job.role === role && gate.isJobForGate(job))
95
+ jobsById.set(job.job_id, job);
96
+ }
97
+ }
98
+ const results = [];
99
+ for (let i = cycleStartIndex; i < events.length; i++) {
100
+ const event = events[i];
101
+ if (event.event_type !== "job_accepted" && event.event_type !== "job_rejected")
102
+ continue;
103
+ const payload = event.payload;
104
+ if (typeof payload.job_id !== "string")
105
+ continue;
106
+ const job = jobsById.get(payload.job_id);
107
+ if (!job)
108
+ continue;
109
+ const findings = Array.isArray(payload.findings) ? payload.findings : undefined;
110
+ results.push({
111
+ job,
112
+ state: event.event_type === "job_accepted" ? "accepted" : "rejected",
113
+ ...(event.event_type === "job_rejected"
114
+ ? { result_kind: reviewResultKind(payload.result_kind) ?? (findings ? "review_failed" : "invalid_report") }
115
+ : {}),
116
+ ...(typeof payload.reason === "string" && payload.reason.trim() !== "" ? { reason: payload.reason } : {}),
117
+ ...(findings ? { findings } : {}),
118
+ });
119
+ }
120
+ return results;
121
+ }
122
+ export function latestReviewHistoryForGateRole(events, gate, role) {
123
+ const terminalResults = reviewTerminalResultsForGateRole(events, gate, role);
124
+ const latest = terminalResults.at(-1);
125
+ if (!latest || latest.state === "accepted")
126
+ return null;
127
+ const resultKind = latest.result_kind ?? "invalid_report";
128
+ const reason = latest.reason ?? (resultKind === "review_failed"
129
+ ? "报告结论为 fail,工作项未通过"
130
+ : "审查报告无效,工作项未通过");
131
+ if (resultKind === "review_failed" && latest.findings && latest.findings.length > 0) {
132
+ return {
133
+ result_kind: resultKind,
134
+ reason,
135
+ job_id: latest.job.job_id,
136
+ ...(latest.findings ? { findings: latest.findings } : {}),
137
+ };
138
+ }
139
+ for (let i = terminalResults.length - 2; i >= 0; i--) {
140
+ const previous = terminalResults[i];
141
+ if (previous.state === "accepted")
142
+ break;
143
+ if (previous.result_kind !== "review_failed" || !previous.findings || previous.findings.length === 0)
144
+ continue;
145
+ return {
146
+ result_kind: resultKind,
147
+ reason,
148
+ job_id: latest.job.job_id,
149
+ ...(previous.findings ? { findings: previous.findings } : {}),
150
+ findings_job_id: previous.job.job_id,
151
+ };
152
+ }
153
+ return {
154
+ result_kind: resultKind,
155
+ reason,
156
+ job_id: latest.job.job_id,
157
+ };
158
+ }
57
159
  export function isReviewReadyVerifier(job) {
58
160
  return job.role === "verifier" && REVIEW_FINAL_VERIFIER_GATE.isJobForGate(job);
59
161
  }
@@ -4,7 +4,8 @@ export interface ReviewGateRule {
4
4
  gate_id: ReviewJobGateId;
5
5
  created_from_transition: "explore" | "propose-ready" | "review-ready";
6
6
  allowedRoles: JobRole[];
7
- reviewedDocPaths: string[];
7
+ reviewTargets: string[];
8
+ readOnlyRefs: string[];
8
9
  requiredRolesForRisk(risk: ReviewRisk): JobRole[];
9
10
  matchesOldJob(job: Job): boolean;
10
11
  isJobForGate(job: Job): boolean;
@@ -28,7 +28,8 @@ export const EXPLORE_DISCOVERY_REVIEW_GATE = makeReviewGateRule({
28
28
  gate_id: EXPLORE_DISCOVERY_REVIEW_GATE_ID,
29
29
  created_from_transition: "explore",
30
30
  allowedRoles: EXPLORE_DISCOVERY_REVIEW_ROLES,
31
- reviewedDocPaths: [".superspec/artifacts/discovery.md"],
31
+ reviewTargets: [".superspec/artifacts/discovery.md"],
32
+ readOnlyRefs: [],
32
33
  requiredRolesForRisk(risk) {
33
34
  return reviewRolesForGate(EXPLORE_DISCOVERY_REVIEW_GATE_ID, risk);
34
35
  },
@@ -40,15 +41,15 @@ export const PROPOSE_FINAL_REVIEW_GATE = makeReviewGateRule({
40
41
  gate_id: PROPOSE_FINAL_REVIEW_GATE_ID,
41
42
  created_from_transition: "propose-ready",
42
43
  allowedRoles: PROPOSAL_REVIEW_ROLES,
43
- reviewedDocPaths: [
44
+ reviewTargets: [
44
45
  "proposal.md",
45
46
  "tasks.md",
46
47
  "design.md",
47
48
  "specs/",
48
- ".superspec/artifacts/discovery.md",
49
49
  ".superspec/artifacts/business-invariants.md",
50
50
  ".superspec/artifacts/test-contract.md",
51
51
  ],
52
+ readOnlyRefs: [".superspec/artifacts/discovery.md"],
52
53
  requiredRolesForRisk(risk) {
53
54
  return reviewRolesForGate(PROPOSE_FINAL_REVIEW_GATE_ID, risk);
54
55
  },
@@ -60,7 +61,8 @@ export const REVIEW_CODE_REVIEW_GATE = makeReviewGateRule({
60
61
  gate_id: REVIEW_CODE_REVIEW_GATE_ID,
61
62
  created_from_transition: "review-ready",
62
63
  allowedRoles: REVIEW_CODE_REVIEW_ROLES,
63
- reviewedDocPaths: [],
64
+ reviewTargets: [],
65
+ readOnlyRefs: [],
64
66
  requiredRolesForRisk(risk) {
65
67
  return reviewRolesForGate(REVIEW_CODE_REVIEW_GATE_ID, risk);
66
68
  },
@@ -72,7 +74,8 @@ export const REVIEW_FINAL_VERIFIER_GATE = makeReviewGateRule({
72
74
  gate_id: REVIEW_FINAL_VERIFIER_GATE_ID,
73
75
  created_from_transition: "review-ready",
74
76
  allowedRoles: REVIEW_FINAL_VERIFIER_ROLES,
75
- reviewedDocPaths: [],
77
+ reviewTargets: [],
78
+ readOnlyRefs: [],
76
79
  requiredRolesForRisk(risk) {
77
80
  return reviewRolesForGate(REVIEW_FINAL_VERIFIER_GATE_ID, risk);
78
81
  },
@@ -42,6 +42,5 @@ export declare function reopen(projectRoot: string, change: string, changeRoot:
42
42
  }): TransitionResult;
43
43
  export declare function reviewReady(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
44
44
  export declare function accept(projectRoot: string, change: string, changeRoot: string): TransitionResult;
45
- export declare function archive(projectRoot: string, change: string, changeRoot: string): TransitionResult;
46
45
  export declare function taskComplete(projectRoot: string, change: string, changeRoot: string, taskId: string, inputContent?: string | null): TransitionResult;
47
46
  export {};
@@ -1,40 +1,48 @@
1
1
  // SuperSpec 流程引擎 — transition:提交协议 + 所有 transition 处理器
2
2
  import { join } from "node:path";
3
3
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
- import { ensureChangeLayout, readEvents, appendEvent, makeEvent, writeSnapshot, snapshotDigest, withLock, idempotencyKey, docRef, listMarkdownFiles, sha256File, sha256Text, } from "./store.js";
4
+ import { ensureChangeLayout, readEvents, appendEvent, makeEvent, writeSnapshot, snapshotDigest, withLock, idempotencyKey, docRef, sha256Text, } from "./store.js";
5
5
  import { rebuildSnapshot } from "./sync.js";
6
6
  import { requiredJobActions } from "./job_action.js";
7
- import { assertCommitPayloadExtension, isFreshReviewVerifier, isReviewReadyVerifier, readReviewPolicyFromEvents, reviewBoundFiles, reviewEvidenceDigest, reviewPolicyForRisk, REVIEW_DOC_PATHS, } from "./review.js";
7
+ import { assertCommitPayloadExtension, isFreshReviewVerifier, isReviewReadyVerifier, latestReviewHistoryForGateRole, readReviewPolicyFromEvents, reviewBoundFiles, reviewEvidenceDigest, reviewPolicyForRisk, REVIEW_DOC_PATHS, } from "./review.js";
8
8
  import { REVIEW_CODE_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE_ID, } from "./review_job_gates.js";
9
9
  import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketContext, codeReviewPacketDigest, collectCodeReviewGateFacts, computeCodeStateCheck, currentCodeReviewWorkingPaths, dismissedCodeReviewSummary, latestCodeReviewDecision, latestCodeReviewFailedStatus, missingCoverageExemptionTestIds, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
10
10
  import { taskEvidenceReadiness } from "./task_evidence.js";
11
11
  import { adoptedContractForTask, findTaskInLines, isReviewFixTaskId, parseTasksMd, parseTestContractEntries, } from "./format.js";
12
- import { applyRequirementModeForCurrentRound, formatPendingTaskMessage, pendingTaskStatusForApply, planTransition, proposalDocsBaseline, } from "./phase_plan.js";
12
+ import { applyRequirementModeForCurrentRound, blockingJobsForApplyDone, formatPendingTaskMessage, latestAcceptedProposalBaseline, pendingTaskStatusForApply, planTransition, proposalDocsBaseline, } from "./phase_plan.js";
13
+ import { latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
13
14
  import { currentGitHead, dirtyCodeFiles } from "./git_state.js";
14
15
  let transitionSeq = 0;
15
16
  function newTransitionId() { return `T-${Date.now()}-${++transitionSeq}`; }
16
17
  let jobSeq = 0;
17
18
  function newJobId(change, role) { return `JOB-${change.slice(0, 8)}-${role.slice(0, 4)}-${Date.now()}-${++jobSeq}`; }
18
- function createReviewJobsForGate(state, gate, roles, changeRoot, change, reason) {
19
+ function createReviewJobsForGate(state, gate, roles, changeRoot, change, reason, events) {
19
20
  const newJobs = roles.map(role => {
20
- // 目录路径(以 / 结尾)始终绑定聚合指纹:审查时不存在、审查后新建同样视为变化
21
- const boundFiles = gate.reviewedDocPaths
22
- .filter(p => p.endsWith("/") || existsSync(join(changeRoot, p)))
21
+ const boundPaths = [...new Set([...gate.reviewTargets, ...gate.readOnlyRefs])];
22
+ // 所有审查目标和只读引用都绑定时点指纹:单文件缺失使用 sha256:missing,目录缺失使用稳定空指纹。
23
+ const boundFiles = boundPaths
23
24
  .map(p => docRef(changeRoot, p));
25
+ const previousRejection = latestReviewHistoryForGateRole(events, gate, role);
24
26
  return {
25
27
  job_id: newJobId(change, role),
26
28
  role,
27
29
  state: "requested",
28
30
  gate_id: gate.gate_id,
29
31
  boundFiles,
32
+ ...(gate.reviewTargets.length > 0 ? { review_targets: [...gate.reviewTargets] } : {}),
33
+ ...(gate.readOnlyRefs.length > 0 ? { read_only_refs: [...gate.readOnlyRefs] } : {}),
30
34
  packet_digest: sha256Text(JSON.stringify({
31
35
  role,
32
36
  gate_id: gate.gate_id,
33
37
  boundFiles,
38
+ review_targets: gate.reviewTargets,
39
+ read_only_refs: gate.readOnlyRefs,
34
40
  created_from_transition: gate.created_from_transition,
41
+ ...(previousRejection ? { previous_rejection: previousRejection } : {}),
35
42
  })),
36
43
  created_from_transition: gate.created_from_transition,
37
44
  created_at: new Date().toISOString(),
45
+ ...(previousRejection ? { previous_rejection: previousRejection } : {}),
38
46
  };
39
47
  });
40
48
  return {
@@ -258,14 +266,17 @@ function evaluateApplyDoneCodeReviewGate(input) {
258
266
  const latest = facts.latestTerminal;
259
267
  if (latest?.state === "accepted") {
260
268
  const acceptedScope = latest.job.packet_context?.code_review_scope;
261
- if (!hasFrozenCodeReviewCurrentHead(acceptedScope)) {
269
+ const staleReason = codeReviewJobStaleReason(input.projectRoot, latest.job, currentWorkingPaths);
270
+ if (staleReason || !hasFrozenCodeReviewCurrentHead(acceptedScope)) {
262
271
  const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
263
272
  return {
264
273
  fromState: "apply_done",
265
274
  toState: "apply_done",
266
275
  outcome: "job_created",
267
276
  newJobs: [job],
268
- reason: `已接受代码审查工作项缺少冻结 current_head,重新创建代码审查工作项;${scanReason}`,
277
+ reason: staleReason
278
+ ? `已接受代码审查工作项不再匹配当前代码状态,重新创建代码审查工作项;${staleReason};${scanReason}`
279
+ : `已接受代码审查工作项缺少冻结 current_head,重新创建代码审查工作项;${scanReason}`,
269
280
  };
270
281
  }
271
282
  return {
@@ -403,14 +414,35 @@ function evaluateFinalVerifierGate(input) {
403
414
  }
404
415
  return { skip: true, message: "已在 review 状态,最终验证仍然有效" };
405
416
  }
406
- function transitionPlanToDecision(snapshot, changeRoot, change, plan) {
417
+ function authorizePhaseAdvance(input) {
418
+ const confirmation = phaseConfirmationForBoundary(input.projectRoot, input.events, input.snapshot, input.boundary);
419
+ const phaseDecision = confirmation
420
+ ? latestAcceptedPhaseDecision(input.events, confirmation)
421
+ : null;
422
+ if (!confirmation || phaseDecision?.decision !== "advance") {
423
+ return {
424
+ skip: true,
425
+ message: confirmation
426
+ ? phaseConfirmationMissingMessage(confirmation)
427
+ : `无法建立 ${input.boundary} 阶段确认范围`,
428
+ };
429
+ }
430
+ return {
431
+ ...input.decision,
432
+ commitPayload: {
433
+ ...(input.decision.commitPayload ?? {}),
434
+ ...phaseConfirmationCommitPayload(confirmation, phaseDecision),
435
+ },
436
+ };
437
+ }
438
+ function transitionPlanToDecision(snapshot, changeRoot, change, plan, events) {
407
439
  switch (plan.kind) {
408
440
  case "skip":
409
441
  return { skip: true, message: plan.message };
410
442
  case "blocked":
411
443
  return { blocked: true, reason: plan.reason, jobs: plan.jobs };
412
444
  case "create_gate_jobs":
413
- return createReviewJobsForGate(snapshot.state, plan.gate, plan.roles, changeRoot, change, plan.reason);
445
+ return createReviewJobsForGate(snapshot.state, plan.gate, plan.roles, changeRoot, change, plan.reason, events);
414
446
  case "advance":
415
447
  return {
416
448
  fromState: plan.fromState,
@@ -520,7 +552,7 @@ export function proposeReady(projectRoot, change, changeRoot, risk = "strict") {
520
552
  snapshot,
521
553
  mode: { kind: "risk", risk },
522
554
  });
523
- return transitionPlanToDecision(snapshot, changeRoot, change, plan);
555
+ return transitionPlanToDecision(snapshot, changeRoot, change, plan, events);
524
556
  },
525
557
  });
526
558
  }
@@ -550,7 +582,7 @@ export function transitionExplore(projectRoot, change, changeRoot, risk = "stric
550
582
  snapshot,
551
583
  mode: { kind: "risk", risk },
552
584
  });
553
- return transitionPlanToDecision(snapshot, changeRoot, change, plan);
585
+ return transitionPlanToDecision(snapshot, changeRoot, change, plan, events);
554
586
  },
555
587
  });
556
588
  }
@@ -568,7 +600,7 @@ export function startApply(projectRoot, change, changeRoot) {
568
600
  snapshot,
569
601
  mode: { kind: "risk", risk: "strict" },
570
602
  });
571
- return transitionPlanToDecision(snapshot, changeRoot, change, plan);
603
+ return transitionPlanToDecision(snapshot, changeRoot, change, plan, events);
572
604
  },
573
605
  });
574
606
  }
@@ -718,6 +750,45 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
718
750
  },
719
751
  };
720
752
  }
753
+ if (to === "propose") {
754
+ if (snapshot.state !== "accepted") {
755
+ return {
756
+ skip: true,
757
+ message: `当前状态 ${snapshot.state},主动 reopen --to propose 只允许从 accepted 发起;apply_done 的代码审查问题请使用 --review-finding`,
758
+ };
759
+ }
760
+ if (snapshot.open_jobs.length > 0) {
761
+ return {
762
+ blocked: true,
763
+ reason: `状态未推进;accepted 仍有 ${snapshot.open_jobs.length} 个待完成工作项`,
764
+ jobs: snapshot.open_jobs,
765
+ };
766
+ }
767
+ const acceptedBaseline = latestAcceptedProposalBaseline(events);
768
+ const currentBaseline = proposalDocsBaseline(changeRoot);
769
+ // 旧版 accepted 事件的基线可能缺少后来纳入 Propose gate 的材料。保留其已冻结
770
+ // 的摘要,并用 reopen 当刻的摘要补齐缺项,确保本轮之后对任一审查目标的修改都能被检测。
771
+ const baselineNeedsBackfill = acceptedBaseline !== null && Object.keys(currentBaseline)
772
+ .some(path => !Object.prototype.hasOwnProperty.call(acceptedBaseline, path));
773
+ const baselineDocs = acceptedBaseline
774
+ ? Object.fromEntries(Object.entries(currentBaseline).map(([path, digest]) => [
775
+ path,
776
+ Object.prototype.hasOwnProperty.call(acceptedBaseline, path) ? acceptedBaseline[path] : digest,
777
+ ]))
778
+ : currentBaseline;
779
+ return {
780
+ fromState: "accepted",
781
+ toState: "propose",
782
+ outcome: "advanced",
783
+ reason: reason.trim(),
784
+ commitPayload: {
785
+ reopen_target: "propose",
786
+ reopen_source: "accepted",
787
+ baseline_source: acceptedBaseline ? (baselineNeedsBackfill ? "accepted_backfill" : "accepted") : "reopen_fallback",
788
+ baseline_docs: baselineDocs,
789
+ },
790
+ };
791
+ }
721
792
  if (to !== "apply")
722
793
  return { skip: true, message: `reopen 当前只支持 --to apply 或 --to propose,不支持 ${to}` };
723
794
  if (snapshot.state !== "apply_done" && snapshot.state !== "review") {
@@ -767,13 +838,34 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
767
838
  };
768
839
  }
769
840
  if (snapshot.state === "apply_done") {
770
- return evaluateApplyDoneCodeReviewGate({
841
+ const blockingJobs = blockingJobsForApplyDone(projectRoot, events, snapshot);
842
+ if (blockingJobs.length > 0) {
843
+ return {
844
+ blocked: true,
845
+ reason: `状态未推进;有 ${blockingJobs.length} 个待完成工作项`,
846
+ jobs: blockingJobs,
847
+ };
848
+ }
849
+ const codeReviewDecision = evaluateApplyDoneCodeReviewGate({
771
850
  events,
772
851
  projectRoot,
773
852
  changeRoot,
774
853
  change,
775
854
  policyPayload,
776
855
  });
856
+ if (!("skip" in codeReviewDecision) &&
857
+ !("blocked" in codeReviewDecision) &&
858
+ codeReviewDecision.fromState === "apply_done" &&
859
+ codeReviewDecision.toState === "review") {
860
+ return authorizePhaseAdvance({
861
+ projectRoot,
862
+ events,
863
+ snapshot,
864
+ boundary: "apply_to_review",
865
+ decision: codeReviewDecision,
866
+ });
867
+ }
868
+ return codeReviewDecision;
777
869
  }
778
870
  if (snapshot.state === "review") {
779
871
  return evaluateFinalVerifierGate({
@@ -806,33 +898,7 @@ export function accept(projectRoot, change, changeRoot) {
806
898
  snapshot,
807
899
  mode: { kind: "risk", risk: "strict" },
808
900
  });
809
- return transitionPlanToDecision(snapshot, changeRoot, change, plan);
810
- },
811
- });
812
- }
813
- // ===== archive =====
814
- export function archive(projectRoot, change, changeRoot) {
815
- return commitTransition(projectRoot, change, changeRoot, {
816
- name: "archive", idempotencyInputs: { phase: "archive" },
817
- decide: (snapshot) => {
818
- if (snapshot.state !== "accepted")
819
- return { skip: true, message: `当前状态 ${snapshot.state},需要 accepted` };
820
- // 构建保全清单(Phase 4 简化版:记录文档指纹 + specs/)
821
- const manifest = {};
822
- const docPaths = ["proposal.md", "tasks.md", "design.md", ".superspec/artifacts/discovery.md", ".superspec/artifacts/business-invariants.md", ".superspec/artifacts/test-contract.md"];
823
- for (const p of docPaths) {
824
- manifest[p] = sha256File(join(changeRoot, p)) ?? "sha256:missing";
825
- }
826
- // specs/ 目录:递归收录 .md(覆盖 specs/<capability>/spec.md 布局)
827
- const specsDir = join(changeRoot, "specs");
828
- for (const rel of listMarkdownFiles(specsDir)) {
829
- manifest[`specs/${rel}`] = sha256File(join(specsDir, rel)) ?? "sha256:missing";
830
- }
831
- return {
832
- fromState: "accepted", toState: "archive", outcome: "advanced",
833
- reason: "归档完成",
834
- extraEvents: [{ type: "artifact_recorded", payload: { kind: "archive_preservation_manifest", manifest } }],
835
- };
901
+ return transitionPlanToDecision(snapshot, changeRoot, change, plan, events);
836
902
  },
837
903
  });
838
904
  }
package/dist/types.d.ts CHANGED
@@ -8,23 +8,28 @@ export type JobState = "requested" | "accepted" | "rejected";
8
8
  export type JobRole = "critic" | "architect" | "test-engineer" | "executor" | "test-run" | "verifier" | "code-reviewer";
9
9
  export type ReviewJobGateId = "explore.discovery_review" | "propose.final_review" | "review.code_review" | "review.final_verifier";
10
10
  export type CodeReviewResultKind = "invalid_report" | "non_actionable_report" | "review_failed";
11
- export interface CodeReviewPreviousRejection {
11
+ export interface ReviewPreviousRejection {
12
12
  result_kind: CodeReviewResultKind;
13
13
  reason: string;
14
14
  job_id: string;
15
+ findings?: unknown[];
16
+ findings_job_id?: string;
15
17
  }
18
+ export type CodeReviewPreviousRejection = ReviewPreviousRejection;
16
19
  export interface Job {
17
20
  job_id: string;
18
21
  role: JobRole;
19
22
  state: JobState;
20
23
  gate_id?: ReviewJobGateId;
21
24
  boundFiles: Ref[];
25
+ review_targets?: string[];
26
+ read_only_refs?: string[];
22
27
  review_evidence_digest?: string;
23
28
  packet_digest: string;
24
29
  packet_context?: JobPacketContext;
25
30
  created_from_transition: string;
26
31
  created_at: string;
27
- previous_rejection?: CodeReviewPreviousRejection;
32
+ previous_rejection?: ReviewPreviousRejection;
28
33
  }
29
34
  export interface ExecutionContract {
30
35
  tests: string[];
@@ -92,8 +97,10 @@ export interface JobPacket {
92
97
  gate_id?: ReviewJobGateId;
93
98
  recommended_agent?: string;
94
99
  boundFiles: Ref[];
100
+ review_targets?: string[];
101
+ read_only_refs?: string[];
95
102
  review_evidence_digest?: string;
96
- previous_rejection?: CodeReviewPreviousRejection;
103
+ previous_rejection?: ReviewPreviousRejection;
97
104
  packet_context?: JobPacketContext;
98
105
  code_review_scope?: CodeReviewScope;
99
106
  coverage_exemption_refs?: CoverageExemptionRef[];
@@ -155,6 +162,15 @@ export interface TransitionCommitPayload {
155
162
  head?: string | null;
156
163
  reason?: "no_code_changes";
157
164
  };
165
+ phase_confirmation?: {
166
+ boundary: "explore_to_propose" | "propose_to_apply" | "apply_to_review" | "accepted_to_archive";
167
+ decision: "advance";
168
+ epoch_event_id: string;
169
+ material_digest: string;
170
+ scope: string;
171
+ decision_event_id: string;
172
+ };
173
+ accepted_baseline_docs?: Record<string, string>;
158
174
  }
159
175
  export interface Snapshot {
160
176
  change_id: string;
@@ -207,10 +223,47 @@ export interface MissingInput {
207
223
  expected: string;
208
224
  command_to_fix: string;
209
225
  }
226
+ export type AskUserActionResume = {
227
+ kind: "next";
228
+ argv: string[];
229
+ } | {
230
+ kind: "continue_current_phase";
231
+ instruction: string;
232
+ next_argv_after_completion: string[];
233
+ } | {
234
+ kind: "stop";
235
+ };
236
+ export interface AskUserAction {
237
+ label: string;
238
+ selection: "exact_label";
239
+ reason: "none" | "required" | "optional";
240
+ reason_prompt?: string;
241
+ record_argv: string[];
242
+ record_input: {
243
+ scope: string;
244
+ question: string;
245
+ answer: string;
246
+ reason?: string;
247
+ };
248
+ resume: AskUserActionResume;
249
+ }
210
250
  export interface AskUser {
211
251
  question: string;
212
252
  allowed_answers: string[];
213
253
  scope: string;
254
+ actions?: AskUserAction[];
255
+ }
256
+ export interface AcceptedMaterialFollowupContinuation {
257
+ kind: "accepted_material_followup";
258
+ trigger: "material_user_followup";
259
+ reason_source: "summarize_user_input";
260
+ reopen_argv_template: string[];
261
+ resume: {
262
+ kind: "continue_current_phase";
263
+ instruction: string;
264
+ next_argv_after_completion: string[];
265
+ };
266
+ plan_docs_changed_since_accept: boolean | null;
214
267
  }
215
268
  export type NextOutput = {
216
269
  state: State;
@@ -230,6 +283,7 @@ export type NextOutput = {
230
283
  } | {
231
284
  path: "done";
232
285
  reason: string;
286
+ continuation?: AcceptedMaterialFollowupContinuation;
233
287
  });
234
288
  export interface TransitionResult {
235
289
  transition: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peterxiaoyang/superspec",
3
- "version": "0.1.40",
3
+ "version": "0.1.41",
4
4
  "description": "SuperSpec 流程引擎 — transition engine with lightweight fact-sync",
5
5
  "type": "module",
6
6
  "engines": {
@@ -3,6 +3,8 @@
3
3
 
4
4
  即使用户没有显式调用 `superspec-*`,如果新输入像是在改变业务规则、产品口径、验收标准、示例规范、影响范围,或说明 PRD/文档/原型等需求源已更新,编辑代码前先提醒并做只读确认:这是实现偏差,还是需要先回 `superspec-propose` 更新计划文档;不要直接把这类自然语言当作 apply 授权。
5
5
 
6
+ 用户补充 SuperSpec 相关内容时,先确定对应 change,再按 next 返回处理;无法确定时只询问归属,不执行流转。内部命令由主流程完成,不交给用户。
7
+
6
8
  当用户显式调用 `$superspec-explore` 工作流时,视为已明确授权启动 `explore` subagent 做只读深扫;其他 `$superspec-*` 阶段仅在工作流引擎创建独立工作项时,视为授权启动对应 subagent。
7
9
 
8
10
  SuperSpec 创建的独立审查/验证工作项,视为已授权启动对应 subagent;无需再次询问用户。主会话不得自批这些工作项。
@@ -18,21 +18,22 @@ argument-hint: "本次架构审查说明"
18
18
 
19
19
  ## 计划 / 设计审查口径
20
20
 
21
- 重点判断影响范围、技术决策和任务拆分是否能支撑后续实现,不把文档格式本身当成目标。
22
-
23
- - `proposal.md` `## Impact` 应通过 `Area` / `Reason` 说明受影响区域和原因;只有泛目录、没有原因或把 `Area` 当路径白名单,应提出阻塞或风险
24
- - `design.md` 应记录每个代码影响型需求的实现方向,粒度到路线选择即可;只是复制影响范围、任务清单或代码步骤,说明设计边界不清
25
- - 关键路线未定且未进入 `## 待用户确认`,或 `tasks.md` 无法从 `design.md` 的方向推出,应判为设计缺口
26
- - discovery `## 输入数据来源核查` 时,审查 `数据来源` 是否追到目标字段或集合最后一次会改变形态的位置;停在 consumer、validator、DTO 名称或机械一跳上游,应提出阻塞或风险
27
- - 审查设计是否把输入完整性决策和 consumer 算法决策分开;把“数据是否加载完整”和“如何比较/计算”混成一个决策,应要求拆清
28
- - 相关 `IDC-xxx` 为 `未知阻塞` 时,设计不得 ready;`未知非阻塞` 必须说明为什么不影响验收,并绑定验收口径或反例
29
- - 当 discovery 含 `## 链路五要素` 时,审查实现方向是否基于已确证的链路事实:重复上游已有的规则变形、双重兜底,或未声明为本次目标却改变已确证的持久化语义,应提出阻塞或风险
30
- - `未知阻塞` 链路行中已确证的下游消费者/视图差异未进入 `proposal.md` `## Impact` 且无排除理由时,按设计缺口处理
31
- - 边界保护:输入来源修复不得无说明地扩大相邻规则、查询、缓存或数据形态的语义
32
- - `tasks.md` task `执行依据:` 块时,从架构角度审查 `设计` 与 `边界` 字段:`设计` 引用的实现方向是否真实可行、与 design 路线一致;`边界` 声明的保护范围是否贴合系统边界和数据语义——空泛到保护不了任何东西、与 design 或 Impact 冲突、或漏掉该 task 明显会触碰的高风险边界(持久化语义、对外接口、共享规则)时,应提出阻塞或风险
33
- - `tasks.md` 可以用 Markdown 标题分组,但可执行边界必须落到顶格 checkbox 叶子 task;任务分组应贴合系统边界,高风险模块、跨入口行为或难以 review 的大改动应拆成可独立验证的 task
34
- - 分组标题、task id 或任务文本会让执行者容易启动错任务,应使用失败结论(`verdict:"fail"`)
35
- - 不要为了弥补拆分不清而要求新增父子任务状态、额外设计字段或 tasks 反向引用 design;先要求更清楚的分组和叶子 task
21
+ 重点审查技术方案是否可落地、系统责任边界是否合理、关键设计契约是否充分。文档结构、跨文档登记和 task 格式由 Critic 主责;测试可验证性由 Test Engineer 主责。不要重复报告纯标题、空章节、复述、task ID / 顺序或 TEST 映射格式问题,但发现真实技术影响未登记时仍须直接报告。
22
+
23
+ - `design.md` 的代码影响型能力应有技术上可行的实现路线,说明方案落在哪个系统责任边界,以及本次实际涉及的数据、接口、状态或控制流如何变化;缺少到实现者无法落地时必须 `verdict:"fail"`
24
+ - 如果不同实现会产生不同的行为、字段组或数据语义、接口增量或兼容结果、状态转换、优先级、一致性、事务 / 幂等 / 并发、恢复结果或关键算法语义,design 必须明确对应绑定性契约;仍需实现者自行选择关键语义时必须失败。逐行代码、完整 SQL,以及以文件修改、task 执行或测试操作为对象的机械清单不属于 design;算法阶段、数据 / 控制流、状态转换和事务顺序可以有序表达
25
+ - discovery / proposal / specs 中影响实现的事实和约束必须转成具体设计安排;只罗列材料、关键约束未进入方案,或方案建立在与已确认事实不符的假设上时,必须 `verdict:"fail"`
26
+ - 声明复用现有链路时,应能确认复用对象、接入位置、本次差异和保持不变的语义;重复上游已有变形、增加双重兜底或改变持久化语义却没有明确理由时,应失败。改变已确认的规则变形或持久化语义时,不仅要说明理由和边界,还必须明确声明为本次目标,并与 proposal、specs Impact 按适用范围完成对账
27
+ - 多个功能点共享字段组、接口语义、状态机、优先级或一致性规则时,应形成统一契约;不同方案对同一契约给出冲突解释时,应失败
28
+ - 关键路线未定且未进入 `## 待用户确认`,或 `tasks.md` 无法从实现方案和边界约束中技术性推出时,必须失败
29
+ - 不按固定章节判定设计质量,也不要要求虚假替代方案或风险;但存在明显误走路线、非显然风险、共享契约、兼容、迁移、回滚或发布顺序约束却完全未说明时,必须失败。只有不影响方案落地和边界判定、但仍值得关注的问题才列为非阻塞风险
30
+ - discovery `## 输入数据来源核查` 时,数据来源必须追到目标字段或集合最后一次改变形态的位置;输入完整性方案与 consumer 获得完整输入后的算法方案必须分开说明
31
+ - `IDC-xxx` 为 `未知阻塞` 时 design 不得 ready;为 `未知非阻塞` 时,理由必须在技术上成立,并绑定验收口径或反例
32
+ - discovery `## 链路五要素` 时,方案不得违背已确认的来源、规则变形、持久化语义、消费者或视图差异;确需重复防御或二次变形时必须说明原因和边界
33
+ - 发现 Impact 未登记的真实系统边界、消费者、视图差异或用户 / 系统可观察行为影响时,应直接判为 Impact / design 对账缺口;纯测试脆弱性和实现复杂度不要求进入 Impact
34
+ - 输入来源修复不得无说明地扩大相邻规则、查询、缓存或数据形态的语义
35
+ - task `设计` 引用应指向技术上可行的实现方案、共享契约或边界约束;`边界` 应保护具体系统行为、数据语义、外部接口或共享规则。引用存在但方案不可行、边界与 design / Impact 冲突或漏掉明显高风险边界时,应失败
36
+ - task 分组应符合系统责任边界;高风险模块、跨入口行为或无法独立验证的大改动,应拆成可独立审查和验证的 task
36
37
 
37
38
  ## 输出风格
38
39
 
@@ -23,7 +23,7 @@ argument-hint: "本次代码审查说明"
23
23
 
24
24
  - 只读,不修改文件。
25
25
  - 不创建 task,不修改 proposal/design/tasks/test-contract。
26
- - 不执行 reopen、accept、archive 或其他状态推进命令。
26
+ - 不执行 reopen、accept 或其他状态推进命令。
27
27
  - 不把主流程没有提供、自己也没读过的材料当作已审查范围。
28
28
  - 上下文不足时,明确写出缺口和需要主流程补充的来源,不猜测。
29
29