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.
Files changed (32) hide show
  1. package/assets/agents/gentle-ai-explore.md +5 -1
  2. package/assets/agents/jd-judge-a.md +33 -0
  3. package/assets/agents/jd-judge-b.md +33 -0
  4. package/assets/agents/review-readability.md +29 -0
  5. package/assets/agents/review-reliability.md +29 -0
  6. package/assets/agents/review-resilience.md +29 -0
  7. package/assets/agents/review-risk.md +29 -0
  8. package/extensions/codegraph-tools.ts +236 -0
  9. package/extensions/gentle-ai.ts +95 -18
  10. package/lib/review-compact-contract.ts +183 -0
  11. package/lib/review-compact-store.ts +124 -4
  12. package/lib/review-compact.ts +108 -2
  13. package/lib/review-facade.ts +117 -10
  14. package/lib/review-reset.ts +69 -10
  15. package/lib/review-runtime-contract.ts +68 -0
  16. package/lib/review-transaction.ts +23 -11
  17. package/package.json +1 -1
  18. package/skills/judgment-day/references/prompts-and-formats.md +36 -3
  19. package/tests/codegraph-tools.test.ts +213 -0
  20. package/tests/package-manifest.test.ts +11 -3
  21. package/tests/review-compact-contract.test.ts +58 -0
  22. package/tests/review-compact-gate.test.ts +13 -19
  23. package/tests/review-compact-store.test.ts +15 -0
  24. package/tests/review-compact.test.ts +12 -6
  25. package/tests/review-controller.test.ts +169 -0
  26. package/tests/review-facade.test.ts +83 -17
  27. package/tests/review-ledger-contract.test.ts +76 -9
  28. package/tests/review-reset.test.ts +81 -16
  29. package/tests/review-runtime-contract.test.ts +70 -0
  30. package/tests/review-transaction.test.ts +121 -0
  31. package/tests/sdd-agent-tools.test.ts +8 -1
  32. 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
- return parseRecord(readFileSync(this.statePath, "utf8"), this.#lineageId);
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
- return readdirSync(root, { withFileTypes: true })
573
- .filter((entry) => entry.isDirectory() && LINEAGE_ID.test(entry.name))
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
+ }
@@ -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 ?? "")) {