@peterxiaoyang/superspec 0.1.58 → 0.1.60

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/cli.js CHANGED
@@ -8,7 +8,7 @@ import { writeSnapshot } from "./store.js";
8
8
  import { rebuildSnapshot } from "./sync.js";
9
9
  import { next as nextCmd } from "./next.js";
10
10
  import { proposeReady, commitTransition, transitionInit, transitionExplore, startApply, taskStart, taskComplete, reopen, reviewReady, accept } from "./transition.js";
11
- import { recordJobSubmit, recordJobSubmitContent, recordUserDecision, recordUserDecisionContent, jobsList, jobsPacket } from "./record.js";
11
+ import { recordJobSubmit, recordJobSubmitContent, recordUserDecision, recordUserDecisionContent, jobsList, jobsPacket, jobsContract } from "./record.js";
12
12
  import { recordTestRun, recordTestRunContent } from "./task.js";
13
13
  import { RecordInputDecodingError, decodeRecordInput } from "./record_input.js";
14
14
  import { probeOpenSpec, openspecStatus, changeRoot } from "./openspec.js";
@@ -573,10 +573,20 @@ record 子命令:
573
573
  test-run --input <F|->
574
574
 
575
575
  jobs 子命令:
576
- list / packet --job <J>
576
+ list / packet --job <J> / contract --job <J> [--skeleton]
577
577
  `;
578
578
  }
579
579
  function commandHelp(command, subcommand) {
580
+ if (command === "jobs" && subcommand === "contract") {
581
+ return `用法:superspec jobs contract --change <C> --job <J> [--skeleton]
582
+
583
+ 输出该工作项的报告契约(默认)或可直接填写的报告骨架(--skeleton)。
584
+ 契约与 packet 顶层的 report_skeleton 同源;审查角色应在产出报告前先取骨架,逐字段填写。
585
+
586
+ 示例:
587
+ superspec jobs contract --change <C> --job <J> --skeleton
588
+ `;
589
+ }
580
590
  if (command === "record" && subcommand === "user-decision") {
581
591
  return `用法:superspec record user-decision --change <C> --input <F|->
582
592
 
