gentle-pi 1.0.0 → 1.0.2
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/assets/agents/gentle-ai-explore.md +5 -1
- package/assets/agents/jd-judge-a.md +33 -0
- package/assets/agents/jd-judge-b.md +33 -0
- package/assets/agents/review-readability.md +29 -0
- package/assets/agents/review-reliability.md +29 -0
- package/assets/agents/review-resilience.md +29 -0
- package/assets/agents/review-risk.md +29 -0
- package/extensions/codegraph-tools.ts +236 -0
- package/extensions/gentle-ai.ts +95 -18
- package/lib/review-compact-contract.ts +183 -0
- package/lib/review-compact-store.ts +124 -4
- package/lib/review-compact.ts +108 -2
- package/lib/review-facade.ts +117 -10
- package/lib/review-reset.ts +69 -10
- package/lib/review-runtime-contract.ts +68 -0
- package/lib/review-transaction.ts +23 -11
- package/package.json +1 -1
- package/skills/judgment-day/references/prompts-and-formats.md +36 -3
- package/tests/codegraph-tools.test.ts +213 -0
- package/tests/package-manifest.test.ts +11 -3
- package/tests/review-compact-contract.test.ts +58 -0
- package/tests/review-compact-gate.test.ts +13 -19
- package/tests/review-compact-store.test.ts +15 -0
- package/tests/review-compact.test.ts +12 -6
- package/tests/review-controller.test.ts +169 -0
- package/tests/review-facade.test.ts +83 -17
- package/tests/review-ledger-contract.test.ts +76 -9
- package/tests/review-reset.test.ts +81 -16
- package/tests/review-runtime-contract.test.ts +70 -0
- package/tests/review-transaction.test.ts +121 -0
- package/tests/sdd-agent-tools.test.ts +8 -1
- package/tests/support/review-lens-parity.ts +12 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CAUSAL_DISPOSITION,
|
|
3
|
+
COMPACT_EVIDENCE_CLASS,
|
|
4
|
+
COMPACT_FINDING_OUTCOME,
|
|
5
|
+
COMPACT_SEVERITY,
|
|
6
|
+
type CompactRefuterResultInput,
|
|
7
|
+
type CompactValidationProofInput,
|
|
8
|
+
type CompactReviewResultInput,
|
|
9
|
+
type CompactTargetedValidationInput,
|
|
10
|
+
} from "./review-compact.ts";
|
|
11
|
+
import { REVIEW_LENS } from "./review-triggers.ts";
|
|
12
|
+
|
|
13
|
+
const DIGEST = /^[0-9a-f]{64}$/;
|
|
14
|
+
const LINEAGE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
15
|
+
|
|
16
|
+
export class CompactReviewContractError extends Error {
|
|
17
|
+
readonly area: string;
|
|
18
|
+
readonly code: string;
|
|
19
|
+
|
|
20
|
+
constructor(area: string, code: string, message: string) {
|
|
21
|
+
super(`${area}: ${message}`);
|
|
22
|
+
this.name = "CompactReviewContractError";
|
|
23
|
+
this.area = area;
|
|
24
|
+
this.code = code;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CompactStartContractInput {
|
|
29
|
+
cwd: string;
|
|
30
|
+
lineageId?: string;
|
|
31
|
+
policyHash: string;
|
|
32
|
+
projection?: { kind: "complete" };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CompactFinalizeContractInput {
|
|
36
|
+
cwd: string;
|
|
37
|
+
lineageId?: string;
|
|
38
|
+
review_result?: CompactReviewResultInput;
|
|
39
|
+
correction_line_forecast?: number;
|
|
40
|
+
validation_proof?: CompactValidationProofInput;
|
|
41
|
+
validation?: CompactTargetedValidationInput;
|
|
42
|
+
final_evidence?: string;
|
|
43
|
+
final_verification_passed?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function fail(area: string, code: string, message: string): never {
|
|
47
|
+
throw new CompactReviewContractError(area, code, message);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function record(value: unknown, area: string): Record<string, unknown> {
|
|
51
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) {
|
|
52
|
+
return fail(area, "type", "must be a plain object");
|
|
53
|
+
}
|
|
54
|
+
return value as Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function exact(value: unknown, area: string, required: readonly string[], optional: readonly string[] = []): Record<string, unknown> {
|
|
58
|
+
const object = record(value, area);
|
|
59
|
+
for (const key of Object.keys(object)) if (!required.includes(key) && !optional.includes(key)) fail(area, "unknown-key", `contains unknown field ${key}`);
|
|
60
|
+
for (const key of required) if (!(key in object)) fail(area, "required", `requires ${key}`);
|
|
61
|
+
return object;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function string(value: unknown, area: string): string {
|
|
65
|
+
if (typeof value !== "string") return fail(area, "type", "must be a string");
|
|
66
|
+
if (value.length === 0 || value.trim() !== value) return fail(area, "canonical-string", "must be non-empty and trimmed");
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function optionalString(value: unknown, area: string): string | undefined {
|
|
71
|
+
return value === undefined ? undefined : string(value, area);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function strings(value: unknown, area: string): string[] {
|
|
75
|
+
if (!Array.isArray(value)) return fail(area, "type", "must be an array");
|
|
76
|
+
const parsed = value.map((item, index) => string(item, `${area}[${index}]`));
|
|
77
|
+
if (new Set(parsed).size !== parsed.length) fail(area, "duplicate", "must not contain duplicates");
|
|
78
|
+
return parsed;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function enumValue<T extends Record<string, string>>(value: unknown, values: T, area: string): T[keyof T] {
|
|
82
|
+
const parsed = string(value, area);
|
|
83
|
+
if (!Object.values(values).includes(parsed)) return fail(area, "enum", "contains an unsupported value");
|
|
84
|
+
return parsed as T[keyof T];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function optionalLineage(value: unknown, area: string): string | undefined {
|
|
88
|
+
const parsed = optionalString(value, area);
|
|
89
|
+
if (parsed !== undefined && !LINEAGE_ID.test(parsed)) fail(area, "lineage", "is malformed");
|
|
90
|
+
return parsed;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseFinding(value: unknown, area: string) {
|
|
94
|
+
const row = exact(value, area, ["location", "severity", "claim", "evidence_class", "causal_disposition", "proof_refs"], ["id", "lens"]);
|
|
95
|
+
return {
|
|
96
|
+
...(row.id === undefined ? {} : { id: string(row.id, `${area}.id`) }),
|
|
97
|
+
...(row.lens === undefined ? {} : { lens: enumValue(row.lens, REVIEW_LENS, `${area}.lens`) }),
|
|
98
|
+
location: string(row.location, `${area}.location`),
|
|
99
|
+
severity: enumValue(row.severity, COMPACT_SEVERITY, `${area}.severity`),
|
|
100
|
+
claim: string(row.claim, `${area}.claim`),
|
|
101
|
+
evidence_class: enumValue(row.evidence_class, COMPACT_EVIDENCE_CLASS, `${area}.evidence_class`),
|
|
102
|
+
causal_disposition: enumValue(row.causal_disposition, CAUSAL_DISPOSITION, `${area}.causal_disposition`),
|
|
103
|
+
proof_refs: strings(row.proof_refs, `${area}.proof_refs`),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseReviewResult(value: unknown, area: string): CompactReviewResultInput {
|
|
108
|
+
const input = exact(value, area, ["lens_results"], ["refuter_request_hash", "refuter_results"]);
|
|
109
|
+
if (!Array.isArray(input.lens_results)) fail(`${area}.lens_results`, "type", "must be an array");
|
|
110
|
+
const lens_results = input.lens_results.map((item, index) => {
|
|
111
|
+
const row = exact(item, `${area}.lens_results[${index}]`, ["findings", "evidence"], ["lens"]);
|
|
112
|
+
if (!Array.isArray(row.findings)) fail(`${area}.lens_results[${index}].findings`, "type", "must be an array");
|
|
113
|
+
return {
|
|
114
|
+
...(row.lens === undefined ? {} : { lens: enumValue(row.lens, REVIEW_LENS, `${area}.lens_results[${index}].lens`) }),
|
|
115
|
+
findings: row.findings.map((finding, findingIndex) => parseFinding(finding, `${area}.lens_results[${index}].findings[${findingIndex}]`)),
|
|
116
|
+
evidence: strings(row.evidence, `${area}.lens_results[${index}].evidence`),
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
const refuter_request_hash = optionalString(input.refuter_request_hash, `${area}.refuter_request_hash`);
|
|
120
|
+
if (refuter_request_hash !== undefined && !DIGEST.test(refuter_request_hash)) fail(`${area}.refuter_request_hash`, "digest", "is malformed");
|
|
121
|
+
let refuter_results: CompactRefuterResultInput[] | undefined;
|
|
122
|
+
if (input.refuter_results !== undefined) {
|
|
123
|
+
if (!Array.isArray(input.refuter_results)) fail(`${area}.refuter_results`, "type", "must be an array");
|
|
124
|
+
refuter_results = input.refuter_results.map((item, index) => {
|
|
125
|
+
const row = exact(item, `${area}.refuter_results[${index}]`, ["finding_id", "outcome", "proof_refs"]);
|
|
126
|
+
return { finding_id: string(row.finding_id, `${area}.refuter_results[${index}].finding_id`), outcome: enumValue(row.outcome, COMPACT_FINDING_OUTCOME, `${area}.refuter_results[${index}].outcome`), proof_refs: strings(row.proof_refs, `${area}.refuter_results[${index}].proof_refs`) };
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return { lens_results, ...(refuter_request_hash === undefined ? {} : { refuter_request_hash }), ...(refuter_results === undefined ? {} : { refuter_results }) };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseValidationProof(value: unknown, area: string): CompactValidationProofInput {
|
|
133
|
+
const input = exact(value, area, ["original_criteria", "correction_regression"]);
|
|
134
|
+
const check = (item: unknown, label: string) => {
|
|
135
|
+
const row = exact(item, label, ["passed", "evidence"]);
|
|
136
|
+
if (typeof row.passed !== "boolean") fail(`${label}.passed`, "type", "must be boolean");
|
|
137
|
+
return { passed: row.passed, evidence: strings(row.evidence, `${label}.evidence`) };
|
|
138
|
+
};
|
|
139
|
+
return { original_criteria: check(input.original_criteria, `${area}.original_criteria`), correction_regression: check(input.correction_regression, `${area}.correction_regression`) };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseValidation(value: unknown, area: string): CompactTargetedValidationInput {
|
|
143
|
+
const input = exact(value, area, ["request_hash", "correction_ids", "original_criteria", "correction_regression", "fix_caused_findings", "follow_ups"]);
|
|
144
|
+
const check = (item: unknown, label: string) => {
|
|
145
|
+
const row = exact(item, label, ["passed", "evidence"]);
|
|
146
|
+
if (typeof row.passed !== "boolean") fail(`${label}.passed`, "type", "must be boolean");
|
|
147
|
+
return { passed: row.passed, evidence: strings(row.evidence, `${label}.evidence`) };
|
|
148
|
+
};
|
|
149
|
+
if (!Array.isArray(input.fix_caused_findings) || input.fix_caused_findings.length !== 0) fail(`${area}.fix_caused_findings`, "scope", "must be an explicitly empty array");
|
|
150
|
+
if (!Array.isArray(input.follow_ups)) fail(`${area}.follow_ups`, "type", "must be an array");
|
|
151
|
+
const follow_ups = input.follow_ups.map((item, index) => {
|
|
152
|
+
const row = exact(item, `${area}.follow_ups[${index}]`, ["finding_id", "location", "summary", "proof_refs"]);
|
|
153
|
+
return { finding_id: string(row.finding_id, `${area}.follow_ups[${index}].finding_id`), location: string(row.location, `${area}.follow_ups[${index}].location`), summary: string(row.summary, `${area}.follow_ups[${index}].summary`), proof_refs: strings(row.proof_refs, `${area}.follow_ups[${index}].proof_refs`) };
|
|
154
|
+
});
|
|
155
|
+
const request_hash = string(input.request_hash, `${area}.request_hash`);
|
|
156
|
+
if (!DIGEST.test(request_hash)) fail(`${area}.request_hash`, "digest", "is malformed");
|
|
157
|
+
return { request_hash, correction_ids: strings(input.correction_ids, `${area}.correction_ids`), original_criteria: check(input.original_criteria, `${area}.original_criteria`), correction_regression: check(input.correction_regression, `${area}.correction_regression`), fix_caused_findings: [], follow_ups };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function parseCompactStartInput(value: unknown): CompactStartContractInput {
|
|
161
|
+
const input = exact(value, "review/start", ["cwd", "policyHash"], ["lineageId", "projection"]);
|
|
162
|
+
const policyHash = string(input.policyHash, "review/start.policyHash");
|
|
163
|
+
if (!DIGEST.test(policyHash)) fail("review/start.policyHash", "digest", "is malformed");
|
|
164
|
+
let projection: { kind: "complete" } | undefined;
|
|
165
|
+
if (input.projection !== undefined) {
|
|
166
|
+
const raw = exact(input.projection, "review/start.projection", ["kind"]);
|
|
167
|
+
if (raw.kind !== "complete") fail("review/start.projection.kind", "enum", "must be complete");
|
|
168
|
+
projection = { kind: "complete" };
|
|
169
|
+
}
|
|
170
|
+
return { cwd: string(input.cwd, "review/start.cwd"), ...(optionalLineage(input.lineageId, "review/start.lineageId") === undefined ? {} : { lineageId: optionalLineage(input.lineageId, "review/start.lineageId")! }), policyHash, ...(projection === undefined ? {} : { projection }) };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function parseCompactFinalizeInput(value: unknown): CompactFinalizeContractInput {
|
|
174
|
+
const input = exact(value, "review/finalize", ["cwd"], ["lineageId", "review_result", "correction_line_forecast", "validation_proof", "validation", "final_evidence", "final_verification_passed"]);
|
|
175
|
+
if ((input.final_evidence === undefined) !== (input.final_verification_passed === undefined)) fail("review/finalize", "field-pair", "final evidence and result must appear together");
|
|
176
|
+
let correction_line_forecast: number | undefined;
|
|
177
|
+
if (input.correction_line_forecast !== undefined) {
|
|
178
|
+
if (!Number.isSafeInteger(input.correction_line_forecast) || input.correction_line_forecast <= 0) fail("review/finalize.correction_line_forecast", "range", "must be a positive safe integer");
|
|
179
|
+
correction_line_forecast = input.correction_line_forecast;
|
|
180
|
+
}
|
|
181
|
+
if (input.final_verification_passed !== undefined && typeof input.final_verification_passed !== "boolean") fail("review/finalize.final_verification_passed", "type", "must be boolean");
|
|
182
|
+
return { cwd: string(input.cwd, "review/finalize.cwd"), ...(optionalLineage(input.lineageId, "review/finalize.lineageId") === undefined ? {} : { lineageId: optionalLineage(input.lineageId, "review/finalize.lineageId")! }), ...(input.review_result === undefined ? {} : { review_result: parseReviewResult(input.review_result, "review/finalize.review_result") }), ...(correction_line_forecast === undefined ? {} : { correction_line_forecast }), ...(input.validation_proof === undefined ? {} : { validation_proof: parseValidationProof(input.validation_proof, "review/finalize.validation_proof") }), ...(input.validation === undefined ? {} : { validation: parseValidation(input.validation, "review/finalize.validation") }), ...(input.final_evidence === undefined ? {} : { final_evidence: string(input.final_evidence, "review/finalize.final_evidence") }), ...(input.final_verification_passed === undefined ? {} : { final_verification_passed: input.final_verification_passed }) };
|
|
183
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
closeSync,
|
|
4
4
|
existsSync,
|
|
5
5
|
fsyncSync,
|
|
6
|
+
lstatSync,
|
|
6
7
|
mkdirSync,
|
|
7
8
|
openSync,
|
|
8
9
|
readdirSync,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
type RepositoryAuthorityV1,
|
|
33
34
|
} from "./review-repository.ts";
|
|
34
35
|
import { ReviewTransactionStore } from "./review-transaction.ts";
|
|
36
|
+
import { assertLoadedReviewRuntimeIdentity } from "./review-runtime-contract.ts";
|
|
35
37
|
import {
|
|
36
38
|
captureCurrentReviewCandidateTree,
|
|
37
39
|
captureOrdinaryCorrectionSnapshot,
|
|
@@ -43,6 +45,7 @@ export const COMPACT_STORE_OPERATION = {
|
|
|
43
45
|
START: "review/start",
|
|
44
46
|
COMPLETE_REVIEW: "review/complete-review",
|
|
45
47
|
BEGIN_CORRECTION: "review/begin-correction",
|
|
48
|
+
FREEZE_VALIDATOR_REQUEST: "review/freeze-validator-request",
|
|
46
49
|
COMPLETE_CORRECTION: "review/complete-correction",
|
|
47
50
|
COMPLETE_VERIFICATION: "review/complete-verification",
|
|
48
51
|
} as const;
|
|
@@ -61,6 +64,28 @@ export interface CompactReviewStoreOptions {
|
|
|
61
64
|
faultInjector?: (point: "before-state-rename" | "before-receipt-rename") => void;
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
export const COMPACT_AUTHORITY_OUTCOME = {
|
|
68
|
+
NONE: "none",
|
|
69
|
+
ACTIVE: "active",
|
|
70
|
+
APPROVED: "approved",
|
|
71
|
+
ESCALATED: "escalated",
|
|
72
|
+
INVALID: "invalid",
|
|
73
|
+
} as const;
|
|
74
|
+
|
|
75
|
+
export type CompactAuthorityOutcomeV2 =
|
|
76
|
+
(typeof COMPACT_AUTHORITY_OUTCOME)[keyof typeof COMPACT_AUTHORITY_OUTCOME];
|
|
77
|
+
|
|
78
|
+
export interface CompactAuthorityLineageV2 {
|
|
79
|
+
lineage_id: string;
|
|
80
|
+
state: CompactReviewStateV2["state"];
|
|
81
|
+
revision: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface CompactAuthorityInspectionV2 {
|
|
85
|
+
outcome: CompactAuthorityOutcomeV2;
|
|
86
|
+
lineages: readonly CompactAuthorityLineageV2[];
|
|
87
|
+
}
|
|
88
|
+
|
|
64
89
|
export class CompactReviewStoreError extends Error {
|
|
65
90
|
constructor(message: string) {
|
|
66
91
|
super(message);
|
|
@@ -75,7 +100,7 @@ const STATE_KEYS = new Set([
|
|
|
75
100
|
"current_candidate_tree", "genesis_paths", "intended_untracked", "policy_hash",
|
|
76
101
|
"risk_tier", "selected_lenses", "original_changed_lines", "correction_budget",
|
|
77
102
|
"lens_results", "findings", "outcomes", "correction_ids", "follow_ups",
|
|
78
|
-
"correction_line_forecast", "correction", "validation", "final_evidence_hash",
|
|
103
|
+
"correction_line_forecast", "validator_request", "runtime_identity", "correction", "validation", "final_evidence_hash",
|
|
79
104
|
"escalation_reasons",
|
|
80
105
|
]);
|
|
81
106
|
const SNAPSHOT_KEYS = new Set([
|
|
@@ -108,6 +133,15 @@ const VALIDATION_KEYS = new Set([
|
|
|
108
133
|
"correction_ids", "original_criteria", "correction_regression", "follow_ups",
|
|
109
134
|
]);
|
|
110
135
|
const VALIDATION_CHECK_KEYS = new Set(["passed", "evidence"]);
|
|
136
|
+
const VALIDATOR_REQUEST_KEYS = new Set(["body", "request_hash"]);
|
|
137
|
+
const VALIDATOR_REQUEST_BODY_KEYS = new Set([
|
|
138
|
+
"schema", "purpose", "scope", "lineage_id", "candidate_tree", "fix_diff_hash", "correction_ids", "correction_paths",
|
|
139
|
+
"frozen_rows", "frozen_ledger_hash",
|
|
140
|
+
]);
|
|
141
|
+
const RUNTIME_IDENTITY_KEYS = new Set([
|
|
142
|
+
"schema", "compact_contract", "operation_contract", "state_schema", "record_schema", "receipt_schema",
|
|
143
|
+
"canonicalization", "identity_hash",
|
|
144
|
+
]);
|
|
111
145
|
const RECEIPT_BODY_KEYS = new Set([
|
|
112
146
|
"schema", "lineage_id", "generation", "authority_revision", "base_tree",
|
|
113
147
|
"initial_review_tree", "final_candidate_tree", "genesis_paths_hash",
|
|
@@ -168,6 +202,20 @@ function assertExactStateShape(state: CompactReviewStateV2): void {
|
|
|
168
202
|
assertObject(state.correction, "Compact correction");
|
|
169
203
|
assertExactKeys(state.correction, CORRECTION_KEYS, "Compact correction");
|
|
170
204
|
}
|
|
205
|
+
assertObject(state.runtime_identity, "Compact runtime identity");
|
|
206
|
+
assertExactKeys(state.runtime_identity, RUNTIME_IDENTITY_KEYS, "Compact runtime identity");
|
|
207
|
+
if (state.validator_request !== undefined) {
|
|
208
|
+
assertObject(state.validator_request, "Compact validator request");
|
|
209
|
+
assertExactKeys(state.validator_request, VALIDATOR_REQUEST_KEYS, "Compact validator request");
|
|
210
|
+
assertObject(state.validator_request.body, "Compact validator request body");
|
|
211
|
+
assertExactKeys(state.validator_request.body, VALIDATOR_REQUEST_BODY_KEYS, "Compact validator request body");
|
|
212
|
+
for (const finding of assertObjectArray(state.validator_request.body.frozen_rows, "Compact validator frozen rows")) {
|
|
213
|
+
assertExactKeys(finding, FINDING_KEYS, "Compact validator frozen row");
|
|
214
|
+
}
|
|
215
|
+
if (!Array.isArray(state.validator_request.body.scope) || state.validator_request.body.scope.some((scope) => typeof scope !== "string")) {
|
|
216
|
+
throw new CompactReviewStoreError("Compact validator request scope is malformed");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
171
219
|
if (state.validation !== undefined) {
|
|
172
220
|
assertObject(state.validation, "Compact validation");
|
|
173
221
|
assertExactKeys(state.validation, VALIDATION_KEYS, "Compact validation");
|
|
@@ -184,6 +232,7 @@ function assertExactStateShape(state: CompactReviewStateV2): void {
|
|
|
184
232
|
|
|
185
233
|
function operationMatchesState(operation: CompactStoreOperation, state: CompactReviewStateV2): boolean {
|
|
186
234
|
if (operation === COMPACT_STORE_OPERATION.START) return state.state === COMPACT_REVIEW_STATE.REVIEWING;
|
|
235
|
+
if (operation === COMPACT_STORE_OPERATION.FREEZE_VALIDATOR_REQUEST) return state.validator_request !== undefined;
|
|
187
236
|
if (operation === COMPACT_STORE_OPERATION.COMPLETE_VERIFICATION) return state.final_evidence_hash !== undefined;
|
|
188
237
|
if (operation === COMPACT_STORE_OPERATION.COMPLETE_CORRECTION) return state.correction !== undefined;
|
|
189
238
|
if (operation === COMPACT_STORE_OPERATION.BEGIN_CORRECTION) {
|
|
@@ -260,6 +309,7 @@ function immutableBinding(state: CompactReviewStateV2): unknown {
|
|
|
260
309
|
selected_lenses: state.selected_lenses,
|
|
261
310
|
original_changed_lines: state.original_changed_lines,
|
|
262
311
|
correction_budget: state.correction_budget,
|
|
312
|
+
runtime_identity: state.runtime_identity,
|
|
263
313
|
};
|
|
264
314
|
}
|
|
265
315
|
|
|
@@ -306,6 +356,20 @@ function assertSuccessor(
|
|
|
306
356
|
if (!equal(expected, next)) throw new CompactReviewStoreError("Compact correction forecast changed unrelated state");
|
|
307
357
|
return;
|
|
308
358
|
}
|
|
359
|
+
if (operation === COMPACT_STORE_OPERATION.FREEZE_VALIDATOR_REQUEST) {
|
|
360
|
+
if (
|
|
361
|
+
previous.state !== COMPACT_REVIEW_STATE.CORRECTION_REQUIRED ||
|
|
362
|
+
previous.correction_line_forecast === undefined ||
|
|
363
|
+
previous.validator_request !== undefined ||
|
|
364
|
+
next.validator_request === undefined
|
|
365
|
+
) {
|
|
366
|
+
throw new CompactReviewStoreError("Invalid compact validator request successor");
|
|
367
|
+
}
|
|
368
|
+
const expected = clone(previous);
|
|
369
|
+
expected.validator_request = next.validator_request;
|
|
370
|
+
if (!equal(expected, next)) throw new CompactReviewStoreError("Compact validator request changed unrelated state");
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
309
373
|
if (operation === COMPACT_STORE_OPERATION.COMPLETE_CORRECTION) {
|
|
310
374
|
if (
|
|
311
375
|
previous.state !== COMPACT_REVIEW_STATE.CORRECTION_REQUIRED ||
|
|
@@ -313,6 +377,8 @@ function assertSuccessor(
|
|
|
313
377
|
!stateIsOneOf(next.state, [COMPACT_REVIEW_STATE.VALIDATING, COMPACT_REVIEW_STATE.ESCALATED]) ||
|
|
314
378
|
!next.correction ||
|
|
315
379
|
!next.validation ||
|
|
380
|
+
!previous.validator_request ||
|
|
381
|
+
!equal(previous.validator_request, next.validator_request) ||
|
|
316
382
|
next.final_evidence_hash !== undefined ||
|
|
317
383
|
!equal(previous.lens_results, next.lens_results) ||
|
|
318
384
|
!equal(previous.findings, next.findings) ||
|
|
@@ -346,6 +412,10 @@ function graphCurrentExists(authority: RepositoryAuthorityV1): boolean {
|
|
|
346
412
|
return [0, 1, 2].some((slot) => existsSync(join(graphRoot, `CURRENT.${slot}`)));
|
|
347
413
|
}
|
|
348
414
|
|
|
415
|
+
export function hasGraphV1Authority(cwd: string): boolean {
|
|
416
|
+
return graphCurrentExists(resolveRepositoryAuthorityV1(cwd));
|
|
417
|
+
}
|
|
418
|
+
|
|
349
419
|
export function graphV1LineageExists(cwd: string, lineageId: string): boolean {
|
|
350
420
|
const authority = resolveRepositoryAuthorityV1(cwd);
|
|
351
421
|
if (!graphCurrentExists(authority)) return false;
|
|
@@ -403,7 +473,9 @@ export class CompactReviewStoreV2 {
|
|
|
403
473
|
|
|
404
474
|
load(): CompactStateRecordV2 {
|
|
405
475
|
try {
|
|
406
|
-
|
|
476
|
+
const record = parseRecord(readFileSync(this.statePath, "utf8"), this.#lineageId);
|
|
477
|
+
this.assertRuntimeIdentity(record.state);
|
|
478
|
+
return record;
|
|
407
479
|
} catch (error) {
|
|
408
480
|
if (error instanceof CompactReviewStoreError) throw error;
|
|
409
481
|
throw new CompactReviewStoreError(`Compact review state is unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -416,6 +488,7 @@ export class CompactReviewStoreV2 {
|
|
|
416
488
|
nextState: CompactReviewStateV2,
|
|
417
489
|
): string {
|
|
418
490
|
assertCompactReviewState(nextState);
|
|
491
|
+
this.assertRuntimeIdentity(nextState);
|
|
419
492
|
if (nextState.lineage_id !== this.#lineageId) throw new CompactReviewStoreError("Compact state lineage does not match the store");
|
|
420
493
|
const owner = this.#lock.acquire();
|
|
421
494
|
try {
|
|
@@ -531,6 +604,14 @@ export class CompactReviewStoreV2 {
|
|
|
531
604
|
return clone(receipt);
|
|
532
605
|
}
|
|
533
606
|
|
|
607
|
+
private assertRuntimeIdentity(state: CompactReviewStateV2): void {
|
|
608
|
+
try {
|
|
609
|
+
assertLoadedReviewRuntimeIdentity(state.runtime_identity);
|
|
610
|
+
} catch {
|
|
611
|
+
throw new CompactReviewStoreError("Persisted compact runtime identity is incompatible: REVIEW_RUNTIME_INCOMPATIBLE");
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
534
615
|
private assertCurrentAuthority(): void {
|
|
535
616
|
const current = resolveRepositoryAuthorityV1(this.#cwd);
|
|
536
617
|
if (
|
|
@@ -569,8 +650,17 @@ export function discoverCompactReviewStores(cwd: string): CompactReviewStoreV2[]
|
|
|
569
650
|
const authority = resolveRepositoryAuthorityV1(cwd);
|
|
570
651
|
const root = assertManagedStorePathV1(authority.common_directory, join(authority.store_root, "compact-v2"));
|
|
571
652
|
if (!existsSync(root)) return [];
|
|
572
|
-
|
|
573
|
-
|
|
653
|
+
const rootStat = lstatSync(root);
|
|
654
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
655
|
+
throw new CompactReviewStoreError("Invalid compact authority entry: compact-v2 root is not a real directory");
|
|
656
|
+
}
|
|
657
|
+
const entries = readdirSync(root, { withFileTypes: true });
|
|
658
|
+
for (const entry of entries) {
|
|
659
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || !LINEAGE_ID.test(entry.name)) {
|
|
660
|
+
throw new CompactReviewStoreError(`Invalid compact authority entry: ${entry.name}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return entries
|
|
574
664
|
.map((entry) => CompactReviewStoreV2.forRepository(cwd, entry.name))
|
|
575
665
|
.toSorted((left, right) => left.lineageDirectory.localeCompare(right.lineageDirectory));
|
|
576
666
|
}
|
|
@@ -579,3 +669,33 @@ export function compactV2LineageExists(cwd: string, lineageId: string): boolean
|
|
|
579
669
|
const store = CompactReviewStoreV2.forRepository(cwd, lineageId);
|
|
580
670
|
return existsSync(store.statePath) && statSync(store.statePath).isFile();
|
|
581
671
|
}
|
|
672
|
+
|
|
673
|
+
export function inspectCompactReviewAuthorityV2(cwd: string): CompactAuthorityInspectionV2 {
|
|
674
|
+
const lineages: CompactAuthorityLineageV2[] = [];
|
|
675
|
+
try {
|
|
676
|
+
for (const store of discoverCompactReviewStores(cwd)) {
|
|
677
|
+
const record = store.load();
|
|
678
|
+
lineages.push({
|
|
679
|
+
lineage_id: record.state.lineage_id,
|
|
680
|
+
state: record.state.state,
|
|
681
|
+
revision: record.revision,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
} catch {
|
|
685
|
+
return { outcome: COMPACT_AUTHORITY_OUTCOME.INVALID, lineages };
|
|
686
|
+
}
|
|
687
|
+
if (lineages.length === 0) return { outcome: COMPACT_AUTHORITY_OUTCOME.NONE, lineages };
|
|
688
|
+
if (lineages.some((lineage) =>
|
|
689
|
+
lineage.state !== COMPACT_REVIEW_STATE.APPROVED &&
|
|
690
|
+
lineage.state !== COMPACT_REVIEW_STATE.ESCALATED
|
|
691
|
+
)) {
|
|
692
|
+
return { outcome: COMPACT_AUTHORITY_OUTCOME.ACTIVE, lineages };
|
|
693
|
+
}
|
|
694
|
+
if (lineages.some((lineage) => lineage.state === COMPACT_REVIEW_STATE.ESCALATED)) {
|
|
695
|
+
return { outcome: COMPACT_AUTHORITY_OUTCOME.ESCALATED, lineages };
|
|
696
|
+
}
|
|
697
|
+
if (lineages.some((lineage) => lineage.state === COMPACT_REVIEW_STATE.APPROVED)) {
|
|
698
|
+
return { outcome: COMPACT_AUTHORITY_OUTCOME.APPROVED, lineages };
|
|
699
|
+
}
|
|
700
|
+
throw new CompactReviewStoreError("Compact authority inspection found no terminal or active lineage");
|
|
701
|
+
}
|
package/lib/review-compact.ts
CHANGED
|
@@ -15,6 +15,11 @@ import {
|
|
|
15
15
|
REVIEW_LENS,
|
|
16
16
|
type ReviewLens,
|
|
17
17
|
} from "./review-triggers.ts";
|
|
18
|
+
import {
|
|
19
|
+
assertReviewRuntimeIdentity,
|
|
20
|
+
loadedReviewRuntimeIdentity,
|
|
21
|
+
type LoadedReviewRuntimeIdentityV1,
|
|
22
|
+
} from "./review-runtime-contract.ts";
|
|
18
23
|
|
|
19
24
|
export const COMPACT_REVIEW_STATE = {
|
|
20
25
|
REVIEWING: "reviewing",
|
|
@@ -148,7 +153,35 @@ export interface CompactValidationCheckInput {
|
|
|
148
153
|
evidence: string[];
|
|
149
154
|
}
|
|
150
155
|
|
|
156
|
+
export interface CompactValidationProofInput {
|
|
157
|
+
original_criteria: CompactValidationCheckInput;
|
|
158
|
+
correction_regression: CompactValidationCheckInput;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const COMPACT_VALIDATOR_PURPOSE = { TARGETED_CORRECTION: "targeted-correction-validation" } as const;
|
|
162
|
+
|
|
163
|
+
export const COMPACT_VALIDATOR_SCOPE = { ORIGINAL_CRITERIA: "original-criteria", CORRECTION_REGRESSION: "correction-regression" } as const;
|
|
164
|
+
|
|
165
|
+
export interface CompactValidatorRequestBody {
|
|
166
|
+
schema: "gentle-ai.compact-validator-request/v1";
|
|
167
|
+
purpose: (typeof COMPACT_VALIDATOR_PURPOSE)[keyof typeof COMPACT_VALIDATOR_PURPOSE];
|
|
168
|
+
scope: Array<(typeof COMPACT_VALIDATOR_SCOPE)[keyof typeof COMPACT_VALIDATOR_SCOPE]>;
|
|
169
|
+
lineage_id: string;
|
|
170
|
+
candidate_tree: string;
|
|
171
|
+
fix_diff_hash: string;
|
|
172
|
+
correction_ids: string[];
|
|
173
|
+
correction_paths: string[];
|
|
174
|
+
frozen_rows: CompactFinding[];
|
|
175
|
+
frozen_ledger_hash: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface CompactValidatorRequest {
|
|
179
|
+
body: CompactValidatorRequestBody;
|
|
180
|
+
request_hash: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
151
183
|
export interface CompactTargetedValidationInput {
|
|
184
|
+
request_hash: string;
|
|
152
185
|
correction_ids: string[];
|
|
153
186
|
original_criteria: CompactValidationCheckInput;
|
|
154
187
|
correction_regression: CompactValidationCheckInput;
|
|
@@ -193,6 +226,8 @@ export interface CompactReviewStateV2 {
|
|
|
193
226
|
correction_ids: string[];
|
|
194
227
|
follow_ups: CompactFollowUp[];
|
|
195
228
|
correction_line_forecast?: number;
|
|
229
|
+
validator_request?: CompactValidatorRequest;
|
|
230
|
+
runtime_identity: LoadedReviewRuntimeIdentityV1;
|
|
196
231
|
correction?: CompactCorrectionRecord;
|
|
197
232
|
validation?: CompactTargetedValidation;
|
|
198
233
|
final_evidence_hash?: string;
|
|
@@ -490,6 +525,7 @@ export function createCompactReviewState(input: {
|
|
|
490
525
|
outcomes: {},
|
|
491
526
|
correction_ids: [],
|
|
492
527
|
follow_ups: [],
|
|
528
|
+
runtime_identity: loadedReviewRuntimeIdentity(),
|
|
493
529
|
escalation_reasons: [],
|
|
494
530
|
};
|
|
495
531
|
assertCompactReviewState(state);
|
|
@@ -576,6 +612,48 @@ export function completeCompactReview(
|
|
|
576
612
|
return next;
|
|
577
613
|
}
|
|
578
614
|
|
|
615
|
+
export function createCompactValidatorRequest(
|
|
616
|
+
current: CompactReviewStateV2,
|
|
617
|
+
snapshot: CorrectionSnapshotV1,
|
|
618
|
+
): CompactValidatorRequest {
|
|
619
|
+
assertCompactReviewState(current);
|
|
620
|
+
if (current.state !== COMPACT_REVIEW_STATE.CORRECTION_REQUIRED || current.correction_line_forecast === undefined) {
|
|
621
|
+
throw new Error(`Cannot create compact validator request from ${current.state}`);
|
|
622
|
+
}
|
|
623
|
+
const frozenRows = current.findings.filter((finding) => current.correction_ids.includes(finding.id));
|
|
624
|
+
const body: CompactValidatorRequestBody = {
|
|
625
|
+
schema: "gentle-ai.compact-validator-request/v1",
|
|
626
|
+
purpose: COMPACT_VALIDATOR_PURPOSE.TARGETED_CORRECTION,
|
|
627
|
+
scope: Object.values(COMPACT_VALIDATOR_SCOPE),
|
|
628
|
+
lineage_id: current.lineage_id,
|
|
629
|
+
candidate_tree: snapshot.candidate_tree,
|
|
630
|
+
fix_diff_hash: snapshot.fix_diff_hash,
|
|
631
|
+
correction_ids: [...current.correction_ids],
|
|
632
|
+
correction_paths: [...snapshot.changed_paths],
|
|
633
|
+
frozen_rows: clone(frozenRows),
|
|
634
|
+
frozen_ledger_hash: domainHashV1("compact-findings", current.findings),
|
|
635
|
+
};
|
|
636
|
+
return { body, request_hash: domainHashV1("compact-validator-request", body) };
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export function freezeCompactValidatorRequest(
|
|
640
|
+
current: CompactReviewStateV2,
|
|
641
|
+
request: CompactValidatorRequest,
|
|
642
|
+
): CompactReviewStateV2 {
|
|
643
|
+
assertCompactReviewState(current);
|
|
644
|
+
if (current.state !== COMPACT_REVIEW_STATE.CORRECTION_REQUIRED || current.correction_line_forecast === undefined) {
|
|
645
|
+
throw new Error(`Cannot freeze compact validator request from ${current.state}`);
|
|
646
|
+
}
|
|
647
|
+
if (current.validator_request !== undefined) {
|
|
648
|
+
if (!equal(current.validator_request, request)) throw new Error("Targeted validation does not match the frozen native validator request");
|
|
649
|
+
return clone(current);
|
|
650
|
+
}
|
|
651
|
+
const next = clone(current);
|
|
652
|
+
next.validator_request = clone(request);
|
|
653
|
+
assertCompactReviewState(next);
|
|
654
|
+
return next;
|
|
655
|
+
}
|
|
656
|
+
|
|
579
657
|
export function beginCompactCorrection(
|
|
580
658
|
current: CompactReviewStateV2,
|
|
581
659
|
forecast: number,
|
|
@@ -628,6 +706,17 @@ export function completeCompactCorrection(
|
|
|
628
706
|
if (!equal(canonicalStrings(intendedUntracked, "Correction untracked paths"), current.intended_untracked)) {
|
|
629
707
|
throw new Error("Compact correction changed the frozen untracked path set");
|
|
630
708
|
}
|
|
709
|
+
if (!DIGEST.test(input.request_hash)) throw new Error("Targeted validation request hash is invalid");
|
|
710
|
+
if (current.validator_request === undefined || input.request_hash !== current.validator_request.request_hash) {
|
|
711
|
+
throw new Error("Targeted validation does not match the frozen native validator request");
|
|
712
|
+
}
|
|
713
|
+
if (
|
|
714
|
+
snapshot.candidate_tree !== current.validator_request.body.candidate_tree ||
|
|
715
|
+
snapshot.fix_diff_hash !== current.validator_request.body.fix_diff_hash ||
|
|
716
|
+
!equal(snapshot.changed_paths, current.validator_request.body.correction_paths)
|
|
717
|
+
) {
|
|
718
|
+
throw new Error("Recaptured correction does not match the frozen validator request");
|
|
719
|
+
}
|
|
631
720
|
const correctionIds = canonicalStrings(input.correction_ids, "Targeted validation correction IDs");
|
|
632
721
|
if (!equal(correctionIds, current.correction_ids)) {
|
|
633
722
|
throw new Error("Targeted validation must cover exactly the frozen correction IDs");
|
|
@@ -764,6 +853,23 @@ export function assertCompactReviewState(state: CompactReviewStateV2): void {
|
|
|
764
853
|
if (state.correction_line_forecast !== undefined && (!Number.isSafeInteger(state.correction_line_forecast) || state.correction_line_forecast <= 0)) {
|
|
765
854
|
throw new Error("Compact correction forecast must be positive");
|
|
766
855
|
}
|
|
856
|
+
assertReviewRuntimeIdentity(state.runtime_identity);
|
|
857
|
+
if (state.validator_request !== undefined) {
|
|
858
|
+
const request = state.validator_request;
|
|
859
|
+
if (
|
|
860
|
+
state.correction_line_forecast === undefined ||
|
|
861
|
+
request.request_hash !== domainHashV1("compact-validator-request", request.body) ||
|
|
862
|
+
request.body.purpose !== COMPACT_VALIDATOR_PURPOSE.TARGETED_CORRECTION ||
|
|
863
|
+
!equal(request.body.scope, Object.values(COMPACT_VALIDATOR_SCOPE)) ||
|
|
864
|
+
request.body.lineage_id !== state.lineage_id ||
|
|
865
|
+
!equal(request.body.correction_ids, state.correction_ids) ||
|
|
866
|
+
!equal(request.body.frozen_rows, state.findings.filter((finding) => state.correction_ids.includes(finding.id))) ||
|
|
867
|
+
request.body.frozen_ledger_hash !== domainHashV1("compact-findings", state.findings) ||
|
|
868
|
+
request.body.correction_paths.some((path) => !state.genesis_paths.includes(path))
|
|
869
|
+
) {
|
|
870
|
+
throw new Error("Compact validator request is not bound to frozen review evidence");
|
|
871
|
+
}
|
|
872
|
+
}
|
|
767
873
|
if (state.correction) {
|
|
768
874
|
if (!DIGEST.test(state.correction.fix_diff_hash) || state.correction.changed_lines > state.correction_budget || state.correction.candidate_tree !== state.current_candidate_tree || !equal(state.correction.correction_ids, state.correction_ids) || !equal(state.correction.intended_untracked, state.intended_untracked)) {
|
|
769
875
|
throw new Error("Compact correction record is not bound to frozen authority");
|
|
@@ -773,7 +879,7 @@ export function assertCompactReviewState(state: CompactReviewStateV2): void {
|
|
|
773
879
|
throw new Error("Compact targeted validation is incomplete or unbound");
|
|
774
880
|
}
|
|
775
881
|
if (state.state === COMPACT_REVIEW_STATE.REVIEWING) {
|
|
776
|
-
if (state.findings.length > 0 || state.lens_results.length > 0 || state.correction_ids.length > 0 || state.final_evidence_hash !== undefined) {
|
|
882
|
+
if (state.findings.length > 0 || state.lens_results.length > 0 || state.correction_ids.length > 0 || state.validator_request !== undefined || state.final_evidence_hash !== undefined) {
|
|
777
883
|
throw new Error("Reviewing compact state contains post-review data");
|
|
778
884
|
}
|
|
779
885
|
} else if (state.lens_results.length !== state.selected_lenses.length) {
|
|
@@ -782,7 +888,7 @@ export function assertCompactReviewState(state: CompactReviewStateV2): void {
|
|
|
782
888
|
if (state.state === COMPACT_REVIEW_STATE.CORRECTION_REQUIRED && state.correction_ids.length === 0) {
|
|
783
889
|
throw new Error("Correction-required compact state has no correction IDs");
|
|
784
890
|
}
|
|
785
|
-
if (state.state === COMPACT_REVIEW_STATE.VALIDATING && state.correction_ids.length > 0 && (!state.correction || !state.validation)) {
|
|
891
|
+
if (state.state === COMPACT_REVIEW_STATE.VALIDATING && state.correction_ids.length > 0 && (!state.correction || !state.validation || !state.validator_request)) {
|
|
786
892
|
throw new Error("Corrected compact state requires one targeted validation");
|
|
787
893
|
}
|
|
788
894
|
if (state.state === COMPACT_REVIEW_STATE.APPROVED && !DIGEST.test(state.final_evidence_hash ?? "")) {
|