@peterxiaoyang/superspec 0.1.37 → 0.1.39
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 +45 -6
- package/dist/code_review.d.ts +18 -2
- package/dist/code_review.js +473 -81
- package/dist/format.d.ts +46 -4
- package/dist/format.js +311 -26
- package/dist/git_state.d.ts +36 -0
- package/dist/git_state.js +174 -0
- package/dist/job_validity.d.ts +16 -0
- package/dist/job_validity.js +37 -0
- package/dist/next.js +45 -355
- package/dist/phase_plan.d.ts +97 -0
- package/dist/phase_plan.js +582 -0
- package/dist/record.d.ts +2 -2
- package/dist/record.js +67 -31
- package/dist/review.d.ts +3 -2
- package/dist/review.js +66 -33
- package/dist/review_job_gates.d.ts +20 -0
- package/dist/review_job_gates.js +85 -0
- package/dist/store.d.ts +10 -0
- package/dist/store.js +53 -3
- package/dist/sync.js +7 -9
- package/dist/task.js +87 -9
- package/dist/task_evidence.d.ts +10 -0
- package/dist/task_evidence.js +126 -0
- package/dist/transition.d.ts +1 -1
- package/dist/transition.js +449 -337
- package/dist/types.d.ts +81 -1
- package/dist/workflow_profile.d.ts +11 -0
- package/dist/workflow_profile.js +39 -0
- package/package.json +1 -1
- package/templates/workflow/prompts/architect.md +17 -27
- package/templates/workflow/prompts/code-reviewer.md +13 -2
- package/templates/workflow/prompts/critic.md +62 -61
- package/templates/workflow/prompts/executor.md +1 -1
- package/templates/workflow/prompts/explore.md +38 -26
- package/templates/workflow/prompts/test-engineer.md +18 -32
- package/templates/workflow/prompts/verifier.md +5 -3
- package/templates/workflow/skills/superspec-apply/SKILL.md +34 -11
- package/templates/workflow/skills/superspec-explore/SKILL.md +65 -64
- package/templates/workflow/skills/superspec-propose/SKILL.md +64 -43
- package/templates/workflow/skills/superspec-review/SKILL.md +1 -1
package/dist/record.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, sha256Text, withLock, appendRawRecord, } from "./store.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_DECISION_SCOPE_PREFIX, codeReviewDecisionAnswerLabel, normalizeCodeReviewDecisionAnswer, } from "./code_review.js";
|
|
6
|
+
import { invalidReasonForSubmittedReport } from "./job_validity.js";
|
|
7
7
|
import { jobSubmitArgv } from "./job_action.js";
|
|
8
|
+
import { REVIEW_DOC_PATHS } from "./review.js";
|
|
8
9
|
const REVIEW_REPORT_REQUIRED_FIELDS = ["role", "verdict", "findings"];
|
|
9
10
|
const REVIEW_REPORT_OPTIONAL_FIELDS = ["summary", "evidence_refs", "risks", "open_questions"];
|
|
10
11
|
const REVIEWER_KINDS = new Set(["codex-subagent", "human", "external-agent"]);
|
|
@@ -187,6 +188,7 @@ function codeReviewRejectEventPayload(input) {
|
|
|
187
188
|
report_digest: input.reportDigest,
|
|
188
189
|
result_kind: input.resultKind,
|
|
189
190
|
reason: input.reason,
|
|
191
|
+
...(input.reportPath ? { report_path: input.reportPath } : {}),
|
|
190
192
|
...(input.rawRef ?? {}),
|
|
191
193
|
};
|
|
192
194
|
if (input.resultKind === "review_failed" && input.parsedReport) {
|
|
@@ -220,22 +222,6 @@ function projectRelativePath(projectRoot, path) {
|
|
|
220
222
|
return null;
|
|
221
223
|
return rel;
|
|
222
224
|
}
|
|
223
|
-
function staleBoundFilesChecks(job, projectRoot, changeRoot, ignoredCodePaths = []) {
|
|
224
|
-
const checks = [];
|
|
225
|
-
if (job.role === "code-reviewer") {
|
|
226
|
-
const ignored = new Set(ignoredCodePaths);
|
|
227
|
-
const currentPaths = scanCodeChanges(projectRoot).paths.filter(path => !ignored.has(path));
|
|
228
|
-
const reason = codeReviewJobStaleReason(projectRoot, job, currentPaths);
|
|
229
|
-
return reason ? [reason] : [];
|
|
230
|
-
}
|
|
231
|
-
for (const bf of job.boundFiles) {
|
|
232
|
-
const currentSha = sha256File(join(changeRoot, bf.path)) ?? "sha256:missing";
|
|
233
|
-
if (currentSha !== bf.sha) {
|
|
234
|
-
checks.push(`绑定文件 ${bf.path} 已变化(${bf.sha} → ${currentSha})`);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return checks;
|
|
238
|
-
}
|
|
239
225
|
/** 从 events 中查找 job(H4 修复:job 只在 transition_commit 的 new_jobs payload 里) */
|
|
240
226
|
function findJob(events, jobId) {
|
|
241
227
|
for (const ev of events) {
|
|
@@ -312,12 +298,14 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
312
298
|
checks.push("报告必须是有效 JSON");
|
|
313
299
|
}
|
|
314
300
|
const reportPath = reportFile ? projectRelativePath(projectRoot, reportFile) : null;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
301
|
+
const invalidReason = invalidReasonForSubmittedReport(job, {
|
|
302
|
+
projectRoot,
|
|
303
|
+
changeRoot,
|
|
304
|
+
events,
|
|
305
|
+
reportPath,
|
|
306
|
+
});
|
|
307
|
+
if (invalidReason) {
|
|
308
|
+
checks.push(invalidReason);
|
|
321
309
|
}
|
|
322
310
|
if (!reportContent.trim()) {
|
|
323
311
|
checks.push("报告内容为空");
|
|
@@ -337,11 +325,13 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
337
325
|
reason: checks.join("; "),
|
|
338
326
|
parsedReport,
|
|
339
327
|
rawRef,
|
|
328
|
+
reportPath,
|
|
340
329
|
})
|
|
341
330
|
: {
|
|
342
331
|
job_id: jobId,
|
|
343
332
|
role: job.role,
|
|
344
333
|
report_digest: reportDigest,
|
|
334
|
+
...(reportPath ? { report_path: reportPath } : {}),
|
|
345
335
|
reason: checks.join("; "),
|
|
346
336
|
}),
|
|
347
337
|
});
|
|
@@ -362,6 +352,8 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
362
352
|
parsedReport,
|
|
363
353
|
rawRef,
|
|
364
354
|
}));
|
|
355
|
+
if (reportPath)
|
|
356
|
+
rejectEvent.payload.report_path = reportPath;
|
|
365
357
|
appendEvent(projectRoot, change, rejectEvent);
|
|
366
358
|
return {
|
|
367
359
|
event_type: "job_rejected",
|
|
@@ -389,6 +381,7 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
389
381
|
reason,
|
|
390
382
|
parsedReport,
|
|
391
383
|
rawRef,
|
|
384
|
+
reportPath,
|
|
392
385
|
})));
|
|
393
386
|
return {
|
|
394
387
|
event_type: "job_rejected",
|
|
@@ -415,6 +408,7 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
415
408
|
reason,
|
|
416
409
|
parsedReport,
|
|
417
410
|
rawRef,
|
|
411
|
+
reportPath,
|
|
418
412
|
})));
|
|
419
413
|
return {
|
|
420
414
|
event_type: "job_rejected",
|
|
@@ -430,6 +424,7 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
430
424
|
role: job.role,
|
|
431
425
|
report_digest: reportDigest,
|
|
432
426
|
accepted_at: new Date().toISOString(),
|
|
427
|
+
...(reportPath ? { report_path: reportPath } : {}),
|
|
433
428
|
...rawRef,
|
|
434
429
|
});
|
|
435
430
|
appendEvent(projectRoot, change, acceptEvent);
|
|
@@ -508,7 +503,7 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
508
503
|
reason: "missing_scope_or_answer",
|
|
509
504
|
input_digest: inputDigest,
|
|
510
505
|
}));
|
|
511
|
-
return { event_type: "user_decision_recorded", accepted: false, message: "
|
|
506
|
+
return { event_type: "user_decision_recorded", accepted: false, message: "决策文件缺少决策范围(scope)或答复内容(answer)" };
|
|
512
507
|
}
|
|
513
508
|
if (decision.scope.startsWith(CODE_REVIEW_DECISION_SCOPE_PREFIX)) {
|
|
514
509
|
const normalizedAnswer = normalizeCodeReviewDecisionAnswer(decision.answer);
|
|
@@ -561,7 +556,7 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
561
556
|
return {
|
|
562
557
|
event_type: "user_decision_recorded",
|
|
563
558
|
accepted: true,
|
|
564
|
-
message:
|
|
559
|
+
message: `用户决策已登记:决策范围(scope)=${decision.scope}`,
|
|
565
560
|
};
|
|
566
561
|
}
|
|
567
562
|
/** record user-decision:登记用户决策 */
|
|
@@ -620,6 +615,31 @@ export function jobsList(projectRoot, change) {
|
|
|
620
615
|
}
|
|
621
616
|
return { open, accepted, rejected };
|
|
622
617
|
}
|
|
618
|
+
function packetFieldDescriptions() {
|
|
619
|
+
return {
|
|
620
|
+
job_id: "工作项 ID,用于提交本次审查或验证报告。",
|
|
621
|
+
packet_digest: "工作项说明摘要,用于证明报告对应的是当前这份工作项说明。",
|
|
622
|
+
boundFiles: "本工作项绑定的文件清单;代码审查报告必须说明这些文件是否都看过。",
|
|
623
|
+
review_scope: "报告中的审查覆盖范围,说明看了哪些文件、哪些没看及原因。",
|
|
624
|
+
code_review_scope: "代码审查范围:从已审基点到当前 HEAD 的提交改动、工作区改动和未跟踪代码文件。",
|
|
625
|
+
task_execution_index: "按任务汇总的执行证据:每个任务(task)的执行依据、声明测试、测试证据和改动文件。",
|
|
626
|
+
contract: "任务启动时的执行依据快照:tests/design/source/reason/guard 分别对应 测试/设计/来源/原因/边界;null 表示历史任务没有执行依据。",
|
|
627
|
+
changed_paths: "与某个任务(task)或代码状态检查相关的改动文件。",
|
|
628
|
+
changed_paths_partial_reason: "该任务(task)的提交段 diff 失败原因;存在时 changed_paths 只包含工作区对比结果,归属可能不完整。",
|
|
629
|
+
unattributed_paths: "代码审查范围中暂时无法归属到某个任务(task)的文件。",
|
|
630
|
+
unknown_attribution_tasks: "因为缺少边界快照或提交段 diff 失败而无法完整计算改动归属的任务(task)。",
|
|
631
|
+
coverage_exemption_refs: "测试覆盖豁免引用:说明某个 TEST 为什么没有绑定到任务(task)。",
|
|
632
|
+
code_state_check: "代码状态检查:最终验证时用于判断代码审查后代码是否又发生变化。",
|
|
633
|
+
event_id: "事件 ID,用于追溯证据来源。",
|
|
634
|
+
event_digest: "事件摘要,用于确认引用的证据事件没有被替换。",
|
|
635
|
+
attempt_id: "任务尝试 ID;执行依据模式下测试运行必须绑定当前活跃任务尝试。",
|
|
636
|
+
task_structure_digest: "历史模式的任务结构指纹;仅用于兼容旧证据。",
|
|
637
|
+
test_id: "测试契约里的 TEST ID。",
|
|
638
|
+
semantic_status: "测试语义状态:RED 预期失败、GREEN 预期成功或特征化(characterization)通过。",
|
|
639
|
+
covers_task_ids: "回归测试覆盖了哪些已完成任务(task)。",
|
|
640
|
+
scope_note: "范围扩大说明;任务(task)实现超出执行依据边界时填写。",
|
|
641
|
+
};
|
|
642
|
+
}
|
|
623
643
|
/** jobs packet:返回工作项执行说明 */
|
|
624
644
|
export function jobsPacket(projectRoot, change, jobId) {
|
|
625
645
|
const events = readEvents(projectRoot, change);
|
|
@@ -628,15 +648,24 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
628
648
|
return { found: false, message: `工作项 ${jobId} 不存在` };
|
|
629
649
|
}
|
|
630
650
|
const isCodeReviewer = job.role === "code-reviewer";
|
|
651
|
+
const packetContext = job.packet_context;
|
|
631
652
|
return {
|
|
632
653
|
found: true,
|
|
633
654
|
packet: {
|
|
634
655
|
job_id: job.job_id,
|
|
635
656
|
role: job.role,
|
|
657
|
+
...(job.gate_id ? { gate_id: job.gate_id } : {}),
|
|
636
658
|
recommended_agent: recommendedAgentForRole(job.role),
|
|
637
659
|
boundFiles: job.boundFiles,
|
|
638
660
|
...(job.review_evidence_digest ? { review_evidence_digest: job.review_evidence_digest } : {}),
|
|
639
661
|
...(job.previous_rejection ? { previous_rejection: job.previous_rejection } : {}),
|
|
662
|
+
...(packetContext ? { packet_context: packetContext } : {}),
|
|
663
|
+
...(packetContext?.code_review_scope ? { code_review_scope: packetContext.code_review_scope } : {}),
|
|
664
|
+
...(packetContext?.coverage_exemption_refs ? { coverage_exemption_refs: packetContext.coverage_exemption_refs } : {}),
|
|
665
|
+
...(packetContext?.task_execution_index ? { task_execution_index: packetContext.task_execution_index } : {}),
|
|
666
|
+
...(packetContext?.unattributed_paths ? { unattributed_paths: packetContext.unattributed_paths } : {}),
|
|
667
|
+
...(packetContext?.unknown_attribution_tasks ? { unknown_attribution_tasks: packetContext.unknown_attribution_tasks } : {}),
|
|
668
|
+
...(packetContext?.code_state_check ? { code_state_check: packetContext.code_state_check } : {}),
|
|
640
669
|
packet_digest: job.packet_digest,
|
|
641
670
|
required_output_kind: "job_report_json",
|
|
642
671
|
preferred_input_mode: "stdin",
|
|
@@ -647,16 +676,23 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
647
676
|
? [...REVIEW_REPORT_REQUIRED_FIELDS, "reviewer", "review_scope"]
|
|
648
677
|
: requiresReviewer(job.role) ? [...REVIEW_REPORT_REQUIRED_FIELDS, "reviewer"] : [...REVIEW_REPORT_REQUIRED_FIELDS],
|
|
649
678
|
output_contract_optional_fields: [...REVIEW_REPORT_OPTIONAL_FIELDS],
|
|
679
|
+
字段说明: packetFieldDescriptions(),
|
|
650
680
|
output_instructions: `${roleDescription(job.role)}。请审查 ${job.boundFiles.map(f => f.path).join(", ")},` +
|
|
651
681
|
(job.review_evidence_digest ? `本工作项对应的执行证据版本为 ${job.review_evidence_digest},` : "") +
|
|
652
682
|
(job.previous_rejection ? `上一次代码审查没有形成可推进结论,原因:${job.previous_rejection.reason}。本次请根据该原因重新审查,` : "") +
|
|
653
|
-
(requiresReviewer(job.role) ? `必须由独立 ${recommendedAgentForRole(job.role)}
|
|
654
|
-
`产出 JSON 报告内容并优先通过 --report - 从 stdin
|
|
683
|
+
(requiresReviewer(job.role) ? `必须由独立 ${recommendedAgentForRole(job.role)} 审查角色执行,并在审查者来源字段(reviewer.kind/id)中记录来源,` : "") +
|
|
684
|
+
`产出 JSON 报告内容并优先通过 --report - 从 stdin 登记;文件路径模式仅作备用。协议字段含义见 packet 顶层“字段说明”,普通对话不要原样复述 JSON。` +
|
|
655
685
|
(isCodeReviewer
|
|
656
|
-
? `最小格式:{"role":"code-reviewer","verdict":"pass|fail","review_scope":{"job_id":"${job.job_id}","packet_digest":"${job.packet_digest}","checked_paths":${JSON.stringify(job.boundFiles.map(f => f.path))},"checked_docs"
|
|
657
|
-
+ `报告结论为 fail
|
|
686
|
+
? `最小格式:{"role":"code-reviewer","verdict":"pass|fail","review_scope":{"job_id":"${job.job_id}","packet_digest":"${job.packet_digest}","checked_paths":${JSON.stringify(job.boundFiles.map(f => f.path))},"checked_docs":${JSON.stringify(REVIEW_DOC_PATHS)},"unchecked":[]},"findings":[],"reviewer":{"kind":"codex-subagent","id":"<thread-or-agent-id>"}};审查覆盖范围(review_scope)用来说明本次审查覆盖了哪些文件和文档,已检查路径(checked_paths)与未检查项(unchecked)必须合起来覆盖全部绑定文件(boundFiles),unchecked 条目格式为 {"path":"<path>","reason":"<reason>"}。`
|
|
687
|
+
+ `报告结论为 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 表示需要使用者判断的混合问题。`
|
|
688
|
+
+ (packetContext?.task_execution_index
|
|
689
|
+
? `本工作项带任务执行索引(task_execution_index):按 task 对照其执行依据快照(contract)审查——实现路线对照 design 引用原文、累计 diff 对照 guard 边界、测试断言对照 tests 声明的 scenario;每项的 scope_note 是执行者登记的范围扩大说明,判断其合理性与验证充分性;changed_paths 是归属线索不是结论(null 表示未知);unattributed_paths 中的无主改动逐个判断合理性;coverage_exemption_refs 解释未绑定 task 的 TEST 豁免。`
|
|
690
|
+
: "")
|
|
658
691
|
: job.role === "verifier"
|
|
659
|
-
? `最小格式:{"role":"verifier","verdict":"pass|fail","findings":[]}
|
|
692
|
+
? `最小格式:{"role":"verifier","verdict":"pass|fail","findings":[]}。核对代码审查记录(code_review_gate):passed 必须能追溯到已接受的代码审查工作项,skipped 必须能证明本次没有代码类改动。核对代码审查问题闭环:实现修复任务必须带审查修复引用(review_fix_of:<job_id>#<problem_id>),方案/混合问题必须有用户决策或后续修复证据。核对 RED/GREEN:证据须在同一已完成任务的任务尝试 ID(task_completed.attempt_id)下闭环——普通 TDD 任务至少一个同 TEST 先 RED(expected_failure)后 GREEN(expected_success)配对且每个声明 TEST 都有 GREEN;特征化任务(no_tdd_reason:characterization)可用 characterization_pass 作为通过证据,不要求 RED;测试运行证据应包含测试 ID(test_id)、命令(command)、工作目录(cwd)、退出码(exit_code)、语义状态(semantic_status);审查修复的回归测试运行可用回归覆盖任务列表(covers_task_ids)说明覆盖了哪些已完成任务;缺少任务尝试 ID(attempt_id)的旧证据只能弱引用。` +
|
|
693
|
+
(packetContext?.code_state_check
|
|
694
|
+
? `本工作项带代码状态检查(code_state_check):head_matches 为 false 或 changed_paths 非空表示代码审查后代码又发生变化,须在报告中列出差异并交主流程与用户裁决,不自行判定无害,也不据此自动否定已接受的代码审查。`
|
|
695
|
+
: "")
|
|
660
696
|
: requiresReviewer(job.role)
|
|
661
697
|
? `最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[],"reviewer":{"kind":"codex-subagent","id":"<thread-or-agent-id>"}}`
|
|
662
698
|
: `最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[]}`),
|
package/dist/review.d.ts
CHANGED
|
@@ -13,5 +13,6 @@ export declare function reviewBoundFiles(changeRoot: string): Ref[];
|
|
|
13
13
|
export declare function reviewEvidenceDigest(events: Event[]): string;
|
|
14
14
|
export declare function boundFilesStaleReason(job: Job, changeRoot: string): string | null;
|
|
15
15
|
export declare function reviewEvidenceStaleReason(job: Job, currentDigest: string): string | null;
|
|
16
|
-
export declare function
|
|
17
|
-
export declare function
|
|
16
|
+
export declare function codeStateCheckStaleReason(job: Job, projectRoot: string | undefined, events: Event[] | undefined, ignoredCodePaths?: string[]): string | null;
|
|
17
|
+
export declare function reviewVerifierStaleReason(job: Job, changeRoot: string, currentEvidenceDigest: string, projectRoot?: string, events?: Event[], ignoredCodePaths?: string[]): string | null;
|
|
18
|
+
export declare function isFreshReviewVerifier(job: Job, changeRoot: string, currentEvidenceDigest: string, projectRoot?: string, events?: Event[]): boolean;
|
package/dist/review.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// SuperSpec review helpers: policy, verifier freshness, evidence digest
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { sha256File, sha256Text } from "./store.js";
|
|
4
|
+
import { docRef, sha256File, sha256Text } from "./store.js";
|
|
5
|
+
import { REVIEW_FINAL_VERIFIER_GATE } from "./review_job_gates.js";
|
|
6
|
+
import { computeCodeStateCheck, effectiveCoverageExemptionRefsFromEvents } from "./code_review.js";
|
|
5
7
|
export const REVIEW_DOC_PATHS = [
|
|
6
8
|
"proposal.md",
|
|
7
9
|
"tasks.md",
|
|
8
10
|
"design.md",
|
|
11
|
+
"specs/",
|
|
9
12
|
".superspec/artifacts/discovery.md",
|
|
10
13
|
".superspec/artifacts/business-invariants.md",
|
|
11
14
|
".superspec/artifacts/test-contract.md",
|
|
@@ -52,35 +55,24 @@ export function readReviewPolicyFromEvents(events) {
|
|
|
52
55
|
return null;
|
|
53
56
|
}
|
|
54
57
|
export function isReviewReadyVerifier(job) {
|
|
55
|
-
return job.role === "verifier" && job
|
|
58
|
+
return job.role === "verifier" && REVIEW_FINAL_VERIFIER_GATE.isJobForGate(job);
|
|
56
59
|
}
|
|
57
60
|
export function reviewBoundFiles(changeRoot) {
|
|
61
|
+
// 目录路径(以 / 结尾)始终绑定聚合指纹,与 boundFilesStaleReason 的 docRef 比对保持一致
|
|
58
62
|
return REVIEW_DOC_PATHS
|
|
59
|
-
.filter(path => existsSync(join(changeRoot, path)))
|
|
60
|
-
.map(path => (
|
|
63
|
+
.filter(path => path.endsWith("/") || existsSync(join(changeRoot, path)))
|
|
64
|
+
.map(path => docRef(changeRoot, path));
|
|
61
65
|
}
|
|
66
|
+
// 方案排序键:kind → task_id → test_id → attempt_id → event_id;缺失字段按空字符串。
|
|
67
|
+
// event_id 全局唯一,作为末位排序键足以保证稳定。
|
|
62
68
|
function evidenceSortKey(record) {
|
|
63
|
-
|
|
69
|
+
return [
|
|
64
70
|
record.kind,
|
|
65
71
|
record.task_id ?? "",
|
|
66
|
-
record.attempt_id ?? "",
|
|
67
|
-
record.task_structure_digest ?? "",
|
|
68
72
|
record.test_id ?? "",
|
|
69
|
-
record.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
parts.push(`covers:${JSON.stringify(record.covers_task_ids)}`);
|
|
73
|
-
}
|
|
74
|
-
parts.push(record.command ?? "", record.cwd ?? "", record.exit_code == null ? "" : String(record.exit_code), record.target_fingerprint ?? "", record.event_digest);
|
|
75
|
-
return parts.join("\u0000");
|
|
76
|
-
}
|
|
77
|
-
function normalizedCoveredTaskIds(value) {
|
|
78
|
-
if (!Array.isArray(value))
|
|
79
|
-
return [];
|
|
80
|
-
return [...new Set(value
|
|
81
|
-
.filter((item) => typeof item === "string")
|
|
82
|
-
.map(item => item.trim())
|
|
83
|
-
.filter(Boolean))].sort();
|
|
73
|
+
record.attempt_id ?? "",
|
|
74
|
+
record.event_id ?? "",
|
|
75
|
+
].join("\u0000");
|
|
84
76
|
}
|
|
85
77
|
export function reviewEvidenceDigest(events) {
|
|
86
78
|
const attemptsById = new Map();
|
|
@@ -99,6 +91,7 @@ export function reviewEvidenceDigest(events) {
|
|
|
99
91
|
task_id: payload.task_id,
|
|
100
92
|
attempt_id: payload.attempt_id,
|
|
101
93
|
task_structure_digest: attempt?.task_structure_digest ?? null,
|
|
94
|
+
event_id: ev.event_id,
|
|
102
95
|
event_digest: ev.event_digest,
|
|
103
96
|
});
|
|
104
97
|
}
|
|
@@ -107,11 +100,12 @@ export function reviewEvidenceDigest(events) {
|
|
|
107
100
|
const completedStructureDigests = new Set(completed
|
|
108
101
|
.map(item => item.task_structure_digest)
|
|
109
102
|
.filter((digest) => typeof digest === "string" && digest.length > 0));
|
|
103
|
+
// scope_note / boundary_snapshot / checkbox_update 属于 task_completed 事件 payload,已由 event_digest 覆盖
|
|
110
104
|
const records = completed.map(item => ({
|
|
111
105
|
kind: "task_completed",
|
|
112
106
|
task_id: item.task_id,
|
|
113
107
|
attempt_id: item.attempt_id,
|
|
114
|
-
|
|
108
|
+
event_id: item.event_id,
|
|
115
109
|
event_digest: item.event_digest,
|
|
116
110
|
}));
|
|
117
111
|
for (const ev of events) {
|
|
@@ -124,18 +118,41 @@ export function reviewEvidenceDigest(events) {
|
|
|
124
118
|
const matchesLegacyDigest = attemptId == null && structureDigest != null && completedStructureDigests.has(structureDigest);
|
|
125
119
|
if (!matchesCompletedAttempt && !matchesLegacyDigest)
|
|
126
120
|
continue;
|
|
127
|
-
const coversTaskIds = normalizedCoveredTaskIds(payload.covers_task_ids);
|
|
128
121
|
records.push({
|
|
129
122
|
kind: "test_run_recorded",
|
|
130
123
|
test_id: typeof payload.test_id === "string" ? payload.test_id : null,
|
|
131
124
|
attempt_id: attemptId,
|
|
132
|
-
task_structure_digest: structureDigest,
|
|
133
125
|
semantic_status: typeof payload.semantic_status === "string" ? payload.semantic_status : null,
|
|
134
|
-
|
|
126
|
+
exit_code: typeof payload.exit_code === "number" ? payload.exit_code : null,
|
|
135
127
|
command: typeof payload.command === "string" ? payload.command : "",
|
|
136
128
|
cwd: typeof payload.cwd === "string" ? payload.cwd : "",
|
|
137
|
-
|
|
138
|
-
|
|
129
|
+
event_id: ev.event_id,
|
|
130
|
+
event_digest: ev.event_digest,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
for (const ref of effectiveCoverageExemptionRefsFromEvents(events)) {
|
|
134
|
+
records.push({
|
|
135
|
+
kind: "coverage_exemption",
|
|
136
|
+
test_id: ref.test_id,
|
|
137
|
+
event_id: ref.event_id,
|
|
138
|
+
event_digest: ref.event_digest,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
for (const ev of events) {
|
|
142
|
+
if (ev.event_type !== "transition_commit")
|
|
143
|
+
continue;
|
|
144
|
+
const payload = ev.payload;
|
|
145
|
+
if (payload.transition !== "review-ready" || payload.from_state !== "apply_done" || payload.to_state !== "review")
|
|
146
|
+
continue;
|
|
147
|
+
const gate = payload.code_review_gate;
|
|
148
|
+
if (!gate || (gate.decision !== "passed" && gate.decision !== "skipped"))
|
|
149
|
+
continue;
|
|
150
|
+
records.push({
|
|
151
|
+
kind: "code_review_gate",
|
|
152
|
+
decision: gate.decision,
|
|
153
|
+
job_id: typeof gate.job_id === "string" ? gate.job_id : null,
|
|
154
|
+
packet_digest: typeof gate.packet_digest === "string" ? gate.packet_digest : null,
|
|
155
|
+
event_id: ev.event_id,
|
|
139
156
|
event_digest: ev.event_digest,
|
|
140
157
|
});
|
|
141
158
|
}
|
|
@@ -144,7 +161,8 @@ export function reviewEvidenceDigest(events) {
|
|
|
144
161
|
}
|
|
145
162
|
export function boundFilesStaleReason(job, changeRoot) {
|
|
146
163
|
for (const bf of job.boundFiles) {
|
|
147
|
-
|
|
164
|
+
// 目录绑定(path 以 / 结尾)比对聚合指纹,覆盖目录内文件的增/删/改
|
|
165
|
+
const current = docRef(changeRoot, bf.path).sha;
|
|
148
166
|
if (current !== bf.sha) {
|
|
149
167
|
return `绑定文件 ${bf.path} 已变化(${bf.sha} → ${current})`;
|
|
150
168
|
}
|
|
@@ -161,9 +179,24 @@ export function reviewEvidenceStaleReason(job, currentDigest) {
|
|
|
161
179
|
}
|
|
162
180
|
return null;
|
|
163
181
|
}
|
|
164
|
-
export function
|
|
165
|
-
|
|
182
|
+
export function codeStateCheckStaleReason(job, projectRoot, events, ignoredCodePaths = []) {
|
|
183
|
+
if (!isReviewReadyVerifier(job))
|
|
184
|
+
return null;
|
|
185
|
+
if (!job.packet_context?.code_state_check)
|
|
186
|
+
return null;
|
|
187
|
+
if (!projectRoot || !events)
|
|
188
|
+
return null;
|
|
189
|
+
const current = computeCodeStateCheck(projectRoot, events, ignoredCodePaths);
|
|
190
|
+
if (JSON.stringify(current) !== JSON.stringify(job.packet_context.code_state_check)) {
|
|
191
|
+
return "最终验证工作项的代码状态事实已变化";
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
export function reviewVerifierStaleReason(job, changeRoot, currentEvidenceDigest, projectRoot, events, ignoredCodePaths = []) {
|
|
196
|
+
return boundFilesStaleReason(job, changeRoot)
|
|
197
|
+
?? reviewEvidenceStaleReason(job, currentEvidenceDigest)
|
|
198
|
+
?? codeStateCheckStaleReason(job, projectRoot, events, ignoredCodePaths);
|
|
166
199
|
}
|
|
167
|
-
export function isFreshReviewVerifier(job, changeRoot, currentEvidenceDigest) {
|
|
168
|
-
return isReviewReadyVerifier(job) && reviewVerifierStaleReason(job, changeRoot, currentEvidenceDigest) == null;
|
|
200
|
+
export function isFreshReviewVerifier(job, changeRoot, currentEvidenceDigest, projectRoot, events) {
|
|
201
|
+
return isReviewReadyVerifier(job) && reviewVerifierStaleReason(job, changeRoot, currentEvidenceDigest, projectRoot, events) == null;
|
|
169
202
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReviewRisk } from "./review.ts";
|
|
2
|
+
import type { Job, JobRole, ReviewJobGateId, Snapshot } from "./types.ts";
|
|
3
|
+
export interface ReviewGateRule {
|
|
4
|
+
gate_id: ReviewJobGateId;
|
|
5
|
+
created_from_transition: "explore" | "propose-ready" | "review-ready";
|
|
6
|
+
allowedRoles: JobRole[];
|
|
7
|
+
reviewedDocPaths: string[];
|
|
8
|
+
requiredRolesForRisk(risk: ReviewRisk): JobRole[];
|
|
9
|
+
matchesOldJob(job: Job): boolean;
|
|
10
|
+
isJobForGate(job: Job): boolean;
|
|
11
|
+
openJobsForGate(snapshot: Snapshot): Job[];
|
|
12
|
+
}
|
|
13
|
+
export declare const EXPLORE_DISCOVERY_REVIEW_GATE_ID: "explore.discovery_review";
|
|
14
|
+
export declare const PROPOSE_FINAL_REVIEW_GATE_ID: "propose.final_review";
|
|
15
|
+
export declare const REVIEW_CODE_REVIEW_GATE_ID: "review.code_review";
|
|
16
|
+
export declare const REVIEW_FINAL_VERIFIER_GATE_ID: "review.final_verifier";
|
|
17
|
+
export declare const EXPLORE_DISCOVERY_REVIEW_GATE: ReviewGateRule;
|
|
18
|
+
export declare const PROPOSE_FINAL_REVIEW_GATE: ReviewGateRule;
|
|
19
|
+
export declare const REVIEW_CODE_REVIEW_GATE: ReviewGateRule;
|
|
20
|
+
export declare const REVIEW_FINAL_VERIFIER_GATE: ReviewGateRule;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { reviewRolesForGate } from "./workflow_profile.js";
|
|
2
|
+
function makeReviewGateRule(input) {
|
|
3
|
+
const gate = {
|
|
4
|
+
...input,
|
|
5
|
+
isJobForGate(job) {
|
|
6
|
+
if (!input.allowedRoles.includes(job.role))
|
|
7
|
+
return false;
|
|
8
|
+
if (job.gate_id)
|
|
9
|
+
return job.gate_id === input.gate_id;
|
|
10
|
+
return input.matchesOldJob(job);
|
|
11
|
+
},
|
|
12
|
+
openJobsForGate(snapshot) {
|
|
13
|
+
return snapshot.open_jobs.filter(job => gate.isJobForGate(job));
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
return gate;
|
|
17
|
+
}
|
|
18
|
+
export const EXPLORE_DISCOVERY_REVIEW_GATE_ID = "explore.discovery_review";
|
|
19
|
+
export const PROPOSE_FINAL_REVIEW_GATE_ID = "propose.final_review";
|
|
20
|
+
export const REVIEW_CODE_REVIEW_GATE_ID = "review.code_review";
|
|
21
|
+
export const REVIEW_FINAL_VERIFIER_GATE_ID = "review.final_verifier";
|
|
22
|
+
const EXPLORE_DISCOVERY_REVIEW_ROLES = ["critic"];
|
|
23
|
+
const PROPOSAL_REVIEW_ROLES = ["critic", "architect", "test-engineer"];
|
|
24
|
+
const PROPOSAL_REVIEW_ROLE_SET = new Set(PROPOSAL_REVIEW_ROLES);
|
|
25
|
+
const REVIEW_CODE_REVIEW_ROLES = ["code-reviewer"];
|
|
26
|
+
const REVIEW_FINAL_VERIFIER_ROLES = ["verifier"];
|
|
27
|
+
export const EXPLORE_DISCOVERY_REVIEW_GATE = makeReviewGateRule({
|
|
28
|
+
gate_id: EXPLORE_DISCOVERY_REVIEW_GATE_ID,
|
|
29
|
+
created_from_transition: "explore",
|
|
30
|
+
allowedRoles: EXPLORE_DISCOVERY_REVIEW_ROLES,
|
|
31
|
+
reviewedDocPaths: [".superspec/artifacts/discovery.md"],
|
|
32
|
+
requiredRolesForRisk(risk) {
|
|
33
|
+
return reviewRolesForGate(EXPLORE_DISCOVERY_REVIEW_GATE_ID, risk);
|
|
34
|
+
},
|
|
35
|
+
matchesOldJob(job) {
|
|
36
|
+
return job.created_from_transition === "explore" && job.role === "critic";
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
export const PROPOSE_FINAL_REVIEW_GATE = makeReviewGateRule({
|
|
40
|
+
gate_id: PROPOSE_FINAL_REVIEW_GATE_ID,
|
|
41
|
+
created_from_transition: "propose-ready",
|
|
42
|
+
allowedRoles: PROPOSAL_REVIEW_ROLES,
|
|
43
|
+
reviewedDocPaths: [
|
|
44
|
+
"proposal.md",
|
|
45
|
+
"tasks.md",
|
|
46
|
+
"design.md",
|
|
47
|
+
"specs/",
|
|
48
|
+
".superspec/artifacts/discovery.md",
|
|
49
|
+
".superspec/artifacts/business-invariants.md",
|
|
50
|
+
".superspec/artifacts/test-contract.md",
|
|
51
|
+
],
|
|
52
|
+
requiredRolesForRisk(risk) {
|
|
53
|
+
return reviewRolesForGate(PROPOSE_FINAL_REVIEW_GATE_ID, risk);
|
|
54
|
+
},
|
|
55
|
+
matchesOldJob(job) {
|
|
56
|
+
return job.created_from_transition === "propose-ready" && PROPOSAL_REVIEW_ROLE_SET.has(job.role);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
export const REVIEW_CODE_REVIEW_GATE = makeReviewGateRule({
|
|
60
|
+
gate_id: REVIEW_CODE_REVIEW_GATE_ID,
|
|
61
|
+
created_from_transition: "review-ready",
|
|
62
|
+
allowedRoles: REVIEW_CODE_REVIEW_ROLES,
|
|
63
|
+
reviewedDocPaths: [],
|
|
64
|
+
requiredRolesForRisk(risk) {
|
|
65
|
+
return reviewRolesForGate(REVIEW_CODE_REVIEW_GATE_ID, risk);
|
|
66
|
+
},
|
|
67
|
+
matchesOldJob(job) {
|
|
68
|
+
return job.created_from_transition === "review-ready" && job.role === "code-reviewer";
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
export const REVIEW_FINAL_VERIFIER_GATE = makeReviewGateRule({
|
|
72
|
+
gate_id: REVIEW_FINAL_VERIFIER_GATE_ID,
|
|
73
|
+
created_from_transition: "review-ready",
|
|
74
|
+
allowedRoles: REVIEW_FINAL_VERIFIER_ROLES,
|
|
75
|
+
reviewedDocPaths: [],
|
|
76
|
+
requiredRolesForRisk(risk) {
|
|
77
|
+
return reviewRolesForGate(REVIEW_FINAL_VERIFIER_GATE_ID, risk);
|
|
78
|
+
},
|
|
79
|
+
matchesOldJob(job) {
|
|
80
|
+
return job.created_from_transition === "review-ready" &&
|
|
81
|
+
job.role === "verifier" &&
|
|
82
|
+
typeof job.review_evidence_digest === "string" &&
|
|
83
|
+
job.review_evidence_digest.length > 0;
|
|
84
|
+
},
|
|
85
|
+
});
|
package/dist/store.d.ts
CHANGED
|
@@ -17,6 +17,16 @@ export declare function jobsDir(projectRoot: string, change: string): string;
|
|
|
17
17
|
export declare function ensureChangeLayout(projectRoot: string, change: string): void;
|
|
18
18
|
export declare function sha256Text(text: string): string;
|
|
19
19
|
export declare function sha256File(filePath: string): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* 递归列出目录下全部 .md 文件(相对路径,确定性顺序:逐层排序的深度优先)。
|
|
22
|
+
* 目录缺失返回空数组;symlink、并发删除的条目跳过(不参与聚合),
|
|
23
|
+
* 避免 specs/ 出现异常项时打断 rebuildSnapshot 的所有调用方。
|
|
24
|
+
*/
|
|
25
|
+
export declare function listMarkdownFiles(dirPath: string): string[];
|
|
26
|
+
/** 目录聚合指纹:排序后的相对路径 + 逐文件内容 sha,只聚合 .md(临时/系统文件不参与);增/删/改任一 .md 都会变化。目录缺失或为空返回稳定空指纹 */
|
|
27
|
+
export declare function sha256Dir(dirPath: string): string;
|
|
28
|
+
/** 文档引用:路径以 / 结尾按目录聚合指纹绑定,其余按单文件 sha */
|
|
29
|
+
export declare function docRef(root: string, path: string): Ref;
|
|
20
30
|
export declare function computeDocumentDigests(changeRoot: string, docPaths: string[]): Record<string, string>;
|
|
21
31
|
export declare function digestOf(obj: unknown): string;
|
|
22
32
|
export declare function appendEvent(projectRoot: string, change: string, event: Event): void;
|
package/dist/store.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SuperSpec 流程引擎 — 存储层:路径、指纹、事件日志、快照、锁
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, openSync, closeSync, unlinkSync, statSync, renameSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, openSync, closeSync, unlinkSync, statSync, lstatSync, renameSync } from "node:fs";
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
5
|
import { hostname } from "node:os";
|
|
6
6
|
// ===== 路径 =====
|
|
@@ -48,11 +48,61 @@ export function sha256File(filePath) {
|
|
|
48
48
|
return null;
|
|
49
49
|
return "sha256:" + createHash("sha256").update(readFileSync(filePath)).digest("hex");
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* 递归列出目录下全部 .md 文件(相对路径,确定性顺序:逐层排序的深度优先)。
|
|
53
|
+
* 目录缺失返回空数组;symlink、并发删除的条目跳过(不参与聚合),
|
|
54
|
+
* 避免 specs/ 出现异常项时打断 rebuildSnapshot 的所有调用方。
|
|
55
|
+
*/
|
|
56
|
+
export function listMarkdownFiles(dirPath) {
|
|
57
|
+
const out = [];
|
|
58
|
+
const lstatOrNull = (p) => { try {
|
|
59
|
+
return lstatSync(p);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
} };
|
|
64
|
+
const walk = (dir, prefix) => {
|
|
65
|
+
const dirStat = lstatOrNull(dir);
|
|
66
|
+
if (!dirStat?.isDirectory() || dirStat.isSymbolicLink())
|
|
67
|
+
return;
|
|
68
|
+
let names;
|
|
69
|
+
try {
|
|
70
|
+
names = readdirSync(dir).sort();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
for (const name of names) {
|
|
76
|
+
const full = join(dir, name);
|
|
77
|
+
const rel = prefix ? `${prefix}/${name}` : name;
|
|
78
|
+
const st = lstatOrNull(full);
|
|
79
|
+
if (!st || st.isSymbolicLink())
|
|
80
|
+
continue;
|
|
81
|
+
if (st.isDirectory())
|
|
82
|
+
walk(full, rel);
|
|
83
|
+
else if (name.endsWith(".md"))
|
|
84
|
+
out.push(rel);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
walk(dirPath, "");
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
/** 目录聚合指纹:排序后的相对路径 + 逐文件内容 sha,只聚合 .md(临时/系统文件不参与);增/删/改任一 .md 都会变化。目录缺失或为空返回稳定空指纹 */
|
|
91
|
+
export function sha256Dir(dirPath) {
|
|
92
|
+
const entries = listMarkdownFiles(dirPath)
|
|
93
|
+
.map(rel => `${rel}\u0000${sha256File(join(dirPath, rel)) ?? "sha256:missing"}`);
|
|
94
|
+
return sha256Text(entries.join("\n"));
|
|
95
|
+
}
|
|
96
|
+
/** 文档引用:路径以 / 结尾按目录聚合指纹绑定,其余按单文件 sha */
|
|
97
|
+
export function docRef(root, path) {
|
|
98
|
+
if (path.endsWith("/"))
|
|
99
|
+
return { path, sha: sha256Dir(join(root, path)) };
|
|
100
|
+
return { path, sha: sha256File(join(root, path)) ?? "sha256:missing" };
|
|
101
|
+
}
|
|
51
102
|
export function computeDocumentDigests(changeRoot, docPaths) {
|
|
52
103
|
const digests = {};
|
|
53
104
|
for (const p of docPaths) {
|
|
54
|
-
|
|
55
|
-
digests[p] = sha256File(full) ?? "sha256:missing";
|
|
105
|
+
digests[p] = docRef(changeRoot, p).sha;
|
|
56
106
|
}
|
|
57
107
|
return digests;
|
|
58
108
|
}
|
package/dist/sync.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { readEvents, eventsDigest, computeDocumentDigests, sha256Text, ensureChangeLayout, } from "./store.js";
|
|
5
|
-
import { reviewEvidenceDigest
|
|
6
|
-
import {
|
|
5
|
+
import { reviewEvidenceDigest } from "./review.js";
|
|
6
|
+
import { invalidReasonForSnapshot } from "./job_validity.js";
|
|
7
7
|
const TRACKED_DOCS = [
|
|
8
|
-
"proposal.md", "design.md", "tasks.md",
|
|
8
|
+
"proposal.md", "design.md", "tasks.md", "specs/",
|
|
9
9
|
".superspec/artifacts/discovery.md",
|
|
10
10
|
".superspec/artifacts/business-invariants.md",
|
|
11
11
|
".superspec/artifacts/test-contract.md",
|
|
@@ -81,12 +81,10 @@ function replayEvents(events) {
|
|
|
81
81
|
return { state, openJobs, acceptedJobs, activeAttempts, taskStatuses, lastTransition };
|
|
82
82
|
}
|
|
83
83
|
/** 粗粒度失效:检查 job 的 boundFiles 是否仍匹配当前文档 */
|
|
84
|
-
function checkStaleJobs(jobs, projectRoot, changeRoot, currentReviewEvidenceDigest) {
|
|
84
|
+
function checkStaleJobs(jobs, projectRoot, changeRoot, events, currentReviewEvidenceDigest) {
|
|
85
85
|
const stale = [];
|
|
86
86
|
for (const job of jobs) {
|
|
87
|
-
const reason = job
|
|
88
|
-
? codeReviewJobStaleReason(projectRoot, job)
|
|
89
|
-
: reviewVerifierStaleReason(job, changeRoot, currentReviewEvidenceDigest);
|
|
87
|
+
const reason = invalidReasonForSnapshot({ job, projectRoot, changeRoot, events, currentReviewEvidenceDigest });
|
|
90
88
|
if (reason) {
|
|
91
89
|
stale.push({ job_id: job.job_id, reason });
|
|
92
90
|
}
|
|
@@ -114,8 +112,8 @@ export function rebuildSnapshot(projectRoot, change, changeRoot, openspecStatusD
|
|
|
114
112
|
const currentReviewEvidenceDigest = reviewEvidenceDigest(events);
|
|
115
113
|
// 粗粒度失效检查(只读,不写事件):open code-reviewer job 防止提交过期报告;
|
|
116
114
|
// accepted code-reviewer pass 不做持续 freshness gate,避免 apply_done 循环重审。
|
|
117
|
-
const staleOpenInfo = checkStaleJobs(openJobs, projectRoot, changeRoot, currentReviewEvidenceDigest);
|
|
118
|
-
const staleAcceptedInfo = checkStaleJobs(acceptedJobs.filter(j => j.role !== "code-reviewer"), projectRoot, changeRoot, currentReviewEvidenceDigest);
|
|
115
|
+
const staleOpenInfo = checkStaleJobs(openJobs, projectRoot, changeRoot, events, currentReviewEvidenceDigest);
|
|
116
|
+
const staleAcceptedInfo = checkStaleJobs(acceptedJobs.filter(j => j.role !== "code-reviewer"), projectRoot, changeRoot, events, currentReviewEvidenceDigest);
|
|
119
117
|
const freshOpen = openJobs
|
|
120
118
|
.filter(j => !staleOpenInfo.some(s => s.job_id === j.job_id))
|
|
121
119
|
.filter(j => j.role !== "code-reviewer" || state === "apply_done");
|