@@ -590,7 +600,7 @@ function commandHelp(command, subcommand) {
590
600
  if (command === "record" && subcommand === "job-submit") {
591
601
  return `用法:superspec record job-submit --change <C> --job <J> --report <F|->
592
602
 
593
- 提交 reviewer JSON 报告;--report - 表示从 stdin 读取。报告契约以 jobs packet 返回的 report_schema 为准。
603
+ 提交 reviewer JSON 报告;--report - 表示从 stdin 读取。报告契约(report_schema)见 superspec jobs contract --change <C> --job <J>;packet 顶层给出预填骨架(report_skeleton)。
594
604
  需要落盘时写到 packet 的 report_file_path(.superspec/changes/<C>/jobs/<J>.report.json),不要放进 openspec/changes 计划材料目录。
595
605
 
596
606
  示例:
@@ -958,6 +968,17 @@ async function main(argv) {
958
968
  console.log(JSON.stringify(result, null, 2));
959
969
  return result.found ? 0 : 1;
960
970
  }
971
+ case "contract": {
972
+ const jobId = opts.job;
973
+ if (!jobId) {
974
+ console.error("jobs contract 需要 --job");
975
+ return 1;
976
+ }
977
+ const skeletonOnly = opts.skeleton === "true";
978
+ const result = jobsContract(projectRoot, change, jobId, { skeleton: skeletonOnly });
979
+ console.log(JSON.stringify(skeletonOnly && result.found ? result.report_skeleton : result, null, 2));
980
+ return result.found ? 0 : 1;
981
+ }
961
982
  default:
962
983
  console.error(`未知的 jobs 子命令:${subcommand}`);
963
984
  return 1;
@@ -105,4 +105,9 @@ export declare function latestApplyDoneToReviewGate(events: Event[]): {
105
105
  export declare function latestCodeReviewGateEvidence(events: Event[]): CodeReviewGateEvidence | null;
106
106
  export declare function requiresFinalVerifierForCurrentReview(events: Event[]): boolean;
107
107
  export declare function computeCodeStateCheck(projectRoot: string, events: Event[], ignoredCodePaths?: string[]): CodeStateCheck;
108
+ /** 已接受代码审查工作项在事件流里留下的范围外未检查摘要(pass 的置信边界)。 */
109
+ export declare function outOfScopeUncheckedForJob(events: Event[], jobId: string): {
110
+ path: string;
111
+ reason: string;
112
+ }[];
108
113
  export {};
@@ -867,6 +867,9 @@ export function latestCodeReviewGateEvidence(events) {
867
867
  job_id: typeof gate.job_id === "string" ? gate.job_id : null,
868
868
  packet_digest: typeof gate.packet_digest === "string" ? gate.packet_digest : null,
869
869
  ...(gate.reason === "no_code_changes" ? { reason: gate.reason } : {}),
870
+ ...(Array.isArray(gate.out_of_scope_unchecked) && gate.out_of_scope_unchecked.length > 0
871
+ ? { out_of_scope_unchecked: gate.out_of_scope_unchecked }
872
+ : {}),
870
873
  event_id: event.event_id,
871
874
  event_digest: event.event_digest,
872
875
  };
@@ -969,3 +972,21 @@ export function computeCodeStateCheck(projectRoot, events, ignoredCodePaths = []
969
972
  scope_reason: scopeReason,
970
973
  };
971
974
  }
975
+ /** 已接受代码审查工作项在事件流里留下的范围外未检查摘要(pass 的置信边界)。 */
976
+ export function outOfScopeUncheckedForJob(events, jobId) {
977
+ for (let i = events.length - 1; i >= 0; i--) {
978
+ const event = events[i];
979
+ if (event.event_type !== "job_accepted")
980
+ continue;
981
+ const payload = event.payload;
982
+ if (payload.job_id !== jobId)
983
+ continue;
984
+ if (!Array.isArray(payload.out_of_scope_unchecked))
985
+ return [];
986
+ return payload.out_of_scope_unchecked.filter((item) => {
987
+ const candidate = item;
988
+ return typeof candidate?.path === "string" && typeof candidate?.reason === "string";
989
+ });
990
+ }
991
+ return [];
992
+ }
package/dist/record.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { RecordResult, Job, JobPacket } from "./types.ts";
1
+ import type { RecordResult, Job, JobPacket, JobRole } from "./types.ts";
2
+ import { type ReportSchemaContract } from "./report_contract.ts";
2
3
  /** 报告文件的推荐落盘位置:工作流记录目录,不进入计划材料。 */
3
4
  export declare function jobReportFilePath(change: string, jobId: string): string;
4
5
  /** record job-submit:登记工作项结果 */
@@ -24,3 +25,22 @@ export declare function jobsPacket(projectRoot: string, change: string, jobId: s
24
25
  packet?: JobPacket;
25
26
  message: string;
26
27
  };
28
+ /**
29
+ * jobs contract:单独取报告契约(默认)或可填骨架(--skeleton)。
30
+ *
31
+ * 与 packet 顶层的 report_skeleton 同源,供审查角色在产出报告前自查结构,
32
+ * 不必从 packet 的长段落里提取。
33
+ */
34
+ export declare function jobsContract(projectRoot: string, change: string, jobId: string, opts?: {
35
+ skeleton?: boolean;
36
+ }): {
37
+ found: boolean;
38
+ job_id?: string;
39
+ role?: JobRole;
40
+ report_schema?: ReportSchemaContract;
41
+ report_skeleton?: Record<string, unknown>;
42
+ fill_items?: string[];
43
+ contract_command?: string;
44
+ skeleton_command?: string;
45
+ message: string;
46
+ };
package/dist/record.js CHANGED
@@ -15,20 +15,8 @@ import { RecordInputDecodingError, readRecordInputFile } from "./record_input.js
15
15
  import { currentExploreRoundId } from "./explore_round.js";
16
16
  import { currentProposeOpenQuestion, currentProposeQuestionContent, currentProposeRoundId, } from "./propose_round.js";
17
17
  import { discoveryOpenQuestionDisplayText, discoveryQuestionContextFingerprint, discoveryQuestionDecisionBasisDigest, discoveryOpenQuestionScope, legacyDiscoveryOpenQuestionScope, EXPLORE_OPEN_QUESTION_SCOPE_PREFIX, parseDiscoveryOpenQuestions, proposeOpenQuestionDisplayText, proposeOpenQuestionScope, proposeQuestionContextFingerprint, proposeQuestionDecisionBasisDigest, legacyProposeOpenQuestionScope, PROPOSE_OPEN_QUESTION_SCOPE_PREFIX, } from "./format.js";
18
- const REVIEW_REPORT_REQUIRED_FIELDS = ["role", "verdict", "findings"];
19
- const REVIEW_REPORT_OPTIONAL_FIELDS = ["summary", "evidence_refs", "risks", "open_questions"];
20
- const REVIEWER_KINDS = new Set(["subagent", "codex-subagent", "human", "external-agent"]);
18
+ import { COVERAGE_MESSAGES, REVIEWER_KINDS, outOfScopeUncheckedFromReport, REVIEW_REPORT_OPTIONAL_FIELDS, REVIEW_REPORT_REQUIRED_FIELDS, isReviewRole, requiresReviewer, requiresReviewScope, reportSchemaForJob, reportSkeletonFillItems, reportSkeletonForJob, } from "./report_contract.js";
21
19
  const CODE_REVIEW_FINDING_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
22
- function isReviewRole(role) {
23
- return role === "critic" || role === "architect" || role === "test-engineer" || role === "code-reviewer" || role === "verifier";
24
- }
25
- function requiresReviewer(role) {
26
- return role === "critic" || role === "architect" || role === "test-engineer" || role === "code-reviewer";
27
- }
28
- /** All review reports with bound material acknowledge full file coverage. */
29
- function requiresReviewScope(job) {
30
- return job.boundFiles.length > 0 && isReviewRole(job.role);
31
- }
32
20
  function projectLocalInvalidReportMustTerminate(job) {
33
21
  return job.role === "code-reviewer" || job.packet_context?.code_state_check !== undefined;
34
22
  }
@@ -256,7 +244,7 @@ function uncheckedCodeReviewPaths(value, checks) {
256
244
  for (const item of value) {
257
245
  const obj = asObject(item);
258
246
  if (!obj) {
259
- checks.push("代码审查覆盖范围里的未检查项必须是包含 path/reason 的对象");
247
+ checks.push(COVERAGE_MESSAGES.uncheckedItemNotObject);
260
248
  continue;
261
249
  }
262
250
  if (!nonEmptyString(obj.path)) {
@@ -284,8 +272,8 @@ function validateReviewer(obj, checks) {
284
272
  checks.push("报告 reviewer 必须是包含 kind/id 的对象");
285
273
  }
286
274
  else {
287
- if (typeof reviewer.kind !== "string" || !REVIEWER_KINDS.has(reviewer.kind)) {
288
- checks.push(`报告 reviewer.kind 必须是 ${[...REVIEWER_KINDS].join("|")} 之一`);
275
+ if (typeof reviewer.kind !== "string" || !REVIEWER_KINDS.includes(reviewer.kind)) {
276
+ checks.push(`报告 reviewer.kind 必须是 ${REVIEWER_KINDS.join("|")} 之一`);
289
277
  }
290
278
  if (typeof reviewer.id !== "string" || reviewer.id.trim() === "") {
291
279
  checks.push("报告 reviewer.id 必须是非空字符串");
@@ -295,7 +283,7 @@ function validateReviewer(obj, checks) {
295
283
  function validateCodeReviewScope(obj, job, checks) {
296
284
  const scope = asObject(obj.review_scope);
297
285
  if (!scope) {
298
- checks.push("代码审查报告缺少覆盖范围字段 review_scope");
286
+ checks.push(COVERAGE_MESSAGES.codeReviewScopeMissing);
299
287
  return;
300
288
  }
301
289
  if (scope.job_id !== job.job_id) {
@@ -305,11 +293,11 @@ function validateCodeReviewScope(obj, job, checks) {
305
293
  checks.push("代码审查覆盖范围里的 packet_digest 与工作项不匹配,请使用当前工作项说明重新生成报告");
306
294
  }
307
295
  if (!stringArray(scope.checked_paths))
308
- checks.push("代码审查覆盖范围里的 checked_paths 必须是字符串数组");
296
+ checks.push(COVERAGE_MESSAGES.checkedPathsNotStringArray);
309
297
  if (!stringArray(scope.checked_docs))
310
- checks.push("代码审查覆盖范围里的 checked_docs 必须是字符串数组");
298
+ checks.push(COVERAGE_MESSAGES.checkedDocsNotStringArray);
311
299
  if (!Array.isArray(scope.unchecked))
312
- checks.push("代码审查覆盖范围里的 unchecked 必须是数组");
300
+ checks.push(COVERAGE_MESSAGES.uncheckedNotArray);
313
301
  if (stringArray(scope.checked_paths) && Array.isArray(scope.unchecked)) {
314
302
  const checkedPaths = new Set(scope.checked_paths);
315
303
  const uncheckedPaths = uncheckedCodeReviewPaths(scope.unchecked, checks);
@@ -318,19 +306,21 @@ function validateCodeReviewScope(obj, job, checks) {
318
306
  checks.push(`代码审查报告未说明是否检查了 ${bound.path}`);
319
307
  }
320
308
  }
321
- if (obj.verdict === "pass" && uncheckedPaths.size > 0) {
322
- checks.push("代码审查结论为 pass 时不能包含未检查的绑定文件");
309
+ // 只有绑定文件被列为未检查时才阻塞 pass:范围外观察(未跑构建、无关文件)写进
310
+ // unchecked 是诚实记录,不应让 pass 结论无法提交。
311
+ if (obj.verdict === "pass" && job.boundFiles.some(bound => uncheckedPaths.has(bound.path))) {
312
+ checks.push(COVERAGE_MESSAGES.passWithUncheckedBoundFile);
323
313
  }
324
314
  }
325
315
  }
326
316
  function validateReviewScope(obj, job, checks) {
327
317
  const scope = asObject(obj.review_scope);
328
318
  if (!scope) {
329
- checks.push("审查报告缺少覆盖范围字段 review_scope");
319
+ checks.push(COVERAGE_MESSAGES.reviewScopeMissing);
330
320
  return;
331
321
  }
332
322
  if (!stringArray(scope.checked_paths)) {
333
- checks.push("审查报告覆盖范围里的 checked_paths 必须是字符串数组");
323
+ checks.push(COVERAGE_MESSAGES.reviewCheckedPathsNotStringArray);
334
324
  return;
335
325
  }
336
326
  const checkedPaths = new Set(scope.checked_paths);
@@ -667,11 +657,15 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
667
657
  }
668
658
  }
669
659
  const rawRef = appendRawRecord(projectRoot, change, "review-reports", parsedReport);
660
+ // pass 结论下的范围外未检查项不进门禁,但必须留在决策面:只翻 raw 才能看到
661
+ // 会让「pass 但未跑构建 / 未覆盖范围外文件」这一事实在流程里消失。
662
+ const outOfScopeUnchecked = job.role === "code-reviewer" ? outOfScopeUncheckedFromReport(parsedReport, job.boundFiles) : [];
670
663
  const acceptEvent = makeEvent(change, "job_accepted", {
671
664
  job_id: jobId,
672
665
  role: job.role,
673
666
  report_digest: reportDigest,
674
667
  accepted_at: new Date().toISOString(),
668
+ ...(outOfScopeUnchecked.length > 0 ? { out_of_scope_unchecked: outOfScopeUnchecked } : {}),
675
669
  ...(reportPath ? { report_path: reportPath } : {}),
676
670
  ...rawRef,
677
671
  });
@@ -1199,6 +1193,8 @@ function packetFieldDescriptions() {
1199
1193
  unknown_attribution_tasks: "因为缺少边界快照或提交段 diff 失败而无法完整计算改动归属的任务(task)。",
1200
1194
  coverage_exemption_refs: "测试覆盖豁免引用:说明某个 TEST 为什么没有绑定到任务(task)。",
1201
1195
  report_file_path: "报告需要落盘时的文件位置(项目相对路径),位于工作流记录目录;报告内容登记后由引擎存入 raw 记录,不属于计划材料。",
1196
+ report_skeleton: "按本工作项预填的报告骨架(job_id / packet_digest / 空数组);逐字段填写即可,不要自行设计结构。",
1197
+ report_schema: "报告契约(字段形状、取值、条件、提示消息);由 `superspec jobs contract --change <C> --job <J>` 输出,提交校验引用同一份定义。",
1202
1198
  code_review_gate: "最终验证读取的代码审查门禁事实:passed 指向已接受的代码审查工作项,skipped 表示本轮没有代码类改动。",
1203
1199
  code_state_check: "代码状态检查:最终验证时用于判断代码审查后代码是否又发生变化。",
1204
1200
  event_id: "事件 ID,用于追溯证据来源。",
@@ -1260,8 +1256,9 @@ export function jobsPacket(projectRoot, change, jobId) {
1260
1256
  ...(hasReviewScope ? ["review_scope"] : []),
1261
1257
  ],
1262
1258
  output_contract_optional_fields: [...REVIEW_REPORT_OPTIONAL_FIELDS],
1259
+ report_skeleton: reportSkeletonForJob(job),
1263
1260
  字段说明: packetFieldDescriptions(),
1264
- output_instructions: `${roleDescription(job.role)}。` +
1261
+ output_instructions: `${roleDescription(job.role)}。本工作项的报告结构以 packet 顶层 report_skeleton 为准(完整契约:superspec jobs contract --change "${change}" --job "${job.job_id}"):按骨架逐字段填写,job_id / packet_digest 已按本工作项预填,不要改写,也不要用上一轮报告里的值。` +
1265
1262
  (isReviewer ? reviewScopeInstruction(job, reviewTargets, readOnlyRefs) : "") +
1266
1263
  (isReviewer ? migrationEvidenceInstruction(job) : "") +
1267
1264
  (job.review_evidence_digest ? `本工作项对应的执行证据版本为 ${job.review_evidence_digest},` : "") +
@@ -1269,19 +1266,19 @@ export function jobsPacket(projectRoot, change, jobId) {
1269
1266
  (requiresReviewer(job.role) ? `必须由独立 ${recommendedAgentForRole(job.role)} 审查角色执行,并在审查者来源字段(reviewer.kind/id)中记录来源,` : "") +
1270
1267
  `产出 JSON 报告内容并优先通过 --report - 从 stdin 登记;需要落盘时写到 report_file_path,不要写进 openspec/changes 或 .superspec/artifacts 等计划材料目录。${recordInputInstruction(job)}协议字段含义见 packet 顶层“字段说明”,普通对话不要原样复述 JSON。` +
1271
1268
  (isCodeReviewer
1272
- ? `格式骨架:{"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 不允许仍有未检查的绑定文件。`
1269
+ ? `格式骨架:${JSON.stringify(reportSkeletonForJob(job))}。提交前按真实审查结果填写数组;不得从 boundFiles 自动复制 checked_paths。verdict 只能为 pass 或 fail;审查覆盖范围(review_scope)用来说明本次审查覆盖了哪些文件和文档,已检查路径(checked_paths)与未检查项(unchecked)必须合起来覆盖全部绑定文件(boundFiles),unchecked 条目格式为 {"path":"<path>","reason":"<reason>"};pass 不允许仍有未检查的绑定文件。`
1273
1270
  + `报告结论为 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 见字段说明。`
1274
1271
  + (packetContext?.task_execution_index
1275
1272
  ? `本工作项带任务执行索引(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 是本轮冻结的审查范围;若它来自前一轮审查后的增量,只复核本轮变化及其直接影响链路,不要求重复审查未变化文件,但仍要判断批准行为是否完整闭合。`
1276
1273
  : "")
1277
1274
  : job.role === "verifier"
1278
- ? `最小格式:{"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)的旧证据只能弱引用。` +
1275
+ ? `最小格式:${JSON.stringify(reportSkeletonForJob(job))}。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)的旧证据只能弱引用。` +
1279
1276
  (packetContext?.code_state_check
1280
1277
  ? `本工作项带代码状态检查(code_state_check),它是创建 packet 时的快照:验证期间若代码状态已变化,不要提交该报告;主流程会通过 next 创建携带最新事实的验证工作项。`
1281
1278
  : "")
1282
1279
  : isReviewer
1283
- ? `最小格式:{"role":"${job.role}","verdict":"pass","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""},"reviewer":{"kind":"subagent","id":"<thread-or-agent-id>"}}。verdict 只能为 pass 或 fail。`
1284
- : `最小格式:{"role":"${job.role}","verdict":"pass","findings":[]}。verdict 只能为 pass 或 fail。`),
1280
+ ? `最小格式:${JSON.stringify(reportSkeletonForJob(job))}。verdict 只能为 pass 或 fail。`
1281
+ : `最小格式:${JSON.stringify(reportSkeletonForJob(job))}。verdict 只能为 pass 或 fail。`),
1285
1282
  stop_conditions: isReviewer
1286
1283
  ? ["完成审查后提交报告,不要修改文档"]
1287
1284
  : job.role === "executor"
@@ -1292,3 +1289,28 @@ export function jobsPacket(projectRoot, change, jobId) {
1292
1289
  message: `工作项 ${jobId} 的执行说明`,
1293
1290
  };
1294
1291
  }
