@peterxiaoyang/superspec 0.1.46 → 0.1.47

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,11 @@ import { ensureChangeLayout, readEvents, appendEvent, makeEvent, writeSnapshot,
5
5
  import { rebuildSnapshot } from "./sync.js";
6
6
  import { requiredJobActions } from "./job_action.js";
7
7
  import { assertCommitPayloadExtension, isFreshReviewVerifier, isReviewReadyVerifier, latestReviewHistoryForGateRole, readReviewPolicyFromEvents, reviewBoundFiles, reviewEvidenceDigest, reviewPolicyForRisk, REVIEW_DOC_PATHS, } from "./review.js";
8
- import { REVIEW_CODE_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE_ID, reviewScopeForGateRole, } from "./review_job_gates.js";
9
- import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketContext, codeReviewPacketDigest, collectCodeReviewGateFacts, computeCodeStateCheck, currentCodeReviewWorkingPaths, dismissedCodeReviewSummary, latestCodeReviewDecision, latestCodeReviewFailedStatus, missingCoverageExemptionTestIds, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
8
+ import { REVIEW_CODE_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE, REVIEW_FINAL_VERIFIER_GATE_ID, reviewScopeForGateRole, } from "./review_job_gates.js";
9
+ import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketContext, codeReviewPacketDigest, collectCodeReviewGateFacts, computeCodeStateCheck, currentCodeReviewWorkingPaths, dismissedCodeReviewSummary, effectiveCoverageExemptionRefsFromEvents, latestCodeReviewGateEvidence, latestCodeReviewDecision, latestCodeReviewFailedStatus, missingCoverageExemptionTestIds, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, taskExecutionIndexForReview, } from "./code_review.js";
10
10
  import { taskEvidenceReadiness } from "./task_evidence.js";
