@peterxiaoyang/superspec 0.1.38 → 0.1.40
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 +19 -2
- package/dist/code_review.d.ts +17 -2
- package/dist/code_review.js +471 -80
- 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 +1 -0
- package/dist/job_validity.js +11 -8
- package/dist/next.js +46 -314
- package/dist/phase_plan.d.ts +97 -0
- package/dist/phase_plan.js +582 -0
- package/dist/record.js +56 -7
- package/dist/review.d.ts +3 -2
- package/dist/review.js +64 -32
- package/dist/review_job_gates.js +1 -0
- package/dist/store.d.ts +10 -0
- package/dist/store.js +53 -3
- package/dist/sync.js +5 -5
- package/dist/task.js +87 -9
- package/dist/task_evidence.js +80 -0
- package/dist/transition.d.ts +1 -1
- package/dist/transition.js +301 -210
- package/dist/types.d.ts +77 -1
- package/package.json +1 -1
- package/templates/workflow/agents/explore.toml +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 +66 -64
- package/templates/workflow/skills/superspec-propose/SKILL.md +67 -44
- package/templates/workflow/skills/superspec-review/SKILL.md +1 -1
package/dist/record.js
CHANGED
|
@@ -5,6 +5,7 @@ import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, sha
|
|
|
5
5
|
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_DECISION_SCOPE_PREFIX, codeReviewDecisionAnswerLabel, normalizeCodeReviewDecisionAnswer, } from "./code_review.js";
|
|
6
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) {
|
|
@@ -323,11 +325,13 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
323
325
|
reason: checks.join("; "),
|
|
324
326
|
parsedReport,
|
|
325
327
|
rawRef,
|
|
328
|
+
reportPath,
|
|
326
329
|
})
|
|
327
330
|
: {
|
|
328
331
|
job_id: jobId,
|
|
329
332
|
role: job.role,
|
|
330
333
|
report_digest: reportDigest,
|
|
334
|
+
...(reportPath ? { report_path: reportPath } : {}),
|
|
331
335
|
reason: checks.join("; "),
|
|
332
336
|
}),
|
|
333
337
|
});
|
|
@@ -348,6 +352,8 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
348
352
|
parsedReport,
|
|
349
353
|
rawRef,
|
|
350
354
|
}));
|
|
355
|
+
if (reportPath)
|
|
356
|
+
rejectEvent.payload.report_path = reportPath;
|
|
351
357
|
appendEvent(projectRoot, change, rejectEvent);
|
|
352
358
|
return {
|
|
353
359
|
event_type: "job_rejected",
|
|
@@ -375,6 +381,7 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
375
381
|
reason,
|
|
376
382
|
parsedReport,
|
|
377
383
|
rawRef,
|
|
384
|
+
reportPath,
|
|
378
385
|
})));
|
|
379
386
|
return {
|
|
380
387
|
event_type: "job_rejected",
|
|
@@ -401,6 +408,7 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
401
408
|
reason,
|
|
402
409
|
parsedReport,
|
|
403
410
|
rawRef,
|
|
411
|
+
reportPath,
|
|
404
412
|
})));
|
|
405
413
|
return {
|
|
406
414
|
event_type: "job_rejected",
|
|
@@ -416,6 +424,7 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
416
424
|
role: job.role,
|
|
417
425
|
report_digest: reportDigest,
|
|
418
426
|
accepted_at: new Date().toISOString(),
|
|
427
|
+
...(reportPath ? { report_path: reportPath } : {}),
|
|
419
428
|
...rawRef,
|
|
420
429
|
});
|
|
421
430
|
appendEvent(projectRoot, change, acceptEvent);
|
|
@@ -494,7 +503,7 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
494
503
|
reason: "missing_scope_or_answer",
|
|
495
504
|
input_digest: inputDigest,
|
|
496
505
|
}));
|
|
497
|
-
return { event_type: "user_decision_recorded", accepted: false, message: "
|
|
506
|
+
return { event_type: "user_decision_recorded", accepted: false, message: "决策文件缺少决策范围(scope)或答复内容(answer)" };
|
|
498
507
|
}
|
|
499
508
|
if (decision.scope.startsWith(CODE_REVIEW_DECISION_SCOPE_PREFIX)) {
|
|
500
509
|
const normalizedAnswer = normalizeCodeReviewDecisionAnswer(decision.answer);
|
|
@@ -547,7 +556,7 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
547
556
|
return {
|
|
548
557
|
event_type: "user_decision_recorded",
|
|
549
558
|
accepted: true,
|
|
550
|
-
message:
|
|
559
|
+
message: `用户决策已登记:决策范围(scope)=${decision.scope}`,
|
|
551
560
|
};
|
|
552
561
|
}
|
|
553
562
|
/** record user-decision:登记用户决策 */
|
|
@@ -606,6 +615,31 @@ export function jobsList(projectRoot, change) {
|
|
|
606
615
|
}
|
|
607
616
|
return { open, accepted, rejected };
|
|
608
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
|
+
}
|
|
609
643
|
/** jobs packet:返回工作项执行说明 */
|
|
610
644
|
export function jobsPacket(projectRoot, change, jobId) {
|
|
611
645
|
const events = readEvents(projectRoot, change);
|
|
@@ -614,6 +648,7 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
614
648
|
return { found: false, message: `工作项 ${jobId} 不存在` };
|
|
615
649
|
}
|
|
616
650
|
const isCodeReviewer = job.role === "code-reviewer";
|
|
651
|
+
const packetContext = job.packet_context;
|
|
617
652
|
return {
|
|
618
653
|
found: true,
|
|
619
654
|
packet: {
|
|
@@ -624,6 +659,13 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
624
659
|
boundFiles: job.boundFiles,
|
|
625
660
|
...(job.review_evidence_digest ? { review_evidence_digest: job.review_evidence_digest } : {}),
|
|
626
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 } : {}),
|
|
627
669
|
packet_digest: job.packet_digest,
|
|
628
670
|
required_output_kind: "job_report_json",
|
|
629
671
|
preferred_input_mode: "stdin",
|
|
@@ -634,16 +676,23 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
634
676
|
? [...REVIEW_REPORT_REQUIRED_FIELDS, "reviewer", "review_scope"]
|
|
635
677
|
: requiresReviewer(job.role) ? [...REVIEW_REPORT_REQUIRED_FIELDS, "reviewer"] : [...REVIEW_REPORT_REQUIRED_FIELDS],
|
|
636
678
|
output_contract_optional_fields: [...REVIEW_REPORT_OPTIONAL_FIELDS],
|
|
679
|
+
字段说明: packetFieldDescriptions(),
|
|
637
680
|
output_instructions: `${roleDescription(job.role)}。请审查 ${job.boundFiles.map(f => f.path).join(", ")},` +
|
|
638
681
|
(job.review_evidence_digest ? `本工作项对应的执行证据版本为 ${job.review_evidence_digest},` : "") +
|
|
639
682
|
(job.previous_rejection ? `上一次代码审查没有形成可推进结论,原因:${job.previous_rejection.reason}。本次请根据该原因重新审查,` : "") +
|
|
640
|
-
(requiresReviewer(job.role) ? `必须由独立 ${recommendedAgentForRole(job.role)}
|
|
641
|
-
`产出 JSON 报告内容并优先通过 --report - 从 stdin
|
|
683
|
+
(requiresReviewer(job.role) ? `必须由独立 ${recommendedAgentForRole(job.role)} 审查角色执行,并在审查者来源字段(reviewer.kind/id)中记录来源,` : "") +
|
|
684
|
+
`产出 JSON 报告内容并优先通过 --report - 从 stdin 登记;文件路径模式仅作备用。协议字段含义见 packet 顶层“字段说明”,普通对话不要原样复述 JSON。` +
|
|
642
685
|
(isCodeReviewer
|
|
643
|
-
? `最小格式:{"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"
|
|
644
|
-
+ `报告结论为 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
|
+
: "")
|
|
645
691
|
: job.role === "verifier"
|
|
646
|
-
? `最小格式:{"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
|
+
: "")
|
|
647
696
|
: requiresReviewer(job.role)
|
|
648
697
|
? `最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[],"reviewer":{"kind":"codex-subagent","id":"<thread-or-agent-id>"}}`
|
|
649
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,12 +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
5
|
import { REVIEW_FINAL_VERIFIER_GATE } from "./review_job_gates.js";
|
|
6
|
+
import { computeCodeStateCheck, effectiveCoverageExemptionRefsFromEvents } from "./code_review.js";
|
|
6
7
|
export const REVIEW_DOC_PATHS = [
|
|
7
8
|
"proposal.md",
|
|
8
9
|
"tasks.md",
|
|
9
10
|
"design.md",
|
|
11
|
+
"specs/",
|
|
10
12
|
".superspec/artifacts/discovery.md",
|
|
11
13
|
".superspec/artifacts/business-invariants.md",
|
|
12
14
|
".superspec/artifacts/test-contract.md",
|
|
@@ -56,32 +58,21 @@ export function isReviewReadyVerifier(job) {
|
|
|
56
58
|
return job.role === "verifier" && REVIEW_FINAL_VERIFIER_GATE.isJobForGate(job);
|
|
57
59
|
}
|
|
58
60
|
export function reviewBoundFiles(changeRoot) {
|
|
61
|
+
// 目录路径(以 / 结尾)始终绑定聚合指纹,与 boundFilesStaleReason 的 docRef 比对保持一致
|
|
59
62
|
return REVIEW_DOC_PATHS
|
|
60
|
-
.filter(path => existsSync(join(changeRoot, path)))
|
|
61
|
-
.map(path => (
|
|
63
|
+
.filter(path => path.endsWith("/") || existsSync(join(changeRoot, path)))
|
|
64
|
+
.map(path => docRef(changeRoot, path));
|
|
62
65
|
}
|
|
66
|
+
// 方案排序键:kind → task_id → test_id → attempt_id → event_id;缺失字段按空字符串。
|
|
67
|
+
// event_id 全局唯一,作为末位排序键足以保证稳定。
|
|
63
68
|
function evidenceSortKey(record) {
|
|
64
|
-
|
|
69
|
+
return [
|
|
65
70
|
record.kind,
|
|
66
71
|
record.task_id ?? "",
|
|
67
|
-
record.attempt_id ?? "",
|
|
68
|
-
record.task_structure_digest ?? "",
|
|
69
72
|
record.test_id ?? "",
|
|
70
|
-
record.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
parts.push(`covers:${JSON.stringify(record.covers_task_ids)}`);
|
|
74
|
-
}
|
|
75
|
-
parts.push(record.command ?? "", record.cwd ?? "", record.exit_code == null ? "" : String(record.exit_code), record.target_fingerprint ?? "", record.event_digest);
|
|
76
|
-
return parts.join("\u0000");
|
|
77
|
-
}
|
|
78
|
-
function normalizedCoveredTaskIds(value) {
|
|
79
|
-
if (!Array.isArray(value))
|
|
80
|
-
return [];
|
|
81
|
-
return [...new Set(value
|
|
82
|
-
.filter((item) => typeof item === "string")
|
|
83
|
-
.map(item => item.trim())
|
|
84
|
-
.filter(Boolean))].sort();
|
|
73
|
+
record.attempt_id ?? "",
|
|
74
|
+
record.event_id ?? "",
|
|
75
|
+
].join("\u0000");
|
|
85
76
|
}
|
|
86
77
|
export function reviewEvidenceDigest(events) {
|
|
87
78
|
const attemptsById = new Map();
|
|
@@ -100,6 +91,7 @@ export function reviewEvidenceDigest(events) {
|
|
|
100
91
|
task_id: payload.task_id,
|
|
101
92
|
attempt_id: payload.attempt_id,
|
|
102
93
|
task_structure_digest: attempt?.task_structure_digest ?? null,
|
|
94
|
+
event_id: ev.event_id,
|
|
103
95
|
event_digest: ev.event_digest,
|
|
104
96
|
});
|
|
105
97
|
}
|
|
@@ -108,11 +100,12 @@ export function reviewEvidenceDigest(events) {
|
|
|
108
100
|
const completedStructureDigests = new Set(completed
|
|
109
101
|
.map(item => item.task_structure_digest)
|
|
110
102
|
.filter((digest) => typeof digest === "string" && digest.length > 0));
|
|
103
|
+
// scope_note / boundary_snapshot / checkbox_update 属于 task_completed 事件 payload,已由 event_digest 覆盖
|
|
111
104
|
const records = completed.map(item => ({
|
|
112
105
|
kind: "task_completed",
|
|
113
106
|
task_id: item.task_id,
|
|
114
107
|
attempt_id: item.attempt_id,
|
|
115
|
-
|
|
108
|
+
event_id: item.event_id,
|
|
116
109
|
event_digest: item.event_digest,
|
|
117
110
|
}));
|
|
118
111
|
for (const ev of events) {
|
|
@@ -125,18 +118,41 @@ export function reviewEvidenceDigest(events) {
|
|
|
125
118
|
const matchesLegacyDigest = attemptId == null && structureDigest != null && completedStructureDigests.has(structureDigest);
|
|
126
119
|
if (!matchesCompletedAttempt && !matchesLegacyDigest)
|
|
127
120
|
continue;
|
|
128
|
-
const coversTaskIds = normalizedCoveredTaskIds(payload.covers_task_ids);
|
|
129
121
|
records.push({
|
|
130
122
|
kind: "test_run_recorded",
|
|
131
123
|
test_id: typeof payload.test_id === "string" ? payload.test_id : null,
|
|
132
124
|
attempt_id: attemptId,
|
|
133
|
-
task_structure_digest: structureDigest,
|
|
134
125
|
semantic_status: typeof payload.semantic_status === "string" ? payload.semantic_status : null,
|
|
135
|
-
|
|
126
|
+
exit_code: typeof payload.exit_code === "number" ? payload.exit_code : null,
|
|
136
127
|
command: typeof payload.command === "string" ? payload.command : "",
|
|
137
128
|
cwd: typeof payload.cwd === "string" ? payload.cwd : "",
|
|
138
|
-
|
|
139
|
-
|
|
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,
|
|
140
156
|
event_digest: ev.event_digest,
|
|
141
157
|
});
|
|
142
158
|
}
|
|
@@ -145,7 +161,8 @@ export function reviewEvidenceDigest(events) {
|
|
|
145
161
|
}
|
|
146
162
|
export function boundFilesStaleReason(job, changeRoot) {
|
|
147
163
|
for (const bf of job.boundFiles) {
|
|
148
|
-
|
|
164
|
+
// 目录绑定(path 以 / 结尾)比对聚合指纹,覆盖目录内文件的增/删/改
|
|
165
|
+
const current = docRef(changeRoot, bf.path).sha;
|
|
149
166
|
if (current !== bf.sha) {
|
|
150
167
|
return `绑定文件 ${bf.path} 已变化(${bf.sha} → ${current})`;
|
|
151
168
|
}
|
|
@@ -162,9 +179,24 @@ export function reviewEvidenceStaleReason(job, currentDigest) {
|
|
|
162
179
|
}
|
|
163
180
|
return null;
|
|
164
181
|
}
|
|
165
|
-
export function
|
|
166
|
-
|
|
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);
|
|
167
199
|
}
|
|
168
|
-
export function isFreshReviewVerifier(job, changeRoot, currentEvidenceDigest) {
|
|
169
|
-
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;
|
|
170
202
|
}
|
package/dist/review_job_gates.js
CHANGED
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
|
@@ -5,7 +5,7 @@ import { readEvents, eventsDigest, computeDocumentDigests, sha256Text, ensureCha
|
|
|
5
5
|
import { reviewEvidenceDigest } from "./review.js";
|
|
6
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,10 +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 = invalidReasonForSnapshot({ job, projectRoot, changeRoot, currentReviewEvidenceDigest });
|
|
87
|
+
const reason = invalidReasonForSnapshot({ job, projectRoot, changeRoot, events, currentReviewEvidenceDigest });
|
|
88
88
|
if (reason) {
|
|
89
89
|
stale.push({ job_id: job.job_id, reason });
|
|
90
90
|
}
|
|
@@ -112,8 +112,8 @@ export function rebuildSnapshot(projectRoot, change, changeRoot, openspecStatusD
|
|
|
112
112
|
const currentReviewEvidenceDigest = reviewEvidenceDigest(events);
|
|
113
113
|
// 粗粒度失效检查(只读,不写事件):open code-reviewer job 防止提交过期报告;
|
|
114
114
|
// accepted code-reviewer pass 不做持续 freshness gate,避免 apply_done 循环重审。
|
|
115
|
-
const staleOpenInfo = checkStaleJobs(openJobs, projectRoot, changeRoot, currentReviewEvidenceDigest);
|
|
116
|
-
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);
|
|
117
117
|
const freshOpen = openJobs
|
|
118
118
|
.filter(j => !staleOpenInfo.some(s => s.job_id === j.job_id))
|
|
119
119
|
.filter(j => j.role !== "code-reviewer" || state === "apply_done");
|
package/dist/task.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SuperSpec 流程引擎 — task:测试运行记录 + 任务结构指纹工具
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { sha256Text, ensureChangeLayout, appendEvent, makeEvent, withLock, appendRawRecord } from "./store.js";
|
|
4
|
+
import { sha256Text, ensureChangeLayout, appendEvent, makeEvent, withLock, appendRawRecord, readEvents } from "./store.js";
|
|
5
5
|
import { tasksStructureDigest as formatDigest } from "./format.js";
|
|
6
6
|
/** tasks.md 结构指纹(委托给 format.ts 统一实现) */
|
|
7
7
|
export function tasksStructureDigestOf(changeRoot) {
|
|
@@ -19,30 +19,44 @@ function recordTestRunLoaded(projectRoot, change, content) {
|
|
|
19
19
|
catch {
|
|
20
20
|
return { accepted: false, message: "无效 JSON" };
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const events = readEvents(projectRoot, change);
|
|
23
|
+
const attemptRecord = typeof tr.attempt_id === "string" ? attemptById(events, tr.attempt_id) : null;
|
|
24
|
+
const activeAttempt = attemptRecord?.state === "active" ? attemptRecord.attempt : null;
|
|
25
|
+
if (attemptRecord?.attempt.contract_mode === true && attemptRecord.state !== "active") {
|
|
26
|
+
return { accepted: false, message: "契约测试证据只能登记到当前活跃任务尝试(attempt_id)" };
|
|
27
|
+
}
|
|
28
|
+
if (!activeAttempt && activeContractAttemptExists(events)) {
|
|
29
|
+
return { accepted: false, message: "契约测试证据必须带当前活跃任务尝试 ID(attempt_id)" };
|
|
30
|
+
}
|
|
31
|
+
if (activeAttempt?.contract_mode === true) {
|
|
32
|
+
const contractCheck = validateContractTestRunInput(tr, activeAttempt);
|
|
33
|
+
if (!contractCheck.ok)
|
|
34
|
+
return { accepted: false, message: contractCheck.message };
|
|
35
|
+
}
|
|
36
|
+
else if (!tr.test_id || !tr.task_structure_digest) {
|
|
37
|
+
return { accepted: false, message: "缺少测试 ID(test_id)或历史任务结构指纹(task_structure_digest)" };
|
|
24
38
|
}
|
|
25
39
|
let coversTaskIds;
|
|
26
40
|
if (tr.covers_task_ids !== undefined) {
|
|
27
41
|
if (!Array.isArray(tr.covers_task_ids)) {
|
|
28
|
-
return { accepted: false, message: "covers_task_ids
|
|
42
|
+
return { accepted: false, message: "回归覆盖任务列表(covers_task_ids)必须是字符串数组" };
|
|
29
43
|
}
|
|
30
44
|
if (tr.covers_task_ids.length === 0) {
|
|
31
|
-
return { accepted: false, message: "covers_task_ids
|
|
45
|
+
return { accepted: false, message: "回归覆盖任务列表(covers_task_ids)不能是空数组" };
|
|
32
46
|
}
|
|
33
47
|
for (const raw of tr.covers_task_ids) {
|
|
34
48
|
if (typeof raw !== "string")
|
|
35
|
-
return { accepted: false, message: "covers_task_ids
|
|
49
|
+
return { accepted: false, message: "回归覆盖任务列表(covers_task_ids)必须是字符串数组" };
|
|
36
50
|
const value = raw.trim();
|
|
37
51
|
if (!value)
|
|
38
|
-
return { accepted: false, message: "covers_task_ids
|
|
52
|
+
return { accepted: false, message: "回归覆盖任务列表(covers_task_ids)不能包含空字符串" };
|
|
39
53
|
(coversTaskIds ??= []).push(value);
|
|
40
54
|
}
|
|
41
55
|
coversTaskIds = [...new Set(coversTaskIds)].sort();
|
|
42
56
|
}
|
|
43
57
|
const normalizedTestRun = {
|
|
44
58
|
test_id: tr.test_id,
|
|
45
|
-
task_structure_digest: tr.task_structure_digest,
|
|
59
|
+
task_structure_digest: tr.task_structure_digest ?? activeAttempt?.task_structure_digest ?? "",
|
|
46
60
|
attempt_id: tr.attempt_id ?? null,
|
|
47
61
|
...(coversTaskIds ? { covers_task_ids: coversTaskIds } : {}),
|
|
48
62
|
command: tr.command ?? "",
|
|
@@ -58,7 +72,71 @@ function recordTestRunLoaded(projectRoot, change, content) {
|
|
|
58
72
|
...rawRef,
|
|
59
73
|
});
|
|
60
74
|
appendEvent(projectRoot, change, event);
|
|
61
|
-
return { accepted: true, message:
|
|
75
|
+
return { accepted: true, message: `测试运行已登记:测试 ID(test_id)=${tr.test_id}` };
|
|
76
|
+
}
|
|
77
|
+
function attemptById(events, attemptId) {
|
|
78
|
+
const attempts = new Map();
|
|
79
|
+
for (const ev of events) {
|
|
80
|
+
if (ev.event_type === "task_started") {
|
|
81
|
+
const attempt = ev.payload;
|
|
82
|
+
if (typeof attempt.attempt_id === "string")
|
|
83
|
+
attempts.set(attempt.attempt_id, { attempt, state: "active" });
|
|
84
|
+
}
|
|
85
|
+
else if (ev.event_type === "task_completed") {
|
|
86
|
+
const payload = ev.payload;
|
|
87
|
+
const existing = typeof payload.attempt_id === "string" ? attempts.get(payload.attempt_id) : null;
|
|
88
|
+
if (existing)
|
|
89
|
+
existing.state = "completed";
|
|
90
|
+
}
|
|
91
|
+
else if (ev.event_type === "task_abandoned") {
|
|
92
|
+
const payload = ev.payload;
|
|
93
|
+
const existing = typeof payload.attempt_id === "string" ? attempts.get(payload.attempt_id) : null;
|
|
94
|
+
if (existing)
|
|
95
|
+
existing.state = "abandoned";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return attempts.get(attemptId) ?? null;
|
|
99
|
+
}
|
|
100
|
+
function activeContractAttemptExists(events) {
|
|
101
|
+
const attempts = new Map();
|
|
102
|
+
for (const ev of events) {
|
|
103
|
+
if (ev.event_type === "task_started") {
|
|
104
|
+
const attempt = ev.payload;
|
|
105
|
+
if (typeof attempt.attempt_id === "string")
|
|
106
|
+
attempts.set(attempt.attempt_id, attempt);
|
|
107
|
+
}
|
|
108
|
+
else if (ev.event_type === "task_completed" || ev.event_type === "task_abandoned") {
|
|
109
|
+
const payload = ev.payload;
|
|
110
|
+
if (typeof payload.attempt_id === "string")
|
|
111
|
+
attempts.delete(payload.attempt_id);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return [...attempts.values()].some(attempt => attempt.contract_mode === true);
|
|
115
|
+
}
|
|
116
|
+
function validateContractTestRunInput(tr, attempt) {
|
|
117
|
+
if (!tr.test_id || !tr.command || !tr.cwd || typeof tr.exit_code !== "number" || !tr.semantic_status) {
|
|
118
|
+
return { ok: false, message: "契约测试证据缺少测试 ID(test_id)、命令(command)、工作目录(cwd)、退出码(exit_code)或语义状态(semantic_status)" };
|
|
119
|
+
}
|
|
120
|
+
if (typeof tr.attempt_id !== "string" || tr.attempt_id !== attempt.attempt_id) {
|
|
121
|
+
return { ok: false, message: "契约测试证据必须带当前活跃任务尝试 ID(attempt_id)" };
|
|
122
|
+
}
|
|
123
|
+
if (!["expected_failure", "expected_success", "characterization_pass"].includes(tr.semantic_status)) {
|
|
124
|
+
return { ok: false, message: "语义状态(semantic_status)必须是 expected_failure、expected_success 或 characterization_pass" };
|
|
125
|
+
}
|
|
126
|
+
if (tr.semantic_status === "expected_failure" && tr.exit_code === 0) {
|
|
127
|
+
return { ok: false, message: "RED 预期失败(expected_failure)要求退出码(exit_code)非 0" };
|
|
128
|
+
}
|
|
129
|
+
if ((tr.semantic_status === "expected_success" || tr.semantic_status === "characterization_pass") && tr.exit_code !== 0) {
|
|
130
|
+
return { ok: false, message: `语义状态(semantic_status=${tr.semantic_status})要求退出码(exit_code)为 0` };
|
|
131
|
+
}
|
|
132
|
+
if (tr.semantic_status === "characterization_pass" && !(attempt.tdd_required === false && attempt.no_tdd_reason === "characterization")) {
|
|
133
|
+
return { ok: false, message: "特征化通过(characterization_pass)只适用于无需 TDD 的特征化任务(tdd_required:false,no_tdd_reason:characterization)" };
|
|
134
|
+
}
|
|
135
|
+
const declaredTests = attempt.contract?.tests ?? [];
|
|
136
|
+
if (declaredTests.length > 0 && !declaredTests.includes(tr.test_id)) {
|
|
137
|
+
return { ok: false, message: `测试 ID(test_id=${tr.test_id})不属于当前任务契约声明的测试列表` };
|
|
138
|
+
}
|
|
139
|
+
return { ok: true };
|
|
62
140
|
}
|
|
63
141
|
/** record test-run:登记测试运行记录 */
|
|
64
142
|
export function recordTestRun(projectRoot, change, inputFile) {
|