@peterxiaoyang/superspec 0.1.55 → 0.1.56
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/approved_ref.d.ts +26 -0
- package/dist/approved_ref.js +158 -0
- package/dist/code_review.d.ts +1 -0
- package/dist/code_review.js +36 -0
- package/dist/next.js +4 -1
- package/dist/phase_plan.d.ts +2 -1
- package/dist/phase_plan.js +12 -0
- package/dist/record.js +22 -5
- package/dist/transition.js +19 -4
- package/dist/types.d.ts +11 -0
- package/package.json +1 -1
- package/templates/workflow/AGENTS.md +2 -0
- package/templates/workflow/agents-md/code-reviewer.md +1 -1
- package/templates/workflow/agents-md/critic.md +1 -1
- package/templates/workflow/prompts/code-reviewer.md +3 -4
- package/templates/workflow/prompts/critic.md +1 -0
- package/templates/workflow/prompts/executor.md +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CodeReviewClaimKind } from "./types.ts";
|
|
2
|
+
export declare const CODE_REVIEW_CLAIM_KINDS: readonly ["missing_approved", "breaks_existing", "unjustified_addition"];
|
|
3
|
+
export type ApprovedRefKind = "test" | "requirement" | "task" | "design" | "proposal";
|
|
4
|
+
export interface ResolvedApprovedRef {
|
|
5
|
+
raw: string;
|
|
6
|
+
kind: ApprovedRefKind;
|
|
7
|
+
short: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function isCodeReviewClaimKind(value: unknown): value is CodeReviewClaimKind;
|
|
10
|
+
export declare function shortApprovedRef(raw: string): string;
|
|
11
|
+
export declare function resolveApprovedRef(changeRoot: string, raw: unknown): {
|
|
12
|
+
ok: true;
|
|
13
|
+
value: ResolvedApprovedRef;
|
|
14
|
+
} | {
|
|
15
|
+
ok: false;
|
|
16
|
+
reason: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function resolveApprovedRefs(changeRoot: string, refs: unknown): {
|
|
19
|
+
ok: true;
|
|
20
|
+
values: ResolvedApprovedRef[];
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
reasons: string[];
|
|
24
|
+
};
|
|
25
|
+
export declare function hasBehaviorAnchor(values: readonly ResolvedApprovedRef[]): boolean;
|
|
26
|
+
export declare function reviewFixReason(claimKind: CodeReviewClaimKind, refs: readonly string[]): string;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// SuperSpec 代码审查 approved_refs:只做存在性解析,不做语义匹配。
|
|
2
|
+
import { readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { parseTasksMd, parseTestContractEntries } from "./format.js";
|
|
5
|
+
export const CODE_REVIEW_CLAIM_KINDS = [
|
|
6
|
+
"missing_approved",
|
|
7
|
+
"breaks_existing",
|
|
8
|
+
"unjustified_addition",
|
|
9
|
+
];
|
|
10
|
+
const TEST_ID_RE = /^TEST-[A-Za-z0-9_-]+$/;
|
|
11
|
+
const TEST_CONTRACT_REL = join(".superspec", "artifacts", "test-contract.md");
|
|
12
|
+
function escapeRegex(value) {
|
|
13
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14
|
+
}
|
|
15
|
+
function isPathInside(root, target) {
|
|
16
|
+
const rel = relative(root, target);
|
|
17
|
+
return rel !== "" && !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
|
|
18
|
+
}
|
|
19
|
+
function headingExists(content, title) {
|
|
20
|
+
const heading = new RegExp(`^#{1,6}[\\t ]+${escapeRegex(title)}(?:[\\t ]+#+)?[\\t ]*$`, "m");
|
|
21
|
+
return heading.test(content);
|
|
22
|
+
}
|
|
23
|
+
function shortTestId(raw) {
|
|
24
|
+
const trimmed = raw.trim();
|
|
25
|
+
if (TEST_ID_RE.test(trimmed))
|
|
26
|
+
return trimmed;
|
|
27
|
+
const hash = trimmed.lastIndexOf("#");
|
|
28
|
+
if (hash >= 0) {
|
|
29
|
+
const id = trimmed.slice(hash + 1).trim();
|
|
30
|
+
if (TEST_ID_RE.test(id))
|
|
31
|
+
return id;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function readChangeFile(changeRoot, relPath) {
|
|
36
|
+
const target = resolve(changeRoot, relPath);
|
|
37
|
+
if (!isPathInside(resolve(changeRoot), target) && resolve(changeRoot) !== target)
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
if (!statSync(target).isFile())
|
|
41
|
+
return null;
|
|
42
|
+
return readFileSync(target, "utf8");
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function isCodeReviewClaimKind(value) {
|
|
49
|
+
return typeof value === "string" && CODE_REVIEW_CLAIM_KINDS.includes(value);
|
|
50
|
+
}
|
|
51
|
+
export function shortApprovedRef(raw) {
|
|
52
|
+
const testId = shortTestId(raw);
|
|
53
|
+
if (testId)
|
|
54
|
+
return testId;
|
|
55
|
+
const req = /#Requirement:\s*(.+)$/.exec(raw.trim());
|
|
56
|
+
if (req)
|
|
57
|
+
return `Requirement: ${req[1].trim()}`;
|
|
58
|
+
const hash = raw.lastIndexOf("#");
|
|
59
|
+
if (hash >= 0 && hash < raw.length - 1)
|
|
60
|
+
return raw.slice(hash + 1).trim();
|
|
61
|
+
return raw.trim();
|
|
62
|
+
}
|
|
63
|
+
export function resolveApprovedRef(changeRoot, raw) {
|
|
64
|
+
if (typeof raw !== "string" || raw.trim() === "") {
|
|
65
|
+
return { ok: false, reason: "approved_refs 条目必须是非空字符串" };
|
|
66
|
+
}
|
|
67
|
+
const ref = raw.trim();
|
|
68
|
+
if (/[\u0000-\u001f\u007f]/.test(ref)) {
|
|
69
|
+
return { ok: false, reason: `approved_refs 条目不能包含换行等控制字符:${JSON.stringify(ref)}` };
|
|
70
|
+
}
|
|
71
|
+
const testId = shortTestId(ref);
|
|
72
|
+
if (testId) {
|
|
73
|
+
if (ref.includes("#") && !ref.endsWith(`#${testId}`)) {
|
|
74
|
+
return { ok: false, reason: `TEST 引用只能是裸 TEST-ID 或以 #TEST-ID 结尾指向 test-contract.md:${ref}` };
|
|
75
|
+
}
|
|
76
|
+
if (ref.includes("#")) {
|
|
77
|
+
const path = ref.slice(0, ref.lastIndexOf("#")).replace(/\\/g, "/");
|
|
78
|
+
const allowed = path === TEST_CONTRACT_REL.replace(/\\/g, "/")
|
|
79
|
+
|| path === "test-contract.md"
|
|
80
|
+
|| path.endsWith("/test-contract.md");
|
|
81
|
+
if (!allowed)
|
|
82
|
+
return { ok: false, reason: `TEST 引用只能指向 test-contract.md:${ref}` };
|
|
83
|
+
}
|
|
84
|
+
const content = readChangeFile(changeRoot, TEST_CONTRACT_REL);
|
|
85
|
+
if (content == null)
|
|
86
|
+
return { ok: false, reason: `无法读取 ${TEST_CONTRACT_REL.replace(/\\/g, "/")},无法校验 TEST 引用:${ref}` };
|
|
87
|
+
const parsed = parseTestContractEntries(content);
|
|
88
|
+
if (!parsed.ok || !parsed.entries.some(entry => entry.test_id === testId)) {
|
|
89
|
+
return { ok: false, reason: `${TEST_CONTRACT_REL.replace(/\\/g, "/")} 中不存在 ${testId}` };
|
|
90
|
+
}
|
|
91
|
+
return { ok: true, value: { raw: ref, kind: "test", short: testId } };
|
|
92
|
+
}
|
|
93
|
+
const separator = ref.indexOf("#");
|
|
94
|
+
if (separator <= 0 || separator === ref.length - 1) {
|
|
95
|
+
return { ok: false, reason: `锚点格式无法解析,应为 文件#标题 或 TEST-ID:${ref}` };
|
|
96
|
+
}
|
|
97
|
+
const path = ref.slice(0, separator).replace(/\\/g, "/");
|
|
98
|
+
const anchor = ref.slice(separator + 1).trim();
|
|
99
|
+
const content = readChangeFile(changeRoot, path);
|
|
100
|
+
if (content == null)
|
|
101
|
+
return { ok: false, reason: `${path} 在当前 change 中不存在` };
|
|
102
|
+
if (path === "tasks.md") {
|
|
103
|
+
const tasks = parseTasksMd(content);
|
|
104
|
+
if (!tasks.some(task => task.taskId === anchor))
|
|
105
|
+
return { ok: false, reason: `tasks.md 中不存在任务 ${anchor}` };
|
|
106
|
+
return { ok: true, value: { raw: ref, kind: "task", short: anchor } };
|
|
107
|
+
}
|
|
108
|
+
if (path === "design.md") {
|
|
109
|
+
if (!headingExists(content, anchor))
|
|
110
|
+
return { ok: false, reason: `design.md 中不存在标题「${anchor}」` };
|
|
111
|
+
return { ok: true, value: { raw: ref, kind: "design", short: anchor } };
|
|
112
|
+
}
|
|
113
|
+
if (path === "proposal.md") {
|
|
114
|
+
if (!headingExists(content, anchor))
|
|
115
|
+
return { ok: false, reason: `proposal.md 中不存在标题「${anchor}」` };
|
|
116
|
+
return { ok: true, value: { raw: ref, kind: "proposal", short: anchor } };
|
|
117
|
+
}
|
|
118
|
+
if (/^specs\/[^/]+\/spec\.md$/.test(path)) {
|
|
119
|
+
const requirementTitle = anchor.startsWith("Requirement:")
|
|
120
|
+
? anchor.slice("Requirement:".length).trim()
|
|
121
|
+
: "";
|
|
122
|
+
if (!requirementTitle)
|
|
123
|
+
return { ok: false, reason: `spec 锚点必须以 Requirement: 开头:${ref}` };
|
|
124
|
+
if (!headingExists(content, `Requirement: ${requirementTitle}`)) {
|
|
125
|
+
return { ok: false, reason: `${path} 中不存在 Requirement「${requirementTitle}」` };
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
ok: true,
|
|
129
|
+
value: { raw: ref, kind: "requirement", short: `Requirement: ${requirementTitle}` },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return { ok: false, reason: `只支持 tasks.md、design.md、proposal.md、specs/*/spec.md 与 test-contract.md 的锚点:${ref}` };
|
|
133
|
+
}
|
|
134
|
+
export function resolveApprovedRefs(changeRoot, refs) {
|
|
135
|
+
if (!Array.isArray(refs) || refs.length === 0) {
|
|
136
|
+
return { ok: false, reasons: ["approved_refs 必须是非空字符串数组"] };
|
|
137
|
+
}
|
|
138
|
+
const values = [];
|
|
139
|
+
const reasons = [];
|
|
140
|
+
for (const raw of refs) {
|
|
141
|
+
const resolved = resolveApprovedRef(changeRoot, raw);
|
|
142
|
+
if (!resolved.ok)
|
|
143
|
+
reasons.push(resolved.reason);
|
|
144
|
+
else
|
|
145
|
+
values.push(resolved.value);
|
|
146
|
+
}
|
|
147
|
+
if (reasons.length > 0)
|
|
148
|
+
return { ok: false, reasons };
|
|
149
|
+
return { ok: true, values };
|
|
150
|
+
}
|
|
151
|
+
export function hasBehaviorAnchor(values) {
|
|
152
|
+
return values.some(value => value.kind === "test" || value.kind === "requirement");
|
|
153
|
+
}
|
|
154
|
+
export function reviewFixReason(claimKind, refs) {
|
|
155
|
+
const shorts = refs.map(shortApprovedRef).filter(Boolean);
|
|
156
|
+
const target = shorts.length > 0 ? shorts.join("、") : "已批准行为";
|
|
157
|
+
return `兑现 ${target}(${claimKind})`;
|
|
158
|
+
}
|
package/dist/code_review.d.ts
CHANGED
|
@@ -69,6 +69,7 @@ export declare function codeReviewPacketDigest(input: {
|
|
|
69
69
|
packet_context?: JobPacketContext;
|
|
70
70
|
previous_rejection?: ReviewPreviousRejection;
|
|
71
71
|
}): string;
|
|
72
|
+
export declare function addedCodePathsForScope(projectRoot: string, scope: CodeReviewScope): string[];
|
|
72
73
|
export declare function codeReviewPacketContext(changeRoot: string, projectRoot: string, scope: CodeReviewScope, events: Event[]): JobPacketContext;
|
|
73
74
|
export declare function effectiveCoverageExemptionRefsFromEvents(events: Event[]): CoverageExemptionRef[];
|
|
74
75
|
export declare function missingCoverageExemptionTestIds(changeRoot: string, events: Event[]): string[];
|
package/dist/code_review.js
CHANGED
|
@@ -360,6 +360,41 @@ export function codeReviewJobStaleReason(projectRoot, job, currentPaths, events,
|
|
|
360
360
|
export function codeReviewPacketDigest(input) {
|
|
361
361
|
return sha256Text(JSON.stringify(input));
|
|
362
362
|
}
|
|
363
|
+
export function addedCodePathsForScope(projectRoot, scope) {
|
|
364
|
+
if (!scope.scope_reliable)
|
|
365
|
+
return [];
|
|
366
|
+
const added = new Set();
|
|
367
|
+
let baseHead = scope.base_head;
|
|
368
|
+
if (!baseHead && scope.current_head) {
|
|
369
|
+
// 首轮 start-apply 前仓库还没有提交:以空树为基点,让 Apply 期间产生的首个提交也进入新增清单。
|
|
370
|
+
const emptyTree = gitLines(projectRoot, ["hash-object", "-t", "tree", "/dev/null"]);
|
|
371
|
+
if (emptyTree.ok)
|
|
372
|
+
baseHead = emptyTree.lines[0] ?? null;
|
|
373
|
+
}
|
|
374
|
+
if (baseHead && scope.current_head) {
|
|
375
|
+
const committed = gitLines(projectRoot, [
|
|
376
|
+
"diff", "--no-renames", "--diff-filter=A", "--name-only", `${baseHead}..${scope.current_head}`,
|
|
377
|
+
]);
|
|
378
|
+
if (committed.ok) {
|
|
379
|
+
for (const path of committed.lines) {
|
|
380
|
+
if (isCodeLikePath(path))
|
|
381
|
+
added.add(path);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const dirty = dirtyCodeFiles(projectRoot);
|
|
386
|
+
if (dirty.ok) {
|
|
387
|
+
for (const file of dirty.files) {
|
|
388
|
+
if (file.status === "added" && isCodeLikePath(file.path))
|
|
389
|
+
added.add(file.path);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
for (const path of scope.untracked_paths) {
|
|
393
|
+
if (isCodeLikePath(path))
|
|
394
|
+
added.add(path);
|
|
395
|
+
}
|
|
396
|
+
return [...added].sort();
|
|
397
|
+
}
|
|
363
398
|
export function codeReviewPacketContext(changeRoot, projectRoot, scope, events) {
|
|
364
399
|
const taskExecutionIndex = taskExecutionIndexFromEvents(projectRoot, events);
|
|
365
400
|
// changed_paths 未知(快照缺失)或不完整(committed 段 diff 失败)的 task
|
|
@@ -379,6 +414,7 @@ export function codeReviewPacketContext(changeRoot, projectRoot, scope, events)
|
|
|
379
414
|
task_execution_index: taskExecutionIndex,
|
|
380
415
|
unattributed_paths: scope.review_paths.filter(path => !attributedPaths.has(path)).sort(),
|
|
381
416
|
unknown_attribution_tasks: unknownAttributionTasks,
|
|
417
|
+
added_code_paths: addedCodePathsForScope(projectRoot, scope),
|
|
382
418
|
};
|
|
383
419
|
}
|
|
384
420
|
export function effectiveCoverageExemptionRefsFromEvents(events) {
|
package/dist/next.js
CHANGED
|
@@ -115,14 +115,17 @@ function toNextOutput(change, plan) {
|
|
|
115
115
|
resume: { argv: ["superspec", "transition", "next", "--change", change] },
|
|
116
116
|
reason: plan.reason,
|
|
117
117
|
};
|
|
118
|
-
case "run_transition":
|
|
118
|
+
case "run_transition": {
|
|
119
|
+
const findingContext = plan.reopen?.reason === "review_fix" ? plan.reopen.findingContext : undefined;
|
|
119
120
|
return {
|
|
120
121
|
state: plan.state,
|
|
121
122
|
path: "next_command",
|
|
122
123
|
next_command: transitionCommand(change, plan.transition, formatTransitionArgs(plan)),
|
|
123
124
|
reason: plan.reason,
|
|
124
125
|
missing_inputs: [],
|
|
126
|
+
...(findingContext ? { finding_context: findingContext } : {}),
|
|
125
127
|
};
|
|
128
|
+
}
|
|
126
129
|
case "done":
|
|
127
130
|
return {
|
|
128
131
|
state: plan.state,
|
package/dist/phase_plan.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ReviewGateRule } from "./review_job_gates.ts";
|
|
2
2
|
import { exploreAnswerRegistrationPayload } from "./explore_round.ts";
|
|
3
3
|
import { proposeAnswerRegistrationPayload } from "./propose_round.ts";
|
|
4
|
-
import type { AcceptedMaterialFollowupContinuation, AskUser, Event, ExecutionPolicy, Job, JobRole, PlanningValidationProfile, WorkflowArtifactKind, State } from "./types.ts";
|
|
4
|
+
import type { AcceptedMaterialFollowupContinuation, AskUser, Event, ExecutionPolicy, Job, JobRole, PlanningValidationProfile, ReviewFindingContext, WorkflowArtifactKind, State } from "./types.ts";
|
|
5
5
|
import type { Snapshot } from "./types.ts";
|
|
6
6
|
import type { ReviewRisk } from "./review.ts";
|
|
7
7
|
export type TransitionName = "explore" | "propose-ready" | "start-apply" | "task-start" | "task-complete" | "review-ready" | "reopen" | "accept";
|
|
@@ -29,6 +29,7 @@ export type ReopenNextStep = {
|
|
|
29
29
|
jobId: string;
|
|
30
30
|
findingId: string;
|
|
31
31
|
reopenReason: string;
|
|
32
|
+
findingContext?: ReviewFindingContext;
|
|
32
33
|
} | {
|
|
33
34
|
to: "propose";
|
|
34
35
|
reason: "review_finding";
|
package/dist/phase_plan.js
CHANGED
|
@@ -12,6 +12,16 @@ import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, co
|
|
|
12
12
|
import { isPhaseAdvanceAuthorized, latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
|
|
13
13
|
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
14
14
|
import { workflowRiskForProposeRound, workflowRiskForState } from "./workflow_config.js";
|
|
15
|
+
/** 从失败 finding 提取定位上下文:只回传 evidence(位置事实),不回传 description——那是审查建议叙事,不进执行上下文。 */
|
|
16
|
+
function reviewFindingContext(finding) {
|
|
17
|
+
const evidence = typeof finding?.evidence === "string" ? finding.evidence.trim() : "";
|
|
18
|
+
if (!evidence)
|
|
19
|
+
return undefined;
|
|
20
|
+
return {
|
|
21
|
+
evidence,
|
|
22
|
+
note: "非授权上下文:仅用于定位问题代码;实现范围仍以任务行锚定的已批准行为为准",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
15
25
|
function requiredJobs(state, jobs, reason) {
|
|
16
26
|
return { kind: "required_jobs", state, jobs, reason };
|
|
17
27
|
}
|
|
@@ -837,6 +847,7 @@ function planApplyDoneNext(context) {
|
|
|
837
847
|
jobId: latest.job.job_id,
|
|
838
848
|
findingId,
|
|
839
849
|
reopenReason: `修复代码审查问题 ${findingId}`,
|
|
850
|
+
findingContext: reviewFindingContext(pendingFinding?.finding),
|
|
840
851
|
},
|
|
841
852
|
reason: `代码审查发现纯代码实现问题 ${findingId},回到实现阶段修复`,
|
|
842
853
|
};
|
|
@@ -862,6 +873,7 @@ function planApplyDoneNext(context) {
|
|
|
862
873
|
jobId: latest.job.job_id,
|
|
863
874
|
findingId,
|
|
864
875
|
reopenReason: `根据代码审查问题 ${findingId} 回到实现阶段修复`,
|
|
876
|
+
findingContext: reviewFindingContext(pendingFinding?.finding),
|
|
865
877
|
},
|
|
866
878
|
reason: `使用者已确认问题 ${findingId} 直接回到实现阶段修复`,
|
|
867
879
|
};
|
package/dist/record.js
CHANGED
|
@@ -8,6 +8,7 @@ import { isPhaseConfirmationScope, phaseActionForAnswer, phaseConfirmationForCur
|
|
|
8
8
|
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_DECISION_SCOPE_PREFIX, codeReviewJobStaleReason, codeReviewDecisionAnswerLabel, currentCodeReviewWorkingPaths, latestCodeReviewFailedStatus, normalizeCodeReviewDecisionAnswer, parseCodeReviewDecisionScope, } from "./code_review.js";
|
|
9
9
|
import { invalidReasonForSubmittedReport } from "./job_validity.js";
|
|
10
10
|
import { jobSubmitArgv } from "./job_action.js";
|
|
11
|
+
import { hasBehaviorAnchor, isCodeReviewClaimKind, resolveApprovedRefs, } from "./approved_ref.js";
|
|
11
12
|
import { REVIEW_DOC_PATHS, REVIEW_REJECTION_OVERRIDE_SCOPE_PREFIX, REVIEW_REJECTION_OVERRIDE_ANSWER, parseReviewRejectionOverrideScope, reviewGateRoleResolution, reviewRejectionOverrideScope, } from "./review.js";
|
|
12
13
|
import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./review_job_gates.js";
|
|
13
14
|
import { RecordInputDecodingError, readRecordInputFile } from "./record_input.js";
|
|
@@ -56,7 +57,7 @@ function roleDescription(role) {
|
|
|
56
57
|
case "verifier":
|
|
57
58
|
return "验证 proposal、实现状态、任务完成、测试契约和 SuperSpec 证据是否足以支撑完成结论";
|
|
58
59
|
case "code-reviewer":
|
|
59
|
-
return "
|
|
60
|
+
return "独立审查实现是否以最小语义影响兑现已批准计划,找出真实缺陷、边界条件、安全/性能/兼容问题与关键测试缺口,并区分漏做与计划或验收没有要求的改动";
|
|
60
61
|
case "executor":
|
|
61
62
|
return "执行受限实现工作项";
|
|
62
63
|
case "test-run":
|
|
@@ -339,7 +340,7 @@ function validateReviewScope(obj, job, checks) {
|
|
|
339
340
|
}
|
|
340
341
|
}
|
|
341
342
|
}
|
|
342
|
-
function actionableCodeReviewFindings(findings) {
|
|
343
|
+
function actionableCodeReviewFindings(findings, changeRoot) {
|
|
343
344
|
if (!Array.isArray(findings))
|
|
344
345
|
return { actionable: [], reasons: ["报告字段 findings 必须是数组"] };
|
|
345
346
|
const actionable = [];
|
|
@@ -387,6 +388,18 @@ function actionableCodeReviewFindings(findings) {
|
|
|
387
388
|
if (type === "implementation" && finding.suggested_action !== "apply") {
|
|
388
389
|
findingReasons.push(`纯代码实现问题 ${id} 的 suggested_action 必须是 apply`);
|
|
389
390
|
}
|
|
391
|
+
if (!isCodeReviewClaimKind(finding.claim_kind)) {
|
|
392
|
+
findingReasons.push(`阻塞问题 ${id} 的 claim_kind 必须是 missing_approved|breaks_existing|unjustified_addition`);
|
|
393
|
+
}
|
|
394
|
+
const resolved = resolveApprovedRefs(changeRoot, finding.approved_refs);
|
|
395
|
+
if (!resolved.ok) {
|
|
396
|
+
findingReasons.push(...resolved.reasons.map((reason) => `阻塞问题 ${id} ${reason}`));
|
|
397
|
+
}
|
|
398
|
+
else if (finding.claim_kind === "missing_approved"
|
|
399
|
+
&& finding.suggested_action === "apply"
|
|
400
|
+
&& !hasBehaviorAnchor(resolved.values)) {
|
|
401
|
+
findingReasons.push(`阻塞问题 ${id} 的 missing_approved 在 suggested_action=apply 时必须引用可解析的 TEST 或 spec Requirement;若缺口属于计划或验收本身的问题,改用 type=mixed 且 suggested_action=propose 交使用者裁决`);
|
|
402
|
+
}
|
|
390
403
|
if (findingReasons.length > 0) {
|
|
391
404
|
reasons.push(...findingReasons);
|
|
392
405
|
continue;
|
|
@@ -614,7 +627,7 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
614
627
|
const blockingCount = Array.isArray(findings)
|
|
615
628
|
? findings.filter(item => asObject(item)?.blocking === true).length
|
|
616
629
|
: 0;
|
|
617
|
-
const { actionable, reasons } = actionableCodeReviewFindings(findings);
|
|
630
|
+
const { actionable, reasons } = actionableCodeReviewFindings(findings, changeRoot);
|
|
618
631
|
if (verdict === "pass") {
|
|
619
632
|
if (blockingCount > 0) {
|
|
620
633
|
const reason = "报告结论为 pass 时不能包含 blocking:true 的阻塞问题";
|
|
@@ -1165,6 +1178,9 @@ function packetFieldDescriptions() {
|
|
|
1165
1178
|
changed_paths: "与某个任务(task)或代码状态检查相关的改动文件。",
|
|
1166
1179
|
changed_paths_partial_reason: "该任务(task)的提交段 diff 失败原因;存在时 changed_paths 只包含工作区对比结果,归属可能不完整。",
|
|
1167
1180
|
unattributed_paths: "代码审查范围中暂时无法归属到某个任务(task)的文件。",
|
|
1181
|
+
added_code_paths: "相对本次代码审查基点新建的代码文件,供判断是否服务已批准行为。",
|
|
1182
|
+
claim_kind: "阻塞问题相对已批准计划的关系:漏做、破坏已有行为、或计划或验收没有要求的改动。",
|
|
1183
|
+
approved_refs: "指向当前 change 已批准材料的引用;引擎只检查能否解析,apply 漏做还需要 TEST 或 spec Requirement。",
|
|
1168
1184
|
unknown_attribution_tasks: "因为缺少边界快照或提交段 diff 失败而无法完整计算改动归属的任务(task)。",
|
|
1169
1185
|
coverage_exemption_refs: "测试覆盖豁免引用:说明某个 TEST 为什么没有绑定到任务(task)。",
|
|
1170
1186
|
code_review_gate: "最终验证读取的代码审查门禁事实:passed 指向已接受的代码审查工作项,skipped 表示本轮没有代码类改动。",
|
|
@@ -1210,6 +1226,7 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
1210
1226
|
...(packetContext?.task_execution_index ? { task_execution_index: packetContext.task_execution_index } : {}),
|
|
1211
1227
|
...(packetContext?.unattributed_paths ? { unattributed_paths: packetContext.unattributed_paths } : {}),
|
|
1212
1228
|
...(packetContext?.unknown_attribution_tasks ? { unknown_attribution_tasks: packetContext.unknown_attribution_tasks } : {}),
|
|
1229
|
+
...(packetContext?.added_code_paths ? { added_code_paths: packetContext.added_code_paths } : {}),
|
|
1213
1230
|
...(packetContext?.code_state_check ? { code_state_check: packetContext.code_state_check } : {}),
|
|
1214
1231
|
packet_digest: job.packet_digest,
|
|
1215
1232
|
required_output_kind: "job_report_json",
|
|
@@ -1235,9 +1252,9 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
1235
1252
|
`产出 JSON 报告内容并优先通过 --report - 从 stdin 登记;文件路径模式仅作备用。${recordInputInstruction(job)}协议字段含义见 packet 顶层“字段说明”,普通对话不要原样复述 JSON。` +
|
|
1236
1253
|
(isCodeReviewer
|
|
1237
1254
|
? `格式骨架:{"role":"code-reviewer","verdict":"pass","review_scope":{"job_id":"${job.job_id}","packet_digest":"${job.packet_digest}","checked_paths":[],"checked_docs":[],"unchecked":[]},"findings":[],"reviewer":{"kind":"subagent","id":"<thread-or-agent-id>"}}。提交前按真实审查结果填写数组;不得从 boundFiles 自动复制 checked_paths。verdict 只能为 pass 或 fail;审查覆盖范围(review_scope)用来说明本次审查覆盖了哪些文件和文档,已检查路径(checked_paths)与未检查项(unchecked)必须合起来覆盖全部绑定文件(boundFiles),unchecked 条目格式为 {"path":"<path>","reason":"<reason>"};pass 不允许仍有未检查的绑定文件。`
|
|
1238
|
-
+ `报告结论为 fail 时,问题列表(findings)至少包含一个可处理、可追溯的阻塞问题,字段为 {"id":"<stable-id>","blocking":true,"type":"implementation|spec|mixed","description":"<what>","evidence":"<why>","source_refs":["<path:line>"],"impact":"<impact>","suggested_action":"apply|propose"}。问题类型(type)中 implementation 表示纯代码实现问题,spec 表示方案/需求文档问题,mixed
|
|
1255
|
+
+ `报告结论为 fail 时,问题列表(findings)至少包含一个可处理、可追溯的阻塞问题,字段为 {"id":"<stable-id>","blocking":true,"type":"implementation|spec|mixed","claim_kind":"missing_approved|breaks_existing|unjustified_addition","approved_refs":["TEST-001"],"description":"<what>","evidence":"<why>","source_refs":["<path:line>"],"impact":"<impact>","suggested_action":"apply|propose"}。问题类型(type)中 implementation 表示纯代码实现问题,spec 表示方案/需求文档问题,mixed 表示需要使用者判断的混合问题;claim_kind 与 approved_refs 见字段说明。`
|
|
1239
1256
|
+ (packetContext?.task_execution_index
|
|
1240
|
-
? `本工作项带任务执行索引(task_execution_index):按 task 对照其执行依据快照(contract)审查——实现路线对照 design 引用原文、累计 diff 对照 guard 边界、测试断言对照 tests 声明的 scenario;每项的 required_evidence 是 task-start 冻结的证据口径,red_required/green_required 分别说明是否需要 RED/GREEN;fix 非空表示状态机创建的实现修复,source、parent_task_id 和 reason 说明其归属,code_review 来源还需核对 review_finding;scope_note 既可能解释必要的范围扩大,也可能说明代码审查修复为何保留原实现,均需结合 Diff、调用链和验证证据独立判断;changed_paths 是归属线索不是结论(null 表示未知);unattributed_paths
|
|
1257
|
+
? `本工作项带任务执行索引(task_execution_index):按 task 对照其执行依据快照(contract)审查——实现路线对照 design 引用原文、累计 diff 对照 guard 边界、测试断言对照 tests 声明的 scenario;每项的 required_evidence 是 task-start 冻结的证据口径,red_required/green_required 分别说明是否需要 RED/GREEN;fix 非空表示状态机创建的实现修复,source、parent_task_id 和 reason 说明其归属,code_review 来源还需核对 review_finding;scope_note 既可能解释必要的范围扩大,也可能说明代码审查修复为何保留原实现,均需结合 Diff、调用链和验证证据独立判断;changed_paths 是归属线索不是结论(null 表示未知);unattributed_paths 中的无主改动和 added_code_paths 中的新建代码文件,均需判断是否服务已批准行为;coverage_exemption_refs 解释未绑定 task 的 TEST 豁免。当前 packet 的 boundFiles 是本轮冻结的审查范围;若它来自前一轮审查后的增量,只复核本轮变化及其直接影响链路,不要求重复审查未变化文件,但仍要判断批准行为是否完整闭合。`
|
|
1241
1258
|
: "")
|
|
1242
1259
|
: job.role === "verifier"
|
|
1243
1260
|
? `最小格式:{"role":"verifier","verdict":"pass","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""}}。verdict 只能为 pass 或 fail;核对代码审查记录(code_review_gate):passed 必须能追溯到已接受的代码审查工作项,skipped 必须能证明本次没有代码类改动。核对修复闭环:task_execution_index.fix.source=code_review 时必须核对 review_finding 对应问题是否关闭;source=self_test 时必须核对 parent_task_id、记录的自测原因、本次 attempt 验证和最新代码审查是否共同闭环。方案/混合问题必须有用户决策或后续修复证据。按 task_execution_index 的 required_evidence 核对测试证据:red_required 时需要同一 TEST 的 RED(expected_failure)后 GREEN;green_required 时每个声明 TEST 都需要允许的 GREEN 语义状态;测试运行证据应包含测试 ID(test_id)、命令(command)、工作目录(cwd)、退出码(exit_code)、语义状态(semantic_status)。修复 task 的回归测试运行可用回归覆盖任务列表(covers_task_ids)说明覆盖了哪些已完成任务;缺少任务尝试 ID(attempt_id)的旧证据只能弱引用。` +
|
package/dist/transition.js
CHANGED
|
@@ -9,6 +9,7 @@ import { REVIEW_CODE_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE, REVIEW_FINAL_VE
|
|
|
9
9
|
import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketContext, codeReviewPacketDigest, collectCodeReviewGateFacts, computeCodeStateCheck, currentCodeReviewWorkingPaths, dismissedCodeReviewSummary, effectiveCoverageExemptionRefsFromEvents, latestCodeReviewGateEvidence, latestCodeReviewDecision, latestCodeReviewFailedStatus, missingCoverageExemptionTestIds, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, taskExecutionIndexForReview, } from "./code_review.js";
|
|
10
10
|
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
11
11
|
import { adoptedContractForTask, findTaskInLines, isFixTaskId, parseTasksMd, parseTestContractEntries, } from "./format.js";
|
|
12
|
+
import { isCodeReviewClaimKind, reviewFixReason } from "./approved_ref.js";
|
|
12
13
|
import { applyRequirementModeForCurrentRound, applyPlanningDocsChangedSinceBaseline, executionRequirementVersionForCurrentRound, blockingJobsForApplyDone, executionPolicyForCurrentRound, formatPendingTaskMessage, latestAcceptedProposalBaseline, pendingTaskStatusForApply, planningValidationProfileForNewRound, planTransition, discoveryDocsBaseline, exploreAnswerRegistrationPayloadForChange, proposeAnswerRegistrationPayloadForChange, proposalDocsBaseline, } from "./phase_plan.js";
|
|
13
14
|
import { latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
|
|
14
15
|
import { currentGitHead, dirtyCodeFiles, stageProductionJavaFilesSince } from "./git_state.js";
|
|
@@ -359,15 +360,29 @@ function findReviewFailedFinding(events, ref) {
|
|
|
359
360
|
return { event: status.terminal.event, finding };
|
|
360
361
|
}
|
|
361
362
|
function reviewFixDescriptor(ref, finding) {
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
363
|
+
const claimKind = isCodeReviewClaimKind(finding.claim_kind) ? finding.claim_kind : null;
|
|
364
|
+
const approvedRefs = Array.isArray(finding.approved_refs)
|
|
365
|
+
? finding.approved_refs.filter((item) => typeof item === "string" && item.trim() !== "")
|
|
366
|
+
: [];
|
|
367
|
+
const reason = claimKind && approvedRefs.length > 0
|
|
368
|
+
? reviewFixReason(claimKind, approvedRefs).replace(/\s+/g, " ")
|
|
369
|
+
: typeof finding.description === "string" && finding.description.trim()
|
|
370
|
+
? finding.description.trim().replace(/\s+/g, " ")
|
|
371
|
+
: `修复代码审查问题 ${ref.findingId}`;
|
|
372
|
+
const reviewFinding = {
|
|
373
|
+
job_id: ref.jobId,
|
|
374
|
+
finding_id: ref.findingId,
|
|
375
|
+
};
|
|
376
|
+
if (approvedRefs.length > 0)
|
|
377
|
+
reviewFinding.approved_refs = approvedRefs;
|
|
378
|
+
if (claimKind)
|
|
379
|
+
reviewFinding.claim_kind = claimKind;
|
|
365
380
|
return {
|
|
366
381
|
fix_id: `REVIEW-FIX-${ref.jobId}#${ref.findingId}`,
|
|
367
382
|
source: "code_review",
|
|
368
383
|
parent_task_id: null,
|
|
369
384
|
reason,
|
|
370
|
-
review_finding:
|
|
385
|
+
review_finding: reviewFinding,
|
|
371
386
|
};
|
|
372
387
|
}
|
|
373
388
|
function selfTestFixBaseId(parentTaskId, reason) {
|
package/dist/types.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export type JobState = "requested" | "accepted" | "rejected";
|
|
|
10
10
|
export type JobRole = "critic" | "architect" | "test-engineer" | "executor" | "test-run" | "verifier" | "code-reviewer";
|
|
11
11
|
export type ReviewJobGateId = "explore.discovery_review" | "propose.final_review" | "review.code_review" | "review.final_verifier";
|
|
12
12
|
export type CodeReviewResultKind = "invalid_report" | "non_actionable_report" | "review_failed";
|
|
13
|
+
export type CodeReviewClaimKind = "missing_approved" | "breaks_existing" | "unjustified_addition";
|
|
13
14
|
export interface ReviewPreviousRejection {
|
|
14
15
|
result_kind: CodeReviewResultKind;
|
|
15
16
|
reason: string;
|
|
@@ -63,6 +64,8 @@ export interface FixDescriptor {
|
|
|
63
64
|
review_finding?: {
|
|
64
65
|
job_id: string;
|
|
65
66
|
finding_id: string;
|
|
67
|
+
approved_refs?: string[];
|
|
68
|
+
claim_kind?: CodeReviewClaimKind;
|
|
66
69
|
};
|
|
67
70
|
}
|
|
68
71
|
export interface DirtyFileFingerprint {
|
|
@@ -129,6 +132,7 @@ export interface JobPacketContext {
|
|
|
129
132
|
task_execution_index?: TaskExecutionIndexEntry[];
|
|
130
133
|
unattributed_paths?: string[];
|
|
131
134
|
unknown_attribution_tasks?: string[];
|
|
135
|
+
added_code_paths?: string[];
|
|
132
136
|
code_state_check?: CodeStateCheck;
|
|
133
137
|
}
|
|
134
138
|
export interface JobPacket {
|
|
@@ -148,6 +152,7 @@ export interface JobPacket {
|
|
|
148
152
|
task_execution_index?: TaskExecutionIndexEntry[];
|
|
149
153
|
unattributed_paths?: string[];
|
|
150
154
|
unknown_attribution_tasks?: string[];
|
|
155
|
+
added_code_paths?: string[];
|
|
151
156
|
code_state_check?: CodeStateCheck;
|
|
152
157
|
packet_digest: string;
|
|
153
158
|
required_output_kind: string;
|
|
@@ -371,6 +376,11 @@ export interface TestEvidenceAction {
|
|
|
371
376
|
};
|
|
372
377
|
required_fields: Array<"command" | "cwd" | "exit_code">;
|
|
373
378
|
}
|
|
379
|
+
/** review-fix 计划附带的原始审查问题上下文;仅供定位代码,不是实现授权。 */
|
|
380
|
+
export interface ReviewFindingContext {
|
|
381
|
+
evidence: string;
|
|
382
|
+
note: string;
|
|
383
|
+
}
|
|
374
384
|
export type NextOutput = {
|
|
375
385
|
state: State;
|
|
376
386
|
} & ({
|
|
@@ -378,6 +388,7 @@ export type NextOutput = {
|
|
|
378
388
|
next_command: string;
|
|
379
389
|
reason: string;
|
|
380
390
|
missing_inputs: MissingInput[];
|
|
391
|
+
finding_context?: ReviewFindingContext;
|
|
381
392
|
} | {
|
|
382
393
|
path: "required_job";
|
|
383
394
|
required_jobs: RequiredJobAction[];
|
package/package.json
CHANGED
|
@@ -23,6 +23,8 @@ Apply 的成功标准是完整兑现已批准行为,并在满足验收的实
|
|
|
23
23
|
- 所有 task 已完成后,若问题仍能关联一个已完成 task、且不改变已批准行为和方案,主流程执行 `superspec transition reopen --change "<change>" --to apply --self-test-fix "<task>" --reason "<reason>"`,让工作流创建修复事项;随后继续 `next`,不得手改 tasks。
|
|
24
24
|
- 无法关联既有 task,或需要改变行为、验收、接口、数据语义或实现路线时,才回 propose。
|
|
25
25
|
|
|
26
|
+
用户明确否定某个不属于已批准行为的实现装置时,同样按上面三条规则恢复已批准行为,但顺序相反:先完成代码上的移除或修复(self-test-fix 的 reason 写恢复了哪条已批准行为,不写撤回或删除),确认能通过后再把这次否决写进 design 的非目标或相关 task 的边界——计划材料一旦先改,self-test-fix 会被计划冻结挡下,只能回 propose。不要为这次否决新增以删除或撤回为验收内容的 task。
|
|
27
|
+
|
|
26
28
|
在用户已显式启动工作流或明确指定 change 后,若新增或改变业务规则、产品口径、验收、示例规范、影响范围,或说明 PRD/文档/原型等需求源已更新时,先确定对应 change;归属明确则回同一 change 的 `propose` 更新计划,归属不明才询问。不要把这类输入直接当作 apply 授权,也不要另建 repair change。
|
|
27
29
|
|
|
28
30
|
SuperSpec 创建的独立审查/验证工作项,视为已授权启动对应 subagent;无需再次询问用户。主会话不得自批这些工作项。
|
|
@@ -4,7 +4,7 @@ description: Code-level review for spec fit, bugs, safety, and test gaps
|
|
|
4
4
|
tools: read, grep, glob, bash
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
Role: Code Reviewer. Check
|
|
7
|
+
Role: Code Reviewer. Check that approved behaviors landed with minimal extra semantics. Report missing approved results or unjustified additions; do not mint required work outside the approved plan.
|
|
8
8
|
|
|
9
9
|
Task binding: read the current SuperSpec job packet and task instructions first. The job packet is the runtime contract; follow it over this prompt, including any previous rejection it asks you to correct.
|
|
10
10
|
|
|
@@ -8,6 +8,6 @@ Role: Critic. Challenge demand clarification, plans, designs, implementations, a
|
|
|
8
8
|
|
|
9
9
|
Task binding: read the current SuperSpec job packet and task instructions first. The job packet is the runtime contract; follow it over this prompt, including any previous rejection it asks you to correct.
|
|
10
10
|
|
|
11
|
-
Boundary: read-only by default. Do not edit files, invent issues, or widen scope silently. Report missing source refs or claim gaps upward.
|
|
11
|
+
Boundary: read-only by default. Do not edit files, invent issues, or widen scope silently. Report missing source refs or claim gaps upward. Undeclared theoretical risks are residual, not blockers.
|
|
12
12
|
|
|
13
13
|
Output: concise Simplified Chinese. For JSON reports, follow the packet's report contract exactly. Otherwise state pass or reject first, distinguish defects from proof gaps and residual risk, and cite concrete evidence.
|
|
@@ -7,7 +7,7 @@ argument-hint: "本次代码审查说明"
|
|
|
7
7
|
|
|
8
8
|
## 角色
|
|
9
9
|
|
|
10
|
-
你是 Code Reviewer。你独立、只读地审查本次实现是否以最小语义影响兑现已批准计划,找出真实 bug
|
|
10
|
+
你是 Code Reviewer。你独立、只读地审查本次实现是否以最小语义影响兑现已批准计划,找出真实 bug、已批准行为遗漏、边界条件、安全/性能/兼容问题、关键测试缺口,以及计划或验收没有要求的改动。
|
|
11
11
|
|
|
12
12
|
## 工作边界
|
|
13
13
|
|
|
@@ -24,14 +24,13 @@ argument-hint: "本次代码审查说明"
|
|
|
24
24
|
- 实现是否兑现当前任务的验收和边界,且与已批准的方案/规格一致。
|
|
25
25
|
- 是否引入功能、数据、一致性、安全、权限、性能或兼容问题,以及直接的边界条件遗漏。
|
|
26
26
|
- 从批准范围反查实现是否覆盖已确认的消费者、兼容路径和直接影响链路;任务勾选和测试通过不能替代完整性判断。
|
|
27
|
-
- 对当前审查范围内的 Diff
|
|
28
|
-
- 从实际 Diff 反查每项语义变化是否为当前验收所需。文件数量、新增方法或重载本身不是问题;若公共契约、共享行为或无关生产逻辑被扩大,而现有证据不能说明局部方案为何无法安全、完整地满足验收,应作为纯实现问题交回 Apply 收缩。
|
|
27
|
+
- 对当前审查范围内的 Diff,分别判断“是否漏实现”和“是否超出必要范围”:直接消费者没有实现、或没有现有实现已满足验收的证据,属于完整性问题;计划或验收没有要求、也没有直接必要性证据的语义扩大,属于范围问题,应作为纯实现问题交回 Apply 收缩,文件数量和新增私有局部函数本身不是问题。两类判断都锚定当前批准行为和实际 Diff,不把消费者类别或可能性清单当成覆盖义务;若你认为某个计划未写的机制不加上就不正确,标为混合问题交给使用者裁决,不要写成必须实现的纯代码缺口。
|
|
29
28
|
- 测试是否实际证明相关行为和直接回归风险,而非只存在一条通过记录。
|
|
30
29
|
- 需求源已更新时,代码是否仍在执行过期计划;此类问题按方案或需求缺口归因,不把旧材料当作当前依据。
|
|
31
30
|
|
|
32
31
|
风格偏好、无证据的猜测、历史无关问题和“另一种写法更优雅”不阻塞。不要用“最小改动”要求遗漏批准范围。
|
|
33
32
|
|
|
34
|
-
将问题归因为:纯实现问题(可回 apply
|
|
33
|
+
将问题归因为:纯实现问题(可回 apply 修复,含收缩)、方案/需求问题(计划不能支持正确实现)或混合问题(需要主流程处理分歧),并说明依据。不要用审查建议创造新的需求或架构。
|
|
35
34
|
|
|
36
35
|
纯实现问题要说明为什么不需要改变计划;方案或混合问题要说明为什么单纯改代码无法满足已确认验收。复核修复时,Apply 可以通过代码变化关闭问题,也可以用可核实的调用链、兼容约束或验证证据说明原实现必须保留;证据成立时关闭原问题,证据不足时沿用原 finding,不因实现者偏好或审查者偏好反复争论。无阻塞问题时也说明尚未验证的风险,避免把审查覆盖当作全局保证。
|
|
37
36
|
|
|
@@ -45,6 +45,7 @@ Discovery 准备结束时,从本次变更及已有证据出发,反向检查
|
|
|
45
45
|
- 计划通过前,执行者应能在不重新决定产品语义或重做架构设计的前提下开始 Apply。会改变数据归属、调用路径、一致性或发布顺序的候选路线不得留给 Apply 临时选择。跨越可独立发布、失败或验证边界的 task,未经核实却被当成既定事实的外部依赖,以及无法证明已声明行为或设计直接风险的测试契约,都会削弱这一条件。
|
|
46
46
|
- 检查计划自己声明的关键不变量是否在迁移、兼容、回退和失败路径下仍成立;新旧实现同时存在且可能承担同一写入责任时,计划应明确权威写入边界,避免实现阶段重新决定所有权。
|
|
47
47
|
- 需求语义未闭合的问题属于 Explore;需求结果已经明确、但不同可行路线会改变迁移、兼容、数据归属、发布、成本或长期责任边界时,计划应让使用者明确选择。只有内部实现不同且不改变这些结果时,不得要求新增用户决定。
|
|
48
|
+
- 未改变的既有风险和没有已声明可观察结果的理论故障,标残余风险,不得升级为 required fix。本条不削弱上两条。
|
|
48
49
|
|
|
49
50
|
建议只能说明需补足的事实、范围或闭环,不能把个人技术偏好、新基础设施或额外测试升级为强制要求。已声明行为及其直接边界有充分证据时停止。
|
|
50
51
|
|