@peterxiaoyang/superspec 0.1.41 → 0.1.43
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 +6 -5
- package/dist/record.js +136 -57
- package/dist/record_input.d.ts +9 -0
- package/dist/record_input.js +34 -0
- package/dist/task.js +9 -1
- package/dist/types.d.ts +3 -1
- package/package.json +1 -1
- package/templates/workflow/prompts/architect.md +18 -8
- package/templates/workflow/prompts/critic.md +21 -10
- package/templates/workflow/prompts/test-engineer.md +17 -8
- package/templates/workflow/skills/superspec-propose/SKILL.md +11 -14
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { next as nextCmd } from "./next.js";
|
|
|
10
10
|
import { proposeReady, commitTransition, transitionInit, transitionExplore, startApply, taskStart, taskComplete, reopen, reviewReady, accept } from "./transition.js";
|
|
11
11
|
import { recordJobSubmit, recordJobSubmitContent, recordUserDecision, recordUserDecisionContent, jobsList, jobsPacket } from "./record.js";
|
|
12
12
|
import { recordTestRun, recordTestRunContent } from "./task.js";
|
|
13
|
+
import { RecordInputDecodingError, decodeRecordInput } from "./record_input.js";
|
|
13
14
|
import { probeOpenSpec, openspecStatus, changeRoot } from "./openspec.js";
|
|
14
15
|
import { SUPERSPEC_VERSION } from "./version.js";
|
|
15
16
|
const PACKAGE_NAME = "@peterxiaoyang/superspec";
|
|
@@ -54,7 +55,7 @@ function readStdinRecordContent(flag) {
|
|
|
54
55
|
if (process.stdin.isTTY === true) {
|
|
55
56
|
throw new StdinRecordInputError(flag);
|
|
56
57
|
}
|
|
57
|
-
return readFileSync(0
|
|
58
|
+
return decodeRecordInput(readFileSync(0));
|
|
58
59
|
}
|
|
59
60
|
function transitionExitCode(result) {
|
|
60
61
|
if (result.outcome === "blocked")
|
|
@@ -709,7 +710,7 @@ jobs 子命令:
|
|
|
709
710
|
inputContent = readStdinRecordContent("--input");
|
|
710
711
|
}
|
|
711
712
|
catch (err) {
|
|
712
|
-
if (err instanceof StdinRecordInputError) {
|
|
713
|
+
if (err instanceof StdinRecordInputError || err instanceof RecordInputDecodingError) {
|
|
713
714
|
console.error(err.message);
|
|
714
715
|
return 1;
|
|
715
716
|
}
|
|
@@ -773,7 +774,7 @@ jobs 子命令:
|
|
|
773
774
|
: recordJobSubmit(projectRoot, change, cr, jobId, report);
|
|
774
775
|
}
|
|
775
776
|
catch (err) {
|
|
776
|
-
if (err instanceof StdinRecordInputError) {
|
|
777
|
+
if (err instanceof StdinRecordInputError || err instanceof RecordInputDecodingError) {
|
|
777
778
|
console.error(err.message);
|
|
778
779
|
return 1;
|
|
779
780
|
}
|
|
@@ -795,7 +796,7 @@ jobs 子命令:
|
|
|
795
796
|
: recordUserDecision(projectRoot, change, inputFile);
|
|
796
797
|
}
|
|
797
798
|
catch (err) {
|
|
798
|
-
if (err instanceof StdinRecordInputError) {
|
|
799
|
+
if (err instanceof StdinRecordInputError || err instanceof RecordInputDecodingError) {
|
|
799
800
|
console.error(err.message);
|
|
800
801
|
return 1;
|
|
801
802
|
}
|
|
@@ -817,7 +818,7 @@ jobs 子命令:
|
|
|
817
818
|
: recordTestRun(projectRoot, change, inputFile);
|
|
818
819
|
}
|
|
819
820
|
catch (err) {
|
|
820
|
-
if (err instanceof StdinRecordInputError) {
|
|
821
|
+
if (err instanceof StdinRecordInputError || err instanceof RecordInputDecodingError) {
|
|
821
822
|
console.error(err.message);
|
|
822
823
|
return 1;
|
|
823
824
|
}
|
package/dist/record.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SuperSpec 流程引擎 — record:工作项结果登记
|
|
2
|
-
import {
|
|
2
|
+
import { 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
5
|
import { rebuildSnapshot } from "./sync.js";
|
|
@@ -10,6 +10,7 @@ import { invalidReasonForSubmittedReport } from "./job_validity.js";
|
|
|
10
10
|
import { jobSubmitArgv } from "./job_action.js";
|
|
11
11
|
import { REVIEW_DOC_PATHS } from "./review.js";
|
|
12
12
|
import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./review_job_gates.js";
|
|
13
|
+
import { RecordInputDecodingError, readRecordInputFile } from "./record_input.js";
|
|
13
14
|
const REVIEW_REPORT_REQUIRED_FIELDS = ["role", "verdict", "findings"];
|
|
14
15
|
const REVIEW_REPORT_OPTIONAL_FIELDS = ["summary", "evidence_refs", "risks", "open_questions"];
|
|
15
16
|
const REVIEWER_KINDS = new Set(["codex-subagent", "human", "external-agent"]);
|
|
@@ -17,6 +18,13 @@ const CODE_REVIEW_FINDING_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
|
17
18
|
function requiresReviewer(role) {
|
|
18
19
|
return role === "critic" || role === "architect" || role === "test-engineer" || role === "code-reviewer";
|
|
19
20
|
}
|
|
21
|
+
/** All review reports with bound material acknowledge full file coverage. */
|
|
22
|
+
function requiresReviewScope(job) {
|
|
23
|
+
return job.boundFiles.length > 0 && job.role !== "executor" && job.role !== "test-run";
|
|
24
|
+
}
|
|
25
|
+
function projectLocalInvalidReportMustTerminate(job) {
|
|
26
|
+
return job.role === "code-reviewer" || job.packet_context?.code_state_check !== undefined;
|
|
27
|
+
}
|
|
20
28
|
function isOrdinaryReviewer(role) {
|
|
21
29
|
return role === "critic" || role === "architect" || role === "test-engineer";
|
|
22
30
|
}
|
|
@@ -55,8 +63,8 @@ function previousRejectionInstruction(job) {
|
|
|
55
63
|
return "";
|
|
56
64
|
const reason = `上一次同角色审查没有形成可推进结论,原因:${previous.reason}。`;
|
|
57
65
|
if (!previous.findings || previous.findings.length === 0)
|
|
58
|
-
return `${reason}
|
|
59
|
-
return `${reason}
|
|
66
|
+
return `${reason}本轮是修复复核;完整读取材料只用于核对当前修正和直接一致性,不得借复核重新审计与修正无关的历史设计,`;
|
|
67
|
+
return `${reason}本轮是修复复核:逐项判断本工作项附带的上一次同角色 finding 是否仍成立。Finding 中的 recommendation 只是非绑定建议,不是需求或验收标准;先独立核对 underlying problem、直接证据和本次验收,不得因原建议指定了某种架构就要求照做。同一问题仍存在时复用原 finding ID;legacy finding 没有 ID 时沿用其原始语义并补一个稳定 ID;已解决或已由等价证据闭环的问题不要重复报告,不得通过更换 ID、标题或措辞重复同一问题。默认只复核历史 finding;新 blocker 仅允许是本次修正直接引入的回归,并必须说明“修正动作 → 新问题”的因果链,不得展开无关的故障模型、消费者或架构议题。`;
|
|
60
68
|
}
|
|
61
69
|
function ordinaryReviewerFindingInstruction(job) {
|
|
62
70
|
if (!isOrdinaryReviewer(job.role))
|
|
@@ -86,6 +94,23 @@ function reviewScopeInstruction(job, reviewTargets, readOnlyRefs) {
|
|
|
86
94
|
: "";
|
|
87
95
|
return targets + refs;
|
|
88
96
|
}
|
|
97
|
+
function reviewCoverageInstruction(job) {
|
|
98
|
+
if (!requiresReviewScope(job))
|
|
99
|
+
return "";
|
|
100
|
+
return `审查顺序固定为:先建立覆盖索引,按文件和标题/行段完整浏览全部 boundFiles 至文件末尾;长文档必须分段读取,不能在发现第一个 blocker 时停止覆盖。完整读取只用于核对本次 change 的目标、直接修改及跨文档一致性,不等于允许重新审计全部历史设计。若 proposal.md 存在“需求变化”,以其中记录的受影响能力、直接修改章节和保持不变范围作为本轮增量审查的权威锚点;本轮新 finding 必须由该需求变化、为接入变化所做的直接修改,或这些修改造成的跨文档矛盾引起,并说明因果链。此前已通过且被明确记录为保持不变的设计不得重新打开为 blocker;不要凭通用风险类别猜测变化范围。注意事项和故障类别是条件式检查项,不是必须穷举的清单。Recommendation 只能描述需要补足的结果、契约或证据,不得把未经 proposal、design 或用户决定选定的新基础设施写成 required fix。报告中的 review_scope.checked_paths 必须列出全部已浏览的 boundFiles;它只是覆盖回执,不能代替语义审查,也不扩大可报告问题的范围。`;
|
|
101
|
+
}
|
|
102
|
+
function migrationEvidenceInstruction(job) {
|
|
103
|
+
const isProposeReview = PROPOSE_FINAL_REVIEW_GATE.isJobForGate(job);
|
|
104
|
+
if (!isProposeReview)
|
|
105
|
+
return "";
|
|
106
|
+
return "迁移与环境证据按阶段分层:Explore 记录迁移风险、兼容约束和外部未知;Propose 只需审查迁移策略、回滚/兼容设计、任务与可执行测试计划;Apply/Review 才记录实际 datasource、制品 SHA、Flyway history、审计日志和运行证据。可因策略、计划或文档矛盾而 fail;不得只因实际环境证据尚未产生而 fail。";
|
|
107
|
+
}
|
|
108
|
+
function recordInputInstruction(job) {
|
|
109
|
+
const encoding = "stdin 请传 UTF-8 JSON;Windows PowerShell 请设置 $OutputEncoding 和 [Console]::OutputEncoding 为 UTF-8,文件备用方式请使用 Set-Content -Encoding utf8。为兼容旧版 PowerShell,带 BOM 的 UTF-16LE 文件也可接受。";
|
|
110
|
+
if (!projectLocalInvalidReportMustTerminate(job))
|
|
111
|
+
return encoding;
|
|
112
|
+
return `${encoding}项目内 fallback 报告若存在解码或协议错误会终结当前工作项,以免被后续代码状态检查当作改动;如需保留同一工作项重交,请使用 stdin 或工作区外临时文件。`;
|
|
113
|
+
}
|
|
89
114
|
function asObject(value) {
|
|
90
115
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
91
116
|
}
|
|
@@ -169,6 +194,23 @@ function validateCodeReviewScope(obj, job, checks) {
|
|
|
169
194
|
}
|
|
170
195
|
}
|
|
171
196
|
}
|
|
197
|
+
function validateReviewScope(obj, job, checks) {
|
|
198
|
+
const scope = asObject(obj.review_scope);
|
|
199
|
+
if (!scope) {
|
|
200
|
+
checks.push("审查报告缺少覆盖范围字段 review_scope");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (!stringArray(scope.checked_paths)) {
|
|
204
|
+
checks.push("审查报告覆盖范围里的 checked_paths 必须是字符串数组");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const checkedPaths = new Set(scope.checked_paths);
|
|
208
|
+
for (const bound of job.boundFiles) {
|
|
209
|
+
if (!checkedPaths.has(bound.path)) {
|
|
210
|
+
checks.push(`审查报告未说明已检查 ${bound.path}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
172
214
|
function actionableCodeReviewFindings(findings) {
|
|
173
215
|
if (!Array.isArray(findings))
|
|
174
216
|
return { actionable: [], reasons: ["报告字段 findings 必须是数组"] };
|
|
@@ -307,6 +349,35 @@ function terminalJobSubmitResult(events, jobId, terminal, reportDigest) {
|
|
|
307
349
|
message: `工作项 ${jobId} 已结束(${terminal}),不接受新报告。需要新工作项请重新执行对应的 transition。`,
|
|
308
350
|
};
|
|
309
351
|
}
|
|
352
|
+
function retryableJobSubmitResult(jobId, checks) {
|
|
353
|
+
return {
|
|
354
|
+
accepted: false,
|
|
355
|
+
message: `工作项 ${jobId} 的报告未接受,可修正后以同一工作项重新提交:${checks.join("; ")}`,
|
|
356
|
+
job_state: "requested",
|
|
357
|
+
events_written: 0,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function terminalInvalidReportResult(input) {
|
|
361
|
+
const rawRef = input.job.role === "code-reviewer" && input.parsedReport
|
|
362
|
+
? appendRawRecord(input.projectRoot, input.change, "review-reports", input.parsedReport)
|
|
363
|
+
: null;
|
|
364
|
+
appendEvent(input.projectRoot, input.change, makeEvent(input.change, "job_rejected", codeReviewRejectEventPayload({
|
|
365
|
+
jobId: input.jobId,
|
|
366
|
+
job: input.job,
|
|
367
|
+
reportDigest: input.reportDigest,
|
|
368
|
+
resultKind: "invalid_report",
|
|
369
|
+
reason: input.reason,
|
|
370
|
+
parsedReport: input.parsedReport,
|
|
371
|
+
rawRef,
|
|
372
|
+
reportPath: input.reportPath,
|
|
373
|
+
})));
|
|
374
|
+
return {
|
|
375
|
+
event_type: "job_rejected",
|
|
376
|
+
accepted: false,
|
|
377
|
+
message: `工作项 ${input.jobId} 被拒绝:${input.reason}`,
|
|
378
|
+
job_state: "rejected",
|
|
379
|
+
};
|
|
380
|
+
}
|
|
310
381
|
function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, events, reportContent, reportDigest, reportFile) {
|
|
311
382
|
const checks = [];
|
|
312
383
|
let parsedReport = null;
|
|
@@ -340,6 +411,9 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
340
411
|
if (job.role === "code-reviewer") {
|
|
341
412
|
validateCodeReviewScope(parsedReport, job, checks);
|
|
342
413
|
}
|
|
414
|
+
else if (requiresReviewScope(job)) {
|
|
415
|
+
validateReviewScope(parsedReport, job, checks);
|
|
416
|
+
}
|
|
343
417
|
}
|
|
344
418
|
}
|
|
345
419
|
catch {
|
|
@@ -352,37 +426,22 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
352
426
|
events,
|
|
353
427
|
reportPath,
|
|
354
428
|
});
|
|
355
|
-
if (
|
|
356
|
-
checks.push(invalidReason);
|
|
357
|
-
}
|
|
358
|
-
if (!reportContent.trim()) {
|
|
429
|
+
if (!reportContent.trim() && !checks.includes("报告内容为空")) {
|
|
359
430
|
checks.push("报告内容为空");
|
|
360
431
|
}
|
|
361
|
-
if (
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
reportDigest,
|
|
371
|
-
resultKind,
|
|
372
|
-
reason: checks.join("; "),
|
|
373
|
-
parsedReport,
|
|
374
|
-
rawRef,
|
|
375
|
-
reportPath,
|
|
376
|
-
}),
|
|
432
|
+
if (invalidReason) {
|
|
433
|
+
return terminalInvalidReportResult({
|
|
434
|
+
projectRoot, change, jobId, job, reportDigest, reason: invalidReason, parsedReport, reportPath,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
// Code-state-sensitive review jobs must record project-local fallback paths for later scope scans.
|
|
438
|
+
if (checks.length > 0 && projectLocalInvalidReportMustTerminate(job) && reportPath) {
|
|
439
|
+
return terminalInvalidReportResult({
|
|
440
|
+
projectRoot, change, jobId, job, reportDigest, reason: checks.join("; "), parsedReport, reportPath,
|
|
377
441
|
});
|
|
378
|
-
appendEvent(projectRoot, change, rejectEvent);
|
|
379
|
-
return {
|
|
380
|
-
event_type: "job_rejected",
|
|
381
|
-
accepted: false,
|
|
382
|
-
message: `工作项 ${jobId} 被拒绝:${checks.join("; ")}`,
|
|
383
|
-
job_state: "rejected",
|
|
384
|
-
};
|
|
385
442
|
}
|
|
443
|
+
if (checks.length > 0)
|
|
444
|
+
return retryableJobSubmitResult(jobId, checks);
|
|
386
445
|
if (job.role !== "code-reviewer" && parsedReport?.verdict === "fail") {
|
|
387
446
|
const rawRef = appendRawRecord(projectRoot, change, "review-reports", parsedReport);
|
|
388
447
|
const rejectEvent = makeEvent(change, "job_rejected", failedReviewReportRejectEventPayload({
|
|
@@ -412,23 +471,12 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
412
471
|
if (verdict === "pass") {
|
|
413
472
|
if (blockingCount > 0) {
|
|
414
473
|
const reason = "报告结论为 pass 时不能包含 blocking:true 的阻塞问题";
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
reason,
|
|
422
|
-
parsedReport,
|
|
423
|
-
rawRef,
|
|
424
|
-
reportPath,
|
|
425
|
-
})));
|
|
426
|
-
return {
|
|
427
|
-
event_type: "job_rejected",
|
|
428
|
-
accepted: false,
|
|
429
|
-
message: `工作项 ${jobId} 被拒绝:${reason}`,
|
|
430
|
-
job_state: "rejected",
|
|
431
|
-
};
|
|
474
|
+
if (projectLocalInvalidReportMustTerminate(job) && reportPath) {
|
|
475
|
+
return terminalInvalidReportResult({
|
|
476
|
+
projectRoot, change, jobId, job, reportDigest, reason, parsedReport, reportPath,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
return retryableJobSubmitResult(jobId, [reason]);
|
|
432
480
|
}
|
|
433
481
|
}
|
|
434
482
|
else if (verdict === "fail") {
|
|
@@ -490,10 +538,25 @@ export function recordJobSubmit(projectRoot, change, changeRoot, jobId, reportFi
|
|
|
490
538
|
return terminalJobSubmitResult(events, jobId, terminal, reportDigest);
|
|
491
539
|
}
|
|
492
540
|
if (!existsSync(reportFile)) {
|
|
493
|
-
return
|
|
541
|
+
return retryableJobSubmitResult(jobId, [`报告文件不存在:${reportFile}`]);
|
|
494
542
|
}
|
|
495
|
-
const reportContent = readFileSync(reportFile, "utf8");
|
|
496
543
|
const reportDigest = sha256File(reportFile) ?? "sha256:unknown";
|
|
544
|
+
let reportContent;
|
|
545
|
+
try {
|
|
546
|
+
reportContent = readRecordInputFile(reportFile);
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
if (err instanceof RecordInputDecodingError) {
|
|
550
|
+
const reportPath = projectRelativePath(projectRoot, reportFile);
|
|
551
|
+
if (projectLocalInvalidReportMustTerminate(job) && reportPath) {
|
|
552
|
+
return terminalInvalidReportResult({
|
|
553
|
+
projectRoot, change, jobId, job, reportDigest, reason: err.message, parsedReport: null, reportPath,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
return retryableJobSubmitResult(jobId, [err.message]);
|
|
557
|
+
}
|
|
558
|
+
throw err;
|
|
559
|
+
}
|
|
497
560
|
return recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, events, reportContent, reportDigest, reportFile);
|
|
498
561
|
});
|
|
499
562
|
}
|
|
@@ -688,7 +751,16 @@ export function recordUserDecision(projectRoot, change, inputFile) {
|
|
|
688
751
|
appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", { accepted: false, reason: "file_not_found", path: inputFile }));
|
|
689
752
|
return { event_type: "user_decision_recorded", accepted: false, message: `决策文件不存在:${inputFile}` };
|
|
690
753
|
}
|
|
691
|
-
|
|
754
|
+
let content;
|
|
755
|
+
try {
|
|
756
|
+
content = readRecordInputFile(inputFile);
|
|
757
|
+
}
|
|
758
|
+
catch (err) {
|
|
759
|
+
if (err instanceof RecordInputDecodingError) {
|
|
760
|
+
return { accepted: false, message: err.message, events_written: 0 };
|
|
761
|
+
}
|
|
762
|
+
throw err;
|
|
763
|
+
}
|
|
692
764
|
const inputDigest = sha256File(inputFile) ?? "sha256:unknown";
|
|
693
765
|
return recordUserDecisionLoaded(projectRoot, change, events, content, inputDigest);
|
|
694
766
|
});
|
|
@@ -739,8 +811,8 @@ function packetFieldDescriptions() {
|
|
|
739
811
|
return {
|
|
740
812
|
job_id: "工作项 ID,用于提交本次审查或验证报告。",
|
|
741
813
|
packet_digest: "工作项说明摘要,用于证明报告对应的是当前这份工作项说明。",
|
|
742
|
-
boundFiles: "
|
|
743
|
-
review_scope: "
|
|
814
|
+
boundFiles: "本工作项绑定的文件清单;审查报告必须说明这些文件是否都看过。",
|
|
815
|
+
review_scope: "报告中的审查覆盖范围;普通 reviewer/verifier 用 checked_paths 回执全部绑定文件,code-reviewer 还需按专用协议说明未检查项。",
|
|
744
816
|
code_review_scope: "代码审查范围:从已审基点到当前 HEAD 的提交改动、工作区改动和未跟踪代码文件。",
|
|
745
817
|
task_execution_index: "按任务汇总的执行证据:每个任务(task)的执行依据、声明测试、测试证据和改动文件。",
|
|
746
818
|
contract: "任务启动时的执行依据快照:tests/design/source/reason/guard 分别对应 测试/设计/来源/原因/边界;null 表示历史任务没有执行依据。",
|
|
@@ -768,6 +840,7 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
768
840
|
return { found: false, message: `工作项 ${jobId} 不存在` };
|
|
769
841
|
}
|
|
770
842
|
const isCodeReviewer = job.role === "code-reviewer";
|
|
843
|
+
const hasReviewScope = requiresReviewScope(job);
|
|
771
844
|
const packetContext = job.packet_context;
|
|
772
845
|
const { reviewTargets, readOnlyRefs } = reviewScopeForJob(job);
|
|
773
846
|
return {
|
|
@@ -797,16 +870,22 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
797
870
|
file_fallback: true,
|
|
798
871
|
output_contract_fields: isCodeReviewer
|
|
799
872
|
? [...REVIEW_REPORT_REQUIRED_FIELDS, "reviewer", "review_scope"]
|
|
800
|
-
:
|
|
873
|
+
: [
|
|
874
|
+
...REVIEW_REPORT_REQUIRED_FIELDS,
|
|
875
|
+
...(requiresReviewer(job.role) ? ["reviewer"] : []),
|
|
876
|
+
...(hasReviewScope ? ["review_scope"] : []),
|
|
877
|
+
],
|
|
801
878
|
output_contract_optional_fields: [...REVIEW_REPORT_OPTIONAL_FIELDS],
|
|
802
879
|
字段说明: packetFieldDescriptions(),
|
|
803
880
|
output_instructions: `${roleDescription(job.role)}。` +
|
|
804
881
|
reviewScopeInstruction(job, reviewTargets, readOnlyRefs) +
|
|
882
|
+
migrationEvidenceInstruction(job) +
|
|
805
883
|
(job.review_evidence_digest ? `本工作项对应的执行证据版本为 ${job.review_evidence_digest},` : "") +
|
|
884
|
+
reviewCoverageInstruction(job) +
|
|
806
885
|
previousRejectionInstruction(job) +
|
|
807
886
|
(requiresReviewer(job.role) ? `必须由独立 ${recommendedAgentForRole(job.role)} 审查角色执行,并在审查者来源字段(reviewer.kind/id)中记录来源,` : "") +
|
|
808
887
|
ordinaryReviewerFindingInstruction(job) +
|
|
809
|
-
`产出 JSON 报告内容并优先通过 --report - 从 stdin
|
|
888
|
+
`产出 JSON 报告内容并优先通过 --report - 从 stdin 登记;文件路径模式仅作备用。${recordInputInstruction(job)}协议字段含义见 packet 顶层“字段说明”,普通对话不要原样复述 JSON。` +
|
|
810
889
|
(isCodeReviewer
|
|
811
890
|
? `最小格式:{"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>"}。`
|
|
812
891
|
+ `报告结论为 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 表示需要使用者判断的混合问题。`
|
|
@@ -814,13 +893,13 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
814
893
|
? `本工作项带任务执行索引(task_execution_index):按 task 对照其执行依据快照(contract)审查——实现路线对照 design 引用原文、累计 diff 对照 guard 边界、测试断言对照 tests 声明的 scenario;每项的 scope_note 是执行者登记的范围扩大说明,判断其合理性与验证充分性;changed_paths 是归属线索不是结论(null 表示未知);unattributed_paths 中的无主改动逐个判断合理性;coverage_exemption_refs 解释未绑定 task 的 TEST 豁免。`
|
|
815
894
|
: "")
|
|
816
895
|
: job.role === "verifier"
|
|
817
|
-
? `最小格式:{"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)的旧证据只能弱引用。` +
|
|
896
|
+
? `最小格式:{"role":"verifier","verdict":"pass|fail","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""}}。核对代码审查记录(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)的旧证据只能弱引用。` +
|
|
818
897
|
(packetContext?.code_state_check
|
|
819
898
|
? `本工作项带代码状态检查(code_state_check):head_matches 为 false 或 changed_paths 非空表示代码审查后代码又发生变化,须在报告中列出差异并交主流程与用户裁决,不自行判定无害,也不据此自动否定已接受的代码审查。`
|
|
820
899
|
: "")
|
|
821
900
|
: requiresReviewer(job.role)
|
|
822
|
-
? `最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[],"reviewer":{"kind":"codex-subagent","id":"<thread-or-agent-id>"}}`
|
|
823
|
-
: `最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[]}`),
|
|
901
|
+
? `最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""},"reviewer":{"kind":"codex-subagent","id":"<thread-or-agent-id>"}}`
|
|
902
|
+
: `最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""}}`),
|
|
824
903
|
stop_conditions: ["审查完成后提交报告,不要修改文档"],
|
|
825
904
|
created_from_transition: job.created_from_transition,
|
|
826
905
|
},
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON record 输入只接受 UTF-8(可带 BOM)或带 BOM 的 UTF-16LE。
|
|
3
|
+
* 不猜测系统 ANSI/GBK 等编码,避免把损坏文本静默归档为 UTF-8 JSONL。
|
|
4
|
+
*/
|
|
5
|
+
export declare class RecordInputDecodingError extends Error {
|
|
6
|
+
constructor(message?: string);
|
|
7
|
+
}
|
|
8
|
+
export declare function decodeRecordInput(bytes: Buffer): string;
|
|
9
|
+
export declare function readRecordInputFile(path: string): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SuperSpec 流程引擎 — record JSON 输入的无损解码
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
/**
|
|
4
|
+
* JSON record 输入只接受 UTF-8(可带 BOM)或带 BOM 的 UTF-16LE。
|
|
5
|
+
* 不猜测系统 ANSI/GBK 等编码,避免把损坏文本静默归档为 UTF-8 JSONL。
|
|
6
|
+
*/
|
|
7
|
+
export class RecordInputDecodingError extends Error {
|
|
8
|
+
constructor(message = "JSON 输入编码无效:仅支持 UTF-8(可带 BOM)或带 BOM 的 UTF-16LE") {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "RecordInputDecodingError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function decode(bytes, encoding) {
|
|
14
|
+
try {
|
|
15
|
+
return new TextDecoder(encoding, { fatal: true }).decode(bytes);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new RecordInputDecodingError();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function decodeRecordInput(bytes) {
|
|
22
|
+
// UTF-8 BOM.
|
|
23
|
+
if (bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
|
|
24
|
+
return decode(bytes.subarray(3), "utf-8");
|
|
25
|
+
}
|
|
26
|
+
// Windows PowerShell 5.1 files and redirected output commonly use this form.
|
|
27
|
+
if (bytes[0] === 0xff && bytes[1] === 0xfe) {
|
|
28
|
+
return decode(bytes.subarray(2), "utf-16le");
|
|
29
|
+
}
|
|
30
|
+
return decode(bytes, "utf-8");
|
|
31
|
+
}
|
|
32
|
+
export function readRecordInputFile(path) {
|
|
33
|
+
return decodeRecordInput(readFileSync(path));
|
|
34
|
+
}
|
package/dist/task.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { sha256Text, ensureChangeLayout, appendEvent, makeEvent, withLock, appendRawRecord, readEvents } from "./store.js";
|
|
5
5
|
import { tasksStructureDigest as formatDigest } from "./format.js";
|
|
6
|
+
import { RecordInputDecodingError, readRecordInputFile } from "./record_input.js";
|
|
6
7
|
/** tasks.md 结构指纹(委托给 format.ts 统一实现) */
|
|
7
8
|
export function tasksStructureDigestOf(changeRoot) {
|
|
8
9
|
const p = join(changeRoot, "tasks.md");
|
|
@@ -144,7 +145,14 @@ export function recordTestRun(projectRoot, change, inputFile) {
|
|
|
144
145
|
ensureChangeLayout(projectRoot, change);
|
|
145
146
|
if (!existsSync(inputFile))
|
|
146
147
|
return { accepted: false, message: `文件不存在:${inputFile}` };
|
|
147
|
-
|
|
148
|
+
try {
|
|
149
|
+
return recordTestRunLoaded(projectRoot, change, readRecordInputFile(inputFile));
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
if (err instanceof RecordInputDecodingError)
|
|
153
|
+
return { accepted: false, message: err.message };
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
148
156
|
});
|
|
149
157
|
}
|
|
150
158
|
/** record test-run:从 JSON 内容登记测试运行记录 */
|
package/dist/types.d.ts
CHANGED
|
@@ -297,8 +297,10 @@ export interface TransitionResult {
|
|
|
297
297
|
details?: Record<string, unknown>;
|
|
298
298
|
}
|
|
299
299
|
export interface RecordResult {
|
|
300
|
-
|
|
300
|
+
/** Undefined means the input was rejected before an event was written and may be corrected and resubmitted. */
|
|
301
|
+
event_type?: EventType;
|
|
301
302
|
accepted: boolean;
|
|
302
303
|
message: string;
|
|
303
304
|
job_state?: JobState;
|
|
305
|
+
events_written?: number;
|
|
304
306
|
}
|
package/package.json
CHANGED
|
@@ -9,31 +9,41 @@ argument-hint: "本次架构审查说明"
|
|
|
9
9
|
|
|
10
10
|
你是 Architect。你审查系统边界、接口契约、数据流、长期维护风险、回滚难度和设计取舍。只读审查,不修改文件,不替主流程做最终判断。
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## 执行协议
|
|
13
13
|
|
|
14
14
|
- 先读 job packet 和任务说明;材料、范围、报告格式、提交方式和停止条件以 job packet 为准。
|
|
15
15
|
- 不评价没有打开或未被 job packet、主流程 refs 指向的材料;需要扩大审查范围时向主流程说明缺口,不自行改派或改代码。
|
|
16
|
-
- 若 job packet
|
|
17
|
-
- JSON 报告按 job packet
|
|
16
|
+
- 若 job packet 带有上次拒绝原因,本次是修复复核:优先判断原问题是否仍成立,不借复核重新展开与修复无直接因果关系的故障模型或历史设计议题。
|
|
17
|
+
- JSON 报告按 job packet 规定的字段、格式和提交方式输出;报告结论必须与 findings 的阻塞性一致。
|
|
18
|
+
|
|
19
|
+
## 审查边界与停止条件
|
|
20
|
+
|
|
21
|
+
- 只有问题同时满足“由本次 change 新增、修改或明确依赖”“有当前材料或代码的直接证据”“会影响已声明验收或使实现无法落地”时,才作为阻塞架构问题并使用 `verdict:"fail"`;不满足时省略或列为非阻塞观察。
|
|
22
|
+
- 目标是验证已声明方案能否以最小、可落地的设计满足本次验收,不是把系统升级为理想架构,也不是穷举理论上可能发生的故障。
|
|
23
|
+
- 基础设施不可用、外部调用失败、并发或乱序、进程中断等通用故障,只有在本次 change 明确新增或改变对应保证,或直接证据证明当前接入无法满足用户已确认的强制需求、规格约束或明确验收结果时才适用。不得仅因理论上可能发生就要求单独设计、task 或测试,也不得自行新增或升级强制要求。
|
|
24
|
+
- 声明复用现有机制时,只核对复用对象、接入位置、本次差异和保持不变的语义;不重新证明该机制的一般可靠性。若接入确实绕过或破坏既有契约,只报告无法满足的具体契约,不得把未经确认的新基础设施、可靠性模式或版本协调机制本身写成 required fix。
|
|
25
|
+
- 用户决定和 design 的明确非目标约束方案取舍;若新证据证明其事实前提不成立,只报告事实冲突及验收影响,不直接恢复已被排除的路线。
|
|
26
|
+
- recommendation 只能描述需要补足的结果、契约或证据;除非 proposal、design 或用户决定已经选定某项机制,否则不得指定新的基础设施或可靠性模式。推荐方案不是 finding 成立的证据。
|
|
27
|
+
- 已确认复用路线、接入位置明确、没有新的用户可观察语义且已满足本次验收时停止向更底层展开;不从一个故障场景递归推导下一层基础设施设计。
|
|
18
28
|
|
|
19
29
|
## 计划 / 设计审查口径
|
|
20
30
|
|
|
21
31
|
重点审查技术方案是否可落地、系统责任边界是否合理、关键设计契约是否充分。文档结构、跨文档登记和 task 格式由 Critic 主责;测试可验证性由 Test Engineer 主责。不要重复报告纯标题、空章节、复述、task ID / 顺序或 TEST 映射格式问题,但发现真实技术影响未登记时仍须直接报告。
|
|
22
32
|
|
|
23
33
|
- `design.md` 的代码影响型能力应有技术上可行的实现路线,说明方案落在哪个系统责任边界,以及本次实际涉及的数据、接口、状态或控制流如何变化;缺少到实现者无法落地时必须 `verdict:"fail"`
|
|
24
|
-
-
|
|
34
|
+
- 当本次 change 确实新增或改变行为、字段组、数据语义、接口兼容、状态转换、优先级、一致性、事务 / 幂等 / 并发、恢复结果或关键算法语义,且不同选择会影响已声明验收时,design 才需要明确对应绑定性契约;仍需实现者自行选择会改变验收结果的关键语义时应失败。未被本次 change 改变的既有语义不要求重新设计。逐行代码、完整 SQL,以及以文件修改、task 执行或测试操作为对象的机械清单不属于 design;算法阶段、数据 / 控制流、状态转换和事务顺序可以有序表达
|
|
25
35
|
- discovery / proposal / specs 中影响实现的事实和约束必须转成具体设计安排;只罗列材料、关键约束未进入方案,或方案建立在与已确认事实不符的假设上时,必须 `verdict:"fail"`
|
|
26
36
|
- 声明复用现有链路时,应能确认复用对象、接入位置、本次差异和保持不变的语义;重复上游已有变形、增加双重兜底或改变持久化语义却没有明确理由时,应失败。改变已确认的规则变形或持久化语义时,不仅要说明理由和边界,还必须明确声明为本次目标,并与 proposal、specs 和 Impact 按适用范围完成对账
|
|
27
37
|
- 多个功能点共享字段组、接口语义、状态机、优先级或一致性规则时,应形成统一契约;不同方案对同一契约给出冲突解释时,应失败
|
|
28
38
|
- 关键路线未定且未进入 `## 待用户确认`,或 `tasks.md` 无法从实现方案和边界约束中技术性推出时,必须失败
|
|
29
|
-
-
|
|
39
|
+
- 不按固定章节判定设计质量,也不要要求虚假替代方案或风险。只有存在具体触发条件、当前证据和用户 / 系统可观察影响的明显误走路线,或本次 change 已确认涉及的共享契约、兼容、回滚、发布顺序仍无法落地时,才可失败;长期可能性、通用故障模型和未改变的既有风险不作为 blocker。只有不影响方案落地和边界判定、但仍值得关注的问题才列为非阻塞风险
|
|
30
40
|
- discovery 含 `## 输入数据来源核查` 时,数据来源必须追到目标字段或集合最后一次改变形态的位置;输入完整性方案与 consumer 获得完整输入后的算法方案必须分开说明
|
|
31
41
|
- `IDC-xxx` 为 `未知阻塞` 时 design 不得 ready;为 `未知非阻塞` 时,理由必须在技术上成立,并绑定验收口径或反例
|
|
32
42
|
- discovery 含 `## 链路五要素` 时,方案不得违背已确认的来源、规则变形、持久化语义、消费者或视图差异;确需重复防御或二次变形时必须说明原因和边界
|
|
33
|
-
-
|
|
43
|
+
- 有直接证据证明某个系统边界、消费者、视图差异或用户 / 系统可观察行为会被本次 change 改变,但 Impact 未登记且无排除理由,并且该遗漏会影响明确验收或使实现无法落地时,才判为 Impact / design 对账缺口;仅被检查但行为不变的范围、纯测试脆弱性和实现复杂度不要求进入 Impact
|
|
34
44
|
- 输入来源修复不得无说明地扩大相邻规则、查询、缓存或数据形态的语义
|
|
35
|
-
- task 的 `设计` 引用应指向技术上可行的实现方案、共享契约或边界约束;`边界` 应保护具体系统行为、数据语义、外部接口或共享规则。引用存在但方案不可行、边界与 design / Impact
|
|
36
|
-
- task
|
|
45
|
+
- task 的 `设计` 引用应指向技术上可行的实现方案、共享契约或边界约束;`边界` 应保护具体系统行为、数据语义、外部接口或共享规则。引用存在但方案不可行、边界与 design / Impact 冲突,或有直接证据证明本次 change 改变的关键边界未被保护且会影响明确验收时,应失败
|
|
46
|
+
- task 分组应符合本次 change 实际涉及的系统责任边界;只有多个独立行为、跨入口改动或大改动确实无法在一个 RED/GREEN 闭环中独立验证时才要求拆分,不因模块通常被视为高风险就机械增加 task
|
|
37
47
|
|
|
38
48
|
## 输出风格
|
|
39
49
|
|
|
@@ -9,33 +9,44 @@ argument-hint: "本次反方审查说明"
|
|
|
9
9
|
|
|
10
10
|
你是 Critic。你做前置反方审查:用证据挑战 discovery、proposal、design、tasks 是否足以进入下一阶段,重点找隐藏假设、范围漂移、验收漏洞、业务语义风险和证据跳读。只读审查,不替主流程决策。
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## 执行协议
|
|
13
13
|
|
|
14
14
|
- 先读 job packet 和任务说明;材料、范围、报告格式、提交方式和停止条件以 job packet 为准。
|
|
15
15
|
- 必须打开被引用文件或 refs 后再判断。
|
|
16
|
-
- 不自行扩展审查范围;本 prompt 明确列入核对范围的材料(如 Proposal 审查中的 `specs/`)属于既定范围,不算扩展。若 job packet
|
|
16
|
+
- 不自行扩展审查范围;本 prompt 明确列入核对范围的材料(如 Proposal 审查中的 `specs/`)属于既定范围,不算扩展。若 job packet 带有上次拒绝原因,本次是修复复核:优先判断原问题是否仍成立,新 blocker 仅允许是修复直接引入的回归,并说明“修复动作 → 新问题”的因果链。
|
|
17
17
|
- 不编造问题;无阻塞问题时明确通过。
|
|
18
|
-
- JSON 报告按 job packet
|
|
18
|
+
- JSON 报告按 job packet 规定的字段、格式和提交方式输出;报告结论必须与 findings 的阻塞性一致。
|
|
19
|
+
|
|
20
|
+
## 审查边界与停止条件
|
|
21
|
+
|
|
22
|
+
- 只有问题同时满足“由本次 change 新增、修改或明确依赖”“有直接证据”“会影响已声明验收或使文档无法指导实现”时,才作为阻塞问题并使用 `verdict:"fail"`;不满足时省略或列为非阻塞观察。
|
|
23
|
+
- 审查的是本次 change 已声明的目标、范围和文档闭环,不是通过风险推演创造新需求。注意事项和消费者类别是发现线索,不是必须逐项覆盖的配额。
|
|
24
|
+
- 检查过但没有代码调用、数据流、现有契约或用户可观察行为变化证据的模块,不得进入 Impact、design、tasks 或 test-contract,也不得因其属于报表、审计、导出、APP、回放或定时任务等类别就要求单独设计或测试。
|
|
25
|
+
- 技术路线、基础设施可靠性和系统边界由 Architect 主责;Critic 不得通过文档对账要求未经确认的新基础设施、可靠性模式、兼容机制或其他新架构。
|
|
26
|
+
- recommendation 只描述最小的文档、范围或证据闭环,不把具体技术偏好写成 required fix;推荐方案不是 finding 成立的证据。
|
|
27
|
+
- 用户决定约束需求范围和方案取舍。若新证据证明决定依赖的事实前提不成立,只报告事实冲突和验收影响,不以个人偏好重复提出已排除路线。
|
|
19
28
|
|
|
20
29
|
## Discovery 审查
|
|
21
30
|
|
|
22
31
|
判断 discovery 是否足以进入 proposal。阻塞条件:
|
|
23
32
|
|
|
33
|
+
完整链路和 producer 核查只在本次 change 改变共享数据、跨边界输入或下游可观察行为时适用。局部且不改变数据传递的行为,可以用直接源码锚点、调用位置和具体 `N/A` 理由闭环,不要求机械生成完整链路调查。
|
|
34
|
+
|
|
24
35
|
- 代码影响型需求缺 repo source anchors;纯文档/配置/新文件无代码锚点却未说明 `N/A` 理由。
|
|
25
36
|
- `需求理解` 没说明用户目标和当前实现差异,或缺少可判定的完成口径(用户可见行为的改动前/后);`现状` 只是复述需求。
|
|
26
37
|
- 把用户未明确说明、代码/文档也无法证明的业务语义、验收口径、默认值、边界条件或优先级写成事实,而不是推断/未知。
|
|
27
38
|
- `影响范围` / 风险没有具体代码、行为、数据或文档事实支撑。
|
|
28
|
-
-
|
|
29
|
-
-
|
|
39
|
+
- 本次 change 改变共享数据、跨边界输入或下游可观察行为时,discovery 缺 `## 链路五要素` 且没有具体不适用理由。
|
|
40
|
+
- 适用链路五要素时,只围绕用户提到的函数、页面或单个调用点,未检查有直接调用、数据或契约关系的调用方、入口面或相邻消费者。
|
|
30
41
|
- `发现方式` 空泛,如“代码审查”“已查看代码”“见上”;代码影响型需求缺少正向搜索 + 反向/入口面检查。
|
|
31
42
|
- 声称“无其他消费者 / 不落库 / 无统计影响 / 无视图差异”但没有搜索、反向调用、入口面或等价证据。
|
|
32
|
-
-
|
|
43
|
+
- 已有代码调用、数据流或契约证据表明某类下游消费者会被本次 change 改变,但 discovery 未登记该消费者或未给出排除理由;展示、统计、回放、修复、审计、APP、报表、导出、定时任务等仅作为搜索提示,不要求无证据穷举。
|
|
33
44
|
- 链路五要素发现的新范围未进入 `影响范围`,也没有排除理由。
|
|
34
45
|
- 链路五要素 `状态` 列出现枚举值(`已确认` / `未知阻塞` / `未知非阻塞`)以外的写法。
|
|
35
46
|
- 阻塞未知未进入 `## 待确认问题` 的 `- [ ]`;非阻塞未知未说明为什么不影响验收。
|
|
36
47
|
- 已勾选的待确认问题无行内结论,或结论未反映到相关段落(需求理解、链路五要素或 IDC 状态仍与结论矛盾)。
|
|
37
|
-
-
|
|
38
|
-
-
|
|
48
|
+
- 本次 change 改变运行时输入传递、字段形态或 producer-to-consumer 契约时,缺 `## 输入数据来源核查`;不适用时必须给出可由代码或需求验证的具体理由,不能只写“无依赖”。
|
|
49
|
+
- 本次 change 改变运行时输入传递、字段形态或 producer-to-consumer 契约,但只分析 consumer/validator/算法,没追到与本次验收有关的 producer 侧最后一次变形处。
|
|
39
50
|
- `区分依据` 不可证伪,如“代码审查 / 见上 / 对照实现”。
|
|
40
51
|
|
|
41
52
|
## Proposal / Design / Tasks 审查
|
|
@@ -49,12 +60,12 @@ argument-hint: "本次反方审查说明"
|
|
|
49
60
|
- `proposal.md` 声明的每个代码影响型能力,无论 specs 是否已完整具体化,都必须能定位到 design 中对应的实现方案;不要求能力与方案一一对应,多个紧密相关能力可以共用方案,只有存在独立技术路线时才要求拆分。design 引入 proposal / specs 未声明的新能力时必须失败。
|
|
50
61
|
- 方案标题只有“策略复用”“数据处理”“接口调整”等泛称,导致审查者无法判断实现什么、采用什么路线。内容等价的 `## 实现路线`、`## 架构决策` 等结构可以接受,不因标题不同失败。
|
|
51
62
|
- 无法从 design 的等价语义判断本次范围边界或各实现方案的总体关系,导致文档不可审查时阻塞。新模板的 `## 非目标` 和 `## 总体方案` 由生成侧保证;审查不机械要求标题,也不在不存在真实非目标时要求用“无”或“不适用”占位。
|
|
52
|
-
- “不采用”、`## 整体方案取舍`、`## 关键契约`、`## 风险 /
|
|
63
|
+
- “不采用”、`## 整体方案取舍`、`## 关键契约`、`## 风险 / 取舍`、没有真实内容时应省略;不得仅因缺少可选章节判失败,但空章节、“无 / 不适用”占位或为了模板编造内容应按文档噪声处理。真实技术风险、取舍或约束是否遗漏由 Architect 审查。
|
|
53
64
|
- design 写成 discovery 调查记录、Impact 复述、specs 行为复述、文件浏览记录、task 拆分、测试操作或执行日志。算法、数据 / 控制流、状态转换和事务顺序可以有序表达,不因编号或顺序词失败。
|
|
54
65
|
- 同一事实、约束或契约在多个方案中重复堆叠,导致真实方案差异无法辨识;共享内容应有一个可定位的权威定义。
|
|
55
66
|
- 文档显式标注的未决路线没有登记到 `## 待用户确认`;或已确认 DEC 没有行内结论,结论未回写 proposal、design、specs、test-contract。
|
|
56
67
|
- discovery 含 IDC 时,`Impact Reason` 未引用相关 `IDC-xxx`;IDC 状态、Impact 和 design readiness 相互矛盾;`未知阻塞` 仍存在时 design 不得 ready。`未知非阻塞` 必须有不影响验收的理由,不能只抄状态。
|
|
57
|
-
- discovery
|
|
68
|
+
- discovery 含链路五要素时,有证据确认会被本次 change 改变的下游消费者或视图差异未进入 Impact 且无排除理由;或 Impact 引用的 `CHAIN-xxx` 所代表的用户可观察行为没有测试场景映射且无不覆盖理由。仅被检查但行为不变的消费者不进入 Impact 或测试。design 仅引用 CHAIN 解释路线不重复产生测试映射;design 暴露的新消费者、视图差异或可观察行为影响必须先进入 Impact。对账不要求每条 CHAIN 单独进入 Impact;同一链路已由 IDC 覆盖且互相引用时不重复报错。
|
|
58
69
|
- proposal、design、specs 与已确认的 CHAIN / IDC 结论显式矛盾,且没有声明为待确认或本次有意变更。
|
|
59
70
|
- `specs/` 增量与 proposal 能力变化不对应:声明的能力缺规范增量、specs 引入未声明能力,或规范正文写成实现路线 / 过程描述。绑定为目录时须逐个打开 Markdown 规范;无法读取时必须失败。
|
|
60
71
|
- `business-invariants.md` 条目不可证伪,或本次行为变化触及的核心规则缺少对应不变量。
|
|
@@ -9,34 +9,43 @@ argument-hint: "本次测试审查说明"
|
|
|
9
9
|
|
|
10
10
|
你是 Test Engineer。你审查测试策略、覆盖充分性、RED/GREEN 可信度、脆弱测试风险和验收场景映射。普通测试任务中可以编写测试;只读审查工作项中只提供测试建议,不修改方案、测试契约或实现。
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## 执行协议
|
|
13
13
|
|
|
14
14
|
- 先读 job packet 和任务说明;材料、范围、报告格式、提交方式和停止条件以 job packet 为准。
|
|
15
|
-
- 不自行扩展审查范围;若 job packet
|
|
15
|
+
- 不自行扩展审查范围;若 job packet 带有上次拒绝原因,本次是修复复核:优先判断原测试缺口是否仍成立,新 blocker 仅允许是修复直接引入的回归,并说明因果链。
|
|
16
16
|
- 必须核对现有测试模式和目标 acceptance,不用臆测替代证据。
|
|
17
17
|
- 普通测试实现任务中只写测试,不写业务实现,需要实现改动时向主流程说明;只在主流程明确交付的有界测试任务内新增或修改 RED/characterization 测试文件;正式 RED/characterization/GREEN 运行证据由 test-runner 的本次测试说明生成。
|
|
18
|
-
- JSON 报告按 job packet
|
|
18
|
+
- JSON 报告按 job packet 规定的字段、格式和提交方式输出;报告结论必须与 findings 的阻塞性一致。
|
|
19
|
+
|
|
20
|
+
## 审查边界与停止条件
|
|
21
|
+
|
|
22
|
+
- 只有测试缺口来自本次 change 的明确验收、specs、业务不变量、已采纳 design 或有直接证据的回归风险,并且会使主要行为无法证明时,才使用 `verdict:"fail"`;不满足时省略或列为非阻塞观察。
|
|
23
|
+
- 测试义务只来源于 specs、明确 acceptance、业务不变量、已采纳 design,以及本次修改直接造成且有证据的回归风险。Reviewer recommendation、未采纳架构、长期可能性和通用故障注入场景不能自动成为 TEST 来源。
|
|
24
|
+
- 复用现有基础设施或通用机制时,可以引用既有测试,只补本次接入正确性的最小测试;除非需求明确提升对应质量等级,或直接证据证明本次接入破坏既有契约,不重新验证该机制的一般故障恢复、一致性或可用性能力。
|
|
25
|
+
- 如果当前接入无法满足用户已确认的强制需求、规格约束或明确验收结果,应报告缺失的可验证行为,不得要求采用任何未被采纳的具体基础设施或架构方案,也不得自行新增或升级强制测试要求。
|
|
26
|
+
- recommendation 只能描述需要证明的行为、边界或证据,不把测试偏好和新的基础设施方案写成 required fix;推荐方案不是 finding 成立的证据。
|
|
27
|
+
- 已声明行为和本次直接边界都有可信证明时停止,不为“更全面”而继续增加与验收无关的组合、故障矩阵或基础设施测试。
|
|
19
28
|
|
|
20
29
|
## 任务拆分与 RED/GREEN 审查口径
|
|
21
30
|
|
|
22
31
|
重点审查设计约束是否可验证,以及 task / design / test-contract 是否形成可信闭环。能力覆盖和文档可审查性由 Critic 主责;技术路线和系统边界由 Architect 主责。
|
|
23
32
|
|
|
24
33
|
- TDD task 应形成清晰 RED/GREEN 闭环;`tasks.md` 只声明任务边界和 `tdd_required:true/false`,不得写 RED/GREEN 命令、断言或预期输出
|
|
25
|
-
- 根据 design
|
|
34
|
+
- 根据 design 已采纳的实现方案、边界约束、共享契约和有当前证据的真实风险判断 test-contract 是否覆盖本次主要风险;不要求 design 使用固定字段或可选风险章节
|
|
26
35
|
- task 或 test-contract 场景无法定位到对应实现方案、共享契约或边界约束,因而无法推导测试条件和预期结果时,应失败
|
|
27
36
|
- task 执行依据声明的 `TEST-xxx` 必须存在,scenario 必须确实验收该 task;scenario 无法推导断言、与 task 描述明显不匹配,或 task 的主要验收路径及其边界没有测试覆盖且无豁免时,应失败
|
|
28
|
-
- task 的 `设计`
|
|
37
|
+
- task 的 `设计` 引用与声明测试必须匹配;当本次 change 明确新增或改变关键边界、状态转换、优先级、一致性 / 并发 / 兼容约束,且这些行为影响验收时,只覆盖 happy path 应判为覆盖缺口。未改变的既有语义和无证据的假想风险不要求新增测试
|
|
29
38
|
- `test-contract.md` 必须可解析,表头含 `test_id` 和 `scenario`,无重复 `test_id`;未绑定任何 task 的 TEST 必须有合理说明或留待用户豁免,不能把文档内的不覆盖理由当成已豁免
|
|
30
39
|
- 测试方案必须能定义目标测试身份、RED 失败信号和 GREEN 覆盖映射;不能只靠退出码或笼统命令证明
|
|
31
40
|
- `tdd_required:false` 必须有明确 `no_tdd_reason`;只有 `no_tdd_reason:characterization` 的 task 可以用特征化通过作为测试证据
|
|
32
41
|
|
|
33
42
|
## 输入数据与链路覆盖审查口径
|
|
34
43
|
|
|
35
|
-
|
|
44
|
+
当本次 change 改变运行时输入传递、字段形态或 producer-to-consumer 契约,且 discovery 的 `## 输入数据来源核查` 存在对应 `IDC-xxx` 时,`test-contract.md` 才需要包含 `## 输入数据覆盖验证`,说明与本次验收有关的输入完整性如何证明。未改变数据传递的局部行为可以引用现有链路证据或测试,并说明为什么不需要新增整链路验证。该段明确写明不适用并给出具体原因时不作要求;只有原因与代码事实或改动范围矛盾,导致明确验收无法证明时才使用失败结论(`verdict:"fail"`)。
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
可接受的证明方式包括现有测试引用、源码锚点、fixture、targeted test、日志或 trace;不强制集成测试,但必须说明对本次变化的证明力。只有本次 change 改变输入传递,而测试只证明 consumer 算法、没有证明目标输入按新契约进入 consumer,导致明确验收无法成立时才使用失败结论(`verdict:"fail"`)。
|
|
38
47
|
|
|
39
|
-
`proposal.md` 的 `## Impact` 引用 `CHAIN-xxx
|
|
48
|
+
`proposal.md` 的 `## Impact` 引用 `CHAIN-xxx`(链路五要素)时,只有其中被本次 change 改变的下游消费者、视图差异或用户可观察行为才需要映射到 test-contract 场景;行为未变化的消费者可以复用既有证据或说明不新增覆盖。缺少映射只有在会使明确验收无法证明时才是阻塞缺口。design 仅引用 CHAIN 作为方案依据时不重复产生测试映射要求;若 design 暴露了 Impact 未记录且确实被本次 change 改变的消费者、视图差异或用户 / 系统可观察行为影响,应先按 Impact 对账缺口处理,再核对对应测试。测试脆弱性、实现复杂度等纯实施风险只在 design / test-contract 内处理,不要求写入 Impact。
|
|
40
49
|
|
|
41
50
|
`未知非阻塞` 的测试策略必须说明为什么该未知不影响验收;缺少说明时按覆盖缺口处理。
|
|
42
51
|
|
|
@@ -38,7 +38,7 @@ metadata:
|
|
|
38
38
|
- `Why` 只说明当前问题、机会、造成的影响和现在需要处理的原因,不提前给出解决路线
|
|
39
39
|
- `What Changes` 以用户或系统可辨识的能力变化为粒度,说明新增、修改或移除什么,已知破坏性变化按 OpenSpec 要求标记 `**BREAKING**`;`Capabilities` 只按原生 New / Modified 分类登记精确 capability 名称和简述,不自创分类
|
|
40
40
|
- 能力变化只描述目标结果和范围,不展开 requirement / scenario,也不写类、函数、字段、算法、数据流、调用顺序或复用机制;这些内容分别属于 specs 和 design
|
|
41
|
-
-
|
|
41
|
+
- 可以说明必须保持不变的相邻能力、兼容边界和高层发布影响,但不为完整而编造非目标;具体顺序、回滚步骤和技术方案属于 design
|
|
42
42
|
- `## Impact` 必须能看出受影响范围和原因,写成:
|
|
43
43
|
|
|
44
44
|
```markdown
|
|
@@ -63,7 +63,7 @@ metadata:
|
|
|
63
63
|
- 每条 requirement 定义一个可独立理解的行为规则,并至少包含一个符合 OpenSpec 格式的 scenario
|
|
64
64
|
- requirement / scenario 按归档后的目标状态书写,不使用「本次改动」「新增规则」「旧有行为」「变更前」「继续保持」等依赖变更历史的表述;OpenSpec 增量结构标题照常使用
|
|
65
65
|
- 只写可观察行为、公开接口契约和会改变业务结果的稳定语义;私有类、函数、仅服务于当前实现的内部字段、处理阶段、复用机制和清理步骤属于 design。内部数据若构成稳定的跨模块契约或会改变可观察结果,specs 写其语义约束,具体承载方式仍由 design 定义。判断标准是:更换内部实现后仍必须成立的规则属于 specs,只有采用某种实现方式时才成立的内容属于 design
|
|
66
|
-
- scenario
|
|
66
|
+
- scenario 应明确前置条件、触发行为和确定的可观察结果;强制性结果直接使用清晰、可判定的自然语言说明“必须做到什么”或“不得发生什么”,不依赖特定规范关键词,也不使用模糊的多选表达或「保持原有行为」代替可判定结论。只有可选性本身属于契约时才写成可选,并同时说明允许范围和始终成立的不变量
|
|
67
67
|
- 一个 scenario 可以包含同一触发下紧密相关的一组结果,但不得混合多个能够独立失败的责任边界
|
|
68
68
|
- 同一业务规则只保留一个权威 requirement;不同边界情况作为其 scenario,不重复建立语义重叠的 requirement
|
|
69
69
|
- 不写实现路线、测试代码、测试命令、测试数据准备过程或文件修改清单
|
|
@@ -112,12 +112,6 @@ metadata:
|
|
|
112
112
|
|---|---|---|
|
|
113
113
|
| <风险> | <可能结果> | <控制方式或测试映射> |
|
|
114
114
|
|
|
115
|
-
<!-- 可选:涉及数据、配置、协议兼容、版本切换或发布顺序时保留 -->
|
|
116
|
-
## 迁移与回滚
|
|
117
|
-
- <迁移或发布顺序>
|
|
118
|
-
- <兼容窗口>
|
|
119
|
-
- <回滚触发条件和恢复路径>
|
|
120
|
-
|
|
121
115
|
<!-- 可选:存在阻塞确认项时保留,并使用本文“待用户确认”的 DEC-xxx 格式 -->
|
|
122
116
|
## 待用户确认
|
|
123
117
|
- [ ] DEC-xxx <阻塞决策>
|
|
@@ -128,13 +122,14 @@ metadata:
|
|
|
128
122
|
- `## 实现方案` 是主体。代码影响型需求必须能映射到可定位的方案,但不要求需求与小节一一对应;紧密相关需求可以共用方案,只有存在独立技术路线时才拆分。
|
|
129
123
|
- 方案按业务功能、运行时阶段或系统边界组织。标题同时写明“针对什么”和“怎么实现”,例如 `班段内最新入/最早出:复用既有 START/END 选择策略`;不要只写“策略复用”“数据处理”“接口调整”等泛称。
|
|
130
124
|
- 每个方案整体说明实现机制、影响范围、设计依据和边界约束;不要求固定字段,只写本次实际涉及的数据、接口、流程和运行边界。共享契约集中定义一次,其他方案引用。
|
|
131
|
-
-
|
|
125
|
+
- 当本次 change 确实新增或改变行为、数据语义、接口兼容、状态、优先级、一致性、并发或恢复结果,且不同选择会影响已声明验收时,才明确对应契约;未改变的既有语义不重新设计。可以使用必要的模块、接口、表 / 字段、关键函数、数据流、状态机、优先级矩阵和简短伪代码。
|
|
132
126
|
- 声明“复用现有逻辑”或“保持行为不变”时,说明复用对象、接入位置、本次差异和需要保持的语义,不能只写抽象结论。
|
|
127
|
+
- 复用现有基础设施或通用机制时,只设计本次接入和差异,不重新证明或升级该机制的一般可靠性。除非用户、proposal 或 specs 明确提升对应质量等级,不新增未经确认的基础设施、可靠性模式或版本协调机制。
|
|
133
128
|
- 可以描述运行时算法、数据 / 控制流、状态转换和事务顺序;不写逐行代码、完整 SQL、文件修改顺序、task、测试命令或 RED/GREEN 步骤。
|
|
134
129
|
- discovery 的 `## 输入数据来源核查` 影响方案时,分别写清 producer→consumer 的输入完整性和 consumer 处理方式;相关 `IDC-xxx` 为 `未知阻塞` 时 design 不得 ready。
|
|
135
130
|
- discovery 含 `## 链路五要素` 时,方案不得违背已确证链路事实。design 可以引用 CHAIN 解释路线;若发现 Impact 未记录的消费者、视图差异或用户 / 系统可观察行为影响,先回写 Impact,再进入 test-contract 映射。
|
|
136
131
|
- `## 非目标` 和 `## 总体方案` 必须生成:非目标写最容易被误认为本次范围的相邻能力或技术路线,不编造无关项;总体方案用 2~5 句概括功能点关系、主要数据流或调用关系,不展开任务步骤。
|
|
137
|
-
- 模板中的可选注释只用于判断是否生成,不写入成品。替代路线、整体方案取舍、关键契约、风险 /
|
|
132
|
+
- 模板中的可选注释只用于判断是否生成,不写入成品。替代路线、整体方案取舍、关键契约、风险 / 取舍与回滚没有真实内容时连标题一起省略;替代路线优先写在对应方案内,只有横跨多个功能点时才集中说明;只有阻塞确认项才追加 `## 待用户确认`。
|
|
138
133
|
|
|
139
134
|
### tasks.md
|
|
140
135
|
使用 OpenSpec tasks 原生分组结构。每个顶格 checkbox 行是一个 SuperSpec 可执行 task,Markdown 标题只用于分组。
|
|
@@ -176,7 +171,7 @@ metadata:
|
|
|
176
171
|
- `REVIEW-FIX-*` task 由引擎在审查返工时追加,不需要手写执行依据
|
|
177
172
|
- `<task_id>` 可以是 `1.1` 或 `TASK-001.1`,必须唯一、稳定;标题不要包含 task id token,例如不要写 `## 1.1 Review verifier`
|
|
178
173
|
- task 内部步骤用普通 bullet,不用缩进 checkbox——引擎只解析顶格 checkbox 行,缩进的会变成无人执行的暗任务
|
|
179
|
-
- `tdd_required:true
|
|
174
|
+
- `tdd_required:true`(默认)——改运行时代码/业务逻辑/权限/外部接口
|
|
180
175
|
- `tdd_required:false` + `no_tdd_reason:xxx`——纯文档/配置/机械改名/生成物
|
|
181
176
|
- task 行只标记是否需要 TDD,不写 RED/GREEN 命令、断言或预期输出;实际 RED/GREEN 由 apply 阶段执行并记录
|
|
182
177
|
- 一个 task 对应一个可独立验证的行为变化,或一个明确的非行为改动
|
|
@@ -222,12 +217,12 @@ scenario 写到能推导断言的程度:给定什么条件、发生什么动
|
|
|
222
217
|
discovery 含 `## 链路五要素` 时,`proposal.md` `## Impact` 中引用的 `CHAIN-xxx` 应映射到测试场景(scenario 内引用对应 `CHAIN-xxx`),或在测试表后写明不覆盖理由。design 只引用 CHAIN 作为方案依据时不重复产生映射要求;若 design 暴露新的消费者、视图差异或用户 / 系统可观察行为影响,先补入 Impact,再按同一规则映射测试。
|
|
223
218
|
|
|
224
219
|
### 待用户确认
|
|
225
|
-
|
|
220
|
+
遇到会影响需求范围、验收标准、用户可见行为、方案取舍、测试策略、安全、权限、数据判断的关键不确定问题,先写入相关计划文档的 `## 待用户确认` 段落;没有阻塞确认项的文档不加该段落:
|
|
226
221
|
|
|
227
222
|
```markdown
|
|
228
223
|
## 待用户确认
|
|
229
224
|
|
|
230
|
-
- [ ] DEC-001 [方案]
|
|
225
|
+
- [ ] DEC-001 [方案] 是否需要兼容历史行为?影响:验收口径(CHAIN-003)。选项:A 兼容(加开关、保留旧路径)/ B 不兼容。建议 A:存量数据仍被报表消费
|
|
231
226
|
```
|
|
232
227
|
|
|
233
228
|
每个确认项只含一个决策点,带稳定 ID `DEC-xxx`:决策类写明影响面(引用相关 `CHAIN-xxx` / `IDC-xxx`)、候选项及后果、建议默认值及理由;事实类写明需要用户提供什么信息、为什么阻塞。选项和补充说明用普通文本或普通 bullet,不要写成 `- [ ]`,引擎会把它们计为未确认项。
|
|
@@ -236,7 +231,9 @@ discovery 含 `## 链路五要素` 时,`proposal.md` `## Impact` 中引用的
|
|
|
236
231
|
|
|
237
232
|
答案来自用户时,先登记再勾选;答案来自需求文档、代码证据等外部事实核对时,行内写明证据来源,不伪造用户决策。用户回答含糊、与候选项不匹配或引出新问题时,不视为已确认;复述理解并获得明确答复后再登记。把结论反映到 proposal/design/test-contract 相关内容,勾选行内注明结论要点;确认项作废或重复时改为 `[x]` 并注明理由,不要删除确认项。局部实现细节、命名、普通文件组织和不影响需求/验收/风险的技术微调不要升级为用户确认。
|
|
238
233
|
|
|
239
|
-
进入 propose 后出现新的业务规则、产品口径、验收标准、示例规范或需求源更新时,不要静默覆盖原计划;默认先在 `proposal.md` 记录 `##
|
|
234
|
+
进入 propose 后出现新的业务规则、产品口径、验收标准、示例规范或需求源更新时,不要静默覆盖原计划;默认先在 `proposal.md` 记录 `## 需求变化`,说明变化来源、变化内容、受影响能力、直接修改的文档章节、确认保持不变的范围和处理方式(更新当前 change / 新建后续 change / 暂不处理)。该段是后续增量审查判断“本轮变化”的权威锚点;不得把未受影响的历史设计重新列为本轮待审范围。只有影响技术路线、测试契约或业务不变量时,才同步更新 `design.md`、`test-contract.md` 或 `business-invariants.md`。
|
|
235
|
+
|
|
236
|
+
审查报告是待验证的独立意见,不会自动创造新需求。主流程处理 finding 时先分离 underlying problem 与 recommendation:根据本次 change 的目标、直接证据和明确验收独立判断问题是否成立;问题成立时选择满足既有需求的最小修复。Recommendation 只是非绑定建议,不是验收标准;与用户决定、已确认复用路线或 `## 非目标` 冲突的具体方案不实施,也不得仅为通过审查增加未经确认的基础设施、兼容、额外任务、故障场景或测试义务。若 reviewer 指出的事实证据证明现有方案无法满足用户已确认的强制需求、规格约束或明确验收结果,补足对应结果、契约或证据,而不是默认采用 reviewer 指定的架构;Reviewer 不得自行新增或升级强制要求。
|
|
240
237
|
|
|
241
238
|
## 完成条件
|
|
242
239
|
|