1292
+ /**
1293
+ * jobs contract:单独取报告契约(默认)或可填骨架(--skeleton)。
1294
+ *
1295
+ * 与 packet 顶层的 report_skeleton 同源,供审查角色在产出报告前自查结构,
1296
+ * 不必从 packet 的长段落里提取。
1297
+ */
1298
+ export function jobsContract(projectRoot, change, jobId, opts = {}) {
1299
+ const job = findJob(readEvents(projectRoot, change), jobId);
1300
+ if (!job)
1301
+ return { found: false, message: `工作项 ${jobId} 不存在` };
1302
+ const base = ["superspec", "jobs", "contract", "--change", change, "--job", job.job_id];
1303
+ return {
1304
+ found: true,
1305
+ job_id: job.job_id,
1306
+ role: job.role,
1307
+ report_schema: reportSchemaForJob(job),
1308
+ report_skeleton: reportSkeletonForJob(job),
1309
+ fill_items: reportSkeletonFillItems(job),
1310
+ contract_command: base.join(" "),
1311
+ skeleton_command: [...base, "--skeleton"].join(" "),
1312
+ message: opts.skeleton
1313
+ ? `工作项 ${job.job_id} 的报告骨架`
1314
+ : `工作项 ${job.job_id} 的报告契约`,
1315
+ };
1316
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 报告契约单源:字段形状与取值、覆盖类提示消息、骨架渲染。
3
+ *
4
+ * 范围止于「形状」:字段名/类型/枚举/必填与否 + 跨字段条件 + 可直接填写的骨架。
5
+ * 面向模型的说明文字仍由 packet 的 output_instructions 承载,判据仍由 record.ts 的
6
+ * 校验函数执行(交叉规则与跨文件解析不声明化)。这样契约表不是第二份散文,
7
+ * 也不与校验器争谁说了算。
8
+ *
9
+ * packet 的 report_skeleton、`superspec jobs contract` 与提交校验共用这里的定义。
10
+ */
11
+ import type { Job, JobRole } from "./types.ts";
12
+ export declare const REVIEW_REPORT_REQUIRED_FIELDS: readonly ["role", "verdict", "findings"];
13
+ export declare const REVIEW_REPORT_OPTIONAL_FIELDS: readonly ["summary", "evidence_refs", "risks", "open_questions"];
14
+ /** reviewer.kind 允许取值:契约、packet 与提交校验共用。 */
15
+ export declare const REVIEWER_KINDS: readonly ["subagent", "codex-subagent", "human", "external-agent"];
16
+ /** 审查/验证类角色:报告需要 review_scope 覆盖回执。 */
17
+ export declare function isReviewRole(role: JobRole): boolean;
18
+ /** 需要 reviewer.kind/id 的角色。 */
19
+ export declare function requiresReviewer(role: JobRole): boolean;
20
+ /** 需要 review_scope 覆盖回执的工作项。 */
21
+ export declare function requiresReviewScope(job: Job): boolean;
22
+ /** 提交校验与契约共用的覆盖类提示消息:改这里即同时影响两侧。 */
23
+ export declare const COVERAGE_MESSAGES: {
24
+ readonly codeReviewScopeMissing: "代码审查报告缺少覆盖范围字段 review_scope";
25
+ readonly reviewScopeMissing: "审查报告缺少覆盖范围字段 review_scope";
26
+ readonly checkedPathsNotStringArray: "代码审查覆盖范围里的 checked_paths 必须是字符串数组";
27
+ readonly reviewCheckedPathsNotStringArray: "审查报告覆盖范围里的 checked_paths 必须是字符串数组";
28
+ readonly checkedDocsNotStringArray: "代码审查覆盖范围里的 checked_docs 必须是字符串数组";
29
+ readonly uncheckedNotArray: "代码审查覆盖范围里的 unchecked 必须是数组";
30
+ readonly uncheckedItemNotObject: "代码审查覆盖范围里的未检查项必须是包含 path/reason 的对象";
31
+ readonly passWithUncheckedBoundFile: "代码审查结论为 pass 时不能包含未检查的绑定文件";
32
+ };
33
+ export interface ReportFieldContract {
34
+ type: "string" | "array" | "object";
35
+ required?: boolean;
36
+ values?: readonly string[];
37
+ pattern?: string;
38
+ item?: string;
39
+ }
40
+ export interface ReportSchemaContract {
41
+ role: JobRole;
42
+ required_fields: string[];
43
+ optional_fields: string[];
44
+ /** 顶层字段与 review_scope / findings 子字段的形状,键用点路径表示。 */
45
+ fields: Record<string, ReportFieldContract>;
46
+ /** 跨字段条件;校验器拒绝时的措辞与此保持一致。 */
47
+ conditions: string[];
48
+ }
49
+ /**
50
+ * 工作项报告契约:只描述形状与取值。模型需要的解释性文字见 packet 的
51
+ * `output_instructions` 与 `字段说明`;判据由 record.ts 执行。
52
+ */
53
+ export declare function reportSchemaForJob(job: Job): ReportSchemaContract;
54
+ /**
55
+ * 可直接填写的报告骨架。
56
+ *
57
+ * 只预填工作项常量(job_id / packet_digest)与空数组:code-reviewer 的 checked_paths
58
+ * 必须由审查者按实际浏览填写,预填会架空覆盖回执的意义。
59
+ * 普通 reviewer / verifier 的 checked_paths 仍按既有协议预填全部绑定文件。
60
+ */
61
+ export declare function reportSkeletonForJob(job: Job): Record<string, unknown>;
62
+ /** 骨架中必须由审查者替换的空值,供 packet 与 CLI 给出同一份填写提示。 */
63
+ export declare function reportSkeletonFillItems(job: Job): string[];
64
+ /** 报告里声明"未检查"且不属于绑定文件的条目(pass 的置信边界,供事件流与门禁透出)。 */
65
+ export declare function outOfScopeUncheckedFromReport(report: Record<string, unknown> | null, boundFiles: {
66
+ path: string;
67
+ }[]): {
68
+ path: string;
69
+ reason: string;
70
+ }[];
@@ -0,0 +1,161 @@
1
+ export const REVIEW_REPORT_REQUIRED_FIELDS = ["role", "verdict", "findings"];
2
+ export const REVIEW_REPORT_OPTIONAL_FIELDS = ["summary", "evidence_refs", "risks", "open_questions"];
3
+ /** reviewer.kind 允许取值:契约、packet 与提交校验共用。 */
4
+ export const REVIEWER_KINDS = ["subagent", "codex-subagent", "human", "external-agent"];
5
+ /** 需要 reviewer 来源字段的角色。 */
6
+ const REVIEWER_ROLE = {
7
+ critic: true,
8
+ architect: true,
9
+ "test-engineer": true,
10
+ "code-reviewer": true,
11
+ };
12
+ /** 需要 review_scope 覆盖回执的角色。 */
13
+ const REVIEW_SCOPE_ROLE = {
14
+ ...REVIEWER_ROLE,
15
+ verifier: true,
16
+ };
17
+ /** 审查/验证类角色:报告需要 review_scope 覆盖回执。 */
18
+ export function isReviewRole(role) {
19
+ return REVIEW_SCOPE_ROLE[role] === true;
20
+ }
21
+ /** 需要 reviewer.kind/id 的角色。 */
22
+ export function requiresReviewer(role) {
23
+ return REVIEWER_ROLE[role] === true;
24
+ }
25
+ /** 需要 review_scope 覆盖回执的工作项。 */
26
+ export function requiresReviewScope(job) {
27
+ return job.boundFiles.length > 0 && isReviewRole(job.role);
28
+ }
29
+ /** 提交校验与契约共用的覆盖类提示消息:改这里即同时影响两侧。 */
30
+ export const COVERAGE_MESSAGES = {
31
+ codeReviewScopeMissing: "代码审查报告缺少覆盖范围字段 review_scope",
32
+ reviewScopeMissing: "审查报告缺少覆盖范围字段 review_scope",
33
+ checkedPathsNotStringArray: "代码审查覆盖范围里的 checked_paths 必须是字符串数组",
34
+ reviewCheckedPathsNotStringArray: "审查报告覆盖范围里的 checked_paths 必须是字符串数组",
35
+ checkedDocsNotStringArray: "代码审查覆盖范围里的 checked_docs 必须是字符串数组",
36
+ uncheckedNotArray: "代码审查覆盖范围里的 unchecked 必须是数组",
37
+ uncheckedItemNotObject: "代码审查覆盖范围里的未检查项必须是包含 path/reason 的对象",
38
+ passWithUncheckedBoundFile: "代码审查结论为 pass 时不能包含未检查的绑定文件",
39
+ };
40
+ const FINDING_TYPES = ["implementation", "spec", "mixed"];
41
+ const CLAIM_KINDS = ["missing_approved", "breaks_existing", "unjustified_addition"];
42
+ const SUGGESTED_ACTIONS = ["apply", "propose"];
43
+ /**
44
+ * 工作项报告契约:只描述形状与取值。模型需要的解释性文字见 packet 的
45
+ * `output_instructions` 与 `字段说明`;判据由 record.ts 执行。
46
+ */
47
+ export function reportSchemaForJob(job) {
48
+ const isCodeReviewer = job.role === "code-reviewer";
49
+ const hasScope = requiresReviewScope(job);
50
+ const fields = {
51
+ role: { type: "string", required: true, values: [job.role] },
52
+ verdict: { type: "string", required: true, values: ["pass", "fail"] },
53
+ findings: { type: "array", required: true },
54
+ summary: { type: "string" },
55
+ evidence_refs: { type: "array" },
56
+ risks: { type: "array" },
57
+ open_questions: { type: "array" },
58
+ };
59
+ const conditions = ["verdict 只能是 pass 或 fail。"];
60
+ if (requiresReviewer(job.role)) {
61
+ fields.reviewer = { type: "object", required: true };
62
+ fields["reviewer.kind"] = { type: "string", required: true, values: REVIEWER_KINDS };
63
+ fields["reviewer.id"] = { type: "string", required: true };
64
+ }
65
+ if (isCodeReviewer) {
66
+ fields.review_scope = { type: "object", required: true };
67
+ fields["review_scope.job_id"] = { type: "string", required: true, values: [job.job_id] };
68
+ fields["review_scope.packet_digest"] = { type: "string", required: true, values: [job.packet_digest] };
69
+ fields["review_scope.checked_paths"] = { type: "array", required: true, item: "path" };
70
+ fields["review_scope.checked_docs"] = { type: "array", required: true, item: "path" };
71
+ fields["review_scope.unchecked"] = { type: "array", required: true, item: "{path, reason}" };
72
+ fields["findings[blocking=true].id"] = { type: "string", required: true, pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]*$" };
73
+ fields["findings[blocking=true].type"] = { type: "string", required: true, values: FINDING_TYPES };
74
+ fields["findings[blocking=true].claim_kind"] = { type: "string", required: true, values: CLAIM_KINDS };
75
+ fields["findings[blocking=true].suggested_action"] = { type: "string", required: true, values: SUGGESTED_ACTIONS };
76
+ fields["findings[blocking=true].source_refs"] = { type: "array", required: true, item: "path:line" };
77
+ fields["findings[blocking=true].approved_refs"] = { type: "array", item: "已批准锚点" };
78
+ for (const field of ["description", "evidence", "impact"]) {
79
+ fields[`findings[blocking=true].${field}`] = { type: "string", required: true };
80
+ }
81
+ conditions.push("verdict=pass 时不得存在 blocking:true 的问题。", "verdict=pass 时 unchecked 中不得包含绑定文件;范围外观察写 unchecked 或 risks 都不会导致拒收。", "checked_paths 与 unchecked[].path 合起来必须覆盖全部绑定文件。", "verdict=fail 时必须给出至少一个字段完整的 blocking 问题,字段缺失会被判为不可处理报告。", "claim_kind=missing_approved 且 suggested_action=apply 时,approved_refs 必须含可解析的 TEST 或 spec Requirement;缺口属于计划或验收本身时,改用 type=mixed 且 suggested_action=propose。", "提交时引擎会比对冻结范围与当前代码状态:绑定文件已变化会被判为过期报告,需等 next 重建工作项,不要补写指纹。");
82
+ }
83
+ else if (hasScope) {
84
+ fields.review_scope = { type: "object", required: true };
85
+ fields["review_scope.checked_paths"] = { type: "array", required: true, item: "path" };
86
+ conditions.push("checked_paths 必须覆盖全部绑定文件。");
87
+ }
88
+ if (job.role !== "code-reviewer" && job.role !== "verifier") {
89
+ conditions.push("verdict=fail 时 findings 至少包含一个问题。");
90
+ }
91
+ return {
92
+ role: job.role,
93
+ required_fields: isCodeReviewer
94
+ ? [...REVIEW_REPORT_REQUIRED_FIELDS, "reviewer", "review_scope"]
95
+ : [
96
+ ...REVIEW_REPORT_REQUIRED_FIELDS,
97
+ ...(requiresReviewer(job.role) ? ["reviewer"] : []),
98
+ ...(hasScope ? ["review_scope"] : []),
99
+ ],
100
+ optional_fields: [...REVIEW_REPORT_OPTIONAL_FIELDS],
101
+ fields,
102
+ conditions,
103
+ };
104
+ }
105
+ /**
106
+ * 可直接填写的报告骨架。
107
+ *
108
+ * 只预填工作项常量(job_id / packet_digest)与空数组:code-reviewer 的 checked_paths
109
+ * 必须由审查者按实际浏览填写,预填会架空覆盖回执的意义。
110
+ * 普通 reviewer / verifier 的 checked_paths 仍按既有协议预填全部绑定文件。
111
+ */
112
+ export function reportSkeletonForJob(job) {
113
+ const skeleton = {
114
+ role: job.role,
115
+ verdict: "pass",
116
+ findings: [],
117
+ };
118
+ if (requiresReviewer(job.role)) {
119
+ skeleton.reviewer = { kind: "subagent", id: "<agent-id>" };
120
+ }
121
+ if (job.role === "code-reviewer") {
122
+ skeleton.review_scope = {
123
+ job_id: job.job_id,
124
+ packet_digest: job.packet_digest,
125
+ checked_paths: [],
126
+ checked_docs: [],
127
+ unchecked: [],
128
+ };
129
+ }
130
+ else if (requiresReviewScope(job)) {
131
+ skeleton.review_scope = { checked_paths: job.boundFiles.map(file => file.path) };
132
+ }
133
+ return skeleton;
134
+ }
135
+ /** 骨架中必须由审查者替换的空值,供 packet 与 CLI 给出同一份填写提示。 */
136
+ export function reportSkeletonFillItems(job) {
137
+ const items = ["verdict", "findings"];
138
+ if (requiresReviewer(job.role))
139
+ items.push("reviewer.id");
140
+ if (job.role === "code-reviewer") {
141
+ items.push("review_scope.checked_paths", "review_scope.checked_docs", "review_scope.unchecked");
142
+ }
143
+ return items;
144
+ }
145
+ /** 报告里声明"未检查"且不属于绑定文件的条目(pass 的置信边界,供事件流与门禁透出)。 */
146
+ export function outOfScopeUncheckedFromReport(report, boundFiles) {
147
+ const scope = report?.review_scope;
148
+ if (!scope || !Array.isArray(scope.unchecked))
149
+ return [];
150
+ const bound = new Set(boundFiles.map(file => file.path));
151
+ const items = [];
152
+ for (const raw of scope.unchecked) {
153
+ const item = raw;
154
+ if (typeof item?.path !== "string" || typeof item?.reason !== "string")
155
+ continue;
156
+ if (bound.has(item.path))
157
+ continue;
158
+ items.push({ path: item.path, reason: item.reason });
159
+ }
160
+ return items;
161
+ }
@@ -6,7 +6,7 @@ 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, codeReviewFindingNeedsUserDecision, isReviewFixCapReached, } from "./code_review.js";
9
+ import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, outOfScopeUncheckedForJob, 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
12
  import { isCodeReviewClaimKind, reviewFixReason } from "./approved_ref.js";
