@peterxiaoyang/superspec 0.1.55 → 0.1.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,41 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { collectProposeQuestions, parseProposeQuestions, proposeQuestionContextFingerprint, proposeQuestionDecisionBasisDigest, proposeQuestionKey, } from "./format.js";
4
4
  const PROPOSE_ANSWER_REGISTRATION_VERSION = 1;
5
+ // ===== planning validation profile =====
6
+ //
7
+ // profile 在进入 propose 的边界事件上冻结,propose-ready 时复制到 propose_ready
8
+ // 事件。所有读取方共用这里的判定,避免各处复制一份而在升级时漏改。
9
+ export function isPlanningValidationProfile(value) {
10
+ if (!value || typeof value !== "object" || Array.isArray(value))
11
+ return false;
12
+ const profile = value;
13
+ if (profile.version !== 2 || !profile.openspec || typeof profile.openspec !== "object")
14
+ return false;
15
+ const designValid = profile.design == null
16
+ || profile.design.schema_version === 1
17
+ || profile.design.schema_version === 2;
18
+ return designValid && (profile.openspec.mode === "disabled" ||
19
+ profile.openspec.mode === "strict" && typeof profile.openspec.config_digest === "string");
20
+ }
21
+ /**
22
+ * 当前 planning round 的冻结 profile:优先取最近一次 propose-ready 写入的快照,
23
+ * 否则取进入 propose 的边界事件。两者都没有时是升级前的 v1 change。
24
+ */
25
+ export function planningValidationProfileForCurrentRound(events) {
26
+ for (let index = events.length - 1; index >= 0; index--) {
27
+ const event = events[index];
28
+ if (event.event_type !== "transition_commit")
29
+ continue;
30
+ const payload = event.payload;
31
+ const isReady = payload.transition === "propose-ready" && payload.to_state === "propose_ready";
32
+ if (!isReady && !isProposeRoundEntry(event))
33
+ continue;
34
+ return isPlanningValidationProfile(payload.planning_validation_profile)
35
+ ? payload.planning_validation_profile
36
+ : null;
37
+ }
38
+ return null;
39
+ }
5
40
  function isProposeRoundEntry(event) {
6
41
  if (event.event_type !== "transition_commit")
7
42
  return false;
package/dist/record.js CHANGED
@@ -5,9 +5,10 @@ import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, sha
5
5
  import { rebuildSnapshot } from "./sync.js";
6
6
  import { changeRoot as openspecChangeRoot } from "./openspec.js";
7
7
  import { isPhaseConfirmationScope, phaseActionForAnswer, phaseConfirmationForCurrentState, workflowRiskForPhaseConfirmation, } from "./phase_confirmation.js";
8
- import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_DECISION_SCOPE_PREFIX, codeReviewJobStaleReason, codeReviewDecisionAnswerLabel, currentCodeReviewWorkingPaths, latestCodeReviewFailedStatus, normalizeCodeReviewDecisionAnswer, parseCodeReviewDecisionScope, } from "./code_review.js";
8
+ import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_DECISION_SCOPE_PREFIX, codeReviewFindingNeedsUserDecision, codeReviewJobStaleReason, codeReviewDecisionAnswerLabel, currentCodeReviewWorkingPaths, isReviewFixCapReached, latestCodeReviewFailedStatus, normalizeCodeReviewDecisionAnswer, parseCodeReviewDecisionScope, } from "./code_review.js";
9
9
  import { invalidReasonForSubmittedReport } from "./job_validity.js";
10
10
  import { jobSubmitArgv } from "./job_action.js";
11
+ import { hasBehaviorAnchor, isCodeReviewClaimKind, resolveApprovedRefs, } from "./approved_ref.js";
11
12
  import { REVIEW_DOC_PATHS, REVIEW_REJECTION_OVERRIDE_SCOPE_PREFIX, REVIEW_REJECTION_OVERRIDE_ANSWER, parseReviewRejectionOverrideScope, reviewGateRoleResolution, reviewRejectionOverrideScope, } from "./review.js";
12
13
  import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./review_job_gates.js";
13
14
  import { RecordInputDecodingError, readRecordInputFile } from "./record_input.js";
@@ -56,7 +57,7 @@ function roleDescription(role) {
56
57
  case "verifier":
57
58
  return "验证 proposal、实现状态、任务完成、测试契约和 SuperSpec 证据是否足以支撑完成结论";
58
59
  case "code-reviewer":
59
- return "审查 apply 后的代码实现质量、健壮性、性能、安全、兼容、边界条件、是否偏离 proposal/design/tasks/test-contract 以及关键测试缺口";
60
+ return "独立审查实现是否以最小语义影响兑现已批准计划,找出真实缺陷、边界条件、安全/性能/兼容问题与关键测试缺口,并区分漏做与计划或验收没有要求的改动";
60
61
  case "executor":
61
62
  return "执行受限实现工作项";
62
63
  case "test-run":
@@ -339,7 +340,7 @@ function validateReviewScope(obj, job, checks) {
339
340
  }
340
341
  }
341
342
  }