11
- import { adoptedContractForTask, findTaskInLines, isReviewFixTaskId, parseTasksMd, parseTestContractEntries, } from "./format.js";
12
- import { applyRequirementModeForCurrentRound, executionRequirementVersionForCurrentRound, blockingJobsForApplyDone, executionPolicyForCurrentRound, formatPendingTaskMessage, latestAcceptedProposalBaseline, pendingTaskStatusForApply, planningValidationProfileForNewRound, planTransition, discoveryDocsBaseline, proposalDocsBaseline, } from "./phase_plan.js";
11
+ import { adoptedContractForTask, findTaskInLines, isFixTaskId, parseTasksMd, parseTestContractEntries, } from "./format.js";
12
+ import { applyRequirementModeForCurrentRound, executionRequirementVersionForCurrentRound, blockingJobsForApplyDone, executionPolicyForCurrentRound, formatPendingTaskMessage, latestAcceptedProposalBaseline, pendingTaskStatusForApply, planningValidationProfileForNewRound, planTransition, discoveryDocsBaseline, exploreAnswerRegistrationPayloadForChange, proposalDocsBaseline, } from "./phase_plan.js";
13
13
  import { latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
14
14
  import { currentGitHead, dirtyCodeFiles, stageProductionJavaFilesSince } from "./git_state.js";
15
15
  import { workflowRiskForProject } from "./workflow_config.js";
@@ -173,13 +173,13 @@ function validateTaskStartContract(changeRoot, taskId, parsedContract) {
173
173
  */
174
174
  function validateLegacyTaskStartContract(changeRoot, taskId, tddRequired, parsedContract) {
175
175
  if (!parsedContract) {
176
- return tddRequired && !isReviewFixTaskId(taskId)
176
+ return tddRequired && !isFixTaskId(taskId)
177
177
  ? `执行依据模式下,普通 TDD 任务 ${taskId} 缺少执行依据`
178
178
  : null;
179
179
  }
180
180
  if (parsedContract.errors.length > 0)
181
181
  return parsedContract.errors.join(";");
182
- if (tddRequired && !isReviewFixTaskId(taskId) && parsedContract.contract.tests.length === 0) {
182
+ if (tddRequired && !isFixTaskId(taskId) && parsedContract.contract.tests.length === 0) {
183
183
  return `${taskId} 是普通 TDD 任务,执行依据缺少测试`;
184
184
  }
185
185
  if (parsedContract.contract.tests.length === 0)
@@ -199,7 +199,7 @@ function validateLegacyTaskStartContract(changeRoot, taskId, tddRequired, parsed
199
199
  * RED 是否要求由这里读取已冻结的 execution_policy 决定,之后不再依赖 tasks.md。
200
200
  */
201
201
  function compileRequiredEvidence(executionPolicy, testIds, requiresVerificationWithoutDeclaredTest) {
202
- // REVIEW-FIX 没有计划阶段声明的 TEST,但仍必须登记一次真实回归验证。
202
+ // Fix task 没有计划阶段声明的 TEST,但仍必须登记一次真实回归验证。
203
203
  // 是否需要 RED 始终由已冻结的 execution_policy 决定。
204
204
  const requiresVerification = testIds.length > 0 || requiresVerificationWithoutDeclaredTest;
205
205
  return {
@@ -279,26 +279,133 @@ function findReviewFailedFinding(events, ref) {
279
279
  return null;
280
280
  return { event: status.terminal.event, finding };
281
281
  }
282
- function reviewFixMarker(ref) {
283
- return `review_fix_of:${ref.jobId}#${ref.findingId}`;
282
+ function reviewFixDescriptor(ref, finding) {
283
+ const reason = typeof finding.description === "string" && finding.description.trim()
284
+ ? finding.description.trim().replace(/\s+/g, " ")
285
+ : `修复代码审查问题 ${ref.findingId}`;
286
+ return {
287
+ fix_id: `REVIEW-FIX-${ref.jobId}#${ref.findingId}`,
288
+ source: "code_review",
289
+ parent_task_id: null,
290
+ reason,
291
+ review_finding: { job_id: ref.jobId, finding_id: ref.findingId },
292
+ };
293
+ }
294
+ function selfTestFixBaseId(parentTaskId, reason) {
295
+ const normalizedReason = reason.trim().replace(/\s+/g, " ");
296
+ const normalizedTaskId = parentTaskId.replace(/[^A-Za-z0-9_-]/g, "-");
297
+ const digest = sha256Text(`${parentTaskId}\n${normalizedReason}`).replace(/^sha256:/, "").slice(0, 12);
298
+ return `FIX-SELFTEST-${normalizedTaskId}-${digest}`;
299
+ }
300
+ function selfTestFixDescriptor(parentTaskId, reason, occurrence) {
301
+ const normalizedReason = reason.trim().replace(/\s+/g, " ");
302
+ const baseId = selfTestFixBaseId(parentTaskId, normalizedReason);
303
+ return {
304
+ fix_id: occurrence === 1 ? baseId : `${baseId}-${occurrence}`,
305
+ source: "self_test",
306
+ parent_task_id: parentTaskId,
307
+ reason: normalizedReason,
308
+ };
309
+ }
310
+ function selfTestFixOccurrence(taskId, baseId) {
311
+ if (taskId === baseId)
312
+ return 1;
313
+ const suffix = taskId.slice(baseId.length + 1);
314
+ if (!taskId.startsWith(`${baseId}-`) || !/^\d+$/.test(suffix))
315
+ return null;
316
+ const occurrence = Number(suffix);
317
+ return Number.isSafeInteger(occurrence) && occurrence >= 2 ? occurrence : null;
318
+ }
319
+ function nextSelfTestFixDescriptor(changeRoot, events, parentTaskId, reason) {
320
+ const normalizedReason = reason.trim().replace(/\s+/g, " ");
321
+ const baseId = selfTestFixBaseId(parentTaskId, normalizedReason);
322
+ let maxOccurrence = 0;
323
+ const completion = pendingTaskStatusForApply(changeRoot, events);
324
+ const pendingTaskIds = new Set(completion.pending);
325
+ const taskInfos = parseTasksMd(readFileSync(join(changeRoot, "tasks.md"), "utf8"));
326
+ for (const task of taskInfos) {
327
+ const occurrence = selfTestFixOccurrence(task.taskId, baseId);
328
+ if (occurrence == null)
329
+ continue;
330
+ maxOccurrence = Math.max(maxOccurrence, occurrence);
331
+ if (pendingTaskIds.has(task.taskId))
332
+ return { activeFixTaskId: task.taskId };
333
+ }
334
+ for (const event of events) {
335
+ if (event.event_type !== "transition_commit")
336
+ continue;
337
+ const fix = event.payload.fix;
338
+ if (!fix || typeof fix !== "object" || Array.isArray(fix))
339
+ continue;
340
+ const candidate = fix;
341
+ if (candidate.source !== "self_test" || candidate.parent_task_id !== parentTaskId || candidate.reason !== normalizedReason)
342
+ continue;
343
+ if (typeof candidate.fix_id !== "string")
344
+ continue;
345
+ const occurrence = selfTestFixOccurrence(candidate.fix_id, baseId);
346
+ if (occurrence != null)
347
+ maxOccurrence = Math.max(maxOccurrence, occurrence);
348
+ }
349
+ return { fix: selfTestFixDescriptor(parentTaskId, normalizedReason, maxOccurrence + 1) };
284
350
  }
285
- function reviewFixTaskId(ref) {
286
- return `REVIEW-FIX-${ref.jobId}#${ref.findingId}`;
351
+ function fixMarker(fix) {
352
+ if (fix.source === "code_review" && fix.review_finding) {
353
+ return `review_fix_of:${fix.review_finding.job_id}#${fix.review_finding.finding_id}`;
354
+ }
355
+ return `self_test_fix_of:${fix.parent_task_id ?? "unknown"}:${fix.fix_id}`;
287
356
  }
288
- function appendReviewFixTask(changeRoot, ref, finding) {
357
+ function appendFixTask(changeRoot, fix) {
289
358
  const tasksPath = join(changeRoot, "tasks.md");
290
359
  const content = readFileSync(tasksPath, "utf8");
291
- const marker = reviewFixMarker(ref);
292
- if (content.includes(marker))
293
- return "exists";
294
- const description = typeof finding.description === "string" && finding.description.trim()
295
- ? finding.description.trim().replace(/\s+/g, " ")
296
- : `修复代码审查问题 ${ref.findingId}`;
297
- const line = `- [ ] ${reviewFixTaskId(ref)} ${description} ${marker}`;
360
+ const marker = fixMarker(fix);
361
+ if (content.includes(marker)) {
362
+ const matchingTask = content.split("\n").some(line => line.includes(marker) && line.startsWith(`- [ ] ${fix.fix_id} `));
363
+ if (matchingTask)
364
+ return "exists";
365
+ throw new Error(`修复标记 ${marker} 已被其它 task 占用,拒绝创建 ${fix.fix_id}`);
366
+ }
367
+ if (parseTasksMd(content).some(task => task.taskId === fix.fix_id)) {
368
+ throw new Error(`修复 task ID ${fix.fix_id} 已被其它任务占用,拒绝创建重复修复`);
369
+ }
370
+ const description = fix.source === "self_test"
371
+ ? `修复自测问题(关联 ${fix.parent_task_id}):${fix.reason}`
372
+ : fix.reason;
373
+ const line = `- [ ] ${fix.fix_id} ${description} ${marker}`;
298
374
  const suffix = content.endsWith("\n") ? "" : "\n";
299
375
  writeFileSync(tasksPath, `${content}${suffix}${line}\n`);
300
376
  return "created";
301
377
  }
378
+ function fixDescriptorForTask(events, taskId) {
379
+ for (let i = events.length - 1; i >= 0; i--) {
380
+ const event = events[i];
381
+ if (event.event_type !== "transition_commit")
382
+ continue;
383
+ const fix = event.payload.fix;
384
+ if (!fix || typeof fix !== "object" || Array.isArray(fix))
385
+ continue;
386
+ const candidate = fix;
387
+ if (candidate.fix_id !== taskId)
388
+ continue;
389
+ if (candidate.source !== "code_review" && candidate.source !== "self_test")
390
+ continue;
391
+ if (typeof candidate.parent_task_id !== "string" && candidate.parent_task_id !== null)
392
+ continue;
393
+ if (typeof candidate.reason !== "string")
394
+ continue;
395
+ return candidate;
396
+ }
397
+ // 兼容发布前已写入 tasks.md、但 transition payload 尚未保存修复描述符的记录。
398
+ const legacy = /^REVIEW-FIX-(.+)#([^#]+)$/.exec(taskId);
399
+ return legacy
400
+ ? {
401
+ fix_id: taskId,
402
+ source: "code_review",
403
+ parent_task_id: null,
404
+ reason: "历史代码审查修复任务",
405
+ review_finding: { job_id: legacy[1], finding_id: legacy[2] },
406
+ }
407
+ : null;
408
+ }
302
409
  function isFreshOpenCodeReviewerJob(job, projectRoot, currentWorkingPaths) {
303
410
  return codeReviewJobStaleReason(projectRoot, job, currentWorkingPaths) == null;
304
411
  }
@@ -356,6 +463,17 @@ function evaluateApplyDoneCodeReviewGate(input) {
356
463
  };
357
464
  }
358
465
  if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
466
+ const staleReason = codeReviewJobStaleReason(input.projectRoot, latest.job, currentWorkingPaths);
467
+ if (staleReason) {
468
+ const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
469
+ return {
470
+ fromState: "apply_done",
471
+ toState: "apply_done",
472
+ outcome: "job_created",
473
+ newJobs: [job],
474
+ reason: `代码审查结论已不再匹配当前代码状态,重新创建代码审查工作项;${staleReason};${scanReason}`,
475
+ };
476
+ }
359
477
  const reviewFailedStatus = latestCodeReviewFailedStatus(input.events);
360
478
  if (reviewFailedStatus && reviewFailedStatus.findings.length > 0 && reviewFailedStatus.unresolved.length === 0) {
361
479
  const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
@@ -400,9 +518,14 @@ function evaluateApplyDoneCodeReviewGate(input) {
400
518
  }
401
519
  function createFinalVerifierJob(change, projectRoot, changeRoot, events, currentEvidenceDigest) {
402
520
  const boundFiles = reviewBoundFiles(changeRoot);
521
+ const codeReviewGate = latestCodeReviewGateEvidence(events);
403
522
  const packetContext = {
404
523
  code_state_check: computeCodeStateCheck(projectRoot, events),
524
+ coverage_exemption_refs: effectiveCoverageExemptionRefsFromEvents(events),
525
+ task_execution_index: taskExecutionIndexForReview(projectRoot, events),
526
+ ...(codeReviewGate ? { code_review_gate: codeReviewGate } : {}),
405
527
  };
528
+ const previousRejection = latestReviewHistoryForGateRole(events, REVIEW_FINAL_VERIFIER_GATE, "verifier");
406
529
  return {
407
530
  job_id: newJobId(change, "verifier"),
408
531
  role: "verifier",
@@ -418,9 +541,11 @@ function createFinalVerifierJob(change, projectRoot, changeRoot, events, current
418
541
  review_evidence_digest: currentEvidenceDigest,
419
542
  packet_context: packetContext,
420
543
  created_from_transition: "review-ready",
544
+ ...(previousRejection ? { previous_rejection: previousRejection } : {}),
421
545
  })),
422
546
  created_from_transition: "review-ready",
423
547
  created_at: new Date().toISOString(),
548
+ ...(previousRejection ? { previous_rejection: previousRejection } : {}),
424
549
  };
425
550
  }
426
551
  function evaluateFinalVerifierGate(input) {
@@ -695,11 +820,15 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
695
820
  // 统一出口:adopted.contract 非 null 当且仅当契约模式下有绑定块;
696
821
  // legacy 轮即使 task 带执行依据文本也输出 null,避免"看似契约、实按 legacy 校验"的误导态
697
822
  const adopted = adoptedContractForTask(tasksContent, taskId, contractMode);
698
- const reviewFix = isReviewFixTaskId(taskId);
699
- if (contractMode && executionRequirementVersion === 2 && !reviewFix && !adopted.parsed) {
823
+ const fix = fixDescriptorForTask(events, taskId);
824
+ const isFixTask = isFixTaskId(taskId);
825
+ if (isFixTask && !fix) {
826
+ return { skip: true, message: `Fix task ${taskId} 缺少状态机创建记录,不能直接手工追加` };
827
+ }
828
+ if (contractMode && executionRequirementVersion === 2 && !isFixTask && !adopted.parsed) {
700
829
  return { skip: true, message: `执行依据模式下,任务 ${taskId} 缺少执行依据` };
701
830
  }
702
- if (contractMode && executionRequirementVersion === 2 && !reviewFix && adopted.parsed) {
831
+ if (contractMode && executionRequirementVersion === 2 && !isFixTask && adopted.parsed) {
703
832
  const contractError = validateTaskStartContract(changeRoot, taskId, adopted.parsed);
704
833
  if (contractError)
705
834
  return { skip: true, message: contractError };
@@ -711,17 +840,19 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
711
840
  }
712
841
  const structureDigest = sha256Text(tasksContent.replace(/- \[[xX]\]/g, "- [ ]"));
713
842
  const effectivePolicy = executionPolicy;
714
- const requiredEvidence = contractMode && executionRequirementVersion === 2
715
- ? compileRequiredEvidence(effectivePolicy, adopted.contract?.tests ?? [], reviewFix)
843
+ const requiredEvidence = isFixTask || (contractMode && executionRequirementVersion === 2)
844
+ ? compileRequiredEvidence(effectivePolicy, adopted.contract?.tests ?? [], isFixTask)
716
845
  : null;
717
846
  const attempt = {
718
847
  attempt_id: `ATT-${taskId}-${Date.now()}-${++attemptSeq}`,
719
848
  task_id: taskId, state: "active",
720
849
  task_structure_digest: structureDigest,
850
+ ...(fix ? { fix } : {}),
721
851
  contract_mode: contractMode,
722
852
  contract: adopted.contract,
723
853
  ...(requiredEvidence ? { required_evidence: requiredEvidence } : {
724
- // 历史模式以及 v1 契约轮继续使用 task 行标记回放;v2 只消费快照。
854
+ // Fix 的历史模式以及 v1 契约轮继续使用 task 行标记回放;新建 Fix
855
+ // 无论来自哪个版本,均只消费本轮冻结的有效证据要求。
725
856
  tdd_required: taskInfo.tddRequired,
726
857
  no_tdd_reason: taskInfo.noTddReason,
727
858
  }),
@@ -745,8 +876,9 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
745
876
  task_id: taskId,
746
877
  execution_policy: effectivePolicy,
747
878
  contract: adopted.contract,
879
+ ...(fix ? { fix } : {}),
748
880
  ...(requiredEvidence ? { required_evidence: requiredEvidence } : {}),
749
- // legacy 轮统一标注历史模式(无论有无执行依据文本);契约轮无块时标 false(如 REVIEW-FIX
881
+ // legacy 轮统一标注历史模式(无论有无执行依据文本);契约轮无块时标 false(如 Fix task
750
882
  ...(adopted.contract ? {} : { legacy_contract: !contractMode }),
751
883
  },
752
884
  };
@@ -792,11 +924,20 @@ function canReopenToPropose(from) {
792
924
  }
793
925
  export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
794
926
  return commitTransition(projectRoot, change, changeRoot, {
795
- name: "reopen", idempotencyInputs: { to, reason, reviewFix: opts.reviewFix ?? "", reviewFinding: opts.reviewFinding ?? "" },
927
+ name: "reopen", idempotencyInputs: {
928
+ to,
929
+ reason,
930
+ reviewFix: opts.reviewFix ?? "",
931
+ reviewFinding: opts.reviewFinding ?? "",
932
+ selfTestFix: opts.selfTestFix ?? "",
933
+ },
796
934
  decide: (snapshot) => {
797
935
  if (!reason || reason.trim() === "")
798
936
  return { skip: true, message: "reopen 需要非空 --reason" };
799
937
  const events = readEvents(projectRoot, change);
938
+ const specialFixCount = [opts.reviewFix, opts.reviewFinding, opts.selfTestFix].filter(Boolean).length;
939
+ if (specialFixCount > 1)
940
+ return { skip: true, message: "一次 reopen 只能指定一种修复或审查引用" };
800
941
  if (opts.reviewFinding) {
801
942
  if (to !== "propose")
802
943
  return { skip: true, message: "--review-finding 只能用于回到计划阶段(reopen --to propose)" };
@@ -852,6 +993,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
852
993
  else if (type !== "implementation") {
853
994
  return { skip: true, message: "这个代码审查问题不能直接回到实现阶段处理" };
854
995
  }
996
+ const fix = reviewFixDescriptor(ref, found.finding);
855
997
  return {
856
998
  fromState: "apply_done",
857
999
  toState: "apply",
@@ -861,9 +1003,56 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
861
1003
  review_fix_of: `${ref.jobId}#${ref.findingId}`,
862
1004
  source_job_id: ref.jobId,
863
1005
  finding_id: ref.findingId,
1006
+ fix,
1007
+ },
1008
+ postCommit: (_pr, _ch, cr) => {
1009
+ appendFixTask(cr, fix);
864
1010
  },
1011
+ };
1012
+ }
1013
+ if (opts.selfTestFix) {
1014
+ if (to !== "apply")
1015
+ return { skip: true, message: "--self-test-fix 只能用于回到实现阶段(reopen --to apply)" };
1016
+ const allowedStates = ["apply", "apply_done", "review", "accepted"];
1017
+ if (!allowedStates.includes(snapshot.state)) {
1018
+ return { skip: true, message: `当前状态 ${snapshot.state},不能通过自测问题回到实现阶段` };
1019
+ }
1020
+ const parentTaskId = opts.selfTestFix.trim();
1021
+ const parentTask = parseTasksMd(readFileSync(join(changeRoot, "tasks.md"), "utf8"))
1022
+ .find(task => task.taskId === parentTaskId);
1023
+ if (!parentTask)
1024
+ return { skip: true, message: `自测修复关联的 task ${parentTaskId} 不存在` };
1025
+ const nextFix = nextSelfTestFixDescriptor(changeRoot, events, parentTaskId, reason);
1026
+ if ("activeFixTaskId" in nextFix) {
1027
+ return { skip: true, message: `同一自测问题的修复 ${nextFix.activeFixTaskId} 尚未完成,不能重复创建` };
1028
+ }
1029
+ const fix = nextFix.fix;
1030
+ const pendingStatus = pendingTaskStatusForApply(changeRoot, events);
1031
+ if (pendingStatus.pending.length > 0 || snapshot.active_task_attempts.some(attempt => attempt.state === "active")) {
1032
+ return { skip: true, message: "自测修复只允许在当前 task 全部完成且没有活跃执行尝试后创建" };
1033
+ }
1034
+ if (pendingStatus.mode === "contract" && !pendingStatus.completedByEvent.includes(parentTaskId)) {
1035
+ return { skip: true, message: `自测修复关联的 task ${parentTaskId} 缺少完成事件,不能只依赖 checkbox` };
1036
+ }
1037
+ if (pendingStatus.mode === "legacy" && !parentTask.done) {
1038
+ return { skip: true, message: `自测修复关联的 task ${parentTaskId} 尚未完成` };
1039
+ }
1040
+ const failedReview = latestCodeReviewFailedStatus(events);
1041
+ if (failedReview?.unresolved.length) {
1042
+ return {
1043
+ skip: true,
1044
+ message: `已有未处理的代码审查问题 ${failedReview.unresolved[0].id};请先按 next 返回的 --review-fix 处理`,
1045
+ };
1046
+ }
1047
+ return {
1048
+ fromState: snapshot.state,
1049
+ toState: "apply",
1050
+ outcome: "advanced",
1051
+ reason: reason.trim(),
1052
+ commitPayload: { fix },
1053
+ extraEvents: invalidateOpenJobs(snapshot, "apply", reason.trim()),
865
1054
  postCommit: (_pr, _ch, cr) => {
866
- appendReviewFixTask(cr, ref, found.finding);
1055
+ appendFixTask(cr, fix);
867
1056
  },
868
1057
  };
869
1058
  }
@@ -880,6 +1069,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
880
1069
  reopen_target: "explore",
881
1070
  reopen_source: snapshot.state,
882
1071
  baseline_docs: discoveryDocsBaseline(changeRoot),
1072
+ ...exploreAnswerRegistrationPayloadForChange(changeRoot),
883
1073
  },
884
1074
  extraEvents: planningReopenExtraEvents(snapshot, "explore", reason.trim()),
885
1075
  };
package/dist/types.d.ts CHANGED
@@ -50,6 +50,21 @@ export interface EffectiveEvidencePlan {
50
50
  green_required: boolean;
51
51
  accepted_green_statuses: Array<"expected_success" | "characterization_pass">;
52
52
  }
53
+ /**
54
+ * 状态机创建的实现修复工作项。它只纠正已批准 task 的实现,不产生新的计划需求。
55
+ * 旧事件没有该字段时按历史行为回放。
56
+ */
57
+ export type FixSource = "code_review" | "self_test";
58
+ export interface FixDescriptor {
59
+ fix_id: string;
60
+ source: FixSource;
61
+ parent_task_id: string | null;
62
+ reason: string;
63
+ review_finding?: {
64
+ job_id: string;
65
+ finding_id: string;
66
+ };
67
+ }
53
68
  export interface DirtyFileFingerprint {
54
69
  path: string;
55
70
  status: "added" | "modified" | "deleted";
@@ -78,9 +93,19 @@ export interface CodeStateCheck {
78
93
  changed_paths: string[];
79
94
  scope_reason: string;
80
95
  }
96
+ /** 最终验证读取的最新代码审查门禁事实。 */
97
+ export interface CodeReviewGateEvidence {
98
+ decision: "passed" | "skipped";
99
+ job_id: string | null;
100
+ packet_digest: string | null;
101
+ reason?: "no_code_changes";
102
+ event_id: string;
103
+ event_digest: string;
104
+ }
81
105
  export interface TaskExecutionIndexEntry {
82
106
  task_id: string;
83
107
  attempt_id: string;
108
+ fix?: FixDescriptor | null;
84
109
  execution_policy: ExecutionPolicy;
85
110
  changed_paths: string[] | null;
86
111
  changed_paths_partial_reason?: string;
@@ -99,6 +124,7 @@ export interface CoverageExemptionRef {
99
124
  }
100
125
  export interface JobPacketContext {
101
126
  code_review_scope?: CodeReviewScope;
127
+ code_review_gate?: CodeReviewGateEvidence;
102
128
  coverage_exemption_refs?: CoverageExemptionRef[];
103
129
  task_execution_index?: TaskExecutionIndexEntry[];
104
130
  unattributed_paths?: string[];
@@ -117,6 +143,7 @@ export interface JobPacket {
117
143
  previous_rejection?: ReviewPreviousRejection;
118
144
  packet_context?: JobPacketContext;
119
145
  code_review_scope?: CodeReviewScope;
146
+ code_review_gate?: CodeReviewGateEvidence;
120
147
  coverage_exemption_refs?: CoverageExemptionRef[];
121
148
  task_execution_index?: TaskExecutionIndexEntry[];
122
149
  unattributed_paths?: string[];
@@ -227,6 +254,7 @@ export interface TaskAttempt {
227
254
  task_id: string;
228
255
  state: AttemptState;
229
256
  task_structure_digest: string;
257
+ fix?: FixDescriptor | null;
230
258
  contract?: ExecutionContract | null;
231
259
  contract_mode?: boolean;
232
260
  /** task-start 编译出的有效执行要求;缺失表示历史 attempt,按旧字段回放。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peterxiaoyang/superspec",
3
- "version": "0.1.46",
3
+ "version": "0.1.47",
4
4
  "description": "SuperSpec 流程引擎 — transition engine with lightweight fact-sync",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,15 +1,25 @@
1
1
  <!-- SUPERSPEC:AGENTS:START -->
2
- 本项目启用 SuperSpec。使用 `superspec-*` 工作流时,以 `superspec transition next --change "<change>"` 返回的下一步为准;流程完成前不得跳阶段、不得自称完成。
2
+ 只有当用户显式调用 `superspec-*`,或明确要求继续处理某个已有 SuperSpec change 时,才进入或续转 SuperSpec 工作流。普通开发、修复、排查、测试或审查请求,即使项目已安装 SuperSpec,也不得自行启动工作流、创建 change、执行 `transition next`,或切换到某个 `superspec-*` 阶段。
3
3
 
4
- 即使用户没有显式调用 `superspec-*`,如果新输入像是在改变业务规则、产品口径、验收标准、示例规范、影响范围,或说明 PRD/文档/原型等需求源已更新,编辑代码前先提醒并做只读确认:这是实现偏差,还是需要先回 `superspec-propose` 更新计划文档;不要直接把这类自然语言当作 apply 授权。
4
+ 一旦用户已显式启动工作流或明确指定 change,使用 `superspec-*` 工作流时一律以 `superspec transition next --change "<change>"` 返回的下一步推进;主流程执行内部命令,不要求用户手动运行工作流命令。流程完成前不得跳阶段、不得自称完成。
5
5
 
6
- 用户补充 SuperSpec 相关内容时,先确定对应 change,再按 next 返回处理;无法确定时只询问归属,不执行流转。同一 change 的方案、需求、验收或实现约束补充,按影响回到该 change `propose`,不要另建 repair change;内部命令由主流程完成,不交给用户。
6
+ 每完成 `next` 返回的当前事项(材料更新、用户答复回写、实现、验证、审查或修复时),立即再次运行 `superspec transition next --change "<change>"` 并继续处理。完成单个事项不等于完成整个 change;只有工作流明确需要用户决定、当前独立工作项尚未返回结果、遇到真实阻塞或整个 change 已完成时才暂停。
7
7
 
8
- 当用户显式调用 `$superspec-explore` 工作流时,视为已明确授权启动 `explore` subagent 做只读深扫;其他 `$superspec-*` 阶段仅在工作流引擎创建独立工作项时,视为授权启动对应 subagent。
8
+ 当用户显式调用 `$superspec-explore`,或明确要求继续处于 Explore 的已有 change 时,视为已明确授权启动 `explore` subagent 做只读深扫;其他 `$superspec-*` 阶段仅在工作流引擎创建独立工作项时,视为授权启动对应 subagent。
9
+
10
+ Explore 中需要用户决定业务、验收、范围或关键取舍时,先简要说明当前理解、影响和建议,再一次只请用户决定一件事;收到明确答复后,更新相关 discovery 结论,再继续工作流。其他阶段要求用户确认、选择处理方向或补齐材料时,按当前工作流返回的要求登记结论或更新相应材料;不得把这类答复默认写入 discovery。
11
+
12
+ 当前 change 的自测、联调或用户指出的问题若仍能由既有 task 的批准行为、边界和验收解释,就在同一 change 内处理:
13
+
14
+ - 当前 task 尚未完成时,在其范围内直接修复;不要为同一实现问题新增 task 或回 propose。
15
+ - 所有 task 已完成后,若问题仍能关联一个已完成 task、且不改变已批准行为和方案,主流程执行 `superspec transition reopen --change "<change>" --to apply --self-test-fix "<task>" --reason "<reason>"`,让工作流创建修复事项;随后继续 `next`,不得手改 tasks。
16
+ - 无法关联既有 task,或需要改变行为、验收、接口、数据语义或实现路线时,才回 propose。
17
+
18
+ 在用户已显式启动工作流或明确指定 change 后,若新增或改变业务规则、产品口径、验收、示例规范、影响范围,或说明 PRD/文档/原型等需求源已更新时,先确定对应 change;归属明确则回同一 change 的 `propose` 更新计划,归属不明才询问。不要把这类输入直接当作 apply 授权,也不要另建 repair change。
9
19
 
10
20
  SuperSpec 创建的独立审查/验证工作项,视为已授权启动对应 subagent;无需再次询问用户。主会话不得自批这些工作项。
11
21
 
12
22
  审查/验证工作项只授权处理该工作项绑定的内容,不得扩大范围、跳过阶段或代替后续流程。若当前环境无法启动 subagent,只能使用本地或用户已授权的独立来源;没有可用独立来源时停在当前工作项并说明阻塞,主会话不得因此自批。
13
23
 
14
- 执行 CLI 返回命令时优先使用 `*_argv` 字段。用户可见回复使用自然语言;除非用户要求调试信息,不复述内部 JSON 字段。
24
+ 主流程代为执行必要的工作流操作;用户可见回复使用自然语言,除非用户要求调试信息,不展示底层命令或数据。
15
25
  <!-- SUPERSPEC:AGENTS:END -->
@@ -11,9 +11,9 @@ argument-hint: "本次架构审查说明"
11
11
 
12
12
  ## 工作边界
13
13
 
14
- - 先读 job packet、任务说明和被引用材料;范围、停止条件与报告契约以 packet 为准。材料不足时报告缺口,不猜测或自行扩展范围。
14
+ - 先读任务说明和被引用材料;范围和停止条件以任务说明为准。材料不足时报告缺口,不猜测或自行扩展范围。
15
15
  - 修复复核优先判断原架构问题是否已消失;新 blocker 只能是修复直接造成的架构回归。
16
- - 状态机负责文档和报告的机械协议。不要因标题、字段、ID、表格、引用或 checkbox 提出 finding;只判断它们表达的设计是否成立。
16
+ - 不要因标题、字段、编号、表格、引用或勾选提出 finding;只判断它们表达的设计是否成立。
17
17
 
18
18
  ## 架构判断
19
19
 
@@ -11,13 +11,13 @@ argument-hint: "本次代码审查说明"
11
11
 
12
12
  ## 工作边界
13
13
 
14
- - 先读 job packet、任务说明、指定代码范围和相关计划材料;packet 决定审查范围、停止条件和报告契约。不要把未打开的材料当作审查依据。
15
- - 只读;不实现修复、不修改计划或证据、不推进状态。上下文不足时明确指出缺口。
14
+ - 先读任务说明、指定代码范围和相关计划材料;范围和停止条件以任务说明为准。不要把未打开的材料当作审查依据。
15
+ - 只读;不实现修复、不修改计划或证据、不自行宣布完成。上下文不足时明确指出缺口。
16
16
  - 修复复核优先验证原问题是否被修复;新 blocker 必须由修复直接引入并有证据。
17
17
 
18
18
  ## 审查判断
19
19
 
20
- 仅报告可复现、可定位且影响本次 change 的问题。工作项给出的累计 diff、任务归属线索、执行快照、范围扩大记录和测试证据用于缩小对照范围;它们不是替代代码判断的结论。归属未知时按 packet 的保守范围审查。
20
+ 仅报告可复现、可定位且影响本次 change 的问题。任务说明给出的改动、归属线索和测试证据用于缩小对照范围;它们不是替代代码判断的结论。归属未知时按已知范围保守审查。
21
21
 
22
22
  重点检查:
23
23
 
@@ -11,9 +11,9 @@ argument-hint: "本次反方审查说明"
11
11
 
12
12
  ## 工作边界
13
13
 
14
- - 先读 job packet、任务说明和被引用材料;只审查本次 change 的既定范围。材料不足时报告证据缺口,不猜测或静默扩大范围。
14
+ - 先读任务说明和被引用材料;只审查本次 change 的既定范围。材料不足时报告证据缺口,不猜测或静默扩大范围。
15
15
  - 修复复核优先判断原问题是否仍成立;新 blocker 只能来自修复直接引入的回归,并说明因果链。
16
- - 状态机负责 OpenSpec、文档、任务、引用和报告的机械协议。不要因标题、字段、表格、ID、checkbox 或引用写法提出 finding;只判断材料表达的事实、范围和可执行性。
16
+ - 不要因标题、字段、表格、编号、勾选或引用写法提出 finding;只判断材料表达的事实、范围和可执行性。
17
17
 
18
18
  ## 反方判断
19
19
 
@@ -11,15 +11,15 @@ argument-hint: "本次执行说明"
11
11
 
12
12
  ## 工作边界
13
13
 
14
- - 先读本次任务说明。它定义写入范围、冻结的验证要求、可用上下文、报告契约和停止条件;按它执行,不依赖本 prompt 推断字段或阶段。
15
- - 只修改被授权的实现与测试路径。不修改计划材料、`.superspec/**`、task checkbox、正式证据或审查报告。
16
- - 不自行选择或补造 RED/GREEN;只完成任务说明要求的验证。需要由 test-runner 产生的证据,不代跑或伪造。
14
+ - 先读本次任务说明。它定义写入范围、验证要求、可用上下文和停止条件;按它执行,不依赖本 prompt 推断额外工作。
15
+ - 只修改被授权的实现与测试路径。不修改计划材料、工作流记录、验证材料或审查报告。
16
+ - 只完成任务说明要求的验证,不伪造结果或代替后续验证。
17
17
 
18
18
  ## 停止条件
19
19
 
20
20
  写入范围缺失或不安全、上下文或验证命令不足、必须扩大范围,或发现数据来源、业务规则、验收与批准计划不一致时,停止并报告 blocker。用户在执行期间给出的新需求不是本 task 的实现授权,应交回计划阶段。
21
21
 
22
- 实施过程中始终对照 task 的验收和边界:可以做实现所必需且能由当前引用链解释的局部连带改动;发现需要新增能力、改变公开语义、跨出授权责任边界或重写计划假设时停止。不要以“顺手修复”为理由扩张 scope。
22
+ 实施过程中始终对照 task 的验收和边界:可以做实现所必需且能由当前引用链解释的局部连带改动;发现需要新增能力、改变公开语义、跨出授权责任边界或重写计划假设时停止。不要以“顺手修复”为理由扩大范围。
23
23
 
24
24
  ## 实施口径
25
25
 
@@ -29,4 +29,4 @@ argument-hint: "本次执行说明"
29
29
 
30
30
  ## 输出
31
31
 
32
- 结论先行说明完成、部分完成或阻塞;按任务说明的报告契约提交实际改动、验证候选、未验证项和残余风险。长日志与大产物按 packet 作为 artifact refs 返回。
32
+ 结论先行说明完成、部分完成或阻塞;按任务说明报告实际改动、验证结果、未验证项和残余风险。
@@ -21,8 +21,8 @@ argument-hint: "本次探索说明"
21
21
 
22
22
  先从用户目标和已有锚点形成探查问题,再用正向搜索与调用方/入口反查验证。对每个重要结论明确它是事实、基于锚点的推断还是未知;影响范围候选需要说明为什么可能受影响或为什么排除。不要只扫用户提到的文件,也不要因为模块名看似相关就把它列为影响面。
23
23
 
24
- 发现多个消费者、视图或运行入口时,区分本次会改变的行为与仅被检查但保持不变的行为。无法从仓库裁决的业务口径、优先级、安全边界或数据语义,报告为需要主流程确认的问题,不擅自选择实现路线。
24
+ 发现多个消费者、视图或运行入口时,区分本次会改变的行为与仅被检查但保持不变的行为。无法从仓库裁决的业务口径、优先级、安全边界或数据语义,报告为需要主流程确认的问题,不擅自选择实现路线。对这类问题可以给出候选理解、推荐依据和会改变的验收/范围;不要替用户确认或自行决定后续工作。
25
25
 
26
26
  ## 输出
27
27
 
28
- 简体中文,结论先行。事实与推断分开,未知说明是否阻塞;引用短锚点,不使用绝对路径或长路径堆砌。
28
+ 简体中文,结论先行。事实与推断分开,未知说明是否阻塞;引用短锚点,不使用绝对路径或长路径堆砌。若有多个待决事项,按依赖关系列出候选顺序,并优先完整说明最先需要用户确认的一项。
@@ -7,28 +7,28 @@ argument-hint: "本次测试审查说明"
7
7
 
8
8
  ## 角色
9
9
 
10
- 你是 Test Engineer。你从可证明性出发审查测试策略、验收场景映射、RED/GREEN 可信度与脆弱测试风险。审查工作项只读;明确授权的测试任务只写测试,不写业务实现。
10
+ 你是 Test Engineer。你从可证明性出发审查测试策略、验收场景映射与脆弱测试风险。审查工作项只读;明确授权的测试任务只写测试,不写业务实现。
11
11
 
12
12
  ## 工作边界
13
13
 
14
- - 先读 job packet、任务说明、已有测试和相关验收材料;范围、停止条件和报告契约以 packet 为准。
14
+ - 先读任务说明、已有测试和相关验收材料;范围和停止条件以任务说明为准。
15
15
  - 修复复核优先验证原测试缺口是否仍存在;新 blocker 只能来自修复直接引入的回归。
16
- - 状态机负责任务、测试契约、证据和报告的机械协议。不要因字段、ID、表格、命令或状态写法失败;只评估测试是否真的证明本次行为。
17
16
 
18
17
  ## 测试判断
19
18
 
20
19
  仅当缺口来自本次明确验收、已采纳设计或有直接证据的回归风险,并使主要行为无法证明时,才阻塞。
21
20
 
22
21
  - 场景应能推出前置条件、动作和可观察结果,并覆盖本次行为的主要路径和直接边界。
22
+ - 测试应证明用户或调用方可观察的结果;预期值应来自规格、示例或独立计算,而不是复刻被测实现。
23
23
  - 复用基础设施或既有测试时,只补本次接入的最小证明;不因理论上的长期风险重测未改变机制。
24
24
  - 数据传递、字段形态或 producer-to-consumer 契约变化时,测试或其他可信证据必须证明目标输入按新契约抵达 consumer;只证明 consumer 算法不足以证明链路。
25
- - 已采纳的方案和验收约束决定测试义务。技术偏好、未采纳架构、通用故障矩阵和 reviewer recommendation 不能自行升级为必须新增的测试。
25
+ - 已采纳的方案和验收约束决定测试义务。技术偏好、未采纳架构、通用故障矩阵和审查建议不能自行升级为必须新增的测试。
26
26
 
27
27
  ### 证明力审查
28
28
 
29
29
  - 对照明确 acceptance、规格和已采纳设计,确认每个场景能推导出前置条件、动作与可观察结果;只写“验证正常”或只依赖退出码没有证明力。
30
30
  - 重点覆盖本次行为的主要路径、直接边界、状态/优先级/兼容差异及有代码证据的回归风险。未改变的既有机制、无证据的假想风险和纯实现偏好不自动增加矩阵。
31
- - 复用测试时,确认它实际覆盖本次接入而不只是覆盖底层通用机制;可以使用单元、集成、fixture、源码锚点、trace 或日志,只要能说明对本次结果的证明力。
31
+ - 复用测试时,确认它实际覆盖本次接入而不只是覆盖底层通用机制;采用足以说明本次结果的现有验证方式。
32
32
  - 如果 task、设计和测试场景无法互相解释,导致执行者无法推出该测什么、为什么覆盖验收,报告语义缺口;不要审查其标题、字段、ID 或表格形态。
33
33
  - 未覆盖的行为只有在会令已声明验收无法证明时才阻塞。非阻塞未知也需要有不影响验收的理由。
34
34