@peterxiaoyang/superspec 0.1.42 → 0.1.44
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/format.d.ts +0 -1
- package/dist/format.js +3 -13
- package/dist/git_state.js +1 -1
- package/dist/phase_confirmation.js +32 -4
- package/dist/phase_plan.d.ts +1 -1
- package/dist/phase_plan.js +50 -26
- package/dist/record.js +102 -18
- package/dist/review.d.ts +48 -1
- package/dist/review.js +90 -2
- package/dist/review_job_gates.d.ts +5 -0
- package/dist/review_job_gates.js +52 -1
- package/dist/sync.js +0 -1
- package/dist/transition.js +10 -9
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/templates/workflow/agents/executor.toml +1 -1
- package/templates/workflow/agents/test-runner.toml +1 -1
- package/templates/workflow/prompts/architect.md +18 -8
- package/templates/workflow/prompts/critic.md +21 -11
- package/templates/workflow/prompts/test-engineer.md +17 -8
- package/templates/workflow/prompts/test-runner.md +1 -1
- package/templates/workflow/skills/superspec-apply/SKILL.md +1 -1
- package/templates/workflow/skills/superspec-explore/SKILL.md +2 -0
- package/templates/workflow/skills/superspec-propose/SKILL.md +21 -36
package/dist/format.d.ts
CHANGED
package/dist/format.js
CHANGED
|
@@ -320,7 +320,6 @@ export function parseTestContractEntries(content) {
|
|
|
320
320
|
if (!isMarkdownTableSeparator(lines[i + 1]))
|
|
321
321
|
continue;
|
|
322
322
|
sawMatchingHeader = true;
|
|
323
|
-
const invariantIdx = normalizedHeader.indexOf("invariant");
|
|
324
323
|
for (let rowIndex = i + 2; rowIndex < lines.length; rowIndex++) {
|
|
325
324
|
const row = splitMarkdownTableRow(lines[rowIndex]);
|
|
326
325
|
if (row.length === 0)
|
|
@@ -335,7 +334,6 @@ export function parseTestContractEntries(content) {
|
|
|
335
334
|
entries.push({
|
|
336
335
|
test_id: testId,
|
|
337
336
|
scenario: (row[scenarioIdx] ?? "").trim(),
|
|
338
|
-
invariant: invariantIdx >= 0 ? (row[invariantIdx] ?? "").trim() : "",
|
|
339
337
|
});
|
|
340
338
|
}
|
|
341
339
|
}
|
|
@@ -450,21 +448,13 @@ export function validateUserDecision(d) {
|
|
|
450
448
|
return { ok: false, message: "决策文件缺少答复内容(answer)" };
|
|
451
449
|
return { ok: true, message: "" };
|
|
452
450
|
}
|
|
453
|
-
// ===== business-invariants.md =====
|
|
454
|
-
//
|
|
455
|
-
// 格式(propose skill 定义):
|
|
456
|
-
// # Business Invariants
|
|
457
|
-
// - INV-001 用户密码必须加密存储
|
|
458
|
-
//
|
|
459
|
-
// 引擎行为:Phase 1-5 只校验文件存在性(轻量)。
|
|
460
|
-
// 内部结构(INV-XXX 编号)是 agent 指引,引擎不逐行解析。
|
|
461
451
|
// ===== test-contract.md =====
|
|
462
452
|
//
|
|
463
453
|
// 格式(propose skill 定义):
|
|
464
454
|
// # Test Contract
|
|
465
|
-
// | test_id |
|
|
466
|
-
//
|
|
467
|
-
// | TEST-001 |
|
|
455
|
+
// | test_id | scenario |
|
|
456
|
+
// |---|---|
|
|
457
|
+
// | TEST-001 | 注册时密码被加密 |
|
|
468
458
|
//
|
|
469
459
|
// 引擎行为:Phase 1-5 只校验文件存在性(轻量)。
|
|
470
460
|
// 表格结构是 agent 指引,引擎不逐行解析。
|
package/dist/git_state.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, lstatSync, readdirSync, readlinkSync, statSync } from "node
|
|
|
3
3
|
import { extname, join } from "node:path";
|
|
4
4
|
import { sha256File, sha256Text } from "./store.js";
|
|
5
5
|
const PROCESS_DOC_RE = /^(?:openspec\/changes\/[^/]+\/)?(?:proposal|design|tasks)\.md$/;
|
|
6
|
-
const PROCESS_ARTIFACT_RE = /^(?:openspec\/changes\/[^/]+\/)?\.superspec\/artifacts\/(?:discovery|
|
|
6
|
+
const PROCESS_ARTIFACT_RE = /^(?:openspec\/changes\/[^/]+\/)?\.superspec\/artifacts\/(?:discovery|test-contract)\.md$/;
|
|
7
7
|
const CODE_EXTENSIONS = new Set([
|
|
8
8
|
".c", ".cc", ".cpp", ".cs", ".css", ".go", ".h", ".hpp", ".html", ".java", ".js", ".jsx",
|
|
9
9
|
".json", ".kt", ".mjs", ".mts", ".php", ".py", ".rb", ".rs", ".scss", ".sh", ".sql",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { currentGitHead, dirtyCodeFiles } from "./git_state.js";
|
|
2
|
-
import { reviewEvidenceDigest } from "./review.js";
|
|
2
|
+
import { historicalProposeReadyRoles, reviewEvidenceDigest, reviewGateRoleResolution } from "./review.js";
|
|
3
|
+
import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./review_job_gates.js";
|
|
4
|
+
import { changeRoot as openspecChangeRoot } from "./openspec.js";
|
|
3
5
|
import { findLatestEvent, sha256Text } from "./store.js";
|
|
4
6
|
export const PHASE_CONFIRMATION_SCOPE_PREFIX = "phase_confirmation:";
|
|
5
7
|
function latestTransition(events, predicate) {
|
|
@@ -63,7 +65,12 @@ function boundaryForState(state) {
|
|
|
63
65
|
default: return null;
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
|
-
function
|
|
68
|
+
function ordinaryReviewRolesForBoundary(events, boundary, gate, risk) {
|
|
69
|
+
return boundary === "propose_to_apply"
|
|
70
|
+
? historicalProposeReadyRoles(events)
|
|
71
|
+
: gate.requiredRolesForRisk(risk);
|
|
72
|
+
}
|
|
73
|
+
function materialDigest(projectRoot, events, snapshot, boundary, risk) {
|
|
67
74
|
const head = currentGitHead(projectRoot);
|
|
68
75
|
const dirty = dirtyCodeFiles(projectRoot);
|
|
69
76
|
const acceptedJobs = snapshot.accepted_jobs
|
|
@@ -77,10 +84,31 @@ function materialDigest(projectRoot, events, snapshot) {
|
|
|
77
84
|
.sort((a, b) => a.job_id.localeCompare(b.job_id));
|
|
78
85
|
const documents = Object.entries(snapshot.document_digests)
|
|
79
86
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
87
|
+
const ordinaryReviewGate = boundary === "explore_to_propose"
|
|
88
|
+
? EXPLORE_DISCOVERY_REVIEW_GATE
|
|
89
|
+
: boundary === "propose_to_apply"
|
|
90
|
+
? PROPOSE_FINAL_REVIEW_GATE
|
|
91
|
+
: null;
|
|
92
|
+
const currentChangeRoot = openspecChangeRoot(projectRoot, snapshot.change_id);
|
|
93
|
+
const overriddenReviewJobs = ordinaryReviewGate
|
|
94
|
+
? ordinaryReviewRolesForBoundary(events, boundary, ordinaryReviewGate, risk)
|
|
95
|
+
.map(role => reviewGateRoleResolution(events, currentChangeRoot, ordinaryReviewGate, role))
|
|
96
|
+
.filter(resolution => resolution.kind === "overridden")
|
|
97
|
+
.map(resolution => ({
|
|
98
|
+
job_id: resolution.override.job_id,
|
|
99
|
+
role: resolution.override.role,
|
|
100
|
+
gate_id: resolution.override.gate_id,
|
|
101
|
+
packet_digest: resolution.override.packet_digest,
|
|
102
|
+
decision_event_id: resolution.override.decision_event_id,
|
|
103
|
+
decision_event_digest: resolution.override.decision_event_digest,
|
|
104
|
+
}))
|
|
105
|
+
.sort((left, right) => `${left.gate_id}\u0000${left.role}\u0000${left.job_id}`.localeCompare(`${right.gate_id}\u0000${right.role}\u0000${right.job_id}`))
|
|
106
|
+
: [];
|
|
80
107
|
return sha256Text(JSON.stringify({
|
|
81
108
|
documents,
|
|
82
109
|
tasks_structure_digest: snapshot.tasks_structure_digest,
|
|
83
110
|
accepted_jobs: acceptedJobs,
|
|
111
|
+
...(overriddenReviewJobs.length > 0 ? { overridden_review_jobs: overriddenReviewJobs } : {}),
|
|
84
112
|
review_evidence_digest: reviewEvidenceDigest(events),
|
|
85
113
|
code_state: {
|
|
86
114
|
head: head.head,
|
|
@@ -126,7 +154,7 @@ function buildActions(change, boundary, scope, question, specs, risk) {
|
|
|
126
154
|
reason: spec.reason,
|
|
127
155
|
...(spec.reasonPrompt ? { reason_prompt: spec.reasonPrompt } : {}),
|
|
128
156
|
record_argv: phaseRecordArgv(change),
|
|
129
|
-
record_input: { scope, question, answer: spec.label },
|
|
157
|
+
record_input: { scope, question, answer: spec.label, review_risk: risk },
|
|
130
158
|
resume: spec.resume.kind === "next"
|
|
131
159
|
? { kind: "next", argv: nextArgv(change, risk) }
|
|
132
160
|
: spec.resume.kind === "continue_current_phase"
|
|
@@ -147,7 +175,7 @@ export function phaseConfirmationForBoundary(projectRoot, events, snapshot, boun
|
|
|
147
175
|
return null;
|
|
148
176
|
const epoch = spec.epoch(events);
|
|
149
177
|
const epochEventId = epoch?.event_id ?? `legacy-${spec.state}`;
|
|
150
|
-
const digest = materialDigest(projectRoot, events, snapshot);
|
|
178
|
+
const digest = materialDigest(projectRoot, events, snapshot, boundary, risk);
|
|
151
179
|
const scope = `${spec.scopePrefix}:${epochEventId}:${digest}`;
|
|
152
180
|
const actions = buildActions(snapshot.change_id, boundary, scope, spec.question, spec.actions, risk);
|
|
153
181
|
return {
|
package/dist/phase_plan.d.ts
CHANGED
|
@@ -64,6 +64,7 @@ export type TransitionDecisionPlan = {
|
|
|
64
64
|
kind: "blocked";
|
|
65
65
|
jobs: Job[];
|
|
66
66
|
reason: string;
|
|
67
|
+
details?: Record<string, unknown>;
|
|
67
68
|
} | {
|
|
68
69
|
kind: "create_gate_jobs";
|
|
69
70
|
gate: ReviewGateRule;
|
|
@@ -86,7 +87,6 @@ export declare function proposalDocsBaseline(changeRoot: string): Record<string,
|
|
|
86
87
|
export declare function latestAcceptedProposalBaseline(events: Event[]): Record<string, string> | null;
|
|
87
88
|
export declare function latestReopenProposeBaseline(events: Event[]): Record<string, string> | null;
|
|
88
89
|
export declare function proposalDocsChangedSinceBaseline(changeRoot: string, baseline: Record<string, string>): boolean;
|
|
89
|
-
export declare function historicalProposeReadyRoles(events: Event[]): JobRole[];
|
|
90
90
|
export declare function pendingTaskIds(changeRoot: string): string[];
|
|
91
91
|
export declare function applyRequirementModeForCurrentRound(events: Event[]): boolean;
|
|
92
92
|
export declare function pendingTaskStatusForApply(changeRoot: string, events: Event[]): ApplyPendingTaskStatus;
|
package/dist/phase_plan.js
CHANGED
|
@@ -4,7 +4,7 @@ import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./revi
|
|
|
4
4
|
import { collectProposeOpenQuestions, countDiscoveryOpenQuestions, parseTasksMd, pendingTasksInContent, validateDiscovery, validateExecutionRequirements, } from "./format.js";
|
|
5
5
|
import { currentGitHead } from "./git_state.js";
|
|
6
6
|
import { docRef, sha256File } from "./store.js";
|
|
7
|
-
import { isReviewReadyVerifier, isFreshReviewVerifier, readReviewPolicyFromEvents, reviewEvidenceDigest, } from "./review.js";
|
|
7
|
+
import { isReviewReadyVerifier, isFreshReviewVerifier, historicalProposeReadyRoles, readReviewPolicyFromEvents, reviewGateRoleResolution, reviewRejectionOverrideScope, reviewEvidenceDigest, } from "./review.js";
|
|
8
8
|
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, codeReviewDecisionScope, codeReviewJobStaleReason, collectCodeReviewGateFacts, currentCodeReviewWorkingPaths, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
|
|
9
9
|
import { isPhaseAdvanceAuthorized, latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
|
|
10
10
|
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
@@ -22,16 +22,49 @@ function phaseConfirmationStep(context, boundary, reason) {
|
|
|
22
22
|
reason,
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
-
function reviewGatePlan(snapshot, gate, requiredRoles) {
|
|
25
|
+
function reviewGatePlan(snapshot, events, changeRoot, gate, requiredRoles) {
|
|
26
26
|
const missingRoles = [];
|
|
27
27
|
for (const role of requiredRoles) {
|
|
28
28
|
const openForRole = snapshot.open_jobs.find(job => gate.isJobForGate(job) && job.role === role);
|
|
29
29
|
if (openForRole) {
|
|
30
30
|
return { kind: "blocked", reason: `状态未推进;已有待完成工作项 ${role}(${openForRole.job_id})`, jobs: [openForRole] };
|
|
31
31
|
}
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
|
|
32
|
+
const resolution = reviewGateRoleResolution(events, changeRoot, gate, role);
|
|
33
|
+
if (resolution.kind === "accepted" || resolution.kind === "overridden")
|
|
34
|
+
continue;
|
|
35
|
+
if (resolution.kind === "rejected_pending") {
|
|
36
|
+
const terminal = resolution.terminal;
|
|
37
|
+
const overrideScope = reviewRejectionOverrideScope(terminal.job.job_id);
|
|
38
|
+
return {
|
|
39
|
+
kind: "blocked",
|
|
40
|
+
jobs: [],
|
|
41
|
+
reason: `状态未推进;${role} 审查工作项 ${terminal.job.job_id} 已拒绝。请修改绑定材料、在整份报告没有有效 blocker 时登记整体裁决,或在涉及业务决定时询问用户`,
|
|
42
|
+
details: {
|
|
43
|
+
review_rejection: {
|
|
44
|
+
job_id: terminal.job.job_id,
|
|
45
|
+
role,
|
|
46
|
+
gate_id: gate.gate_id,
|
|
47
|
+
packet_digest: terminal.job.packet_digest,
|
|
48
|
+
result_kind: terminal.result_kind,
|
|
49
|
+
reason: terminal.reason ?? "报告结论为 fail,工作项未通过",
|
|
50
|
+
override_scope: overrideScope,
|
|
51
|
+
allowed_actions: ["modify_materials", "record_override", "ask_user"],
|
|
52
|
+
record_input: {
|
|
53
|
+
scope: overrideScope,
|
|
54
|
+
answer: "do_not_block",
|
|
55
|
+
reason: "<说明整份报告为何没有有效 blocker>",
|
|
56
|
+
decision_source: "main_process",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const reason = resolution.kind === "stale"
|
|
63
|
+
? `需求 ${role} 的最新审查工作项已过期:${resolution.stale_reason}`
|
|
64
|
+
: resolution.kind === "rejected_invalid"
|
|
65
|
+
? `需求 ${role} 的最新审查报告无效,不能通过整体裁决绕过`
|
|
66
|
+
: `需求 ${role} 无已接受的工作项`;
|
|
67
|
+
missingRoles.push({ role, reason });
|
|
35
68
|
}
|
|
36
69
|
if (missingRoles.length > 0) {
|
|
37
70
|
return {
|
|
@@ -68,7 +101,7 @@ function missingBaseArtifact(changeRoot, risk) {
|
|
|
68
101
|
if (risk === "minimal")
|
|
69
102
|
return null;
|
|
70
103
|
const artifactsDir = join(changeRoot, ".superspec", "artifacts");
|
|
71
|
-
for (const doc of ["discovery.md", "
|
|
104
|
+
for (const doc of ["discovery.md", "test-contract.md"]) {
|
|
72
105
|
if (!existsSync(join(artifactsDir, doc)))
|
|
73
106
|
return `基础职责缺失:${doc} 不存在(risk=${risk} 需要)`;
|
|
74
107
|
}
|
|
@@ -123,20 +156,6 @@ export function proposalDocsChangedSinceBaseline(changeRoot, baseline) {
|
|
|
123
156
|
const current = proposalDocsBaseline(changeRoot);
|
|
124
157
|
return Object.entries(baseline).some(([path, digest]) => current[path] !== digest);
|
|
125
158
|
}
|
|
126
|
-
export function historicalProposeReadyRoles(events) {
|
|
127
|
-
const roles = new Set();
|
|
128
|
-
for (const ev of events) {
|
|
129
|
-
if (ev.event_type !== "transition_commit")
|
|
130
|
-
continue;
|
|
131
|
-
const newJobs = ev.payload.new_jobs ?? [];
|
|
132
|
-
for (const job of newJobs) {
|
|
133
|
-
if (PROPOSE_FINAL_REVIEW_GATE.isJobForGate(job) &&
|
|
134
|
-
(job.role === "critic" || job.role === "architect" || job.role === "test-engineer"))
|
|
135
|
-
roles.add(job.role);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return [...roles];
|
|
139
|
-
}
|
|
140
159
|
export function pendingTaskIds(changeRoot) {
|
|
141
160
|
const tasksContent = readFileSync(join(changeRoot, "tasks.md"), "utf8");
|
|
142
161
|
return pendingTasksInContent(tasksContent).map(task => task.taskId);
|
|
@@ -286,7 +305,7 @@ export function planNextStep(context) {
|
|
|
286
305
|
return { kind: "ask_user", state: "explore", ask, reason: `有 ${openQs} 个未确认问题` };
|
|
287
306
|
}
|
|
288
307
|
const requiredRoles = EXPLORE_DISCOVERY_REVIEW_GATE.requiredRolesForRisk(mode.risk);
|
|
289
|
-
if (!reviewGatePlan(snapshot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles)) {
|
|
308
|
+
if (!reviewGatePlan(snapshot, events, changeRoot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles)) {
|
|
290
309
|
const confirmation = phaseConfirmationStep(context, "explore_to_propose", "探索完成,等待用户确认进入计划阶段");
|
|
291
310
|
if (confirmation)
|
|
292
311
|
return confirmation;
|
|
@@ -613,10 +632,10 @@ function planExploreTransition(context) {
|
|
|
613
632
|
if (!discoveryCheck.ok)
|
|
614
633
|
return { kind: "skip", message: discoveryCheck.message };
|
|
615
634
|
const requiredRoles = EXPLORE_DISCOVERY_REVIEW_GATE.requiredRolesForRisk(mode.risk);
|
|
616
|
-
const gatePlan = reviewGatePlan(snapshot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles);
|
|
635
|
+
const gatePlan = reviewGatePlan(snapshot, events, changeRoot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles);
|
|
617
636
|
if (gatePlan)
|
|
618
637
|
return gatePlan;
|
|
619
|
-
const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "explore_to_propose");
|
|
638
|
+
const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "explore_to_propose", mode.risk);
|
|
620
639
|
const decision = confirmation ? latestAcceptedPhaseDecision(events, confirmation) : null;
|
|
621
640
|
if (!confirmation || decision?.decision !== "advance") {
|
|
622
641
|
return { kind: "skip", message: confirmation ? phaseConfirmationMissingMessage(confirmation) : "无法建立 Explore 阶段确认范围" };
|
|
@@ -649,10 +668,15 @@ function planProposeReadyTransition(context) {
|
|
|
649
668
|
if (missingArtifact)
|
|
650
669
|
return { kind: "skip", message: missingArtifact };
|
|
651
670
|
const requiredRoles = PROPOSE_FINAL_REVIEW_GATE.requiredRolesForRisk(risk);
|
|
652
|
-
const gatePlan = reviewGatePlan(snapshot, PROPOSE_FINAL_REVIEW_GATE, requiredRoles);
|
|
671
|
+
const gatePlan = reviewGatePlan(snapshot, context.events, changeRoot, PROPOSE_FINAL_REVIEW_GATE, requiredRoles);
|
|
653
672
|
if (gatePlan)
|
|
654
673
|
return gatePlan;
|
|
655
|
-
return {
|
|
674
|
+
return {
|
|
675
|
+
kind: "advance",
|
|
676
|
+
fromState: "propose",
|
|
677
|
+
toState: "propose_ready",
|
|
678
|
+
reason: `risk=${risk},所有需求已满足`,
|
|
679
|
+
};
|
|
656
680
|
}
|
|
657
681
|
function planStartApplyTransition(context, enforceConfirmation = true) {
|
|
658
682
|
const { changeRoot, events, projectRoot, snapshot } = context;
|
|
@@ -665,7 +689,7 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
|
|
|
665
689
|
}
|
|
666
690
|
const reviewedRoles = historicalProposeReadyRoles(events);
|
|
667
691
|
if (reviewedRoles.length > 0) {
|
|
668
|
-
const gatePlan = reviewGatePlan(snapshot, PROPOSE_FINAL_REVIEW_GATE, reviewedRoles);
|
|
692
|
+
const gatePlan = reviewGatePlan(snapshot, events, changeRoot, PROPOSE_FINAL_REVIEW_GATE, reviewedRoles);
|
|
669
693
|
if (gatePlan) {
|
|
670
694
|
return {
|
|
671
695
|
...gatePlan,
|
package/dist/record.js
CHANGED
|
@@ -8,7 +8,7 @@ import { isPhaseConfirmationScope, phaseActionForAnswer, phaseConfirmationForCur
|
|
|
8
8
|
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_DECISION_SCOPE_PREFIX, codeReviewDecisionAnswerLabel, normalizeCodeReviewDecisionAnswer, } from "./code_review.js";
|
|
9
9
|
import { invalidReasonForSubmittedReport } from "./job_validity.js";
|
|
10
10
|
import { jobSubmitArgv } from "./job_action.js";
|
|
11
|
-
import { REVIEW_DOC_PATHS } from "./review.js";
|
|
11
|
+
import { REVIEW_DOC_PATHS, REVIEW_REJECTION_OVERRIDE_SCOPE_PREFIX, REVIEW_REJECTION_OVERRIDE_ANSWER, parseReviewRejectionOverrideScope, reviewGateRoleResolution, reviewRejectionOverrideScope, } from "./review.js";
|
|
12
12
|
import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./review_job_gates.js";
|
|
13
13
|
import { RecordInputDecodingError, readRecordInputFile } from "./record_input.js";
|
|
14
14
|
const REVIEW_REPORT_REQUIRED_FIELDS = ["role", "verdict", "findings"];
|
|
@@ -63,8 +63,8 @@ function previousRejectionInstruction(job) {
|
|
|
63
63
|
return "";
|
|
64
64
|
const reason = `上一次同角色审查没有形成可推进结论,原因:${previous.reason}。`;
|
|
65
65
|
if (!previous.findings || previous.findings.length === 0)
|
|
66
|
-
return `${reason}
|
|
67
|
-
return `${reason}
|
|
66
|
+
return `${reason}本轮是修复复核;完整读取材料只用于核对当前修正和直接一致性,不得借复核重新审计与修正无关的历史设计,`;
|
|
67
|
+
return `${reason}本轮是修复复核:逐项判断本工作项附带的上一次同角色 finding 是否仍成立。Finding 中的 recommendation 只是非绑定建议,不是需求或验收标准;先独立核对 underlying problem、直接证据和本次验收,不得因原建议指定了某种架构就要求照做。同一问题仍存在时复用原 finding ID;legacy finding 没有 ID 时沿用其原始语义并补一个稳定 ID;已解决或已由等价证据闭环的问题不要重复报告,不得通过更换 ID、标题或措辞重复同一问题。默认只复核历史 finding;新 blocker 仅允许是本次修正直接引入的回归,并必须说明“修正动作 → 新问题”的因果链,不得展开无关的故障模型、消费者或架构议题。`;
|
|
68
68
|
}
|
|
69
69
|
function ordinaryReviewerFindingInstruction(job) {
|
|
70
70
|
if (!isOrdinaryReviewer(job.role))
|
|
@@ -90,14 +90,14 @@ function reviewScopeInstruction(job, reviewTargets, readOnlyRefs) {
|
|
|
90
90
|
}
|
|
91
91
|
const targets = reviewTargets.length > 0 ? `本 gate 可提出修改建议的审查目标为 ${reviewTargets.join(", ")}。` : "";
|
|
92
92
|
const refs = readOnlyRefs.length > 0
|
|
93
|
-
? `只读上游引用为 ${readOnlyRefs.join(", ")}
|
|
93
|
+
? `只读上游引用为 ${readOnlyRefs.join(", ")};只允许读取和核对一致性,不得要求在当前阶段修改、追加、删除、重排或格式化这些文件。只读引用中与本次目标或绑定上游事实无直接因果关系的历史质量问题不得作为本 gate 的 fail。如果本次审查目标或 boundFiles 中绑定的上游事实直接造成变更包跨文档不一致,并且会影响明确验收或实施落地,可以 fail;finding 必须锚定为当前变更包未完成一致性闭环,required outcome 只要求消除矛盾,不得指定必须修改哪份文档或采用哪种技术方案。`
|
|
94
94
|
: "";
|
|
95
95
|
return targets + refs;
|
|
96
96
|
}
|
|
97
97
|
function reviewCoverageInstruction(job) {
|
|
98
98
|
if (!requiresReviewScope(job))
|
|
99
99
|
return "";
|
|
100
|
-
return `审查顺序固定为:先建立覆盖索引,按文件和标题/行段完整浏览全部 boundFiles
|
|
100
|
+
return `审查顺序固定为:先建立覆盖索引,按文件和标题/行段完整浏览全部 boundFiles 至文件末尾;长文档必须分段读取,不能在发现第一个 blocker 时停止覆盖。read_only_refs 按本次问题需要读取,用于核对目标与上下游一致性,不要求机械全文遍历或写入 checked_paths。完整读取只用于核对本次 change 的目标、直接修改及跨文档一致性,不等于允许重新审计全部历史设计。若 proposal.md 存在“需求变化”,以其中记录的受影响能力、直接修改章节和保持不变范围作为本轮增量审查的权威锚点;本轮新 finding 必须由该需求变化、为接入变化所做的直接修改,或这些修改造成的跨文档矛盾引起,并说明因果链。此前已通过且被明确记录为保持不变的设计不得重新打开为 blocker;不要凭通用风险类别猜测变化范围。注意事项和故障类别是条件式检查项,不是必须穷举的清单。Recommendation 只能描述需要补足的结果、契约或证据,不得把未经 proposal、design 或用户决定选定的新基础设施写成 required fix。报告中的 review_scope.checked_paths 必须列出全部已浏览的 boundFiles;它只是覆盖回执,不能代替语义审查,也不扩大可报告问题的范围。`;
|
|
101
101
|
}
|
|
102
102
|
function migrationEvidenceInstruction(job) {
|
|
103
103
|
const isProposeReview = PROPOSE_FINAL_REVIEW_GATE.isJobForGate(job);
|
|
@@ -321,6 +321,21 @@ function findJob(events, jobId) {
|
|
|
321
321
|
}
|
|
322
322
|
return null;
|
|
323
323
|
}
|
|
324
|
+
function ordinaryReviewGateForJob(job) {
|
|
325
|
+
return [EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE]
|
|
326
|
+
.find(gate => gate.isJobForGate(job)) ?? null;
|
|
327
|
+
}
|
|
328
|
+
function invalidReviewRejectionOverrideResult(projectRoot, change, inputDigest, decision, reason, message) {
|
|
329
|
+
appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", {
|
|
330
|
+
accepted: false,
|
|
331
|
+
scope: decision.scope,
|
|
332
|
+
answer: decision.answer,
|
|
333
|
+
...(decision.decision_source !== undefined ? { decision_source: decision.decision_source } : {}),
|
|
334
|
+
reason,
|
|
335
|
+
input_digest: inputDigest,
|
|
336
|
+
}));
|
|
337
|
+
return { event_type: "user_decision_recorded", accepted: false, message };
|
|
338
|
+
}
|
|
324
339
|
/** 检查 job 是否已终态 */
|
|
325
340
|
function jobTerminalState(events, jobId) {
|
|
326
341
|
for (const ev of events) {
|
|
@@ -610,6 +625,7 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
610
625
|
}
|
|
611
626
|
let phaseConfirmation = null;
|
|
612
627
|
let phaseAction = null;
|
|
628
|
+
let phaseReviewRisk = null;
|
|
613
629
|
if (isPhaseConfirmationScope(decision.scope)) {
|
|
614
630
|
const existingAccepted = existing &&
|
|
615
631
|
existing.payload.accepted !== false;
|
|
@@ -623,7 +639,14 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
623
639
|
});
|
|
624
640
|
const changeRoot = openspecChangeRoot(projectRoot, change);
|
|
625
641
|
const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
|
|
626
|
-
const
|
|
642
|
+
const decisionRisk = decision.review_risk === undefined
|
|
643
|
+
? "strict"
|
|
644
|
+
: decision.review_risk === "minimal" || decision.review_risk === "normal" || decision.review_risk === "strict"
|
|
645
|
+
? decision.review_risk
|
|
646
|
+
: null;
|
|
647
|
+
const current = decisionRisk
|
|
648
|
+
? phaseConfirmationForCurrentState(projectRoot, events, snapshot, decisionRisk)
|
|
649
|
+
: null;
|
|
627
650
|
if (existingAccepted &&
|
|
628
651
|
current?.scope === decision.scope &&
|
|
629
652
|
latestAcceptedForScope?.event_id === existing.event_id) {
|
|
@@ -634,15 +657,17 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
634
657
|
};
|
|
635
658
|
}
|
|
636
659
|
const action = current ? phaseActionForAnswer(current, decision.answer) : null;
|
|
637
|
-
const rejectionReason = !
|
|
638
|
-
? "
|
|
639
|
-
:
|
|
640
|
-
? "
|
|
641
|
-
:
|
|
642
|
-
? "
|
|
643
|
-
: action
|
|
644
|
-
? "
|
|
645
|
-
:
|
|
660
|
+
const rejectionReason = !decisionRisk
|
|
661
|
+
? "invalid_phase_confirmation_risk"
|
|
662
|
+
: !current
|
|
663
|
+
? "phase_confirmation_not_pending"
|
|
664
|
+
: decision.scope !== current.scope
|
|
665
|
+
? "stale_phase_confirmation_scope"
|
|
666
|
+
: !action
|
|
667
|
+
? "invalid_phase_confirmation_answer"
|
|
668
|
+
: action.reason === "required" && !nonEmptyString(decision.reason)
|
|
669
|
+
? "missing_phase_confirmation_reason"
|
|
670
|
+
: null;
|
|
646
671
|
if (rejectionReason) {
|
|
647
672
|
if (existing &&
|
|
648
673
|
existing.payload.accepted === false &&
|
|
@@ -662,13 +687,16 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
662
687
|
}));
|
|
663
688
|
const message = rejectionReason === "invalid_phase_confirmation_answer" && current
|
|
664
689
|
? `阶段确认答复必须精确为:${current.ask.allowed_answers.join("、")}`
|
|
665
|
-
: rejectionReason === "
|
|
666
|
-
?
|
|
667
|
-
: "
|
|
690
|
+
: rejectionReason === "invalid_phase_confirmation_risk"
|
|
691
|
+
? "阶段确认 review_risk 必须是 minimal、normal 或 strict"
|
|
692
|
+
: rejectionReason === "missing_phase_confirmation_reason" && action
|
|
693
|
+
? action.reason_prompt ?? "当前选择必须写明原因"
|
|
694
|
+
: "阶段确认已失效或当前没有待确认的阶段边界,请重新执行 next";
|
|
668
695
|
return { event_type: "user_decision_recorded", accepted: false, message };
|
|
669
696
|
}
|
|
670
697
|
phaseConfirmation = current;
|
|
671
698
|
phaseAction = action;
|
|
699
|
+
phaseReviewRisk = decisionRisk;
|
|
672
700
|
}
|
|
673
701
|
if (existing && !phaseAction) {
|
|
674
702
|
const accepted = existing.payload.accepted !== false;
|
|
@@ -678,6 +706,59 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
678
706
|
message: accepted ? "幂等返回:同一用户决策已登记" : "幂等返回:同一无效用户决策已登记",
|
|
679
707
|
};
|
|
680
708
|
}
|
|
709
|
+
let reviewRejectionOverride = null;
|
|
710
|
+
let reviewRejectionDecisionSource = null;
|
|
711
|
+
if (decision.scope.startsWith(REVIEW_REJECTION_OVERRIDE_SCOPE_PREFIX)) {
|
|
712
|
+
const jobId = parseReviewRejectionOverrideScope(decision.scope);
|
|
713
|
+
if (!jobId) {
|
|
714
|
+
return invalidReviewRejectionOverrideResult(projectRoot, change, inputDigest, { scope: decision.scope, answer: decision.answer, decision_source: decision.decision_source }, "invalid_review_rejection_override_scope", "审查拒绝裁决 scope 必须包含有效 job ID");
|
|
715
|
+
}
|
|
716
|
+
if (decision.scope !== reviewRejectionOverrideScope(jobId)) {
|
|
717
|
+
return invalidReviewRejectionOverrideResult(projectRoot, change, inputDigest, { scope: decision.scope, answer: decision.answer, decision_source: decision.decision_source }, "invalid_review_rejection_override_scope", `审查拒绝裁决 scope 必须精确为 ${reviewRejectionOverrideScope(jobId)}`);
|
|
718
|
+
}
|
|
719
|
+
if (decision.answer !== REVIEW_REJECTION_OVERRIDE_ANSWER) {
|
|
720
|
+
return invalidReviewRejectionOverrideResult(projectRoot, change, inputDigest, { scope: decision.scope, answer: decision.answer, decision_source: decision.decision_source }, "invalid_review_rejection_override_answer", `审查拒绝裁决 answer 必须是 ${REVIEW_REJECTION_OVERRIDE_ANSWER}`);
|
|
721
|
+
}
|
|
722
|
+
if (decision.decision_source !== "main_process" && decision.decision_source !== "user") {
|
|
723
|
+
return invalidReviewRejectionOverrideResult(projectRoot, change, inputDigest, { scope: decision.scope, answer: decision.answer, decision_source: decision.decision_source }, "invalid_review_rejection_override_source", "审查拒绝裁决 decision_source 必须是 main_process 或 user");
|
|
724
|
+
}
|
|
725
|
+
if (!nonEmptyString(decision.reason)) {
|
|
726
|
+
return invalidReviewRejectionOverrideResult(projectRoot, change, inputDigest, { scope: decision.scope, answer: decision.answer, decision_source: decision.decision_source }, "missing_review_rejection_override_reason", "审查拒绝裁决必须写明原因");
|
|
727
|
+
}
|
|
728
|
+
const job = findJob(events, jobId);
|
|
729
|
+
const gate = job ? ordinaryReviewGateForJob(job) : null;
|
|
730
|
+
if (!job || !gate || !isOrdinaryReviewer(job.role)) {
|
|
731
|
+
return invalidReviewRejectionOverrideResult(projectRoot, change, inputDigest, { scope: decision.scope, answer: decision.answer, decision_source: decision.decision_source }, "review_rejection_override_job_not_found", "审查拒绝裁决必须指向当前 Explore / Propose gate 的普通 Reviewer job");
|
|
732
|
+
}
|
|
733
|
+
const changeRoot = openspecChangeRoot(projectRoot, change);
|
|
734
|
+
const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
|
|
735
|
+
const gateIsCurrent = gate.gate_id === EXPLORE_DISCOVERY_REVIEW_GATE.gate_id
|
|
736
|
+
? snapshot.state === "explore"
|
|
737
|
+
: snapshot.state === "propose" || snapshot.state === "propose_ready";
|
|
738
|
+
if (!gateIsCurrent) {
|
|
739
|
+
return invalidReviewRejectionOverrideResult(projectRoot, change, inputDigest, { scope: decision.scope, answer: decision.answer, decision_source: decision.decision_source }, "review_rejection_override_gate_not_current", "审查拒绝裁决只能在对应 Explore / Propose gate 仍为当前阶段时登记");
|
|
740
|
+
}
|
|
741
|
+
const resolution = reviewGateRoleResolution(events, changeRoot, gate, job.role);
|
|
742
|
+
if (resolution.kind === "none" || resolution.terminal.job.job_id !== job.job_id) {
|
|
743
|
+
return invalidReviewRejectionOverrideResult(projectRoot, change, inputDigest, { scope: decision.scope, answer: decision.answer, decision_source: decision.decision_source }, "review_rejection_override_not_latest_terminal", "审查拒绝裁决只能指向当前 gate cycle 中该角色最新的 terminal job");
|
|
744
|
+
}
|
|
745
|
+
if (resolution.kind === "stale") {
|
|
746
|
+
return invalidReviewRejectionOverrideResult(projectRoot, change, inputDigest, { scope: decision.scope, answer: decision.answer, decision_source: decision.decision_source }, "stale_review_rejection_override_job", `审查拒绝裁决指向的 job 已过期:${resolution.stale_reason}`);
|
|
747
|
+
}
|
|
748
|
+
if (resolution.kind === "accepted") {
|
|
749
|
+
return invalidReviewRejectionOverrideResult(projectRoot, change, inputDigest, { scope: decision.scope, answer: decision.answer, decision_source: decision.decision_source }, "review_rejection_override_job_not_rejected", "审查拒绝裁决只能指向 rejected job");
|
|
750
|
+
}
|
|
751
|
+
if (resolution.kind === "rejected_invalid") {
|
|
752
|
+
return invalidReviewRejectionOverrideResult(projectRoot, change, inputDigest, { scope: decision.scope, answer: decision.answer, decision_source: decision.decision_source }, "review_rejection_override_not_review_failed", "只有 result_kind=review_failed 的拒绝报告可以裁决;无效报告必须修正后重新提交");
|
|
753
|
+
}
|
|
754
|
+
reviewRejectionOverride = {
|
|
755
|
+
job_id: job.job_id,
|
|
756
|
+
role: job.role,
|
|
757
|
+
gate_id: gate.gate_id,
|
|
758
|
+
packet_digest: job.packet_digest,
|
|
759
|
+
};
|
|
760
|
+
reviewRejectionDecisionSource = decision.decision_source;
|
|
761
|
+
}
|
|
681
762
|
if (decision.scope.startsWith(CODE_REVIEW_DECISION_SCOPE_PREFIX)) {
|
|
682
763
|
const normalizedAnswer = normalizeCodeReviewDecisionAnswer(decision.answer);
|
|
683
764
|
if (!normalizedAnswer) {
|
|
@@ -721,10 +802,13 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
721
802
|
? phaseAction.label
|
|
722
803
|
: normalizedAnswer ? codeReviewDecisionAnswerLabel(normalizedAnswer) : decision.answer,
|
|
723
804
|
...(typeof decision.reason === "string" ? { reason: decision.reason.trim() } : {}),
|
|
805
|
+
...(reviewRejectionDecisionSource ? { decision_source: reviewRejectionDecisionSource } : {}),
|
|
806
|
+
...(reviewRejectionOverride ? { review_rejection_override: reviewRejectionOverride } : {}),
|
|
724
807
|
...(phaseAction ? {
|
|
725
808
|
phase_confirmation: {
|
|
726
809
|
boundary: phaseAction.boundary,
|
|
727
810
|
decision: phaseAction.decision,
|
|
811
|
+
review_risk: phaseReviewRisk ?? "strict",
|
|
728
812
|
},
|
|
729
813
|
} : {}),
|
|
730
814
|
};
|
package/dist/review.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ReviewGateRule } from "./review_job_gates.ts";
|
|
2
|
-
import type { Event, Job, JobRole, Ref, ReviewPreviousRejection } from "./types.ts";
|
|
2
|
+
import type { CodeReviewResultKind, Event, Job, JobRole, Ref, ReviewPreviousRejection } from "./types.ts";
|
|
3
3
|
export type ReviewRisk = "minimal" | "normal" | "strict";
|
|
4
4
|
export interface ReviewPolicy {
|
|
5
5
|
review_risk: ReviewRisk;
|
|
@@ -9,7 +9,54 @@ export declare const REVIEW_DOC_PATHS: string[];
|
|
|
9
9
|
export declare function assertCommitPayloadExtension(payload: Record<string, unknown>): void;
|
|
10
10
|
export declare function reviewPolicyForRisk(risk: ReviewRisk): ReviewPolicy;
|
|
11
11
|
export declare function readReviewPolicyFromEvents(events: Event[]): ReviewPolicy | null;
|
|
12
|
+
export declare const REVIEW_REJECTION_OVERRIDE_SCOPE_PREFIX = "review_rejection_override:";
|
|
13
|
+
export declare const REVIEW_REJECTION_OVERRIDE_ANSWER = "do_not_block";
|
|
14
|
+
export type ReviewRejectionDecisionSource = "main_process" | "user";
|
|
15
|
+
export interface ReviewRejectionOverrideAudit {
|
|
16
|
+
job_id: string;
|
|
17
|
+
role: JobRole;
|
|
18
|
+
gate_id: ReviewGateRule["gate_id"];
|
|
19
|
+
packet_digest: string;
|
|
20
|
+
decision_source: ReviewRejectionDecisionSource;
|
|
21
|
+
reason: string;
|
|
22
|
+
decision_event_id: string;
|
|
23
|
+
decision_event_digest: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function reviewRejectionOverrideScope(jobId: string): string;
|
|
26
|
+
export declare function parseReviewRejectionOverrideScope(scope: string): string | null;
|
|
27
|
+
export interface ReviewTerminalResult {
|
|
28
|
+
job: Job;
|
|
29
|
+
event: Event;
|
|
30
|
+
state: "accepted" | "rejected";
|
|
31
|
+
result_kind?: CodeReviewResultKind;
|
|
32
|
+
reason?: string;
|
|
33
|
+
findings?: unknown[];
|
|
34
|
+
}
|
|
35
|
+
export declare function latestReviewTerminalForGateRole(events: Event[], gate: ReviewGateRule, role: JobRole): ReviewTerminalResult | null;
|
|
36
|
+
export declare function effectiveReviewRejectionOverride(events: Event[], gate: ReviewGateRule, terminal: ReviewTerminalResult): ReviewRejectionOverrideAudit | null;
|
|
37
|
+
export type ReviewGateRoleResolution = {
|
|
38
|
+
kind: "none";
|
|
39
|
+
} | {
|
|
40
|
+
kind: "stale";
|
|
41
|
+
terminal: ReviewTerminalResult;
|
|
42
|
+
stale_reason: string;
|
|
43
|
+
} | {
|
|
44
|
+
kind: "accepted";
|
|
45
|
+
terminal: ReviewTerminalResult;
|
|
46
|
+
} | {
|
|
47
|
+
kind: "rejected_invalid";
|
|
48
|
+
terminal: ReviewTerminalResult;
|
|
49
|
+
} | {
|
|
50
|
+
kind: "rejected_pending";
|
|
51
|
+
terminal: ReviewTerminalResult;
|
|
52
|
+
} | {
|
|
53
|
+
kind: "overridden";
|
|
54
|
+
terminal: ReviewTerminalResult;
|
|
55
|
+
override: ReviewRejectionOverrideAudit;
|
|
56
|
+
};
|
|
57
|
+
export declare function reviewGateRoleResolution(events: Event[], changeRoot: string, gate: ReviewGateRule, role: JobRole): ReviewGateRoleResolution;
|
|
12
58
|
export declare function latestReviewHistoryForGateRole(events: Event[], gate: ReviewGateRule, role: JobRole): ReviewPreviousRejection | null;
|
|
59
|
+
export declare function historicalProposeReadyRoles(events: Event[]): JobRole[];
|
|
13
60
|
export declare function isReviewReadyVerifier(job: Job): boolean;
|
|
14
61
|
export declare function reviewBoundFiles(changeRoot: string): Ref[];
|
|
15
62
|
export declare function reviewEvidenceDigest(events: Event[]): string;
|