@@ -552,6 +552,10 @@ function evaluateApplyDoneCodeReviewGate(input) {
552
552
  job_id: latest.job.job_id,
553
553
  packet_digest: latest.job.packet_digest,
554
554
  current_head: acceptedScope.current_head,
555
+ // pass 放行了范围外未检查项;把置信边界写进门禁事实,供最终验证与使用方读取。
556
+ ...(outOfScopeUncheckedForJob(input.events, latest.job.job_id).length > 0
557
+ ? { out_of_scope_unchecked: outOfScopeUncheckedForJob(input.events, latest.job.job_id) }
558
+ : {}),
555
559
  },
556
560
  },
557
561
  };
@@ -1071,6 +1075,46 @@ function canReopenToPropose(from) {
1071
1075
  return from === "propose_ready" || from === "apply" || from === "apply_done" ||
1072
1076
  from === "review" || from === "accepted";
1073
1077
  }
1078
+ /**
1079
+ * 普通 reopen --to propose 成功后附带一次非阻断提示:本次回退本可用 --self-test-fix 完成。
1080
+ * 刻意不按 reason 关键词分类(主会话自由文本不可靠),也刻意不在 apply 状态提示
1081
+ * (apply 内直接修任务即可,无需 reopen)。提示门禁与 self-test-fix 分支保持同一套:
1082
+ * pending 为空、无活跃尝试、contract 模式需要完成事件、无未处理的代码审查问题;
1083
+ * 条件不满足时不提示,避免建议一条必然被 skip 的命令。提示不改变任何转换结果。
1084
+ */
1085
+ function selfTestFixAdvisory(change, changeRoot, snapshot, events, opts) {
1086
+ if (opts.reviewFix || opts.reviewFinding || opts.selfTestFix)
1087
+ return null;
1088
+ if (!["apply_done", "review", "accepted"].includes(snapshot.state))
1089
+ return null;
1090
+ if (applyPlanningMaterialsChanged(changeRoot, events))
1091
+ return null;
1092
+ const status = pendingTaskStatusForApply(changeRoot, events);
1093
+ if (status.pending.length > 0)
1094
+ return null;
1095
+ if (snapshot.active_task_attempts.some(attempt => attempt.state === "active"))
1096
+ return null;
1097
+ const tasksContent = readFileSync(join(changeRoot, "tasks.md"), "utf8");
1098
+ let parentTaskId = null;
1099
+ if (status.mode === "contract") {
1100
+ parentTaskId = status.completedByEvent[0]
1101
+ ?? parseTasksMd(tasksContent).find(task => hasHistoricalTaskCompletion(events, task.taskId))?.taskId
1102
+ ?? null;
1103
+ }
1104
+ else {
1105
+ parentTaskId = parseTasksMd(tasksContent).find(task => task.done)?.taskId ?? null;
1106
+ }
1107
+ if (!parentTaskId)
1108
+ return null;
1109
+ const failedReview = latestCodeReviewFailedStatus(events);
1110
+ if (failedReview?.unresolved.length)
1111
+ return null;
1112
+ return [
1113
+ `提示(猜测,可能不适用):本次 reopen 已回退到 propose。若本意只是不改变已批准行为和方案,这一步本可避免——`,
1114
+ `在 apply_done/review/accepted 状态应执行 superspec transition reopen --change "${change}" --to apply --self-test-fix "${parentTaskId}" --reason "<本次原因>"。`,
1115
+ `当前已在 propose,请继续计划更新流程。`,
1116
+ ].join("");
1117
+ }
1074
1118
  export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
