From f3f209d03fb3b4fc752876fc5803f0dcdd46960f Mon Sep 17 00:00:00 2001 From: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com> Date: Fri, 16 Oct 2026 05:48:03 +0000 Subject: [PATCH] experiment(re)!: preserve partial captures and resolve function entries Keep validated code available when native graph extraction is unavailable, bind output to the actual input/entry, preserve cancellation observations, and share batch source/error semantics. Add filtered function/alias reads and slice field selection across adapters. Verified 38 focused tests, targeted lint, real stock/native MCP workflows, large SQLite graph-capacity fallback, and real cold/warm MCP reuse. Retain actual model costs and unequal-depth/selection limitations instead of claiming general superiority. BREAKING CHANGE: Experimental native snapshots may succeed with readable code and an unavailable recovered_ir facet. Inspect facets before graph reads; unavailable graph counts are null/unknown. Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> --- experiments/atomic-codemode/README.md | 13 +- experiments/atomic-codemode/api.mjs | 33 ++- experiments/atomic-codemode/api.test.mjs | 29 +++ experiments/atomic-codemode/executor.mjs | 24 +- experiments/atomic-codemode/executor.test.mjs | 20 ++ experiments/atomic-codemode/fact-reads.mjs | 59 +++++ .../atomic-codemode/function-facts.mjs | 36 +++ .../atomic-codemode/function-facts.test.mjs | 93 ++++++++ experiments/atomic-codemode/native-engine.md | 36 ++- .../atomic-codemode/native-ir-contract.mjs | 109 ++++++--- experiments/atomic-codemode/native-ir.mjs | 70 +++++- .../atomic-codemode/native-ir.test.mjs | 35 ++- experiments/atomic-codemode/provider.mjs | 224 +++++++++--------- experiments/atomic-codemode/trials.md | 53 +++++ experiments/atomic-codemode/verify-cache.mjs | 109 +++++++++ experiments/atomic-codemode/verify-mcp.mjs | 117 ++++++--- 16 files changed, 847 insertions(+), 213 deletions(-) create mode 100644 experiments/atomic-codemode/fact-reads.mjs create mode 100644 experiments/atomic-codemode/function-facts.mjs create mode 100644 experiments/atomic-codemode/function-facts.test.mjs create mode 100644 experiments/atomic-codemode/verify-cache.mjs diff --git a/experiments/atomic-codemode/README.md b/experiments/atomic-codemode/README.md index 174cf242a2..c352c21712 100644 --- a/experiments/atomic-codemode/README.md +++ b/experiments/atomic-codemode/README.md @@ -68,16 +68,23 @@ unknown. Other providers are not yet wired to this experimental service. The initial [actual model comparison](trials.md) favors the focused traditional workflow. The seeded value-origin task has lower cumulative input and wall time -through Code Mode, but also exposes an IR-to-machine-path error. An equal-quality -whole-task advantage remains unproven; the representation fix needs a new trial. +through Code Mode, but initially exposes an IR-to-machine-path error. The revised +trial correctly explains that machine path with lower observed task costs; +broader claims still require more than this single seeded task. `instructions` reads selected machine byte/decode windows through the same owner. Recovered operations use `origin_address` and blocks `origin_start`; these locations cannot be substituted for machine branch destinations. The value-origin trial demonstrates why this boundary matters. The experiment -currently has eleven fact methods, still behind the same four-tool Code Mode +currently has twelve fact methods, still behind the same four-tool Code Mode catalog. +`functions` provides selected name/alias/entry reads. Native snapshots report +code and recovered-IR facets separately, preserving readable code when graph +extraction is unavailable. Real capacity and cold/warm MCP checks are documented +in [native-engine.md](native-engine.md). Whole-model costs and unequal-depth +comparisons remain separate in the trial report. + Completion requires actual model Code Mode use, independently checked task answers, shared atomic/direct/Code Mode semantics, needed relation coverage, failure/cancellation/identity tests, and equivalent completed-task measurements diff --git a/experiments/atomic-codemode/api.mjs b/experiments/atomic-codemode/api.mjs index 9fab90e1f0..c0c6830dc8 100644 --- a/experiments/atomic-codemode/api.mjs +++ b/experiments/atomic-codemode/api.mjs @@ -41,6 +41,22 @@ const definition = (description, input, method, example) => ({ example, }); export const methods = { + functions: definition( + "Search retained native callable entries by literal name/alias, exact name, or entry addresses. Names can match several entries, including PLT/import stubs and implementations; preserve all matches and select by address. A name does not establish runtime symbol binding.", + { + query: z.string().default(""), + exact: z.boolean().default(false), + case_sensitive: z.boolean().default(true), + addresses: z.array(hex).max(128).optional(), + fields: z + .array(z.enum(["name", "aliases", "size"])) + .min(1) + .optional(), + ...page, + }, + "functions", + 'await rea.functions({query:"format_error",exact:true})', + ), strings: definition( "Search retained string inventory by literal text. Paging does not establish artifact completeness.", { query: z.string(), ...page }, @@ -54,7 +70,7 @@ export const methods = { 'await rea.references({addresses:["0x2000"]})', ), snapshot: definition( - "Analyze an explicit function entry once; retain native C and SSA, return its handle and coverage. Reuses the same source within this target lease.", + "Analyze an explicit function entry once; return a retained capture handle, facets and coverage. Reuses the source within this target lease. A valid code facet remains available when recovered IR exceeds capacity or is malformed; inspect facets before graph reads.", { entry: hex }, "snapshot", 'await rea.snapshot({entry:"0x1000"})', @@ -163,12 +179,13 @@ export const methods = { max_operations: z.number().int().min(1).max(256).default(48), stop_at: z.array(z.string()).max(128).default([]), stop_kinds: z.array(kinds).default(["phi", "load"]), + fields, }, "slice", "await rea.slice({snapshot:s,seeds:branchIds,max_operations:48})", ), code: definition( - "Read native reconstructed C retained with this exact SSA source; offset is zero-based. A large limit can read the complete function inside Code Mode: filter locally before returning text. The bridge byte budget, not an arbitrary line count, bounds the result. No additional decompilation.", + "Read reconstructed code from the same retained function capture, even when its graph facet is unavailable. offset is zero-based. A large limit reads the complete code inside Code Mode for local filtering. The bridge byte budget bounds the result. No additional decompilation.", { snapshot, offset: page.offset, @@ -191,6 +208,8 @@ export const methods = { // Compact return contracts let code compose before first observing a response. const returns = { + functions: + "data:{source:{evidence_id,sha256},items:[{address,name,aliases,size}],total,next_offset,coverage}; size is an upper bound, or zero if unmeasured. Multiple matching addresses are retained, never selected by catalog order or display name alone.", instructions: "data:{results:[{ok,address,source:{evidence_id,sha256},items:[{address,size,status,bytes?,text?,mnemonic?,operands?}],resume_address,coverage}|{ok:false,address,error}]}; decoded rows describe selected machine bytes, not optimized IR branches. Partial batches retain successful windows.", strings: @@ -198,7 +217,7 @@ const returns = { references: "data:{results:[{ok,address,source:{evidence_id},items:[{from,to,kind,fromFunction:{address,name}|null,toFunction:{address,name}|null,instruction}],total,next_offset,coverage}|{ok:false,address,error}]}", snapshot: - "data:{snapshot:string,entry,source:{evidence_id,sha256},coverage:{operations,blocks,control_flow,unrepresented_outgoing_blocks,issues}}", + "data:{snapshot:string,entry,source:{evidence_id,sha256},facets:{code:{availability},recovered_ir:{availability,error?}},coverage}. An unavailable graph has unknown/null counts, never a fabricated empty graph; code remains readable through the same handle.", operations: "data:{items:[{id,origin_address,block,source_line,kind,statement,definitions,constants,inputs?,output?,opcode?}],total,next_offset,evidence_id,coverage}. Native inputs:[{input_index,value_id,phi_predecessor,phi_predecessor_edge}], output:value_id|null; definitions:[{definition,use,input_index,value_id,phi_predecessor,phi_predecessor_edge}]. Diagnostic definitions use printed occurrence instead. fields projects columns; identity remains.", relations: @@ -209,7 +228,7 @@ const returns = { "data:{items:[{id,origin_start,operations,predecessors,successors:[{target,index,polarity,condition_value,condition_test}]}],total,next_offset,evidence_id,coverage}; IDs are local to this snapshot.", slice: "data:{operations:[operation rows as above],coverage:{status,boundaries:[{id,reason}],missing,source_issues,runtime_reachability,memory_alias_dependencies},evidence_id}", - code: "data:{evidence_id,start_line,text,total_lines,next_offset,coverage}; text is reconstructed C. Use a large limit to inspect/filter the full text within code without returning it all.", + code: "data:{evidence_id,start_line,language:string|null,text,total_lines,next_offset,coverage}; language is native-reported or unknown. Read/filter full reconstructed text within Code Mode without returning it all.", evidence: "data:{source:{evidence_id,sha256,bytes,arguments},text,total_code_units,next_offset}; source has complete native output, including code before the SSA graph.", }; @@ -295,6 +314,12 @@ export class AtomicSession { const contract = methods[name]; const parsed = contract.input.parse(input); const data = await this.#owner[contract.method](parsed, signal); + if (signal.aborted) + throw new FactFailure( + "cancelled", + "Request was cancelled after observations were collected", + { partial_observation: data }, + ); return { ok: true, context: this.context, data }; } catch (error) { return { diff --git a/experiments/atomic-codemode/api.test.mjs b/experiments/atomic-codemode/api.test.mjs index b6e3385e70..0b64805a1c 100644 --- a/experiments/atomic-codemode/api.test.mjs +++ b/experiments/atomic-codemode/api.test.mjs @@ -41,6 +41,35 @@ test("named contracts reject extra keys and stale leases before provider admissi assert.deepEqual(provider.calls, ["close"]); await session.close(); }); + +test("cancellation after a completed read retains observations under a cancelled outcome", async () => { + const controller = new AbortController(); + const session = new AtomicSession(async () => ({ + context: { lease: "late-cancel" }, + close: async () => {}, + strings: async () => { + controller.abort(); + return { + source: { evidence_id: "ev_observed" }, + items: [{ value: "retained" }], + }; + }, + })); + await session.open("target"); + const result = await session.call( + "strings", + { query: "x" }, + controller.signal, + ); + assert.equal(result.ok, false); + assert.equal(result.error.code, "cancelled"); + assert.equal( + result.error.details.partial_observation.items[0].value, + "retained", + ); + assert.equal(result.context.lease, "late-cancel"); + await session.close(); +}); test("close drains admitted reads and a failed cleanup retains ownership for retry", async () => { let release; let reads = 0; diff --git a/experiments/atomic-codemode/executor.mjs b/experiments/atomic-codemode/executor.mjs index 6cb8183df3..1bad831905 100644 --- a/experiments/atomic-codemode/executor.mjs +++ b/experiments/atomic-codemode/executor.mjs @@ -102,6 +102,20 @@ export const executeJavaScript = async ( }; }; +const retainEvidence = (response, evidence) => { + const observation = + response.data ?? response.error?.details?.partial_observation; + for (const row of [observation, ...(observation?.results ?? [])]) { + const id = + row?.source?.evidence_id ?? + row?.evidence_id ?? + row?.error?.details?.evidence_id; + if (id) evidence.add(id); + } + if (response.error?.details?.evidence_id) + evidence.add(response.error.details.evidence_id); +}; + const installBridge = ( vm, session, @@ -128,15 +142,7 @@ const installBridge = ( combined, lease, ); - for (const row of [response.data, ...(response.data?.results ?? [])]) { - const id = - row?.source?.evidence_id ?? - row?.evidence_id ?? - row?.error?.details?.evidence_id; - if (id) state.evidence.add(id); - } - if (response.error?.details?.evidence_id) - state.evidence.add(response.error.details.evidence_id); + retainEvidence(response, state.evidence); calls[index].status = response.ok ? "ok" : "error"; if (!response.ok) calls[index].error = response.error; calls[index].elapsed_ms = performance.now() - before; diff --git a/experiments/atomic-codemode/executor.test.mjs b/experiments/atomic-codemode/executor.test.mjs index b225fe06ae..5827b28635 100644 --- a/experiments/atomic-codemode/executor.test.mjs +++ b/experiments/atomic-codemode/executor.test.mjs @@ -48,6 +48,26 @@ test("guest CPU budget is independent from the native-analysis wall deadline", a assert.ok(performance.now() - before < 2000); }); +test("cancelled reads keep partial-observation Evidence in the execution receipt", async () => { + const result = await executeJavaScript( + { + context: { lease: "partial" }, + call: async () => ({ + ok: false, + error: { + code: "cancelled", + details: { + partial_observation: { source: { evidence_id: "ev_partial" } }, + }, + }, + }), + }, + "return await rea.strings({query:'x'})", + ); + assert.deepEqual(result.evidence_ids, ["ev_partial"]); + assert.equal(result.value.error.code, "cancelled"); +}); + test("failed compositions keep completed facts and their provenance after draining", async () => { const owner = { context: { lease: "retained" }, diff --git a/experiments/atomic-codemode/fact-reads.mjs b/experiments/atomic-codemode/fact-reads.mjs new file mode 100644 index 0000000000..a030728feb --- /dev/null +++ b/experiments/atomic-codemode/fact-reads.mjs @@ -0,0 +1,59 @@ +// Shared read projections and partial batch semantics; no provider protocols. +export const sourceSummary = ({ evidence_id, sha256, bytes, elapsed_ms }) => ({ + evidence_id, + sha256, + bytes, + elapsed_ms, +}); + +export const projectFact = (row, fields, identity) => + fields + ? Object.fromEntries( + [...new Set([...identity, ...fields])] + .filter((key) => Object.hasOwn(row, key)) + .map((key) => [key, row[key]]), + ) + : row; + +export const pageFacts = (rows, input, identity = []) => ({ + items: rows + .slice(input.offset, input.offset + input.limit) + .map((row) => projectFact(row, input.fields, identity)), + total: rows.length, + next_offset: + input.offset + input.limit < rows.length + ? input.offset + input.limit + : null, +}); + +export async function readFactBatch(requests, capture, read) { + const results = []; + for (const request of requests) { + let source; + try { + const captured = await capture(request); + source = captured.source; + const facts = await read(captured, request); + results.push({ + ...facts, + ok: true, + address: request.address, + source: sourceSummary(source), + }); + } catch (error) { + results.push({ + ok: false, + address: request.address, + error: { + code: error.code ?? "provider_failure", + message: error.message, + details: { + ...error.details, + ...(source ? { evidence_id: source.evidence_id } : {}), + }, + }, + }); + } + } + return { results }; +} diff --git a/experiments/atomic-codemode/function-facts.mjs b/experiments/atomic-codemode/function-facts.mjs new file mode 100644 index 0000000000..19c25e7a7c --- /dev/null +++ b/experiments/atomic-codemode/function-facts.mjs @@ -0,0 +1,36 @@ +import { parseKunaInventory } from "../../dist/kuna/KunaProjectFiles.js"; +import { requireResult } from "./fact-error.mjs"; +import { pageFacts } from "./fact-reads.mjs"; + +export const parseFunctionFacts = (text) => + Object.freeze( + requireResult(parseKunaInventory(text), "invalid_provider_output").map( + (entry) => + Object.freeze({ ...entry, aliases: Object.freeze([...entry.aliases]) }), + ), + ); + +export function selectFunctionFacts(functions, input) { + const addresses = input.addresses && new Set(input.addresses); + const normalize = (name) => + input.case_sensitive ? name : name.toLowerCase(); + const query = normalize(input.query); + const matches = functions.filter( + (entry) => + (!addresses || addresses.has(entry.address)) && + [entry.name, ...entry.aliases].some((name) => + input.exact + ? normalize(name) === query + : normalize(name).includes(query), + ), + ); + return { + ...pageFacts(matches, input, ["address"]), + coverage: { + scope: "native callable-entry inventory, including import stubs", + discovery: "unknown", + extents: "upper bounds clipped at the next entry; zero means unmeasured", + symbol_binding_at_runtime: "unknown", + }, + }; +} diff --git a/experiments/atomic-codemode/function-facts.test.mjs b/experiments/atomic-codemode/function-facts.test.mjs new file mode 100644 index 0000000000..9f98e2b980 --- /dev/null +++ b/experiments/atomic-codemode/function-facts.test.mjs @@ -0,0 +1,93 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { parseFunctionFacts, selectFunctionFacts } from "./function-facts.mjs"; +import { readFactBatch } from "./fact-reads.mjs"; +import { FactFailure } from "./fact-error.mjs"; + +test("function names retain multiple entries and aliases instead of choosing a PLT or implementation", () => { + const facts = parseFunctionFacts( + JSON.stringify({ + functions: [ + { + name: "format_error", + address_hex: "0x1000", + aliases: ["format_error@plt"], + size: 16, + }, + { + name: "format_error", + address_hex: "0x2000", + aliases: ["FormatError"], + size: 0, + }, + ], + }), + ); + const query = { + query: "format_error", + exact: true, + case_sensitive: true, + offset: 0, + limit: 20, + }; + assert.deepEqual( + selectFunctionFacts(facts, query).items.map((item) => item.address), + ["0x1000", "0x2000"], + ); + assert.equal( + selectFunctionFacts(facts, { ...query, query: "format_error@plt" }).items[0] + .address, + "0x1000", + ); + assert.equal( + selectFunctionFacts(facts, { + ...query, + query: "FORMAT_ERROR", + case_sensitive: false, + }).total, + 2, + ); + assert.deepEqual( + selectFunctionFacts(facts, { + ...query, + addresses: ["0x2000"], + fields: ["size"], + }).items, + [{ address: "0x2000", size: 0 }], + ); + assert.throws(() => { + facts[0].aliases.push("foreign"); + }); +}); + +test("batch transport and decoder failures preserve successful items and captured source identity", async () => { + const attempts = []; + const result = await readFactBatch( + [{ address: "0x1" }, { address: "0x2" }, { address: "0x3" }], + async (request) => { + attempts.push(request.address); + if (request.address === "0x2") + throw new FactFailure("capacity", "transport bound"); + return { + source: { + evidence_id: `ev_${request.address}`, + sha256: "digest", + bytes: 3, + elapsed_ms: 1, + }, + }; + }, + async (_, request) => { + if (request.address === "0x3") + throw new FactFailure( + "invalid_provider_output", + "decoder rejected source", + ); + return { items: [{ value: 1 }] }; + }, + ); + assert.deepEqual(attempts, ["0x1", "0x2", "0x3"]); + assert.equal(result.results[0].items[0].value, 1); + assert.equal(result.results[1].error.code, "capacity"); + assert.equal(result.results[2].error.details.evidence_id, "ev_0x3"); +}); diff --git a/experiments/atomic-codemode/native-engine.md b/experiments/atomic-codemode/native-engine.md index 59a4d96605..a7eab1b232 100644 --- a/experiments/atomic-codemode/native-engine.md +++ b/experiments/atomic-codemode/native-engine.md @@ -73,9 +73,8 @@ Targeted JS checks cover malformed/foreign identities, dangling relations, phi order, exact hex offsets, immutable facts, loop/exit coverage, resource failure, and all 64 four-node DAGs against an independent path-enumeration oracle. Upstream `check-spec` passes. The full upstream corpus, stages and workspace gates -have not run; this patch is not declared upstream-ready. SQLite, more complex -model tasks, paired warm runs and the revised actual model comparison remain -required work. +have not run; this patch is not declared upstream-ready. The model comparisons +and subsequent boundary findings below do not establish wider provider parity. The value-origin model comparison exposed a representation boundary: optimized recovered blocks can absorb a machine predecessor, so an IR edge is not an actual @@ -97,5 +96,32 @@ Windows are capped at 4,096 decoded rows and 16 windows per call, sharing the returns 4,096 rows in an 842,106-byte source, taking 1.227 family wall seconds, 1.042 CPU seconds and 112.0 MiB kernel memory peak. Both real stock and patched MCP tests preserve good windows alongside an unmapped-address failure and report -the actual `JA 0xe26d` / `JMP 0xd9c9` bytes. Targeted tests now total 33; the -representation fix still requires a fresh real-model task trial. +the actual `JA 0xe26d` / `JMP 0xd9c9` bytes. The representation revision had 33 +targeted tests; its subsequent real-model trial is recorded below. + +That fresh trial now verifies the missing machine path correctly; see +[actual task costs](trials.md). The SQLite task then exposes a separate missing +primitive: name/alias lookup across distinct callable entries. `functions` now +reuses the production inventory parser, preserving every matching entry, import +stub ambiguity, original aliases, clipped extents and unknown runtime binding. +The inventory is read/validated once per lease, and query/field/page selection +returns only the requested view. Batch xref and instruction reads share one +partial-outcome owner, retaining good observations and source IDs on failures. + +A real `sqlite3VdbeExec` capture at `0xea6d0` also exceeds the graph export's +operation capacity while producing 126,519 characters of valid C. The old adapter +incorrectly rejects the entire capture. The revised owner validates the function +envelope and input identity independently, then models graph availability as a +facet result. Code and its language remain available under the same handle; +graph counts stay unknown and graph reads return their reason and Evidence ID. +The real MCP regression reads 3,654 code lines after the graph failure and verifies +that `sqlite3Fts3ErrMsg` has distinct entries `0x268f0` and `0x5efc0`. Native graph +capacity remains a real limitation, not a resolved scalability claim. + +`verify-cache.mjs` measures a real cold/warm pair within one open lease. Cold zlib +snapshot reading takes 6,934.4 ms / 6.928 family CPU seconds; warm reading takes +67.7 ms / 0.068 CPU seconds and preserves the exact snapshot/Evidence ID. The +cumulative kernel memory peak is 275.1 MiB for both phases; the warm number is an +upper bound on its isolated phase peak. This is an API-only measurement, with no +model token result or cross-session cache claim. Targeted tests now total 38, +including late cancellation retaining partial facts and their provenance. diff --git a/experiments/atomic-codemode/native-ir-contract.mjs b/experiments/atomic-codemode/native-ir-contract.mjs index 059357aefe..cc902c730a 100644 --- a/experiments/atomic-codemode/native-ir-contract.mjs +++ b/experiments/atomic-codemode/native-ir-contract.mjs @@ -1,14 +1,15 @@ // Native JSON decoding and resource admission; row-wise validation bounds errors. import { z } from "zod"; import { FactFailure } from "./fact-error.mjs"; +import { ok, err } from "../../dist/domain/result.js"; const requireFact = (condition, reason) => { if (!condition) throw new FactFailure("invalid_provider_output", reason); }; -const hex = z.string().regex(/^0x[0-9a-f]+$/u); -const ordinal = z.number().int().nonnegative(); -const valueId = z.string().regex(/^v[0-9a-f]+$/u); -const operationId = z.string().regex(/^0x[0-9a-f]+:[0-9a-f]+$/u); +const hex = z.string().regex(/^0x(?:0|[1-9a-f][0-9a-f]{0,15})$/u); +const ordinal = z.number().int().nonnegative().max(0x7fffffff); +const valueId = z.string().regex(/^v[0-9a-f]{1,16}$/u); +const operationId = z.string().regex(/^0x[0-9a-f]{1,16}:[0-9a-f]{1,8}$/u); const edgeId = z.string().regex(/^\d+:\d+$/u); const edge = z.strictObject({ id: edgeId, @@ -39,7 +40,7 @@ const value = z.strictObject({ id: valueId, space: z.string().nullable(), offset: hex, - size_bytes: z.number().int().positive(), + size_bytes: z.number().int().positive().max(0x7fffffff), is_constant: z.boolean(), is_input: z.boolean(), is_annotation: z.boolean(), @@ -71,13 +72,18 @@ const factsSchema = z.strictObject({ }), }); const documentSchema = z.object({ + binary: z.string(), + count: z.literal(1), + language: z.string(), + error: z.null(), functions: z .array( z.object({ address_hex: hex, code: z.string(), error: z.null(), - atomic_facts: z.unknown(), + atomic_facts: z.unknown().optional(), + atomic_facts_error: z.string().optional(), }), ) .length(1), @@ -115,12 +121,7 @@ const parseFacts = rowParser(factsSchema, { values: (row) => value.parse(row), }); -function preflight(raw) { - requireFact( - Array.isArray(raw?.functions) && raw.functions.length === 1, - "Native source must describe exactly one function", - ); - const facts = raw.functions[0]?.atomic_facts; +function preflight(facts) { requireFact( facts && typeof facts === "object", "Native atomic facts facet is unavailable; inspect the retained source for its reason and C", @@ -159,36 +160,76 @@ function preflight(raw) { "Native relationships exceed resource capacity", ); } -export function parseNativeFunction(text, entry) { +const formatFailure = (error) => { + if (error instanceof FactFailure) return error; + const details = + error instanceof z.ZodError + ? { + issues: error.issues.slice(0, 8).map(({ path, code, message }) => ({ + path, + code, + message: message.slice(0, 160), + })), + } + : { reason: String(error.message).slice(0, 256) }; + return new FactFailure( + "invalid_provider_output", + "Native IR source does not match the declared format", + details, + ); +}; + +const readGraph = (fn) => { + try { + if (fn.atomic_facts_error !== undefined) { + requireFact( + fn.atomic_facts === undefined, + "Native graph is both present and reported unavailable", + ); + return err( + new FactFailure( + "capability_unavailable", + "Native recovered IR facet is unavailable", + { + native_error: fn.atomic_facts_error.slice(0, 1024), + native_error_truncated: fn.atomic_facts_error.length > 1024, + }, + ), + ); + } + preflight(fn.atomic_facts); + return ok(parseFacts(fn.atomic_facts)); + } catch (error) { + return err(formatFailure(error)); + } +}; + +export function parseNativeFunction(text, entry, expectedInput) { try { requireFact( Buffer.byteLength(text) <= 8 * 1024 * 1024, "Native JSON source exceeds transport budget", ); const raw = JSON.parse(text); - preflight(raw); - const fn = documentSchema.parse(raw).functions[0]; requireFact( - fn.address_hex === entry, - "Native function entry differs from requested subject", + Array.isArray(raw?.functions) && raw.functions.length === 1, + "Native source must describe exactly one function", ); - return { code: fn.code, facts: parseFacts(fn.atomic_facts) }; + const doc = documentSchema.parse(raw), + fn = doc.functions[0]; + if (expectedInput !== undefined && doc.binary !== expectedInput) + throw new FactFailure( + "subject_mismatch", + "Native source names a different command input", + { expected: expectedInput, observed: doc.binary }, + ); + if (fn.address_hex !== entry) + throw new FactFailure( + "subject_mismatch", + "Native function entry differs from requested subject", + ); + return { code: fn.code, language: doc.language, facts: readGraph(fn) }; } catch (error) { - if (error instanceof FactFailure) throw error; - const details = - error instanceof z.ZodError - ? { - issues: error.issues.slice(0, 8).map(({ path, code, message }) => ({ - path, - code, - message: message.slice(0, 160), - })), - } - : { reason: String(error.message).slice(0, 256) }; - throw new FactFailure( - "invalid_provider_output", - "Native IR source does not match the declared format", - details, - ); + throw formatFailure(error); } } diff --git a/experiments/atomic-codemode/native-ir.mjs b/experiments/atomic-codemode/native-ir.mjs index 603fdc1718..ae08ba5591 100644 --- a/experiments/atomic-codemode/native-ir.mjs +++ b/experiments/atomic-codemode/native-ir.mjs @@ -340,8 +340,70 @@ function sliceValues(model, values, input) { }; } -export function parseNativeIR(text, entry) { - const { code, facts } = parseNativeFunction(text, entry); - freeze(facts); - return createViews(facts, code, indexNative(facts)); +const projectedFailure = (error) => ({ + code: error.code ?? "invalid_provider_output", + message: error.message, + details: error.details ?? {}, +}); + +export function parseNativeIR(text, entry, expectedInput) { + const capture = parseNativeFunction(text, entry, expectedInput); + let graph = capture.facts; + if (graph.ok) { + try { + freeze(graph.value); + const model = createViews( + graph.value, + capture.code, + indexNative(graph.value), + ); + return Object.freeze({ + ...model, + language: capture.language, + facets: freeze({ + code: { availability: "available" }, + recovered_ir: { availability: "available" }, + }), + }); + } catch (error) { + graph = { ok: false, error }; + } + } + const error = projectedFailure(graph.error); + const unavailable = () => { + throw new FactFailure(error.code, error.message, { + ...error.details, + facet: "recovered_ir", + }); + }; + return Object.freeze({ + code: capture.code, + language: capture.language, + facets: freeze({ + code: { availability: "available" }, + recovered_ir: { availability: "unavailable", error }, + }), + coverage: freeze({ + scope: "native reconstructed code; recovered IR facet unavailable", + operations: null, + blocks: null, + control_flow: "unknown", + machine_semantic_equivalence: "unknown", + issues: [error.message], + }), + ...Object.fromEntries( + [ + "operation", + "block", + "findConstant", + "uses", + "incoming", + "select", + "values", + "blocks", + "edges", + "slice", + ].map((name) => [name, unavailable]), + ), + }); } diff --git a/experiments/atomic-codemode/native-ir.test.mjs b/experiments/atomic-codemode/native-ir.test.mjs index c2f4152c58..cfef506371 100644 --- a/experiments/atomic-codemode/native-ir.test.mjs +++ b/experiments/atomic-codemode/native-ir.test.mjs @@ -43,6 +43,10 @@ const op = (block, opcode, inputs, output = null) => ({ function diamond() { return { + binary: "/private/input", + count: 1, + language: "c", + error: null, functions: [ { address_hex: "0x1000", @@ -180,7 +184,10 @@ test("malformed identities, dangling relationships, foreign entry and phi reorde ]) { const doc = diamond(); mutate(doc.functions[0].atomic_facts); - assert.throws(() => parseNativeIR(JSON.stringify(doc), "0x1000")); + const model = parseNativeIR(JSON.stringify(doc), "0x1000"); + assert.equal(model.facets.recovered_ir.availability, "unavailable"); + assert.equal(model.code, "reconstructed C"); + assert.throws(() => model.select({})); } assert.throws( () => parseNativeIR(JSON.stringify(diamond()), "0x2000"), @@ -203,6 +210,28 @@ test("typed slices retain symbolic input frontiers and precise definition bindin assert.equal(result.coverage.runtime_reachability, "unknown"); }); +test("native facet failure preserves C without fabricating an empty graph; source identity still gates both", () => { + const doc = diamond(); + delete doc.functions[0].atomic_facts; + doc.functions[0].atomic_facts_error = "atomic operation capacity exceeded"; + const model = parseNativeIR(JSON.stringify(doc), "0x1000", "/private/input"); + assert.equal(model.code, "reconstructed C"); + assert.equal(model.language, "c"); + assert.equal(model.coverage.operations, null); + assert.equal(model.facets.code.availability, "available"); + assert.equal(model.facets.recovered_ir.availability, "unavailable"); + assert.throws( + () => model.select({}), + (error) => + error.code === "capability_unavailable" && + error.details.native_error === doc.functions[0].atomic_facts_error, + ); + assert.throws( + () => parseNativeIR(JSON.stringify(doc), "0x1000", "/other/input"), + (error) => error.code === "subject_mismatch", + ); +}); + test("oversized or malformed native arrays fail before diagnostic expansion", () => { const capacity = diamond(); capacity.functions[0].atomic_facts.operations = Array.from( @@ -210,7 +239,7 @@ test("oversized or malformed native arrays fail before diagnostic expansion", () () => ({}), ); assert.throws( - () => parseNativeIR(JSON.stringify(capacity), "0x1000"), + () => parseNativeIR(JSON.stringify(capacity), "0x1000").select({}), /capacity/u, ); const malformed = diamond(); @@ -219,7 +248,7 @@ test("oversized or malformed native arrays fail before diagnostic expansion", () () => ({}), ); assert.throws( - () => parseNativeIR(JSON.stringify(malformed), "0x1000"), + () => parseNativeIR(JSON.stringify(malformed), "0x1000").select({}), (error) => error.code === "invalid_provider_output" && error.details.issues.length <= 8 && diff --git a/experiments/atomic-codemode/provider.mjs b/experiments/atomic-codemode/provider.mjs index 3655f5591a..d82c302637 100644 --- a/experiments/atomic-codemode/provider.mjs +++ b/experiments/atomic-codemode/provider.mjs @@ -11,19 +11,19 @@ import { PrivateRuntimeRoot } from "../../dist/process/PrivateRuntimeRoot.js"; import { parseNativeIR } from "./native-ir.mjs"; import { FactFailure, requireResult } from "./fact-error.mjs"; import { parseInstructions } from "./instruction-facts.mjs"; +import { + sourceSummary, + pageFacts, + projectFact, + readFactBatch, +} from "./fact-reads.mjs"; +import { parseFunctionFacts, selectFunctionFacts } from "./function-facts.mjs"; import { parseKunaSsaDump, backwardSlice, } from "../atomic-re-tools/kuna-ssa.mjs"; const sha256 = (value) => createHash("sha256").update(value).digest("hex"); -const sourceSummary = ({ evidence_id, sha256, bytes, elapsed_ms }) => ({ - evidence_id, - sha256, - bytes, - elapsed_ms, -}); - export class KunaFactOwner { #lifetime = new AbortController(); #sources = new Map(); @@ -36,6 +36,7 @@ export class KunaFactOwner { #installation; #retainedBytes = 0; #strings; + #functions; #captureDirectory; #nativeIR = false; context; @@ -136,16 +137,18 @@ export class KunaFactOwner { ); const file = join(this.#root.path, id); await writeFile(file, snapshot.stdout.text, { flag: "wx" }); - const source = { + const source = Object.freeze({ evidence_id: id, sha256: sha256(snapshot.stdout.text), bytes: Buffer.byteLength(snapshot.stdout.text), - arguments: actualArguments, - display_arguments: actualArguments.map((s) => - s === snapshot.input ? this.#target.path : s, + arguments: Object.freeze(actualArguments), + display_arguments: Object.freeze( + actualArguments.map((s) => + s === snapshot.input ? this.#target.path : s, + ), ), elapsed_ms: performance.now() - start, - }; + }); this.#sources.set(id, { source, file }); this.#retainedBytes += bytes; if (this.#captureDirectory) { @@ -218,15 +221,30 @@ export class KunaFactOwner { }, }; } + async functions(input, signal) { + const capture = await this.#native( + "functions", + (path) => ["functions", path, "--json"], + signal, + ); + this.#functions ??= parseFunctionFacts( + await this.#sourceText(capture.source.evidence_id), + ); + return { + source: sourceSummary(capture.source), + ...selectFunctionFacts(this.#functions, input), + }; + } async references(input, signal) { - const results = []; - for (const address of input.addresses) { - try { - const capture = await this.#native( + return readFactBatch( + input.addresses.map((address) => ({ address })), + ({ address }) => + this.#native( `xref:${address}`, (path) => ["xrefs", path, "--to", address, "--json"], signal, - ); + ), + async (capture, { address }) => { const parsed = requireResult( parseKunaIncomingReferenceFacts( await this.#sourceText(capture.source.evidence_id), @@ -235,33 +253,15 @@ export class KunaFactOwner { ), "invalid_provider_output", ); - results.push({ - ok: true, - address, - source: sourceSummary(capture.source), - items: parsed.facts.slice(input.offset, input.offset + input.limit), - total: parsed.facts.length, - next_offset: - input.offset + input.limit < parsed.facts.length - ? input.offset + input.limit - : null, + return { + ...pageFacts(parsed.facts, input), coverage: { scope: "exact target address; not an object extent", discovery: "unknown", }, - }); - } catch (error) { - results.push({ - ok: false, - address, - error: { - code: error.code ?? "provider_failure", - message: error.message, - }, - }); - } - } - return { results }; + }; + }, + ); } async snapshot(input, signal) { const capture = await this.#native( @@ -281,10 +281,11 @@ export class KunaFactOwner { if (!this.#snapshots.has(id)) this.#snapshots.set(id, { entry: input.entry, + input: capture.input, evidence_id: capture.source.evidence_id, }); const parsed = await this.#load(id); - if (parsed.block(0).origin_start !== input.entry) + if (!this.#nativeIR && parsed.block(0).origin_start !== input.entry) throw new FactFailure( "subject_mismatch", "Native SSA entry does not match the requested function entry", @@ -294,14 +295,17 @@ export class KunaFactOwner { source: sourceSummary(capture.source), entry: input.entry, coverage: parsed.coverage, + facets: parsed.facets ?? { + code: { availability: "available" }, + recovered_ir: { availability: "available", coverage: "partial" }, + }, }; } async instructions(input, signal) { - const results = []; - for (const request of input.windows) { - let capture; - try { - capture = await this.#native( + return readFactBatch( + input.windows, + (request) => + this.#native( `instructions:${request.address}:${request.count}`, (path) => [ "disassemble", @@ -315,35 +319,15 @@ export class KunaFactOwner { "--json", ], signal, - ); - const parsed = parseInstructions( + ), + async (capture, request) => + parseInstructions( await this.#sourceText(capture.source.evidence_id), capture.input, request, input.fields, - ); - results.push({ - ok: true, - address: request.address, - source: sourceSummary(capture.source), - ...parsed, - }); - } catch (error) { - results.push({ - ok: false, - address: request.address, - error: { - code: error.code ?? "provider_failure", - message: error.message, - details: { - ...error.details, - ...(capture ? { evidence_id: capture.source.evidence_id } : {}), - }, - }, - }); - } - } - return { results }; + ), + ); } async #load(id) { const descriptor = this.#snapshots.get(id); @@ -364,7 +348,7 @@ export class KunaFactOwner { let parsed; try { parsed = this.#nativeIR - ? parseNativeIR(text, descriptor.entry) + ? parseNativeIR(text, descriptor.entry, descriptor.input) : parseKunaSsaDump(text); } catch (error) { throw new FactFailure( @@ -383,61 +367,77 @@ export class KunaFactOwner { } return this.#loaded.get(id); } - async operations(input) { + async #query(input, read) { const model = await this.#load(input.snapshot); - return { + const evidence_id = this.#snapshots.get(input.snapshot).evidence_id; + try { + return { ...read(model), evidence_id }; + } catch (error) { + throw new FactFailure(error.code ?? "analysis_failed", error.message, { + ...error.details, + evidence_id, + source_retained: true, + }); + } + } + async operations(input) { + return this.#query(input, (model) => ({ ...model.select(input), - evidence_id: this.#snapshots.get(input.snapshot).evidence_id, coverage: model.coverage, - }; + })); } async relations(input) { - const model = await this.#load(input.snapshot); - if (input.kind === "controllers" && !this.#nativeIR) - this.#requireNativeIR(); - return { + if (input.kind === "controllers") this.#requireNativeIR(input.snapshot); + return this.#query(input, (model) => ({ ...model.edges(input), - evidence_id: this.#snapshots.get(input.snapshot).evidence_id, coverage: model.coverage, - }; + })); } async slice(input) { - const model = await this.#load(input.snapshot); - return { - ...(model.slice + return this.#query(input, (model) => { + const result = model.slice ? model.slice(input) : backwardSlice(model, input.seeds, { maxOperations: input.max_operations, stopAt: input.stop_at, stopKinds: input.stop_kinds, - })), - evidence_id: this.#snapshots.get(input.snapshot).evidence_id, - }; + }); + return { + ...result, + operations: result.operations.map((row) => + projectFact(row, input.fields, [ + "id", + "origin_address", + "block", + "source_line", + ]), + ), + }; + }); } - #requireNativeIR() { + #requireNativeIR(snapshot) { if (!this.#nativeIR) throw new FactFailure( "capability_unavailable", "This relation requires the pinned experimental native IR engine; the diagnostic SSA format does not contain it", + this.#snapshots.has(snapshot) + ? { evidence_id: this.#snapshots.get(snapshot).evidence_id } + : {}, ); } async values(input) { - this.#requireNativeIR(); - const model = await this.#load(input.snapshot); - return { + this.#requireNativeIR(input.snapshot); + return this.#query(input, (model) => ({ ...model.values(input), - evidence_id: this.#snapshots.get(input.snapshot).evidence_id, coverage: model.coverage, - }; + })); } async blocks(input) { - this.#requireNativeIR(); - const model = await this.#load(input.snapshot); - return { + this.#requireNativeIR(input.snapshot); + return this.#query(input, (model) => ({ ...model.blocks(input), - evidence_id: this.#snapshots.get(input.snapshot).evidence_id, coverage: model.coverage, - }; + })); } async code(input) { const descriptor = this.#snapshots.get(input.snapshot); @@ -446,16 +446,18 @@ export class KunaFactOwner { "unknown_snapshot", "Snapshot is not owned by the current lease", ); - const code = this.#nativeIR - ? (await this.#load(input.snapshot)).code - : (await this.#sourceText(descriptor.evidence_id)).split( - "\n0\nBasic Block", - 1, - )[0]; + const model = this.#nativeIR ? await this.#load(input.snapshot) : null; + const code = + model?.code ?? + (await this.#sourceText(descriptor.evidence_id)).split( + "\n0\nBasic Block", + 1, + )[0]; const lines = code.split("\n"); return { evidence_id: descriptor.evidence_id, start_line: input.offset + 1, + language: model?.language ?? null, text: lines.slice(input.offset, input.offset + input.limit).join("\n"), total_lines: lines.length, next_offset: @@ -463,21 +465,15 @@ export class KunaFactOwner { ? input.offset + input.limit : null, coverage: { - authority: "reconstructed C, not original source", + authority: "native reconstructed code", scope: "selected display lines", }, }; } async evidence(input) { - const record = this.#sources.get(input.evidence_id); - if (!record) - throw new FactFailure( - "unknown_evidence", - "Evidence is not retained by the current lease", - ); const text = await this.#sourceText(input.evidence_id); return { - source: record.source, + source: this.#sources.get(input.evidence_id).source, text: text.slice(input.offset, input.offset + input.limit), total_code_units: text.length, next_offset: @@ -494,6 +490,8 @@ export class KunaFactOwner { this.#sources.clear(); this.#work.clear(); this.#snapshots.clear(); + this.#strings = undefined; + this.#functions = undefined; } cancel() { this.#lifetime.abort(); diff --git a/experiments/atomic-codemode/trials.md b/experiments/atomic-codemode/trials.md index 83efbb98ba..a5ecc7145b 100644 --- a/experiments/atomic-codemode/trials.md +++ b/experiments/atomic-codemode/trials.md @@ -187,3 +187,56 @@ representation ambiguity needs a structural fix: distinguish IR origin fields from physical instruction facts and expose selected instruction windows, then rerun the unchanged question. Private artifacts are `zlib-value-codemode-v1*` and `zlib-value-traditional-v1*`. + +After separating IR origins and adding instruction windows, the unchanged value +question was rerun at `4f13bd673`. The Code Mode answer now reads actual machine +bytes and correctly distinguishes recovered predecessor 38 from the intervening +`0xe26d` / `0xe270` machine path. Both answers recover the same local value +equations, byte-order conditions and unknown invariants. These remain individual +stochastic samples, not a general speedup guarantee. + +| Measurement | Code Mode value v2 | Traditional value v2 | +| --- | ---: | ---: | +| MCP attempts / failures | 7 / 0 | 6 / 1 | +| Cumulative input tokens | 296,888 | 573,881 | +| Included cached input | 252,160 | 512,128 | +| Uncached input | 44,728 | 61,753 | +| Output / included reasoning | 3,176 / 594 | 2,830 / 547 | +| Model wall seconds | 96.662 | 124.754 | +| Family wall seconds | 97.654 | 125.410 | +| Family CPU seconds | 19.495 | 44.143 | +| Kernel family peak MiB | 534.4 | 797.4 | + +The traditional failure is the client policy rejection of `binary_session`. +Its cost remains included. Both runtime/target digests stay unchanged, cleanup +is verified and OOM kills are zero. This is evidence of a useful seeded +value-tracing case for the revised interface, with the selected-tool and +single-sample qualifications retained. + +The initial SQLite diagnostic comparison, also at `4f13bd673`, exposes a further +capability/comparison gap: + +| Measurement | Code Mode SQLite v1 | Traditional SQLite v1 | +| --- | ---: | ---: | +| MCP attempts / failures | 14 / 0 | 17 / 8 | +| Cumulative input tokens | 601,445 | 934,237 | +| Included cached input | 535,296 | 831,744 | +| Uncached input | 66,149 | 102,493 | +| Output / included reasoning | 4,790 / 1,668 | 3,878 / 1,121 | +| Model wall seconds | 143.469 | 231.469 | +| Family CPU seconds | 18.996 | 154.132 | +| Kernel family peak MiB | 586.6 | 1,200.2 | + +Both answers find the two malformed-message conditions and distinguish the +depth-error, other-status and success paths. The traditional answer additionally +resolves the named formatter implementation at `0x5efc0`, tracing free, +`sqlite3_vmprintf` and pointer storage; Code Mode stops at its PLT stub and +correctly leaves delivery/allocation unknown. These are unequal-depth answers, +so the cost difference is not an equal-completion win. + +The selected traditional catalog also omitted `list_procedures`, despite errors +advertising it as the discovery route. The trace retains interior-entry failures, +failed resource discovery, a client approval-policy rejection and ambiguous name +lookups. This selection confound must be corrected in the next focused baseline; +the failed attempts and their costs are not discarded. Both runs still preserve +target/runtime identity, clean up their profiles/payloads and have zero OOM kills. diff --git a/experiments/atomic-codemode/verify-cache.mjs b/experiments/atomic-codemode/verify-cache.mjs new file mode 100644 index 0000000000..958f8341eb --- /dev/null +++ b/experiments/atomic-codemode/verify-cache.mjs @@ -0,0 +1,109 @@ +// A real MCP cold/warm read benchmark. No model runs in this verifier. +import assert from "node:assert/strict"; +import { readFile, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { Client } from "@modelcontextprotocol/client"; +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; +import { + createVerifierRun, + completeVerifierRun, + assertVerifierCleanupComplete, +} from "../../scripts/lib/verifier-run.mjs"; + +const [binary, output, entry = "0xc1e0"] = process.argv.slice(2); +const group = (await readFile("/proc/self/cgroup", "utf8")) + .trim() + .split("\n") + .find((row) => row.startsWith("0::")) + ?.slice(3); +assert.ok(group?.includes("rea-efficiency-"), "Run under resource-run.py"); +const counters = async () => ({ + cpu: Number( + (await readFile(`/sys/fs/cgroup${group}/cpu.stat`, "utf8")).match( + /usage_usec (\d+)/u, + )[1], + ), + peak: Number( + (await readFile(`/sys/fs/cgroup${group}/memory.peak`, "utf8")).trim(), + ), +}); +const verifier = createVerifierRun(); +const client = new Client({ name: "rea-cache-verifier", version: "0.1.0" }); +const transport = new StdioClientTransport({ + command: process.execPath, + args: [resolve("experiments/atomic-codemode/server.mjs")], + env: { ...process.env }, + stderr: "pipe", +}); +const phases = [], + failures = []; +const call = async (name, args = {}) => { + const reply = await client.callTool( + { name, arguments: args }, + { timeout: 180000 }, + ); + const value = JSON.parse(reply.content[0].text); + assert.equal(value.ok, true, JSON.stringify(value)); + return value; +}; +try { + await client.connect(transport); + await call("open_binary", { path: binary }); + for (const phase of ["cold_snapshot", "warm_snapshot"]) { + const before = await counters(), + start = performance.now(); + const result = await call("execute_javascript", { + code: `const r=await rea.snapshot({entry:${JSON.stringify(entry)}});if(!r.ok)throw new Error(JSON.stringify(r));return r.data;`, + }); + const elapsed = performance.now() - start, + after = await counters(); + phases.push({ + phase, + elapsed_ms: elapsed, + family_cpu_seconds: (after.cpu - before.cpu) / 1e6, + cumulative_family_memory_peak_bytes: after.peak, + snapshot: result.value.snapshot, + source: result.value.source, + facets: result.value.facets, + }); + } + assert.equal(phases[0].snapshot, phases[1].snapshot); + assert.equal(phases[0].source.evidence_id, phases[1].source.evidence_id); + await call("close_binary"); +} catch (error) { + failures.push(error.stack ?? String(error)); +} finally { + try { + await client.close(); + } catch (error) { + failures.push(String(error)); + } + try { + await transport.close(); + } catch (error) { + failures.push(String(error)); + } +} +const lifecycle = await completeVerifierRun(verifier); +assertVerifierCleanupComplete(lifecycle.process_lineage); +await writeFile( + output, + JSON.stringify({ + phases, + failures, + lifecycle, + scope: + "Real MCP, no model/token measurement. CPU deltas cover the owned cgroup. Memory peaks are cumulative; the warm value is an upper bound on its isolated phase peak. Both reads share one immutable captured artifact/lease; this is not a cross-session cache benchmark.", + }), +); +console.log( + JSON.stringify({ + output, + phases: phases.map(({ phase, elapsed_ms, family_cpu_seconds }) => ({ + phase, + elapsed_ms, + family_cpu_seconds, + })), + }), +); +if (failures.length) process.exitCode = 1; diff --git a/experiments/atomic-codemode/verify-mcp.mjs b/experiments/atomic-codemode/verify-mcp.mjs index ba865e6ff4..90960b3d83 100644 --- a/experiments/atomic-codemode/verify-mcp.mjs +++ b/experiments/atomic-codemode/verify-mcp.mjs @@ -10,7 +10,8 @@ import { assertVerifierCleanupComplete, } from "../../scripts/lib/verifier-run.mjs"; -const [binary, output] = process.argv.slice(2); +const [binary, output, scenario = "zlib"] = process.argv.slice(2); +assert.ok(["zlib", "sqlite-partial"].includes(scenario)); const verifier = createVerifierRun(); const client = new Client({ name: "rea-codemode-verifier", version: "0.1.0" }); const transport = new StdioClientTransport({ @@ -48,9 +49,47 @@ try { const ajv = new Ajv2020({ strict: false }); for (const tool of catalog.tools) ajv.compile(tool.inputSchema); const opened = await call("open_binary", { path: binary }); - await call("reference", { query: "relations" }); - const result = await call("execute_javascript", { - code: ` + if (scenario === "sqlite-partial") { + assert.equal(opened.context.analysis_model, "experimental-native-ir-v1"); + const result = await call("execute_javascript", { + code: ` +const read=async(p)=>{const r=await p;if(!r.ok)throw new Error(JSON.stringify(r));return r.data;}; +const names=await read(rea.functions({query:"sqlite3Fts3ErrMsg",exact:true})); +const entries=await read(rea.functions({query:"sqlite3VdbeExec",exact:true})); +const entry=entries.items.find(fn=>fn.address==="0xea6d0");if(!entry)throw new Error("fixture entry missing"); +const snap=await read(rea.snapshot({entry:entry.address})); +const code=await read(rea.code({snapshot:snap.snapshot,limit:4})); +const graph=await rea.operations({snapshot:snap.snapshot,limit:1}); +const evidence=await read(rea.evidence({evidence_id:snap.source.evidence_id,limit:512})); +return {names,snap,code,graph,evidence_source:evidence.source};`, + }); + assert.deepEqual(result.value.names.items.map((fn) => fn.address).sort(), [ + "0x268f0", + "0x5efc0", + ]); + assert.equal(result.value.snap.facets.code.availability, "available"); + assert.equal( + result.value.snap.facets.recovered_ir.availability, + "unavailable", + ); + assert.equal(result.value.snap.coverage.operations, null); + assert.ok( + result.value.code.text.length > 0 && result.value.code.total_lines > 1000, + ); + assert.equal(result.value.graph.ok, false); + assert.equal(result.value.graph.error.code, "capability_unavailable"); + assert.equal( + result.value.graph.error.details.evidence_id, + result.value.snap.source.evidence_id, + ); + assert.equal( + result.value.evidence_source.sha256, + result.value.snap.source.sha256, + ); + } else { + await call("reference", { query: "relations" }); + const result = await call("execute_javascript", { + code: ` const found=await rea.strings({query:"incorrect header check",limit:1}); if(!found.ok) throw new Error(JSON.stringify(found)); const literal=found.data.items[0].address; @@ -62,15 +101,16 @@ const seed=await rea.operations({snapshot:s,constant:literal}); const uses=await rea.relations({snapshot:s,kind:"uses",seeds:seed.data.items.map(x=>x.id)}); const consumers=await rea.operations({snapshot:s,ids:uses.data.items.map(x=>x.use)}); const incoming=await rea.relations({snapshot:s,kind:"incoming",seeds:consumers.data.items.map(x=>x.block)}); -const slice=await rea.slice({snapshot:s,seeds:incoming.data.items.map(x=>x.operation).filter(Boolean)}); -return {literal,entry,snapshot:s,consumer_blocks:[...new Set(consumers.data.items.map(x=>x.block))],selected:slice.data.operations.length,guards:slice.data.operations.map(x=>x.statement),coverage:slice.data.coverage,evidence_id:slice.data.evidence_id};`, - }); - assert.equal(result.value.selected, 19); - assert.equal(result.value.coverage.status, "partial"); - assert.ok(result.value.guards.some((s) => s.includes("/ #0x1f"))); - if (opened.context.analysis_model === "experimental-native-ir-v1") { - const native = await call("execute_javascript", { - code: `const s=${JSON.stringify(result.value.snapshot)}; +const slice=await rea.slice({snapshot:s,seeds:incoming.data.items.map(x=>x.operation).filter(Boolean),fields:["statement"]}); +return {literal,entry,snapshot:s,consumer_blocks:[...new Set(consumers.data.items.map(x=>x.block))],selected:slice.data.operations.length,projected:slice.data.operations.every(op=>!("inputs" in op)&&typeof op.origin_address==="string"),guards:slice.data.operations.map(x=>x.statement),coverage:slice.data.coverage,evidence_id:slice.data.evidence_id};`, + }); + assert.equal(result.value.selected, 19); + assert.equal(result.value.projected, true); + assert.equal(result.value.coverage.status, "partial"); + assert.ok(result.value.guards.some((s) => s.includes("/ #0x1f"))); + if (opened.context.analysis_model === "experimental-native-ir-v1") { + const native = await call("execute_javascript", { + code: `const s=${JSON.stringify(result.value.snapshot)}; const read=async(p)=>{const r=await p;if(!r.ok)throw new Error(JSON.stringify(r));return r.data;}; const blocks=await read(rea.blocks({snapshot:s,ids:${JSON.stringify(result.value.consumer_blocks)}})); const controls=await read(rea.relations({snapshot:s,kind:"controllers",seeds:blocks.items.map(b=>b.id)})); @@ -78,33 +118,34 @@ const guards=await read(rea.operations({snapshot:s,block_ids:controls.items.map( const phi=await read(rea.operations({snapshot:s,kind:"phi",fields:["inputs","output"],limit:2})); const values=await read(rea.values({snapshot:s,ids:phi.items.flatMap(o=>o.inputs.map(i=>i.value_id)).filter(Boolean)})); return {blocks,controls,guards,phi,values};`, - }); - assert.equal(native.value.controls.total, 2); - assert.deepEqual( - native.value.guards.items.map((op) => op.origin_address).sort(), - ["0xd9ec", "0xda32"], - ); - assert.ok( - native.value.phi.items.every((op) => - op.inputs.every( - (input) => typeof input.phi_predecessor_edge === "string", + }); + assert.equal(native.value.controls.total, 2); + assert.deepEqual( + native.value.guards.items.map((op) => op.origin_address).sort(), + ["0xd9ec", "0xda32"], + ); + assert.ok( + native.value.phi.items.every((op) => + op.inputs.every( + (input) => typeof input.phi_predecessor_edge === "string", + ), ), - ), - ); - assert.equal( - native.value.blocks.coverage.control_flow, - "complete-for-recovered-graph", - ); - assert.ok(native.value.values.items.length > 0); + ); + assert.equal( + native.value.blocks.coverage.control_flow, + "complete-for-recovered-graph", + ); + assert.ok(native.value.values.items.length > 0); + } + const machine = await call("execute_javascript", { + code: 'const r=await rea.instructions({windows:[{address:"0xd97e",count:1},{address:"0xe26d",count:2},{address:"0xdeadbeef",count:1}]});if(!r.ok)throw new Error(JSON.stringify(r));return r.data;', + }); + assert.equal(machine.value.results[0].items[0].bytes, "0f87e9080000"); + assert.match(machine.value.results[0].items[0].text, /e26d/u); + assert.equal(machine.value.results[1].items[1].address, "0xe270"); + assert.match(machine.value.results[1].items[1].text, /d9c9/u); + assert.equal(machine.value.results[2].ok, false); } - const machine = await call("execute_javascript", { - code: 'const r=await rea.instructions({windows:[{address:"0xd97e",count:1},{address:"0xe26d",count:2},{address:"0xdeadbeef",count:1}]});if(!r.ok)throw new Error(JSON.stringify(r));return r.data;', - }); - assert.equal(machine.value.results[0].items[0].bytes, "0f87e9080000"); - assert.match(machine.value.results[0].items[0].text, /e26d/u); - assert.equal(machine.value.results[1].items[1].address, "0xe270"); - assert.match(machine.value.results[1].items[1].text, /d9c9/u); - assert.equal(machine.value.results[2].ok, false); await call("close_binary"); } catch (error) { failure = error.stack ?? String(error);