gentle-pi 1.0.1 → 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.
@@ -5,13 +5,17 @@ tools:
5
5
  - read
6
6
  - grep
7
7
  - find
8
+ - codegraph
8
9
  ---
9
10
 
10
11
  You are the read-only explorer for generic non-SDD work.
11
12
 
12
13
  Map relevant files, symbols, relationships, and uncertainty within the parent-provided scope.
13
14
 
14
- - Read and search only. Do not edit, write, run commands, or mutate state.
15
+ - For structural questions, use the cwd-scoped `codegraph` tool before broad filesystem searches. Initialize the workspace index with `operation: "init"` when it is absent, then use `query` or `explore`; never ask it to target another path.
16
+ - `codegraph` may create or update only the current workspace `.codegraph/` index. This is the sole permitted mutation; all tracked files, source files, and other project content remain read-only.
17
+ - If CodeGraph reports that it is unavailable or fails, then use `read`, `grep`, and `find` as the fallback. Do not use that fallback before CodeGraph is unavailable or fails.
18
+ - Other than the explicit `.codegraph/` index exception, read and search only. Do not edit, write, run commands, or mutate state.
15
19
  - Do not fix findings, delegate to child agents, commit, or push.
16
20
  - Do not use SDD phase protocols or review lenses.
17
21
 