1075
1119
  return commitTransition(projectRoot, change, changeRoot, {
1076
1120
  name: "reopen", idempotencyInputs: {
@@ -1125,6 +1169,8 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
1125
1169
  if (opts.reviewFix) {
1126
1170
  if (to !== "apply")
1127
1171
  return { skip: true, message: "--review-fix 只能用于回到实现阶段(reopen --to apply)" };
1172
+ // 无需 invalidateOpenJobs:--review-fix 的引用来自已终结的代码审查问题,此时 open_jobs
1173
+ // 通常为空;即使残留过期的工作项,也会被 apply_done 侧的新鲜度过滤与重建消化,不构成死锁。
1128
1174
  if (snapshot.state !== "apply_done")
1129
1175
  return { skip: true, message: `当前状态 ${snapshot.state},不能通过代码审查修复回到实现阶段` };
1130
1176
  if (applyPlanningMaterialsChanged(changeRoot, events)) {
@@ -1243,6 +1289,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
1243
1289
  jobs: snapshot.open_jobs,
1244
1290
  };
1245
1291
  }
1292
+ const selfTestFixAdvisoryText = selfTestFixAdvisory(change, changeRoot, snapshot, events, opts);
1246
1293
  const currentBaseline = proposalDocsBaseline(changeRoot);
1247
1294
  const acceptedBaseline = snapshot.state === "accepted" ? latestAcceptedProposalBaseline(events) : null;
1248
1295
  // 旧版 accepted 事件的基线可能缺少后来纳入 Propose gate 的材料。保留其已冻结
@@ -1271,6 +1318,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
1271
1318
  planning_validation_version: 2,
1272
1319
  planning_validation_profile: planningValidationProfileForNewRound(projectRoot),
1273
1320
  },
1321
+ ...(selfTestFixAdvisoryText ? { details: { advisory: selfTestFixAdvisoryText } } : {}),
1274
1322
  extraEvents: planningReopenExtraEvents(snapshot, "propose", reason.trim()),
1275
1323
  };
1276
1324
  }