342
- function actionableCodeReviewFindings(findings) {
343
+ function actionableCodeReviewFindings(findings, changeRoot) {
343
344
  if (!Array.isArray(findings))
344
345
  return { actionable: [], reasons: ["报告字段 findings 必须是数组"] };
345
346
  const actionable = [];
@@ -387,6 +388,18 @@ function actionableCodeReviewFindings(findings) {
387
388
  if (type === "implementation" && finding.suggested_action !== "apply") {
388
389
  findingReasons.push(`纯代码实现问题 ${id} 的 suggested_action 必须是 apply`);
389
390
  }
391
+ if (!isCodeReviewClaimKind(finding.claim_kind)) {
392
+ findingReasons.push(`阻塞问题 ${id} 的 claim_kind 必须是 missing_approved|breaks_existing|unjustified_addition`);
393
+ }
394
+ const resolved = resolveApprovedRefs(changeRoot, finding.approved_refs);
395
+ if (!resolved.ok) {
396
+ findingReasons.push(...resolved.reasons.map((reason) => `阻塞问题 ${id} ${reason}`));
397
+ }
398
+ else if (finding.claim_kind === "missing_approved"
399
+ && finding.suggested_action === "apply"
400
+ && !hasBehaviorAnchor(resolved.values)) {
401
+ findingReasons.push(`阻塞问题 ${id} 的 missing_approved 在 suggested_action=apply 时必须引用可解析的 TEST 或 spec Requirement;若缺口属于计划或验收本身的问题,改用 type=mixed 且 suggested_action=propose 交使用者裁决`);
402
+ }
390
403
  if (findingReasons.length > 0) {
391
404
  reasons.push(...findingReasons);
392
405
  continue;
@@ -614,7 +627,7 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
614
627
  const blockingCount = Array.isArray(findings)
615
628
  ? findings.filter(item => asObject(item)?.blocking === true).length
616
629
  : 0;
617
- const { actionable, reasons } = actionableCodeReviewFindings(findings);
630
+ const { actionable, reasons } = actionableCodeReviewFindings(findings, changeRoot);
618
631
  if (verdict === "pass") {
619
632
  if (blockingCount > 0) {
620
633
  const reason = "报告结论为 pass 时不能包含 blocking:true 的阻塞问题";
@@ -990,8 +1003,9 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
990
1003
  const changeRoot = openspecChangeRoot(projectRoot, change);
991
1004
  const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
992
1005
  const status = latestCodeReviewFailedStatus(events);
1006
+ const reviewFixCapReached = isReviewFixCapReached(projectRoot, events);
993
1007
  const finding = ref && status?.terminal.job.job_id === ref.jobId
994
- ? status.findings.find(item => item.id === ref.findingId && (item.type === "spec" || item.type === "mixed"))
1008
+ ? status.findings.find(item => item.id === ref.findingId && codeReviewFindingNeedsUserDecision(item.type, reviewFixCapReached))
995
1009
  : null;
996
1010
  const staleReason = status && ref && status.terminal.job.job_id === ref.jobId
997
1011
  ? codeReviewJobStaleReason(projectRoot, status.terminal.job, currentCodeReviewWorkingPaths(projectRoot, events), events)
@@ -1165,6 +1179,10 @@ function packetFieldDescriptions() {
1165
1179
  changed_paths: "与某个任务(task)或代码状态检查相关的改动文件。",
1166
1180
  changed_paths_partial_reason: "该任务(task)的提交段 diff 失败原因;存在时 changed_paths 只包含工作区对比结果,归属可能不完整。",
1167
1181
  unattributed_paths: "代码审查范围中暂时无法归属到某个任务(task)的文件。",
1182
+ added_code_paths: "相对本次代码审查基点新建的代码文件,供判断是否服务已批准行为。",
1183
+ structure_ledger: "design.md 中已批准的结构变更清单;清单是已批准结构的边界,清单外结构按 unjustified_addition 处理。",
1184
+ claim_kind: "阻塞问题相对已批准计划的关系:漏做、破坏已有行为、或计划或验收没有要求的改动。",
1185
+ approved_refs: "指向当前 change 已批准材料的引用;引擎只检查能否解析,apply 漏做还需要 TEST 或 spec Requirement。",
1168
1186
  unknown_attribution_tasks: "因为缺少边界快照或提交段 diff 失败而无法完整计算改动归属的任务(task)。",
1169
1187
  coverage_exemption_refs: "测试覆盖豁免引用:说明某个 TEST 为什么没有绑定到任务(task)。",
1170
1188
  code_review_gate: "最终验证读取的代码审查门禁事实:passed 指向已接受的代码审查工作项,skipped 表示本轮没有代码类改动。",
@@ -1210,6 +1228,8 @@ export function jobsPacket(projectRoot, change, jobId) {
1210
1228
  ...(packetContext?.task_execution_index ? { task_execution_index: packetContext.task_execution_index } : {}),
1211
1229
  ...(packetContext?.unattributed_paths ? { unattributed_paths: packetContext.unattributed_paths } : {}),
1212
1230
  ...(packetContext?.unknown_attribution_tasks ? { unknown_attribution_tasks: packetContext.unknown_attribution_tasks } : {}),
1231
+ ...(packetContext?.added_code_paths ? { added_code_paths: packetContext.added_code_paths } : {}),
1232
+ ...(packetContext?.structure_ledger ? { structure_ledger: packetContext.structure_ledger } : {}),
1213
1233
  ...(packetContext?.code_state_check ? { code_state_check: packetContext.code_state_check } : {}),
1214
1234
  packet_digest: job.packet_digest,
1215
1235
  required_output_kind: "job_report_json",
@@ -1235,9 +1255,9 @@ export function jobsPacket(projectRoot, change, jobId) {
1235
1255
  `产出 JSON 报告内容并优先通过 --report - 从 stdin 登记;文件路径模式仅作备用。${recordInputInstruction(job)}协议字段含义见 packet 顶层“字段说明”,普通对话不要原样复述 JSON。` +
1236
1256
  (isCodeReviewer
1237
1257
  ? `格式骨架:{"role":"code-reviewer","verdict":"pass","review_scope":{"job_id":"${job.job_id}","packet_digest":"${job.packet_digest}","checked_paths":[],"checked_docs":[],"unchecked":[]},"findings":[],"reviewer":{"kind":"subagent","id":"<thread-or-agent-id>"}}。提交前按真实审查结果填写数组;不得从 boundFiles 自动复制 checked_paths。verdict 只能为 pass 或 fail;审查覆盖范围(review_scope)用来说明本次审查覆盖了哪些文件和文档,已检查路径(checked_paths)与未检查项(unchecked)必须合起来覆盖全部绑定文件(boundFiles),unchecked 条目格式为 {"path":"<path>","reason":"<reason>"};pass 不允许仍有未检查的绑定文件。`
1238
- + `报告结论为 fail 时,问题列表(findings)至少包含一个可处理、可追溯的阻塞问题,字段为 {"id":"<stable-id>","blocking":true,"type":"implementation|spec|mixed","description":"<what>","evidence":"<why>","source_refs":["<path:line>"],"impact":"<impact>","suggested_action":"apply|propose"}。问题类型(type)中 implementation 表示纯代码实现问题,spec 表示方案/需求文档问题,mixed 表示需要使用者判断的混合问题。`
1258
+ + `报告结论为 fail 时,问题列表(findings)至少包含一个可处理、可追溯的阻塞问题,字段为 {"id":"<stable-id>","blocking":true,"type":"implementation|spec|mixed","claim_kind":"missing_approved|breaks_existing|unjustified_addition","approved_refs":["TEST-001"],"description":"<what>","evidence":"<why>","source_refs":["<path:line>"],"impact":"<impact>","suggested_action":"apply|propose"}。问题类型(type)中 implementation 表示纯代码实现问题,spec 表示方案/需求文档问题,mixed 表示需要使用者判断的混合问题;claim_kind 与 approved_refs 见字段说明。`
1239
1259
  + (packetContext?.task_execution_index
1240
- ? `本工作项带任务执行索引(task_execution_index):按 task 对照其执行依据快照(contract)审查——实现路线对照 design 引用原文、累计 diff 对照 guard 边界、测试断言对照 tests 声明的 scenario;每项的 required_evidence 是 task-start 冻结的证据口径,red_required/green_required 分别说明是否需要 RED/GREEN;fix 非空表示状态机创建的实现修复,source、parent_task_id 和 reason 说明其归属,code_review 来源还需核对 review_finding;scope_note 既可能解释必要的范围扩大,也可能说明代码审查修复为何保留原实现,均需结合 Diff、调用链和验证证据独立判断;changed_paths 是归属线索不是结论(null 表示未知);unattributed_paths 中的无主改动逐个判断合理性;coverage_exemption_refs 解释未绑定 task 的 TEST 豁免。当前 packet 的 boundFiles 是本轮冻结的审查范围;若它来自前一轮审查后的增量,只复核本轮变化及其直接影响链路,不要求重复审查未变化文件,但仍要判断批准行为是否完整闭合。`
1260
+ ? `本工作项带任务执行索引(task_execution_index):按 task 对照其执行依据快照(contract)审查——实现路线对照 design 引用原文、累计 diff 对照 guard 边界、测试断言对照 tests 声明的 scenario;每项的 required_evidence 是 task-start 冻结的证据口径,red_required/green_required 分别说明是否需要 RED/GREEN;fix 非空表示状态机创建的实现修复,source、parent_task_id 和 reason 说明其归属,code_review 来源还需核对 review_finding;scope_note 既可能解释必要的范围扩大,也可能说明代码审查修复为何保留原实现,均需结合 Diff、调用链和验证证据独立判断;changed_paths 是归属线索不是结论(null 表示未知);unattributed_paths 中的无主改动和 added_code_paths 中的新建代码文件,均需判断是否服务已批准行为:放行其中任何计划外文件,都必须写明它服务于哪条已批准锚点、为何无法避免,说不出依据的按 unjustified_addition 收缩;coverage_exemption_refs 解释未绑定 task 的 TEST 豁免。当前 packet 的 boundFiles 是本轮冻结的审查范围;若它来自前一轮审查后的增量,只复核本轮变化及其直接影响链路,不要求重复审查未变化文件,但仍要判断批准行为是否完整闭合。`
1241
1261
  : "")
1242
1262
  : job.role === "verifier"
1243
1263
  ? `最小格式:{"role":"verifier","verdict":"pass","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""}}。verdict 只能为 pass 或 fail;核对代码审查记录(code_review_gate):passed 必须能追溯到已接受的代码审查工作项,skipped 必须能证明本次没有代码类改动。核对修复闭环:task_execution_index.fix.source=code_review 时必须核对 review_finding 对应问题是否关闭;source=self_test 时必须核对 parent_task_id、记录的自测原因、本次 attempt 验证和最新代码审查是否共同闭环。方案/混合问题必须有用户决策或后续修复证据。按 task_execution_index 的 required_evidence 核对测试证据:red_required 时需要同一 TEST 的 RED(expected_failure)后 GREEN;green_required 时每个声明 TEST 都需要允许的 GREEN 语义状态;测试运行证据应包含测试 ID(test_id)、命令(command)、工作目录(cwd)、退出码(exit_code)、语义状态(semantic_status)。修复 task 的回归测试运行可用回归覆盖任务列表(covers_task_ids)说明覆盖了哪些已完成任务;缺少任务尝试 ID(attempt_id)的旧证据只能弱引用。` +
@@ -6,9 +6,10 @@ 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
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";
9
+ import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketContext, codeReviewPacketDigest, collectCodeReviewGateFacts, computeCodeStateCheck, currentCodeReviewWorkingPaths, dismissedCodeReviewSummary, effectiveCoverageExemptionRefsFromEvents, latestCodeReviewGateEvidence, latestCodeReviewDecision, latestCodeReviewFailedStatus, missingCoverageExemptionTestIds, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, taskExecutionIndexForReview, codeReviewFindingNeedsUserDecision, isReviewFixCapReached, } from "./code_review.js";
10
10
  import { taskEvidenceReadiness } from "./task_evidence.js";
11
11
  import { adoptedContractForTask, findTaskInLines, isFixTaskId, parseTasksMd, parseTestContractEntries, } from "./format.js";
12
+ import { isCodeReviewClaimKind, reviewFixReason } from "./approved_ref.js";
12
13
  import { applyRequirementModeForCurrentRound, applyPlanningDocsChangedSinceBaseline, executionRequirementVersionForCurrentRound, blockingJobsForApplyDone, executionPolicyForCurrentRound, formatPendingTaskMessage, latestAcceptedProposalBaseline, pendingTaskStatusForApply, planningValidationProfileForNewRound, planTransition, discoveryDocsBaseline, exploreAnswerRegistrationPayloadForChange, proposeAnswerRegistrationPayloadForChange, proposalDocsBaseline, } from "./phase_plan.js";
13
14
  import { latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
14
15
  import { currentGitHead, dirtyCodeFiles, stageProductionJavaFilesSince } from "./git_state.js";
@@ -359,15 +360,29 @@ function findReviewFailedFinding(events, ref) {
359
360
  return { event: status.terminal.event, finding };
360
361
  }
361
362
  function reviewFixDescriptor(ref, finding) {
362
- const reason = typeof finding.description === "string" && finding.description.trim()
363
- ? finding.description.trim().replace(/\s+/g, " ")
364
- : `修复代码审查问题 ${ref.findingId}`;
363
+ const claimKind = isCodeReviewClaimKind(finding.claim_kind) ? finding.claim_kind : null;
364
+ const approvedRefs = Array.isArray(finding.approved_refs)
365
+ ? finding.approved_refs.filter((item) => typeof item === "string" && item.trim() !== "")
366
+ : [];
367
+ const reason = claimKind && approvedRefs.length > 0
368
+ ? reviewFixReason(claimKind, approvedRefs).replace(/\s+/g, " ")
369
+ : typeof finding.description === "string" && finding.description.trim()
370
+ ? finding.description.trim().replace(/\s+/g, " ")
371
+ : `修复代码审查问题 ${ref.findingId}`;
372
+ const reviewFinding = {
373
+ job_id: ref.jobId,
374
+ finding_id: ref.findingId,
375
+ };
376
+ if (approvedRefs.length > 0)
377
+ reviewFinding.approved_refs = approvedRefs;
378
+ if (claimKind)
379
+ reviewFinding.claim_kind = claimKind;
365
380
  return {
366
381
  fix_id: `REVIEW-FIX-${ref.jobId}#${ref.findingId}`,
367
382
  source: "code_review",
368
383
  parent_task_id: null,
369
384
  reason,
370
- review_finding: { job_id: ref.jobId, finding_id: ref.findingId },
385
+ review_finding: reviewFinding,
371
386
  };
372
387
  }
373
388
  function selfTestFixBaseId(parentTaskId, reason) {
@@ -1121,8 +1136,8 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
1121
1136
  const found = findReviewFailedFinding(events, ref);
1122
1137
  if (!found)
1123
1138
  return { skip: true, message: `找不到有效的代码审查问题 ${opts.reviewFix}` };
1124
- const type = found.finding.type;
1125
- if (type === "spec" || type === "mixed") {
1139
+ const type = typeof found.finding.type === "string" ? found.finding.type : undefined;
1140
+ if (codeReviewFindingNeedsUserDecision(type, isReviewFixCapReached(projectRoot, events))) {
1126
1141
  const scope = codeReviewDecisionScope(ref.jobId, ref.findingId);
1127
1142
  const decision = latestCodeReviewDecision(events, scope);
1128
1143
  if (decision?.answer !== "reopen_apply")
package/dist/types.d.ts CHANGED
@@ -10,6 +10,7 @@ export type JobState = "requested" | "accepted" | "rejected";
10
10
  export type JobRole = "critic" | "architect" | "test-engineer" | "executor" | "test-run" | "verifier" | "code-reviewer";
11
11
  export type ReviewJobGateId = "explore.discovery_review" | "propose.final_review" | "review.code_review" | "review.final_verifier";
12
12
  export type CodeReviewResultKind = "invalid_report" | "non_actionable_report" | "review_failed";
13
+ export type CodeReviewClaimKind = "missing_approved" | "breaks_existing" | "unjustified_addition";
13
14
  export interface ReviewPreviousRejection {
14
15
  result_kind: CodeReviewResultKind;
15
16
  reason: string;
@@ -63,6 +64,8 @@ export interface FixDescriptor {
63
64
  review_finding?: {
64
65
  job_id: string;
65
66
  finding_id: string;
67
+ approved_refs?: string[];
68
+ claim_kind?: CodeReviewClaimKind;
66
69
  };
67
70
  }
68
71
  export interface DirtyFileFingerprint {
@@ -129,6 +132,8 @@ export interface JobPacketContext {
129
132
  task_execution_index?: TaskExecutionIndexEntry[];
130
133
  unattributed_paths?: string[];
131
134
  unknown_attribution_tasks?: string[];
135
+ added_code_paths?: string[];
136
+ structure_ledger?: StructureChangeLedger;
132
137
  code_state_check?: CodeStateCheck;
133
138
  }
134
139
  export interface JobPacket {
@@ -148,6 +153,8 @@ export interface JobPacket {
148
153
  task_execution_index?: TaskExecutionIndexEntry[];
149
154
  unattributed_paths?: string[];
150
155
  unknown_attribution_tasks?: string[];
156
+ added_code_paths?: string[];
157
+ structure_ledger?: StructureChangeLedger;
151
158
  code_state_check?: CodeStateCheck;
152
159
  packet_digest: string;
153
160
  required_output_kind: string;
@@ -193,9 +200,23 @@ export interface PlanningValidationProfile {
193
200
  version: 2;
194
201
  openspec: OpenSpecValidationProfile;
195
202
  design?: {
196
- schema_version: 1;
203
+ schema_version: 1 | 2;
197
204
  };
198
205
  }
206
+ export interface StructureChangeEntry {
207
+ id: string;
208
+ category: string;
209
+ change: string;
210
+ basis: string;
211
+ decision: string;
212
+ }
213
+ export interface StructureChangeLedger {
214
+ present: boolean;
215
+ none: boolean;
216
+ entries: StructureChangeEntry[];
217
+ /** 标题存在但表格无法解析时的原因;有值时 entries 为空。 */
218
+ format_error?: string;
219
+ }
199
220
  export interface TransitionCommitPayload {
200
221
  transition: string;
201
222
  from_state: State;
@@ -371,6 +392,11 @@ export interface TestEvidenceAction {
371
392
  };
372
393
  required_fields: Array<"command" | "cwd" | "exit_code">;
373
394
  }
395
+ /** review-fix 计划附带的原始审查问题上下文;仅供定位代码,不是实现授权。 */
396
+ export interface ReviewFindingContext {
397
+ evidence: string;
398
+ note: string;
399
+ }
374
400
  export type NextOutput = {
375
401
  state: State;
376
402
  } & ({
@@ -378,6 +404,7 @@ export type NextOutput = {
378
404
  next_command: string;
379
405
  reason: string;
380
406
  missing_inputs: MissingInput[];
407
+ finding_context?: ReviewFindingContext;
381
408
  } | {
382
409
  path: "required_job";
383
410
  required_jobs: RequiredJobAction[];
@@ -3,6 +3,16 @@ import type { Event, State } from "./types.ts";
3
3
  export declare const WORKFLOW_CONFIG_PATH = ".superspec/config.json";
4
4
  /** 项目未声明 workflow.mode 时采用的默认档位。 */
5
5
  export declare const DEFAULT_WORKFLOW_RISK: ReviewRisk;
6
+ export declare const DEFAULT_WORKFLOW_BUDGET: {
7
+ readonly tasks: 10;
8
+ readonly tests: 20;
9
+ readonly review_fix_rounds: 2;
10
+ };
11
+ export type WorkflowBudget = {
12
+ tasks: number | null;
13
+ tests: number | null;
14
+ review_fix_rounds: number | null;
15
+ };
6
16
  export declare const WORKFLOW_HOSTS: readonly ["codex", "omp"];
7
17
  export type WorkflowHost = (typeof WORKFLOW_HOSTS)[number];
8
18
  /** 未声明 hosts 的旧项目按 Codex 入口处理。 */
@@ -10,6 +20,11 @@ export declare const DEFAULT_WORKFLOW_HOSTS: WorkflowHost[];
10
20
  export declare class WorkflowConfigError extends Error {
11
21
  constructor(message: string);
12
22
  }
23
+ /**
24
+ * 读取计划规模与 review-fix 上限;minimal 档整体忽略,budget 为 null 整体关闭,单项 null 关闭该项检查。
25
+ * 注意 0 不等于关闭:tasks/tests 为 0 表示任何任务/TEST 都超预算,review_fix_rounds 为 0 表示不允许自动修复。
26
+ */
27
+ export declare function workflowBudgetForRisk(projectRoot: string, risk: ReviewRisk): WorkflowBudget | null;
13
28
  export declare function normalizeWorkflowHosts(values: readonly string[]): WorkflowHost[];
14
29
  export declare function parseWorkflowHostsFlag(raw: string): WorkflowHost[];
15
30
  /** 读取项目已选宿主。缺少配置或缺少 workflow.hosts 时默认 Codex。 */
@@ -5,6 +5,11 @@ import { dirname, join } from "node:path";
5
5
  export const WORKFLOW_CONFIG_PATH = ".superspec/config.json";
6
6
  /** 项目未声明 workflow.mode 时采用的默认档位。 */
7
7
  export const DEFAULT_WORKFLOW_RISK = "normal";
8
+ export const DEFAULT_WORKFLOW_BUDGET = {
9
+ tasks: 10,
10
+ tests: 20,
11
+ review_fix_rounds: 2,
12
+ };
8
13
  export const WORKFLOW_HOSTS = ["codex", "omp"];
9
14
  /** 未声明 hosts 的旧项目按 Codex 入口处理。 */
10
15
  export const DEFAULT_WORKFLOW_HOSTS = ["codex"];
@@ -17,6 +22,48 @@ export class WorkflowConfigError extends Error {
17
22
  function isReviewRisk(value) {
18
23
  return value === "minimal" || value === "normal" || value === "strict";
19
24
  }
25
+ function parseWorkflowBudgetValue(value, field) {
26
+ if (value === null)
27
+ return null;
28
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
29
+ throw new WorkflowConfigError(`${WORKFLOW_CONFIG_PATH} 的 workflow.budget.${field} 必须是非负整数或 null`);
30
+ }
31
+ return value;
32
+ }
33
+ function workflowBudgetFromObject(budget) {
34
+ if (!budget)
35
+ return { ...DEFAULT_WORKFLOW_BUDGET };
36
+ const tasks = budget.tasks === undefined
37
+ ? DEFAULT_WORKFLOW_BUDGET.tasks
38
+ : parseWorkflowBudgetValue(budget.tasks, "tasks");
39
+ const tests = budget.tests === undefined
40
+ ? DEFAULT_WORKFLOW_BUDGET.tests
41
+ : parseWorkflowBudgetValue(budget.tests, "tests");
42
+ const review_fix_rounds = budget.review_fix_rounds === undefined
43
+ ? DEFAULT_WORKFLOW_BUDGET.review_fix_rounds
44
+ : parseWorkflowBudgetValue(budget.review_fix_rounds, "review_fix_rounds");
45
+ return { tasks, tests, review_fix_rounds };
46
+ }
47
+ /**
48
+ * 读取计划规模与 review-fix 上限;minimal 档整体忽略,budget 为 null 整体关闭,单项 null 关闭该项检查。
49
+ * 注意 0 不等于关闭:tasks/tests 为 0 表示任何任务/TEST 都超预算,review_fix_rounds 为 0 表示不允许自动修复。
50
+ */
51
+ export function workflowBudgetForRisk(projectRoot, risk) {
52
+ if (risk === "minimal")
53
+ return null;
54
+ const workflow = workflowObject(readWorkflowConfigObject(projectRoot));
55
+ if (!workflow)
56
+ return { ...DEFAULT_WORKFLOW_BUDGET };
57
+ if (workflow.budget === undefined)
58
+ return { ...DEFAULT_WORKFLOW_BUDGET };
59
+ // budget: null 表示整体关闭预算与修复上限。
60
+ if (workflow.budget === null)
61
+ return { tasks: null, tests: null, review_fix_rounds: null };
62
+ if (typeof workflow.budget !== "object" || Array.isArray(workflow.budget)) {
63
+ throw new WorkflowConfigError(`${WORKFLOW_CONFIG_PATH} 的 workflow.budget 必须是 object 或 null`);
64
+ }
65
+ return workflowBudgetFromObject(workflow.budget);
66
+ }
20
67
  function isWorkflowHost(value) {
21
68
  return value === "codex" || value === "omp";
22
69
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peterxiaoyang/superspec",
3
- "version": "0.1.55",
3
+ "version": "0.1.57",
4
4
  "description": "SuperSpec 流程引擎 — transition engine with lightweight fact-sync",
5
5
  "type": "module",
6
6
  "engines": {
@@ -23,6 +23,8 @@ Apply 的成功标准是完整兑现已批准行为,并在满足验收的实
23
23
  - 所有 task 已完成后,若问题仍能关联一个已完成 task、且不改变已批准行为和方案,主流程执行 `superspec transition reopen --change "<change>" --to apply --self-test-fix "<task>" --reason "<reason>"`,让工作流创建修复事项;随后继续 `next`,不得手改 tasks。
24
24
  - 无法关联既有 task,或需要改变行为、验收、接口、数据语义或实现路线时,才回 propose。
25
25
 
26
+ 用户明确否定某个不属于已批准行为的实现装置时,同样按上面三条规则恢复已批准行为,但顺序相反:先完成代码上的移除或修复(self-test-fix 的 reason 写恢复了哪条已批准行为,不写撤回或删除),确认能通过后再把这次否决写进 design 的非目标或相关 task 的边界——计划材料一旦先改,self-test-fix 会被计划冻结挡下,只能回 propose。不要为这次否决新增以删除或撤回为验收内容的 task。
27
+
26
28
  在用户已显式启动工作流或明确指定 change 后,若新增或改变业务规则、产品口径、验收、示例规范、影响范围,或说明 PRD/文档/原型等需求源已更新时,先确定对应 change;归属明确则回同一 change 的 `propose` 更新计划,归属不明才询问。不要把这类输入直接当作 apply 授权,也不要另建 repair change。
27
29
 
28
30
  SuperSpec 创建的独立审查/验证工作项,视为已授权启动对应 subagent;无需再次询问用户。主会话不得自批这些工作项。
@@ -4,7 +4,7 @@ description: Code-level review for spec fit, bugs, safety, and test gaps
4
4
  tools: read, grep, glob, bash
5
5
  ---
6
6
 
7
- Role: Code Reviewer. Check spec fit, correctness, security, test adequacy, code quality, performance, and maintainability without making the workflow heavy.
7
+ Role: Code Reviewer. Check that approved behaviors landed with minimal extra semantics. Report missing approved results or unjustified additions; do not mint required work outside the approved plan.
8
8
 
9
9
  Task binding: read the current SuperSpec job packet and task instructions first. The job packet is the runtime contract; follow it over this prompt, including any previous rejection it asks you to correct.
10
10
 
@@ -8,6 +8,6 @@ Role: Critic. Challenge demand clarification, plans, designs, implementations, a
8
8
 
9
9
  Task binding: read the current SuperSpec job packet and task instructions first. The job packet is the runtime contract; follow it over this prompt, including any previous rejection it asks you to correct.
10
10
 
11
- Boundary: read-only by default. Do not edit files, invent issues, or widen scope silently. Report missing source refs or claim gaps upward.
11
+ Boundary: read-only by default. Do not edit files, invent issues, or widen scope silently. Report missing source refs or claim gaps upward. Undeclared theoretical risks are residual, not blockers.
12
12
 
13
13
  Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. Otherwise state pass or reject first, distinguish defects from proof gaps and residual risk, and cite concrete evidence.
@@ -24,6 +24,7 @@ argument-hint: "本次架构审查说明"
24
24
  - 复用既有机制时,核对接入点、本次差异和保持语义;只审查本次接入是否破坏现有契约,不要求重建或重新证明底层基础设施。
25
25
  - 参考实现证明的是可复用能力和候选机制,不自动决定本次的接口、资源、数据模型或模块形态。新增 Controller、API、实体、表、公共类型、独立模块或跨仓库改动时,检查其是否承担不可由现有边界表达的独立责任;不要规定数量,但应挑战无必要依据的平行结构和机械复制。
26
26
  - 只有本次确实改变的兼容、并发、恢复、数据语义或发布顺序才需要明确设计;未改变的既有风险和理论故障不是 blocker。
27
+ - 结构变更清单是本次已批准结构的边界。方案正文出现而清单未列的表、接口、开关、迁移或签名变化,或清单条目缺少 Requirement / TEST 依据,是 blocker;处理方向是删除或补依据。
27
28
  - 设计取舍受用户决定和明确非目标约束。报告无法满足的结果或事实冲突,不把未经采纳的架构方案写成 required fix。
28
29
 
29
30
  ### 方案可行性
@@ -7,7 +7,7 @@ argument-hint: "本次代码审查说明"
7
7
 
8
8
  ## 角色
9
9
 
10
- 你是 Code Reviewer。你独立、只读地审查本次实现是否以最小语义影响兑现已批准计划,找出真实 bug、范围遗漏、边界条件、安全/性能/兼容问题、关键测试缺口和无关改动。
10
+ 你是 Code Reviewer。你独立、只读地审查本次实现是否以最小语义影响兑现已批准计划,找出真实 bug、已批准行为遗漏、边界条件、安全/性能/兼容问题、关键测试缺口,以及计划或验收没有要求的改动。
11
11
 
12
12
  ## 工作边界
13
13
 
@@ -24,14 +24,14 @@ argument-hint: "本次代码审查说明"
24
24
  - 实现是否兑现当前任务的验收和边界,且与已批准的方案/规格一致。
25
25
  - 是否引入功能、数据、一致性、安全、权限、性能或兼容问题,以及直接的边界条件遗漏。
26
26
  - 从批准范围反查实现是否覆盖已确认的消费者、兼容路径和直接影响链路;任务勾选和测试通过不能替代完整性判断。
27
- - 对当前审查范围内的 Diff,分别判断“是否漏实现”和“是否超出必要范围”:直接消费者没有实现或没有现有实现已满足验收的证据,属于完整性问题;新增共享语义、公共契约或无关生产逻辑没有直接必要性证据,属于范围问题。两者都应锚定当前批准行为和实际 Diff,不把消费者类别或可能性清单当成覆盖义务。
28
- - 从实际 Diff 反查每项语义变化是否为当前验收所需。文件数量、新增方法或重载本身不是问题;若公共契约、共享行为或无关生产逻辑被扩大,而现有证据不能说明局部方案为何无法安全、完整地满足验收,应作为纯实现问题交回 Apply 收缩。
27
+ - 对当前审查范围内的 Diff,分别判断“是否漏实现”和“是否超出必要范围”:直接消费者没有实现、或没有现有实现已满足验收的证据,属于完整性问题;计划或验收没有要求、也没有直接必要性证据的语义扩大,属于范围问题,应作为纯实现问题交回 Apply 收缩,文件数量和新增私有局部函数本身不是问题。两类判断都锚定当前批准行为和实际 Diff,不把消费者类别或可能性清单当成覆盖义务。放行任何计划外的文件前,必须能说清它服务于哪条已批准锚点、为何无法避免;给不出依据就按范围问题收缩。若你认为某个计划未写的机制不加上就不正确,标为混合问题交给使用者裁决,不要写成必须实现的纯代码缺口。
28
+ - 任务说明提供的结构变更清单是已批准结构的边界。代码中出现清单外的新表、列、实体、DAO、开关、迁移、公共接口,或清单外的既有签名变化、兼容路径删除,按 `unjustified_addition` 报告并把锚点指向最接近的清单条目或清单本身;清单内的结构不因"可以更简单"而报告。
29
29
  - 测试是否实际证明相关行为和直接回归风险,而非只存在一条通过记录。
30
30
  - 需求源已更新时,代码是否仍在执行过期计划;此类问题按方案或需求缺口归因,不把旧材料当作当前依据。
31
31
 
32
32
  风格偏好、无证据的猜测、历史无关问题和“另一种写法更优雅”不阻塞。不要用“最小改动”要求遗漏批准范围。
33
33
 
34
- 将问题归因为:纯实现问题(可回 apply 修复)、方案/需求问题(计划不能支持正确实现)或混合问题(需要主流程处理分歧),并说明依据。不要用审查建议创造新的需求或架构。
34
+ 将问题归因为:纯实现问题(可回 apply 修复,含收缩)、方案/需求问题(计划不能支持正确实现)或混合问题(需要主流程处理分歧),并说明依据。不要用审查建议创造新的需求或架构。
35
35
 
36
36
  纯实现问题要说明为什么不需要改变计划;方案或混合问题要说明为什么单纯改代码无法满足已确认验收。复核修复时,Apply 可以通过代码变化关闭问题,也可以用可核实的调用链、兼容约束或验证证据说明原实现必须保留;证据成立时关闭原问题,证据不足时沿用原 finding,不因实现者偏好或审查者偏好反复争论。无阻塞问题时也说明尚未验证的风险,避免把审查覆盖当作全局保证。
37
37
 
@@ -43,8 +43,10 @@ Discovery 准备结束时,从本次变更及已有证据出发,反向检查
43
43
  - task 的来源、设计依据、验收和边界应能让执行者判断是否越界;这些材料与 task 实质无关、空泛或互相矛盾时才报告。不要检查字段、ID 或引用写法本身。
44
44
  - 参考实现只证明已有能力和候选机制,不自动证明其接口数量、资源拆分、数据模型或模块边界适合本次 change。新增公共表面或跨系统改动缺少独立责任与必要性依据,或者明显存在可复用、合并、缩减空间并影响实施边界时,应要求计划补足判断,而不是规定具体数量或替代方案。
45
45
  - 计划通过前,执行者应能在不重新决定产品语义或重做架构设计的前提下开始 Apply。会改变数据归属、调用路径、一致性或发布顺序的候选路线不得留给 Apply 临时选择。跨越可独立发布、失败或验证边界的 task,未经核实却被当成既定事实的外部依赖,以及无法证明已声明行为或设计直接风险的测试契约,都会削弱这一条件。
46
- - 检查计划自己声明的关键不变量是否在迁移、兼容、回退和失败路径下仍成立;新旧实现同时存在且可能承担同一写入责任时,计划应明确权威写入边界,避免实现阶段重新决定所有权。
46
+ - 只有当本次变更自身引入迁移、双写或新旧并存时,才检查其权威写入边界与失败路径;变更没有引入这些机制时,缺少回滚装置、开关或迁移清单不是 blocker,不要用"回退保护"把它们要出来。
47
+ - 结构变更清单是本次已批准结构的边界。清单中没有 Requirement 或 TEST 依据的新表、开关、迁移、签名变化,以及方案正文出现而清单未列的结构,是 blocker;处理方向是删除或补依据,不是补任务。
47
48
  - 需求语义未闭合的问题属于 Explore;需求结果已经明确、但不同可行路线会改变迁移、兼容、数据归属、发布、成本或长期责任边界时,计划应让使用者明确选择。只有内部实现不同且不改变这些结果时,不得要求新增用户决定。
49
+ - 未改变的既有风险和没有已声明可观察结果的理论故障,标残余风险,不得升级为 required fix。本条不削弱上两条。
48
50
 
49
51
  建议只能说明需补足的事实、范围或闭环,不能把个人技术偏好、新基础设施或额外测试升级为强制要求。已声明行为及其直接边界有充分证据时停止。
50
52
 
@@ -25,6 +25,8 @@ argument-hint: "本次执行说明"
25
25
 
26
26
  - 先理解已有实现、调用点与测试模式,再作最小可维护改动;不要为局部任务引入未经计划的新框架、基础设施或重构。
27
27
  - 保持已有公共接口、数据语义、错误处理和兼容行为,除非 task 明确要求改变。
28
+ - 代码审查修复只兑现该问题锚定的已批准行为;审查建议里的架构不是实现授权。
29
+ - 新建非任务直接要求的文件(如配置、脚手架)时,在完成报告中说明必要性;说不清必要性的不要新建。
28
30
  - 记录实际修改、验证候选和不能验证的原因。失败或不确定不是完成,不要用推测补足证据。
29
31
 
30
32
  ## 输出
@@ -17,7 +17,7 @@ argument-hint: "本次探索说明"
17
17
 
18
18
  ## 探查口径
19
19
 
20
- 为代码影响型需求提供能定位的短锚点,如 `ClassName.java:123` 或 `file.ts:45`;没有代码锚点的纯文档/配置/新文件说明 `N/A` 理由。对数据或跨边界行为,沿调用和数据流检查上游来源、关键变形、持久化语义、下游消费者与视图差异;“未发现”必须说明检索方式与范围。运行时数据依赖追到 producer 侧相关字段的最后一次变形,并说明区分依据。
20
+ 为代码影响型需求提供能定位的短锚点,如 `ClassName.java:123` 或 `file.ts:45`;没有代码锚点的纯文档/配置/新文件说明 `N/A` 理由。对数据或跨边界行为,沿调用和数据流检查上游来源、关键变形、持久化语义、下游消费者与视图差异;“未发现”必须说明检索方式与范围。运行时数据依赖追到 producer 侧相关字段的最后一次变形,并说明区分依据。变更把单值扩展为集合或引入新持久化数据时,明确报告是否发现按该数据筛选、检索、报表或迁移存量的消费者,以及检索方式与范围——这个事实决定实现路线能有多轻。
21
21
 
22
22
  先从用户目标和已有锚点形成探查问题,再用正向搜索与调用方/入口反查验证。对每个重要结论明确它是事实、基于锚点的推断还是未知;影响范围候选需要说明为什么可能受影响或为什么排除。不要只扫用户提到的文件,也不要因为模块名看似相关就把它列为影响面。
23
23
 
@@ -53,27 +53,29 @@ metadata:
53
53
  ## 影响范围
54
54
  - <受影响的代码面、相邻模块、用户/系统可观察面及排除理由>
55
55
 
56
+ ## 风险和边界
57
+ - <有证据的技术、兼容、依赖或发布风险;只陈述风险,不在此处写解法>
58
+
59
+ ## 待确认问题
60
+ - [ ] Q-001 [验收] <一个待决问题>。影响:<范围或验收>。选项:A <后果> / B <后果>。建议:<理由>
61
+ - [ ] Q-002 [事实] <需要用户补充的事实>。影响:<缺少它会阻塞的范围或验收>。现有证据:<为什么仓库无法裁决>
62
+ ```
63
+
64
+ 以下两节只在 change 改变共享数据、跨边界输入或持久化语义时加入;纯逻辑、纯文档或不改变数据传递的改动不写这两节,也不写"不适用"占位表:
65
+
66
+ ```markdown
56
67
  ## 链路五要素
57
68
  | ID | 发现方式 | 上游来源 | 规则变形 | 持久化语义 | 下游消费者 | 视图差异 | 未知/排除 | 证据 | 状态 |
58
69
  |---|---|---|---|---|---|---|---|---|---|
59
70
  | CHAIN-001 | <发现路径> | <输入/配置/历史数据> | <关键变形或无> | <持久化含义或不落库> | <消费者或无> | <可观察差异或无> | <未知或排除理由> | <锚点> | 已确认 |
60
71
 
61
- ## 风险和边界
62
- - <有证据的技术、兼容、依赖或发布风险>
63
-
64
72
  ## 输入数据来源核查
65
- - <无运行时数据依赖时,说明不适用原因>
66
-
67
73
  | 核查ID | 消费位置 | 必需输入 | 数据来源 | 区分依据 | 状态/理由 |
68
74
  |---|---|---|---|---|---|
69
75
  | IDC-001 | <入口/规则/算法> | <字段/集合/状态> | <相关 producer 或组装位置> | <可证伪依据> | 已证明 |
70
-
71
- ## 待确认问题
72
- - [ ] Q-001 [验收] <一个待决问题>。影响:<范围或验收>。选项:A <后果> / B <后果>。建议:<理由>
73
- - [ ] Q-002 [事实] <需要用户补充的事实>。影响:<缺少它会阻塞的范围或验收>。现有证据:<为什么仓库无法裁决>
74
76
  ```
75
77
 
76
- 模板提供稳定骨架;不要为了填满每个章节或表格而制造事实、链路或风险。
78
+ 模板提供稳定骨架;不要为了填满每个章节或表格而制造事实、链路或风险。风险条目描述"什么会出错、证据是什么",需要设计决定来处理的风险交给 Propose,不在 discovery 里预写状态字段、锁或预检之类的解法。
77
79
 
78
80
  ## 写作原则
79
81
 
@@ -93,7 +95,7 @@ Discovery 必须明确区分已确认事实、基于证据的推断和仍未裁
93
95
 
94
96
  当 change 涉及共享数据、跨边界输入、持久化语义或下游可观察行为时,建立足以判断影响的链路视图:输入来自哪里、关键形态如何变化、由谁持久化或解释、哪些消费者和视图会观察到结果。调查应追到决定本次输入语义的责任点,而不止停在 consumer、DTO 或校验器。
95
97
 
96
- 不涉及此类链路时,说明不适用的具体原因;不要为了填表虚构链路。
98
+ 不涉及此类链路时不建链路表;影响范围里的直接锚点和具体排除理由就是闭环。
97
99
 
98
100
  ### 未知与用户决策
99
101
 
@@ -101,6 +103,8 @@ Discovery 必须明确区分已确认事实、基于证据的推断和仍未裁
101
103
 
102
104
  需求目标与验收已经明确,但不同技术路线会改变迁移、兼容、数据归属、发布方式、成本或长期责任边界时,将它作为 Propose 的设计决策候选写入调查结论和风险依据,不在 Explore 提前替用户选择,也不把它伪装成需求问题。若路线差异会改变产品行为或验收,则仍属于 Explore。
103
105
 
106
+ 有一类事实例外:当变更把单值扩展为集合、引入新的持久化数据或改变既有字段含义时,"是否存在按该数据筛选、检索、报表、审计或迁移存量的场景"决定了实现路线能有多轻。它是需求侧事实,不是设计取舍:先在需求源和代码中核实;核实不了时作为待确认事项交给用户,并说明不同答案会导向什么样的存储与兼容路线。不要把这个事实留给 Propose 去猜,也不要在 Explore 替用户选存储方式。
107
+
104
108
  每个待决问题只表达一个会改变结果的确认点,并说明影响、可选方向或需要补充的信息及建议依据。选择型问题给出候选结果和推荐;事实型问题说明需要用户提供什么、现有证据为什么无法裁决,以及缺少它会阻塞什么。
105
109
 
106
110
  Discovery 中有多个待确认事项时,按依赖逐项与用户沟通。每轮先给当前事项的简短理解和决策信息;可以说明还有后续事项,但不要同时展开多件事或要求一次确认全部内容。用户主动回答多个问题时,先回写当前结论并重新核对其余事项,再继续沟通。用户答复改变前提时,先更新受影响的调查结论,不沿用旧前提继续提问。
@@ -117,5 +121,5 @@ Discovery 中有多个待确认事项时,按依赖逐项与用户沟通。每
117
121
 
118
122
  - 不改业务代码或计划材料。
119
123
  - 不自行扩大范围、选择未确认的业务语义或宣布探索完成。
120
- - 审查意见用于补足证据,不自动创造新范围、新需求或新方案。
124
+ - 审查意见用于补足证据,不自动创造新范围、新需求或新方案。审查通过后列出的残余风险记入 Propose 的风险表,不回写 discovery。
121
125
  - discovery 发生实质变化后,以 `next` 决定后续审查或推进。