@peterxiaoyang/superspec 0.1.40 → 0.1.42

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.
@@ -5,11 +5,23 @@ import { collectProposeOpenQuestions, countDiscoveryOpenQuestions, parseTasksMd,
5
5
  import { currentGitHead } from "./git_state.js";
6
6
  import { docRef, sha256File } from "./store.js";
7
7
  import { isReviewReadyVerifier, isFreshReviewVerifier, readReviewPolicyFromEvents, reviewEvidenceDigest, } from "./review.js";
8
- import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, codeReviewDecisionScope, collectCodeReviewGateFacts, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, } from "./code_review.js";
8
+ import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, codeReviewDecisionScope, codeReviewJobStaleReason, collectCodeReviewGateFacts, currentCodeReviewWorkingPaths, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
9
+ import { isPhaseAdvanceAuthorized, latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
9
10
  import { taskEvidenceReadiness } from "./task_evidence.js";
10
11
  function requiredJobs(state, jobs, reason) {
11
12
  return { kind: "required_jobs", state, jobs, reason };
12
13
  }
14
+ function phaseConfirmationStep(context, boundary, reason) {
15
+ const confirmation = phaseConfirmationForBoundary(context.projectRoot, context.events, context.snapshot, boundary, context.mode.risk);
16
+ if (!confirmation || isPhaseAdvanceAuthorized(context.events, confirmation))
17
+ return null;
18
+ return {
19
+ kind: "ask_user",
20
+ state: context.snapshot.state,
21
+ ask: confirmation.ask,
22
+ reason,
23
+ };
24
+ }
13
25
  function reviewGatePlan(snapshot, gate, requiredRoles) {
14
26
  const missingRoles = [];
15
27
  for (const role of requiredRoles) {
@@ -63,14 +75,36 @@ function missingBaseArtifact(changeRoot, risk) {
63
75
  return null;
64
76
  }
65
77
  export function proposalDocsBaseline(changeRoot) {
66
- // specs/ 用目录聚合指纹:审查可能只对 specs 提出修复,reopen 后仅改 specs 也算文档变化
67
- const docs = ["proposal.md", "design.md", "tasks.md", "specs/", ".superspec/artifacts/test-contract.md"];
78
+ // 与 Propose gate 的可修改审查目标保持同一来源:specs/ 用目录聚合指纹,避免 reopen 基线漏掉任一个可修改的计划材料。
79
+ const docs = PROPOSE_FINAL_REVIEW_GATE.reviewTargets;
68
80
  const baseline = {};
69
81
  for (const doc of docs) {
70
82
  baseline[doc] = docRef(changeRoot, doc).sha;
71
83
  }
72
84
  return baseline;
73
85
  }
86
+ function isDigestMap(value) {
87
+ if (!value || typeof value !== "object" || Array.isArray(value))
88
+ return false;
89
+ const entries = Object.entries(value);
90
+ return entries.length > 0 && entries.every(([, digest]) => typeof digest === "string");
91
+ }
92
+ export function latestAcceptedProposalBaseline(events) {
93
+ for (let i = events.length - 1; i >= 0; i--) {
94
+ const ev = events[i];
95
+ if (ev.event_type !== "transition_commit")
96
+ continue;
97
+ const payload = ev.payload;
98
+ if (payload.transition !== "accept" ||
99
+ payload.from_state !== "review" ||
100
+ payload.to_state !== "accepted")
101
+ continue;
102
+ return isDigestMap(payload.accepted_baseline_docs)
103
+ ? payload.accepted_baseline_docs
104
+ : null;
105
+ }
106
+ return null;
107
+ }
74
108
  export function latestReopenProposeBaseline(events) {
75
109
  for (let i = events.length - 1; i >= 0; i--) {
76
110
  const ev = events[i];
@@ -185,8 +219,42 @@ export function pendingTaskStatusForApply(changeRoot, events) {
185
219
  export function formatPendingTaskMessage(ids, action) {
186
220
  return `尚有未完成任务:${ids.join(", ")};${action}`;
187
221
  }
222
+ function nextArgv(change, risk) {
223
+ return [
224
+ "superspec",
225
+ "transition",
226
+ "next",
227
+ "--change",
228
+ change,
229
+ ...(risk === "strict" ? [] : ["--risk", risk]),
230
+ ];
231
+ }
232
+ function acceptedMaterialFollowup(change, risk, planDocsChangedSinceAccept) {
233
+ return {
234
+ kind: "accepted_material_followup",
235
+ trigger: "material_user_followup",
236
+ reason_source: "summarize_user_input",
237
+ reopen_argv_template: [
238
+ "superspec",
239
+ "transition",
240
+ "reopen",
241
+ "--change",
242
+ change,
243
+ "--to",
244
+ "propose",
245
+ "--reason",
246
+ "{{reason}}",
247
+ ],
248
+ resume: {
249
+ kind: "continue_current_phase",
250
+ instruction: "根据用户补充更新 proposal/specs/design/tasks/test-contract;完成后重新执行 next。",
251
+ next_argv_after_completion: nextArgv(change, risk),
252
+ },
253
+ plan_docs_changed_since_accept: planDocsChangedSinceAccept,
254
+ };
255
+ }
188
256
  export function planNextStep(context) {
189
- const { changeRoot, mode, snapshot } = context;
257
+ const { change, changeRoot, events, mode, snapshot } = context;
190
258
  switch (snapshot.state) {
191
259
  case "init":
192
260
  if (snapshot.open_jobs.length > 0) {
@@ -217,6 +285,12 @@ export function planNextStep(context) {
217
285
  };
218
286
  return { kind: "ask_user", state: "explore", ask, reason: `有 ${openQs} 个未确认问题` };
219
287
  }
288
+ const requiredRoles = EXPLORE_DISCOVERY_REVIEW_GATE.requiredRolesForRisk(mode.risk);
289
+ if (!reviewGatePlan(snapshot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles)) {
290
+ const confirmation = phaseConfirmationStep(context, "explore_to_propose", "探索完成,等待用户确认进入计划阶段");
291
+ if (confirmation)
292
+ return confirmation;
293
+ }
220
294
  return {
221
295
  kind: "run_transition",
222
296
  state: "explore",
@@ -253,6 +327,12 @@ export function planNextStep(context) {
253
327
  if (proposalReviewJobs.length > 0) {
254
328
  return requiredJobs("propose_ready", proposalReviewJobs, `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`);
255
329
  }
330
+ const startApplyPlan = planStartApplyTransition(context, false);
331
+ if (startApplyPlan.kind === "advance") {
332
+ const confirmation = phaseConfirmationStep(context, "propose_to_apply", "计划阶段完成,等待用户确认开始实现");
333
+ if (confirmation)
334
+ return confirmation;
335
+ }
256
336
  return { kind: "run_transition", state: "propose_ready", transition: "start-apply", reason: "计划就绪,开始执行" };
257
337
  }
258
338
  case "apply":
@@ -263,14 +343,28 @@ export function planNextStep(context) {
263
343
  return planReviewNext(context);
264
344
  case "accepted":
265
345
  if (snapshot.open_jobs.length > 0) {
266
- return requiredJobs("accepted", snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项,暂不归档`);
346
+ return requiredJobs("accepted", snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项,暂不结束流程`);
347
+ }
348
+ {
349
+ const acceptedBaseline = latestAcceptedProposalBaseline(events);
350
+ const planDocsChanged = acceptedBaseline
351
+ ? proposalDocsChangedSinceBaseline(changeRoot, acceptedBaseline)
352
+ : null;
353
+ const driftNote = planDocsChanged === true
354
+ ? "检测到 accepted 后计划材料已变化;当前完成结论仍对应 accepted 时冻结的版本。"
355
+ : "";
356
+ return {
357
+ kind: "done",
358
+ state: "accepted",
359
+ reason: `${driftNote}审查已接受,流程完成;后续若使用者补充或修改需求、方案、验收或实现约束,按 continuation 自动回到 propose 后继续,不得要求使用者执行工作流命令`,
360
+ continuation: acceptedMaterialFollowup(change, mode.risk, planDocsChanged),
361
+ };
267
362
  }
268
- return { kind: "ask_archive_confirmation", state: "accepted", reason: "审查通过,等待用户确认归档" };
269
363
  case "archive":
270
364
  if (snapshot.open_jobs.length > 0) {
271
- return requiredJobs("archive", snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项,暂不结束`);
365
+ return requiredJobs("archive", snapshot.open_jobs, `历史 archive 状态仍有 ${snapshot.open_jobs.length} 个待完成工作项`);
272
366
  }
273
- return { kind: "done", state: "archive", reason: "已归档,流程完成。" };
367
+ return { kind: "done", state: "archive", reason: "历史 archive 状态,流程已结束。" };
274
368
  case "abandoned":
275
369
  return { kind: "done", state: "abandoned", reason: "变更已放弃,流程终止。" };
276
370
  default:
@@ -320,6 +414,14 @@ function planApplyNext(context) {
320
414
  reason: "所有任务完成,进入审查",
321
415
  };
322
416
  }
417
+ export function blockingJobsForApplyDone(projectRoot, events, snapshot) {
418
+ const facts = collectCodeReviewGateFacts(events);
419
+ const currentWorkingPaths = currentCodeReviewWorkingPaths(projectRoot, events);
420
+ const freshCodeReviewJobIds = new Set(facts.openJobs
421
+ .filter(job => codeReviewJobStaleReason(projectRoot, job, currentWorkingPaths) == null)
422
+ .map(job => job.job_id));
423
+ return snapshot.open_jobs.filter(job => job.role !== "code-reviewer" || freshCodeReviewJobIds.has(job.job_id));
424
+ }
323
425
  function planApplyDoneNext(context) {
324
426
  const { change, changeRoot, events, mode, snapshot } = context;
325
427
  const pendingTasks = pendingTaskStatusForApply(changeRoot, events).pending;
@@ -332,10 +434,12 @@ function planApplyDoneNext(context) {
332
434
  reason: `发现未完成任务 ${pendingTasks[0]},回到执行阶段`,
333
435
  };
334
436
  }
335
- if (snapshot.open_jobs.length > 0) {
336
- return requiredJobs("apply_done", snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项`);
337
- }
338
437
  const facts = collectCodeReviewGateFacts(events);
438
+ const currentWorkingPaths = currentCodeReviewWorkingPaths(context.projectRoot, events);
439
+ const relevantOpenJobs = blockingJobsForApplyDone(context.projectRoot, events, snapshot);
440
+ if (relevantOpenJobs.length > 0) {
441
+ return requiredJobs("apply_done", relevantOpenJobs, `有 ${relevantOpenJobs.length} 个待完成工作项`);
442
+ }
339
443
  const latest = facts.latestTerminal;
340
444
  if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
341
445
  const status = latestCodeReviewFailedStatus(events);
@@ -423,6 +527,19 @@ function planApplyDoneNext(context) {
423
527
  };
424
528
  return { kind: "ask_user", state: "apply_done", ask, reason: "代码审查报告连续不符合要求或没有可处理问题" };
425
529
  }
530
+ const codeScan = scanCodeChangesForReview(context.projectRoot, events);
531
+ const acceptedScope = latest?.state === "accepted"
532
+ ? latest.job.packet_context?.code_review_scope
533
+ : undefined;
534
+ const acceptedCurrentHead = acceptedScope?.current_head;
535
+ const acceptedReviewReady = latest?.state === "accepted" &&
536
+ codeReviewJobStaleReason(context.projectRoot, latest.job, currentWorkingPaths) == null && (acceptedCurrentHead === null ||
537
+ (typeof acceptedCurrentHead === "string" && acceptedCurrentHead.trim() !== ""));
538
+ if (!codeScan.hasCodeChanges || acceptedReviewReady) {
539
+ const confirmation = phaseConfirmationStep(context, "apply_to_review", "Apply 与代码审查完成,等待用户确认进入最终审查");
540
+ if (confirmation)
541
+ return confirmation;
542
+ }
426
543
  return {
427
544
  kind: "run_transition",
428
545
  state: "apply_done",
@@ -485,7 +602,7 @@ export function planTransition(name, context) {
485
602
  }
486
603
  }
487
604
  function planExploreTransition(context) {
488
- const { changeRoot, mode, snapshot } = context;
605
+ const { changeRoot, events, mode, projectRoot, snapshot } = context;
489
606
  if (snapshot.state === "init") {
490
607
  return { kind: "advance", fromState: "init", toState: "explore", reason: "进入探索阶段" };
491
608
  }
@@ -499,7 +616,18 @@ function planExploreTransition(context) {
499
616
  const gatePlan = reviewGatePlan(snapshot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles);
500
617
  if (gatePlan)
501
618
  return gatePlan;
502
- return { kind: "advance", fromState: "explore", toState: "propose", reason: "探索完成" };
619
+ const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "explore_to_propose");
620
+ const decision = confirmation ? latestAcceptedPhaseDecision(events, confirmation) : null;
621
+ if (!confirmation || decision?.decision !== "advance") {
622
+ return { kind: "skip", message: confirmation ? phaseConfirmationMissingMessage(confirmation) : "无法建立 Explore 阶段确认范围" };
623
+ }
624
+ return {
625
+ kind: "advance",
626
+ fromState: "explore",
627
+ toState: "propose",
628
+ reason: "探索完成",
629
+ payload: phaseConfirmationCommitPayload(confirmation, decision),
630
+ };
503
631
  }
504
632
  function planProposeReadyTransition(context) {
505
633
  const { changeRoot, mode, snapshot } = context;
@@ -526,7 +654,7 @@ function planProposeReadyTransition(context) {
526
654
  return gatePlan;
527
655
  return { kind: "advance", fromState: "propose", toState: "propose_ready", reason: `risk=${risk},所有需求已满足` };
528
656
  }
529
- function planStartApplyTransition(context) {
657
+ function planStartApplyTransition(context, enforceConfirmation = true) {
530
658
  const { changeRoot, events, projectRoot, snapshot } = context;
531
659
  if (snapshot.state !== "propose_ready")
532
660
  return { kind: "skip", message: `当前状态 ${snapshot.state},需要 propose_ready` };
@@ -548,6 +676,11 @@ function planStartApplyTransition(context) {
548
676
  const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot);
549
677
  if (!executionRequirementPlan.ok)
550
678
  return { kind: "skip", message: executionRequirementPlan.message };
679
+ const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "propose_to_apply");
680
+ const decision = confirmation ? latestAcceptedPhaseDecision(events, confirmation) : null;
681
+ if (enforceConfirmation && (!confirmation || decision?.decision !== "advance")) {
682
+ return { kind: "skip", message: confirmation ? phaseConfirmationMissingMessage(confirmation) : "无法建立 Propose 阶段确认范围" };
683
+ }
551
684
  const gitHead = currentGitHead(projectRoot);
552
685
  return {
553
686
  kind: "advance",
@@ -558,6 +691,7 @@ function planStartApplyTransition(context) {
558
691
  apply_start_head: gitHead.head,
559
692
  apply_start_head_reason: gitHead.reason,
560
693
  apply_contract_mode: executionRequirementPlan.mode,
694
+ ...(confirmation && decision?.decision === "advance" ? phaseConfirmationCommitPayload(confirmation, decision) : {}),
561
695
  },
562
696
  };
563
697
  }
@@ -578,5 +712,11 @@ function planAcceptTransition(context) {
578
712
  if (!verifierAccepted)
579
713
  return { kind: "skip", message: "缺少仍然匹配当前证据的最终验证,请先运行 review-ready" };
580
714
  }
581
- return { kind: "advance", fromState: "review", toState: "accepted", reason: "审查通过" };
715
+ return {
716
+ kind: "advance",
717
+ fromState: "review",
718
+ toState: "accepted",
719
+ reason: "审查通过",
720
+ payload: { accepted_baseline_docs: proposalDocsBaseline(changeRoot) },
721
+ };
582
722
  }