diff --git a/experiments/atomic-codemode/native-contract.mjs b/experiments/atomic-codemode/native-contract.mjs index f8da20fce..3bb44c9c1 100644 --- a/experiments/atomic-codemode/native-contract.mjs +++ b/experiments/atomic-codemode/native-contract.mjs @@ -79,6 +79,10 @@ export const readNativeFacet = (fn, name, label, parse) => { fn[name] === undefined, `Native ${label} is both present and unavailable`, ); + requireFact( + typeof reason === "string", + `Native ${label} failure reason must be text`, + ); return err( new FactFailure( "capability_unavailable", diff --git a/experiments/atomic-codemode/native-facet-failures.test.mjs b/experiments/atomic-codemode/native-facet-failures.test.mjs new file mode 100644 index 000000000..bb52e0b2a --- /dev/null +++ b/experiments/atomic-codemode/native-facet-failures.test.mjs @@ -0,0 +1,77 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { parseNativeIR } from "./native-ir.mjs"; +import { readNativeGraphEvidence } from "./native-ir-contract.mjs"; + +// Minimal valid graph: this regression concerns facet admission, not SSA rules. +const document = () => ({ + binary: "/fixture", + count: 1, + language: "c", + error: null, + functions: [ + { + address_hex: "0x1000", + error: null, + code: "reconstructed C", + atomic_facts: { + schema_version: 3, + blocks: [ + { + id: 0, + start: "0x1000", + operations: [], + predecessors: [], + successors: [], + }, + ], + operations: [], + values: [], + coverage: { + scope: "retained native graph", + machine_semantic_equivalence: "unknown", + indirect_branch_operations: [], + omitted_definition_ids: [], + issues: [], + }, + }, + }, + ], +}); + +test("malformed facet failure values stay local and preserve healthy graph/code/source views", () => { + for (const name of [ + "atomic_facts", + "atomic_calls", + "atomic_parameters", + "atomic_switches", + "atomic_projection", + ]) + for (const reason of [null, 7, false, {}, []]) { + const doc = document(); + if (name === "atomic_facts") delete doc.functions[0].atomic_facts; + doc.functions[0][`${name}_error`] = reason; + const text = JSON.stringify(doc), + model = parseNativeIR(text, "0x1000"); + assert.equal(model.code, "reconstructed C"); + if (name !== "atomic_facts") { + assert.equal(model.facets.recovered_ir.availability, "available"); + assert.equal(model.blocks({}).total, 1); + assert.ok(readNativeGraphEvidence(text).text.length); + } else { + assert.equal( + model.facets.recovered_ir.error.code, + "invalid_provider_output", + ); + assert.throws( + () => readNativeGraphEvidence(text), + (e) => e.code === "invalid_provider_output", + ); + } + if (name === "atomic_parameters") + assert.equal( + model.facets.parameters.error.code, + "invalid_provider_output", + ); + } +}); diff --git a/experiments/atomic-codemode/native-ir-contract.mjs b/experiments/atomic-codemode/native-ir-contract.mjs index ce0a4dc31..189d5ecee 100644 --- a/experiments/atomic-codemode/native-ir-contract.mjs +++ b/experiments/atomic-codemode/native-ir-contract.mjs @@ -69,15 +69,15 @@ const documentSchema = z.object({ code: z.string(), error: z.null(), atomic_facts: z.unknown().optional(), - atomic_facts_error: z.string().optional(), + atomic_facts_error: z.unknown().optional(), atomic_calls: z.unknown().optional(), - atomic_calls_error: z.string().optional(), + atomic_calls_error: z.unknown().optional(), atomic_parameters: z.unknown().optional(), - atomic_parameters_error: z.string().optional(), + atomic_parameters_error: z.unknown().optional(), atomic_switches: z.unknown().optional(), - atomic_switches_error: z.string().optional(), + atomic_switches_error: z.unknown().optional(), atomic_projection: z.unknown().optional(), - atomic_projection_error: z.string().optional(), + atomic_projection_error: z.unknown().optional(), }), ) .length(1), @@ -241,12 +241,15 @@ export function readNativeGraphEvidence(text) { "This retained source is not a native function record", ); const { fn } = readFunction(raw); - if (fn.atomic_facts === undefined) - throw new FactFailure( - "capability_unavailable", - "Recovered graph source was not captured", - { native_error: fn.atomic_facts_error?.slice(0, 1024) }, + if (fn.atomic_facts === undefined) { + const graph = readNativeFacet( + fn, + "atomic_facts", + "recovered IR", + (raw) => raw, ); + if (!graph.ok) throw graph.error; + } const decoded = decodeGraphArchive(fn.atomic_facts); const view = decoded ?? JSON.stringify(fn.atomic_facts); requireFact( diff --git a/experiments/atomic-codemode/native-parameters.test.mjs b/experiments/atomic-codemode/native-parameters.test.mjs index 7a0f84b68..456b85f73 100644 --- a/experiments/atomic-codemode/native-parameters.test.mjs +++ b/experiments/atomic-codemode/native-parameters.test.mjs @@ -128,7 +128,7 @@ test("unknown type/storage is distinct from native void, malformed identities ca test("prototype facet works without graph/calls; unavailable/invalid prototype retains code and reason", () => { const available = model({ atomic_parameters: fixture(), - atomic_facts_error: "capacity", + atomic_facts_error: { malformed: "not a failure string" }, atomic_calls_error: "capacity", }); assert.equal(available.facets.parameters.availability, "available");