@@ -44,11 +44,11 @@ Return only this compact-v2 native JSON envelope, with one lens result for this
44
44
  "review_result": {
45
45
  "lens_results": [
46
46
  {
47
- "lens": "readability",
47
+ "lens": "review-readability",
48
48
  "findings": [
49
49
  {
50
50
  "id": "READABILITY-001",
51
- "lens": "readability",
51
+ "lens": "review-readability",
52
52
  "location": "path/to/file.ts:1",
53
53
  "severity": "CRITICAL",
54
54
  "claim": "Concrete user-impact claim.",
@@ -45,11 +45,11 @@ Return only this compact-v2 native JSON envelope, with one lens result for this
45
45
  "review_result": {
46
46
  "lens_results": [
47
47
  {
48
- "lens": "reliability",
48
+ "lens": "review-reliability",
49
49
  "findings": [
50
50
  {
51
51
  "id": "RELIABILITY-001",
52
- "lens": "reliability",
52
+ "lens": "review-reliability",
53
53
  "location": "path/to/file.ts:1",
54
54
  "severity": "CRITICAL",
55
55
  "claim": "Concrete user-impact claim.",
@@ -44,11 +44,11 @@ Return only this compact-v2 native JSON envelope, with one lens result for this
44
44
  "review_result": {
45
45
  "lens_results": [
46
46
  {
47
- "lens": "resilience",
47
+ "lens": "review-resilience",
48
48
  "findings": [
49
49
  {
50
50
  "id": "RESILIENCE-001",
51
- "lens": "resilience",
51
+ "lens": "review-resilience",
52
52
  "location": "path/to/file.ts:1",
53
53
  "severity": "CRITICAL",
54
54
  "claim": "Concrete user-impact claim.",
@@ -46,11 +46,11 @@ Return only this compact-v2 native JSON envelope, with one lens result for this
46
46
  "review_result": {
47
47
  "lens_results": [
48
48
  {
49
- "lens": "risk",
49
+ "lens": "review-risk",
50
50
  "findings": [
51
51
  {
52
52
  "id": "RISK-001",
53
- "lens": "risk",
53
+ "lens": "review-risk",
54
54
  "location": "path/to/file.ts:1",
55
55
  "severity": "CRITICAL",
56
56
  "claim": "Concrete user-impact claim.",
@@ -0,0 +1,236 @@
1
+ import { execFile, execFileSync } from "node:child_process";
2
+ import { lstatSync, realpathSync } from "node:fs";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+
8
+ const CODEGRAPH_OPERATION = {
9
+ INIT: "init",
10
+ QUERY: "query",
11
+ EXPLORE: "explore",
12
+ } as const;
13
+
14
+ const CODEGRAPH_STATUS = {
15
+ UNAVAILABLE: "unavailable",
16
+ FAILED: "failed",
17
+ } as const;
18
+
19
+ type CodeGraphOperation =
20
+ (typeof CODEGRAPH_OPERATION)[keyof typeof CODEGRAPH_OPERATION];
21
+ type CodeGraphStatus =
22
+ (typeof CODEGRAPH_STATUS)[keyof typeof CODEGRAPH_STATUS];
23
+
24
+ export interface CodeGraphToolParameters {
25
+ operation: CodeGraphOperation;
26
+ query?: string;
27
+ limit?: number;
28
+ }
29
+
30
+ export interface CodeGraphCommandResult {
31
+ stdout: string;
32
+ stderr: string;
33
+ }
34
+
35
+ export interface CodeGraphRunOptions {
36
+ cwd: string;
37
+ signal?: AbortSignal;
38
+ maxBuffer: number;
39
+ }
40
+
41
+ interface CodeGraphFallbackDetails {
42
+ status: CodeGraphStatus;
43
+ operation: CodeGraphOperation;
44
+ cwd: string;
45
+ fallback: string;
46
+ }
47
+
48
+ export type CodeGraphRunner = (
49
+ args: readonly string[],
50
+ options: CodeGraphRunOptions,
51
+ ) => Promise<CodeGraphCommandResult>;
52
+
53
+ const CODEGRAPH_TOOL_PARAMETERS = {
54
+ type: "object",
55
+ additionalProperties: false,
56
+ required: ["operation"],
57
+ properties: {
58
+ operation: { type: "string", enum: Object.values(CODEGRAPH_OPERATION) },
59
+ query: { type: "string", minLength: 1, maxLength: 2_000 },
60
+ limit: { type: "integer", minimum: 1, maximum: 20 },
61
+ },
62
+ } as const;
63
+
64
+ const DEFAULT_LIMIT = 10;
65
+ const MAX_LIMIT = 20;
66
+ const MAX_OUTPUT_CHARS = 100_000;
67
+ const PROCESS_MAX_BUFFER = MAX_OUTPUT_CHARS * 2;
68
+ const FALLBACK_INSTRUCTIONS = "Use read, grep, and find for this exploration.";
69
+ const execFileAsync = promisify(execFile);
70
+
71
+ function resolveWorkspaceCwd(cwd: string): string {
72
+ const resolved = realpathSync(cwd);
73
+ if (!lstatSync(resolved).isDirectory()) {
74
+ throw new Error("CodeGraph can run only in the current workspace directory.");
75
+ }
76
+ if (resolved === realpathSync(homedir()) || resolved === realpathSync(tmpdir())) {
77
+ throw new Error("CodeGraph requires a real Git project root equal to the current workspace, not HOME or a temporary directory.");
78
+ }
79
+ try {
80
+ const root = realpathSync(execFileSync("git", ["rev-parse", "--show-toplevel"], {
81
+ cwd: resolved,
82
+ encoding: "utf8",
83
+ stdio: ["ignore", "pipe", "ignore"],
84
+ }).trim());
85
+ if (root !== resolved) {
86
+ throw new Error("CodeGraph requires a real Git project root equal to the current workspace.");
87
+ }
88
+ return resolved;
89
+ } catch (error) {
90
+ if (error instanceof Error && /real Git project root/.test(error.message)) throw error;
91
+ throw new Error("CodeGraph requires a real Git project root equal to the current workspace.");
92
+ }
93
+ }
94
+
95
+ function assertSafeIndexDirectory(cwd: string): void {
96
+ try {
97
+ const index = lstatSync(join(cwd, ".codegraph"));
98
+ if (index.isSymbolicLink() || !index.isDirectory()) {
99
+ throw new Error("CodeGraph .codegraph must be a real directory when it already exists.");
100
+ }
101
+ } catch (error: unknown) {
102
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return;
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ function resolveLimit(limit: number | undefined): number {
108
+ if (limit === undefined) return DEFAULT_LIMIT;
109
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIMIT) {
110
+ throw new Error(`CodeGraph limit must be an integer between 1 and ${MAX_LIMIT}.`);
111
+ }
112
+ return limit;
113
+ }
114
+
115
+ function requireQuery(query: string | undefined): string {
116
+ if (typeof query !== "string" || query.trim().length === 0) {
117
+ throw new Error("CodeGraph query is required for query and explore operations.");
118
+ }
119
+ if (query.length > 2_000) {
120
+ throw new Error("CodeGraph query must not exceed 2000 characters.");
121
+ }
122
+ return query;
123
+ }
124
+
125
+ function commandArguments(parameters: CodeGraphToolParameters, cwd: string): string[] {
126
+ switch (parameters.operation) {
127
+ case CODEGRAPH_OPERATION.INIT:
128
+ return [CODEGRAPH_OPERATION.INIT, cwd];
129
+ case CODEGRAPH_OPERATION.QUERY: {
130
+ const query = requireQuery(parameters.query);
131
+ return [
132
+ CODEGRAPH_OPERATION.QUERY,
133
+ "--path",
134
+ cwd,
135
+ "--limit",
136
+ String(resolveLimit(parameters.limit)),
137
+ "--",
138
+ query,
139
+ ];
140
+ }
141
+ case CODEGRAPH_OPERATION.EXPLORE: {
142
+ const query = requireQuery(parameters.query);
143
+ return [
144
+ CODEGRAPH_OPERATION.EXPLORE,
145
+ "--path",
146
+ cwd,
147
+ "--max-files",
148
+ String(resolveLimit(parameters.limit)),
149
+ "--",
150
+ query,
151
+ ];
152
+ }
153
+ }
154
+ }
155
+
156
+ function truncateOutput(output: string): string {
157
+ return output.length <= MAX_OUTPUT_CHARS
158
+ ? output
159
+ : `${output.slice(0, MAX_OUTPUT_CHARS)}\n\n[CodeGraph output truncated]`;
160
+ }
161
+
162
+ function codeGraphFailureDetails(
163
+ error: unknown,
164
+ operation: CodeGraphOperation,
165
+ cwd: string,
166
+ ): CodeGraphFallbackDetails {
167
+ const status =
168
+ typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT"
169
+ ? CODEGRAPH_STATUS.UNAVAILABLE
170
+ : CODEGRAPH_STATUS.FAILED;
171
+ return { status, operation, cwd, fallback: FALLBACK_INSTRUCTIONS };
172
+ }
173
+
174
+ function codeGraphFailureMessage(status: CodeGraphStatus): string {
175
+ return status === CODEGRAPH_STATUS.UNAVAILABLE
176
+ ? `CodeGraph is unavailable because the codegraph binary was not found. ${FALLBACK_INSTRUCTIONS}`
177
+ : `CodeGraph failed to run. ${FALLBACK_INSTRUCTIONS}`;
178
+ }
179
+
180
+ const runCodeGraphCommand: CodeGraphRunner = async (args, options) => {
181
+ const result = await execFileAsync("codegraph", [...args], {
182
+ cwd: options.cwd,
183
+ signal: options.signal,
184
+ maxBuffer: options.maxBuffer,
185
+ });
186
+ return { stdout: result.stdout, stderr: result.stderr };
187
+ };
188
+
189
+ export function createCodeGraphTool(runner: CodeGraphRunner = runCodeGraphCommand) {
190
+ return {
191
+ name: "codegraph",
192
+ label: "CodeGraph",
193
+ description:
194
+ "Initialize, search, or explore the CodeGraph index for the current Pi workspace only. This tool never accepts a project path or shell command.",
195
+ promptSnippet: "Initialize and query CodeGraph for the current workspace without shell access",
196
+ promptGuidelines: [
197
+ "Use operation init before querying when the current workspace has no .codegraph index.",
198
+ "Use query for symbol search and explore for source plus call paths. Do not use this tool to run arbitrary commands or target another directory.",
199
+ ],
200
+ parameters: CODEGRAPH_TOOL_PARAMETERS,
201
+ executionMode: "sequential" as const,
202
+ async execute(
203
+ _toolCallId: string,
204
+ parameters: CodeGraphToolParameters,
205
+ signal: AbortSignal | undefined,
206
+ _onUpdate: undefined,
207
+ ctx: ExtensionContext,
208
+ ) {
209
+ const cwd = resolveWorkspaceCwd(ctx.cwd);
210
+ assertSafeIndexDirectory(cwd);
211
+ const args = commandArguments(parameters, cwd);
212
+ try {
213
+ const result = await runner(args, { cwd, signal, maxBuffer: PROCESS_MAX_BUFFER });
214
+ const output = truncateOutput([result.stdout, result.stderr].filter(Boolean).join("\n"));
215
+ return {
216
+ content: [{ type: "text" as const, text: output || "CodeGraph completed without output." }],
217
+ details: { operation: parameters.operation, cwd, args },
218
+ };
219
+ } catch (error: unknown) {
220
+ const details = codeGraphFailureDetails(error, parameters.operation, cwd);
221
+ return {
222
+ content: [{ type: "text" as const, text: codeGraphFailureMessage(details.status) }],
223
+ details,
224
+ };
225
+ }
226
+ },
227
+ };
228
+ }
229
+
230
+ export function registerCodeGraphTool(pi: ExtensionAPI): void {
231
+ pi.registerTool(createCodeGraphTool());
232
+ }
233
+
234
+ export default function codeGraphTools(pi: ExtensionAPI): void {
235
+ registerCodeGraphTool(pi);
236
+ }
@@ -51,16 +51,25 @@ import type { TriggerEvent } from "../lib/review-triggers.ts";
51
51
  import { ReviewBundleExporter, ReviewBundleImporter } from "../lib/review-bundle.ts";
52
52
  import { domainHashV1 } from "../lib/review-canonical.ts";
53
53
  import { inspectLegacyReviewAuthorityV1, type LegacyInspectionV1 } from "../lib/review-legacy-detector.ts";
54
- import { destructiveResetReviewAuthorityV1 } from "../lib/review-reset.ts";
54
+ import { compactResetRequestV1, destructiveResetReviewAuthorityV1 } from "../lib/review-reset.ts";
55
55
  import { ReviewMutationLockV1 } from "../lib/review-lock.ts";
56
56
  import {
57
+ COMPACT_START_BLOCK_ACTION,
57
58
  GRAPH_V1_ORDINARY_READ_ONLY,
59
+ CompactReviewStartBlockedError,
58
60
  discoverCompactReview,
59
61
  finalizeCompactReview,
60
62
  startCompactReview,
61
63
  } from "../lib/review-facade.ts";
62
64
  import { validateCompactReviewGate } from "../lib/review-compact-gate.ts";
63
- import { compactV2LineageExists, graphV1LineageExists } from "../lib/review-compact-store.ts";
65
+ import {
66
+ COMPACT_AUTHORITY_OUTCOME,
67
+ compactV2LineageExists,
68
+ graphV1LineageExists,
69
+ hasGraphV1Authority,
70
+ inspectCompactReviewAuthorityV2,
71
+ type CompactAuthorityInspectionV2,
72
+ } from "../lib/review-compact-store.ts";
64
73
  import {
65
74
  inheritedUnsafeGitEnvironmentKeys,
66
75
  resolveRepositoryAuthorityV1,
@@ -2327,11 +2336,38 @@ function durableResetRecoveryRequest(cwd: string): LegacyInspectionV1["reset_req
2327
2336
  };
2328
2337
  }
2329
2338
 
2330
- function inspectReviewAuthorityForController(cwd: string): LegacyInspectionV1 {
2331
- const inspection = inspectLegacyReviewAuthorityV1(cwd);
2332
- return inspection.outcome === "reset-in-progress"
2333
- ? { ...inspection, reset_request: durableResetRecoveryRequest(cwd) }
2334
- : inspection;
2339
+ interface ControllerReviewAuthorityInspection extends LegacyInspectionV1 {
2340
+ compact_authority?: CompactAuthorityInspectionV2;
2341
+ }
2342
+
2343
+ function inspectReviewAuthorityForController(cwd: string): ControllerReviewAuthorityInspection {
2344
+ const legacy = inspectLegacyReviewAuthorityV1(cwd);
2345
+ const compact = inspectCompactReviewAuthorityV2(cwd);
2346
+ const inspection = legacy.outcome === "reset-in-progress"
2347
+ ? { ...legacy, reset_request: durableResetRecoveryRequest(cwd) }
2348
+ : compact.outcome === COMPACT_AUTHORITY_OUTCOME.INVALID
2349
+ ? { ...legacy, reset_request: compactResetRequestV1(cwd, legacy) }
2350
+ : legacy;
2351
+ return compact.outcome === COMPACT_AUTHORITY_OUTCOME.NONE
2352
+ ? inspection
2353
+ : { ...inspection, compact_authority: compact };
2354
+ }
2355
+
2356
+ function compactAuthorityAction(inspection: ControllerReviewAuthorityInspection): string | undefined {
2357
+ switch (inspection.compact_authority?.outcome) {
2358
+ case COMPACT_AUTHORITY_OUTCOME.APPROVED:
2359
+ return COMPACT_START_BLOCK_ACTION.APPROVED;
2360
+ case COMPACT_AUTHORITY_OUTCOME.ESCALATED:
2361
+ return COMPACT_START_BLOCK_ACTION.ESCALATED;
2362
+ case COMPACT_AUTHORITY_OUTCOME.ACTIVE:
2363
+ return "finalize-existing-ordinary-review";
2364
+ case COMPACT_AUTHORITY_OUTCOME.INVALID:
2365
+ return inspection.outcome === "clean"
2366
+ ? "request-explicit-reset-authorization"
2367
+ : "stop-and-report-ambiguous-authority";
2368
+ default:
2369
+ return undefined;
2370
+ }
2335
2371
  }
2336
2372
 
2337
2373
  async function authorizeDestructiveReviewOperation(
@@ -3085,9 +3121,20 @@ function executeReviewControllerOperation(
3085
3121
  operation: parameters.operation,
3086
3122
  inspection,
3087
3123
  lock: lock.inspect(),
3088
- ...(inspection.outcome === "clean"
3089
- ? { status: "ready", next_action: "start-ordinary-review" }
3090
- : inspection.outcome === "blocked-ambiguous"
3124
+ ...(inspection.outcome === "clean" && inspection.compact_authority !== undefined
3125
+ ? {
3126
+ status: inspection.compact_authority.outcome === COMPACT_AUTHORITY_OUTCOME.ESCALATED
3127
+ ? "escalated"
3128
+ : inspection.compact_authority.outcome === COMPACT_AUTHORITY_OUTCOME.APPROVED
3129
+ ? "terminal"
3130
+ : inspection.compact_authority.outcome === COMPACT_AUTHORITY_OUTCOME.ACTIVE
3131
+ ? "in-progress"
3132
+ : "blocked",
3133
+ next_action: compactAuthorityAction(inspection),
3134
+ }
3135
+ : inspection.outcome === "clean"
3136
+ ? { status: "ready", next_action: "start-ordinary-review" }
3137
+ : inspection.outcome === "blocked-ambiguous"
3091
3138
  ? { status: "blocked", next_action: "stop-and-report-ambiguous-authority" }
3092
3139
  : {
3093
3140
  status: "blocked",
@@ -3121,6 +3168,20 @@ function executeReviewControllerOperation(
3121
3168
  return { operation: parameters.operation, result, inspection, next_action: inspection.outcome === "clean" ? "start-fresh-ordinary-review-after-verified-clean" : "inspect-reset-recovery" };
3122
3169
  }
3123
3170
  if (parameters.operation === REVIEW_CONTROLLER_OPERATION.REPAIR) {
3171
+ const inspection = inspectReviewAuthorityForController(defaultCwd);
3172
+ if (
3173
+ inspection.outcome === "clean" &&
3174
+ inspection.compact_authority !== undefined &&
3175
+ !hasGraphV1Authority(defaultCwd)
3176
+ ) {
3177
+ return {
3178
+ operation: parameters.operation,
3179
+ repaired: false,
3180
+ compact_authority: "immutable-untouched",
3181
+ inspection,
3182
+ next_action: compactAuthorityAction(inspection),
3183
+ };
3184
+ }
3124
3185
  const store = ReviewTransactionStore.forRepository(defaultCwd);
3125
3186
  store.repairCurrentAuthority();
3126
3187
  return { operation: parameters.operation, repaired: true };
@@ -3154,14 +3215,29 @@ function executeReviewControllerOperation(
3154
3215
  ? { kind: REVIEW_PROJECTION.COMPLETE } as const
3155
3216
  : undefined;
3156
3217
  if (!projection) throw new Error("New compact ordinary START requires the complete projection");
3157
- const result = startCompactReview({
3158
- cwd: defaultCwd,
3159
- ...(parameters.lineageId === undefined ? {} : { lineageId: parameters.lineageId }),
3160
- policyHash: rawStart.policyHash,
3161
- projection,
3162
- });
3163
- const state = discoverCompactReview(defaultCwd, result.lineage_id).record.state;
3164
- return { operation: parameters.operation, result, state };
3218
+ try {
3219
+ const result = startCompactReview({
3220
+ cwd: defaultCwd,
3221
+ ...(parameters.lineageId === undefined ? {} : { lineageId: parameters.lineageId }),
3222
+ policyHash: rawStart.policyHash,
3223
+ projection,
3224
+ });
3225
+ const state = discoverCompactReview(defaultCwd, result.lineage_id).record.state;
3226
+ return { operation: parameters.operation, result, state };
3227
+ } catch (error) {
3228
+ if (error instanceof CompactReviewStartBlockedError) {
3229
+ return {
3230
+ operation: parameters.operation,
3231
+ status: "blocked",
3232
+ lineage_created: false,
3233
+ lifecycle: `compact-${error.state}`,
3234
+ lineage_id: error.lineageId,
3235
+ next_action: error.action,
3236
+ inspection: inspectReviewAuthorityForController(defaultCwd),
3237
+ };
3238
+ }
3239
+ throw error;
3240
+ }
3165
3241
  }
3166
3242
  const idempotencyKey = requiredControllerString(parameters, "idempotencyKey");
3167
3243
  if (typeof parameters.lineageId !== "string" || parameters.lineageId.trim().length === 0) {
@@ -3228,6 +3304,7 @@ function executeReviewControllerOperation(
3228
3304
  ...(parameters.lineageId === undefined ? {} : { lineageId: parameters.lineageId }),
3229
3305
  ...(raw.review_result === undefined ? {} : { review_result: raw.review_result as never }),
3230
3306
  ...(raw.correction_line_forecast === undefined ? {} : { correction_line_forecast: Number(raw.correction_line_forecast) }),
3307
+ ...(raw.validation_proof === undefined ? {} : { validation_proof: raw.validation_proof as never }),
3231
3308
  ...(raw.validation === undefined ? {} : { validation: raw.validation as never }),
3232
3309
  ...(raw.final_evidence === undefined ? {} : { final_evidence: raw.final_evidence }),
3233
3310
  ...(raw.final_verification_passed === undefined ? {} : { final_verification_passed: raw.final_verification_passed }),
@@ -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
+ }