@@ -1290,6 +1338,9 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
1290
1338
  toState: "apply",
1291
1339
  outcome: "advanced",
1292
1340
  reason: `${reason.trim()}(pending tasks: ${pending.join(", ")})`,
1341
+ // 回到 Apply 意味着代码会被改动:仍在等待报告的审查工作项依据已失效,
1342
+ // 在此显式作废,避免审查者继续投入后才发现报告过期。
1343
+ extraEvents: invalidateOpenJobs(snapshot, "apply", reason.trim()),
1293
1344
  };
1294
1345
  },
1295
1346
  });
package/dist/types.d.ts CHANGED
@@ -102,6 +102,11 @@ export interface CodeReviewGateEvidence {
102
102
  job_id: string | null;
103
103
  packet_digest: string | null;
104
104
  reason?: "no_code_changes";
105
+ /** pass 结论下的范围外未检查项:门禁事实的一部分,供最终验证与使用方判断置信边界。 */
106
+ out_of_scope_unchecked?: {
107
+ path: string;
108
+ reason: string;
109
+ }[];
105
110
  event_id: string;
106
111
  event_digest: string;
107
112
  }
@@ -166,6 +171,8 @@ export interface JobPacket {
166
171
  report_file_path?: string;
167
172
  output_contract_fields?: string[];
168
173
  output_contract_optional_fields?: string[];
174
+ /** 按本工作项预填的报告骨架;完整契约见 `superspec jobs contract`。 */
175
+ report_skeleton?: Record<string, unknown>;
169
176
  字段说明?: Record<string, string>;
170
177
  output_instructions?: string;
171
178
  stop_conditions: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peterxiaoyang/superspec",
3
- "version": "0.1.58",
3
+ "version": "0.1.60",
4
4
  "description": "SuperSpec 流程引擎 — transition engine with lightweight fact-sync",
5
5
  "type": "module",
6
6
  "engines": {
@@ -10,5 +10,5 @@ Task binding: load `.codex/prompts/architect.md` first, then read the current jo
10
10
 
11
11
  Boundary: read-only. Do not edit files or judge materials you have not opened. Report missing context upward instead of guessing.
12
12
 
13
- Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. Otherwise put the conclusion first, cite file:line evidence, and write `无阻塞问题` when no blocking issue is found.
13
+ Output: concise Simplified Chinese. For JSON reports, follow the packet's `report_skeleton` (full contract via `superspec jobs contract`) exactly. Otherwise put the conclusion first, cite file:line evidence, and write `无阻塞问题` when no blocking issue is found.
14
14
  """
@@ -10,5 +10,5 @@ Task binding: load `.codex/prompts/code-reviewer.md` first, then read the curren
10
10
 
11
11
  Boundary: read-only. Do not implement fixes, write evidence, mark tasks complete, decide GREEN, reopen, accept, or replace main-thread workflow decisions. Start from packet-provided materials and report missing context upward instead of guessing.
12
12
 
13
- Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. Blocking issues must be traceable and actionable. Write `无阻塞问题` when no blocking issue is found.
13
+ Output: concise Simplified Chinese. For JSON reports, follow the packet's `report_skeleton` (full contract via `superspec jobs contract`) exactly. Blocking issues must be traceable and actionable. Write `无阻塞问题` when no blocking issue is found.
14
14
  """
@@ -10,5 +10,5 @@ Task binding: load `.codex/prompts/critic.md` first, then read the current job p
10
10
 
11
11
  Boundary: read-only by default. Do not edit files, invent issues, or widen scope silently. Report missing source refs or claim gaps upward.
12
12
 
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.
13
+ Output: concise Simplified Chinese. For JSON reports, follow the packet's `report_skeleton` (full contract via `superspec jobs contract`) exactly. Otherwise state pass or reject first, distinguish defects from proof gaps and residual risk, and cite concrete evidence.
14
14
  """
@@ -10,5 +10,5 @@ Task binding: load `.codex/prompts/test-engineer.md` first, then read the curren
10
10
 
11
11
  Boundary: review jobs are read-only. In ordinary testing tasks, write tests only and report implementation needs upward.
12
12
 
13
- Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. Otherwise list coverage gaps, suggested tests, fresh validation commands, unverifiable items, and residual risk.
13
+ Output: concise Simplified Chinese. For JSON reports, follow the packet's `report_skeleton` (full contract via `superspec jobs contract`) exactly. Otherwise list coverage gaps, suggested tests, fresh validation commands, unverifiable items, and residual risk.
14
14
  """
@@ -8,7 +8,7 @@ Role: Test Runner. Execute exactly one SuperSpec apply test phase from the curre
8
8
 
9
9
  Task binding: load `.codex/prompts/test-runner.md` first, then read the current task instructions. Their task id, test id, phase, allowed command, expected semantic status, guard fingerprint, report policy, and stop conditions override static prompt memory.
10
10
 
11
- Boundary: read-only by default. Do not edit production code, OpenSpec artifacts, `.superspec/**`, task checkboxes, evidence, review reports, or archives. Run only the allowed command from current task instructions and report blockers for missing command, unsafe side effects, or incomplete raw transcript refs.
11
+ Boundary: read-only by default. Do not edit production code, OpenSpec artifacts, `.superspec/**`, task checkboxes, evidence, review reports, or archives. Run only the allowed command from current task instructions and report blockers for missing command, unsafe side effects, or incomplete raw transcript refs. One run may back several declared TEST ids: register evidence per TEST id, and do not re-run the same command for a second registration.
12
12
 
13
13
  Output: concise Simplified Chinese test report with command, cwd, phase, task/test id, exit status, semantic status candidate, result summary, raw transcript ref, repo head, dirty-state summary, guard fingerprint, and unverified items.
14
14
  """
@@ -10,5 +10,5 @@ Task binding: load `.codex/prompts/verifier.md` first, then read the current job
10
10
 
11
11
  Boundary: read-only. Check commands, test output, artifacts, evidence refs, acceptance criteria, code-reviewer closure, and whether the verifier job still matches the packet-provided evidence version. Use diffs only as evidence references when the packet requires them. Do not edit files, write evidence, mark tasks complete, or add an extra code-diff blocker outside the packet contract.
12
12
 
13
- Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. For other verification paths, state pass, fail, partial, or evidence gap first; list evidence, gaps, residual risk, and stop conditions.
13
+ Output: concise Simplified Chinese. For JSON reports, follow the packet's `report_skeleton` (full contract via `superspec jobs contract`) exactly. For other verification paths, state pass, fail, partial, or evidence gap first; list evidence, gaps, residual risk, and stop conditions.
14
14
  """
@@ -10,4 +10,4 @@ Task binding: read the current SuperSpec job packet and task instructions first.
10
10
 
11
11
  Boundary: read-only. Do not edit files or judge materials you have not opened. Report missing context upward instead of guessing.
12
12
 
13
- Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. Otherwise put the conclusion first, cite file:line evidence, and write `无阻塞问题` when no blocking issue is found.
13
+ Output: concise Simplified Chinese. For JSON reports, follow the packet's `report_skeleton` (full contract via `superspec jobs contract`) exactly. Otherwise put the conclusion first, cite file:line evidence, and write `无阻塞问题` when no blocking issue is found.
@@ -6,8 +6,8 @@ tools: read, grep, glob, bash
6
6
 
7
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
- 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.
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. Report structure comes from the packet's `report_skeleton` (full contract via `superspec jobs contract`); never invent structure or reuse a previous round's report.
10
10
 
11
11
  Boundary: read-only. Do not implement fixes, write evidence, mark tasks complete, decide GREEN, reopen, accept, or replace main-thread workflow decisions. Start from packet-provided materials and report missing context upward instead of guessing.
12
12
 
13
- Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. Blocking issues must be traceable and actionable. Write `无阻塞问题` when no blocking issue is found.
13
+ Output: concise Simplified Chinese. For JSON reports, follow the packet's `report_skeleton` (full contract via `superspec jobs contract`) exactly. Blocking issues must be traceable and actionable. Write `无阻塞问题` when no blocking issue is found.
@@ -10,4 +10,4 @@ Task binding: read the current SuperSpec job packet and task instructions first.
10
10
 
11
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
- 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.
13
+ Output: concise Simplified Chinese. For JSON reports, follow the packet's `report_skeleton` (full contract via `superspec jobs contract`) exactly. Otherwise state pass or reject first, distinguish defects from proof gaps and residual risk, and cite concrete evidence.
@@ -10,4 +10,4 @@ Task binding: read the current SuperSpec job packet and task instructions first.
10
10
 
11
11
  Boundary: review jobs are read-only. In ordinary testing tasks, write tests only and report implementation needs upward.
12
12
 
13
- Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. Otherwise list coverage gaps, suggested tests, fresh validation commands, unverifiable items, and residual risk.
13
+ Output: concise Simplified Chinese. For JSON reports, follow the packet's `report_skeleton` (full contract via `superspec jobs contract`) exactly. Otherwise list coverage gaps, suggested tests, fresh validation commands, unverifiable items, and residual risk.
@@ -8,6 +8,6 @@ Role: Test Runner. Execute exactly one SuperSpec apply test phase from the curre
8
8
 
9
9
  Task binding: read the current SuperSpec task instructions first. Their task id, test id, phase, allowed command, expected semantic status, guard fingerprint, report policy, and stop conditions override this prompt.
10
10
 
11
- Boundary: read-only by default. Do not edit production code, OpenSpec artifacts, `.superspec/**`, task checkboxes, evidence, review reports, or archives. Run only the allowed command from current task instructions and report blockers for missing command, unsafe side effects, or incomplete raw transcript refs.
11
+ Boundary: read-only by default. Do not edit production code, OpenSpec artifacts, `.superspec/**`, task checkboxes, evidence, review reports, or archives. Run only the allowed command from current task instructions and report blockers for missing command, unsafe side effects, or incomplete raw transcript refs. One run may back several declared TEST ids: register evidence per TEST id, and do not re-run the same command for a second registration.
12
12
 
13
13
  Output: concise Simplified Chinese test report with command, cwd, phase, task/test id, exit status, semantic status candidate, result summary, raw transcript ref, repo head, dirty-state summary, guard fingerprint, and unverified items.
@@ -10,4 +10,4 @@ Task binding: read the current SuperSpec job packet and task instructions first.
10
10
 
11
11
  Boundary: read-only. Check commands, test output, artifacts, evidence refs, acceptance criteria, code-reviewer closure, and whether the verifier job still matches the packet-provided evidence version. Use diffs only as evidence references when the packet requires them. Do not edit files, write evidence, mark tasks complete, or add an extra code-diff blocker outside the packet contract.
12
12
 
13
- Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. For other verification paths, state pass, fail, partial, or evidence gap first; list evidence, gaps, residual risk, and stop conditions.
13
+ Output: concise Simplified Chinese. For JSON reports, follow the packet's `report_skeleton` (full contract via `superspec jobs contract`) exactly. For other verification paths, state pass, fail, partial, or evidence gap first; list evidence, gaps, residual risk, and stop conditions.