mirror of
https://github.com/morluto/rea.git
synced 2026-11-04 22:06:10 +00:00
feat(native): match retained function inventories across builds (#5390)
Match retained native inventories through shared CLI/MCP workflows with conservative symbol/byte correspondence, unresolved structural candidates, complete Evidence provenance and bounded paged reads. Closes #5343.
This commit is contained in:
1 parent
e84468c0bb
commit
1417bf943b
38 files changed
+3422
-72
No files matched your search
@@ -0,0 +1,165 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { z } from "zod";
|
||||
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import type { EvidenceBundle } from "../../domain/evidenceBundle.js";
|
||||
import { immutableEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
EvidenceIntegrityError,
|
||||
EvidenceReferenceError,
|
||||
} from "../../domain/evidenceErrors.js";
|
||||
import { digestCanonicalValue } from "../../domain/canonicalDigest.js";
|
||||
import { retainedEvidenceReferenceSchema } from "../../domain/evidenceReference.js";
|
||||
import type { matchNativeFunctionsInputSchema } from "../../contracts/native/functionMatchingContracts.js";
|
||||
import { readImmutableEvidenceBundle } from "../EvidenceBundleFiles.js";
|
||||
import type { EvidenceLookup } from "../EvidenceInputResolver.js";
|
||||
import type { NativeMatchingAdmission } from "./NativeMatchingAdmission.js";
|
||||
|
||||
type Request = z.output<typeof matchNativeFunctionsInputSchema>;
|
||||
interface SourceEntry {
|
||||
readonly file: number;
|
||||
readonly links: readonly string[];
|
||||
}
|
||||
interface Options {
|
||||
readonly input: Request;
|
||||
readonly paths: readonly string[];
|
||||
readonly resources: ArtifactResourceScope;
|
||||
readonly admission: NativeMatchingAdmission;
|
||||
}
|
||||
const reference = (evidence_id: string) => ({
|
||||
kind: "retained-evidence" as const,
|
||||
evidence_id,
|
||||
});
|
||||
// IDs and links have authenticated ASCII digest syntax. Own these short strings
|
||||
// independently of the discarded bundle's potentially large JSON backing store.
|
||||
const ownId = (value: string): string =>
|
||||
Buffer.from(value, "ascii").toString("ascii");
|
||||
|
||||
/** IDs bind semantic payloads; subjects add the display metadata IDs intentionally omit. */
|
||||
const recordSnapshot = async (
|
||||
bundle: EvidenceBundle,
|
||||
admission: NativeMatchingAdmission,
|
||||
): Promise<string> => {
|
||||
const hash = createHash("sha256");
|
||||
for (const record of bundle.records) {
|
||||
await admission.checkpoint();
|
||||
hash
|
||||
.update(record.evidence_id)
|
||||
.update(digestCanonicalValue(record.subject));
|
||||
}
|
||||
return hash.digest("hex");
|
||||
};
|
||||
|
||||
/** Index only graph metadata; keep at most the last whole bundle between passes. */
|
||||
export const loadNativeMatchingBundleSources = async ({
|
||||
input,
|
||||
paths,
|
||||
resources,
|
||||
admission,
|
||||
}: Options): Promise<{ input: Request; lookup: EvidenceLookup }> => {
|
||||
const records = new Map<string, Evidence>(),
|
||||
index = new Map<string, SourceEntry>();
|
||||
const rootIds: string[] = [];
|
||||
for (const value of [
|
||||
input.left,
|
||||
input.right,
|
||||
...input.left_observations,
|
||||
...input.right_observations,
|
||||
]) {
|
||||
const parsed = retainedEvidenceReferenceSchema.safeParse(value);
|
||||
if (parsed.success) rootIds.push(parsed.data.evidence_id);
|
||||
else {
|
||||
await admission.admit(value);
|
||||
const record = immutableEvidence(value);
|
||||
rootIds.push(record.evidence_id);
|
||||
records.set(record.evidence_id, record);
|
||||
index.set(record.evidence_id, { file: -1, links: record.evidence_links });
|
||||
}
|
||||
}
|
||||
const snapshots: string[] = [];
|
||||
let last: { file: number; bundle: EvidenceBundle } | undefined;
|
||||
for (let file = 0; file < paths.length; file++) {
|
||||
last = undefined;
|
||||
const loaded = await readImmutableEvidenceBundle(
|
||||
paths[file]!,
|
||||
admission.signal,
|
||||
resources,
|
||||
);
|
||||
if (!loaded.ok) throw loaded.error;
|
||||
snapshots.push(await recordSnapshot(loaded.value, admission));
|
||||
for (const record of loaded.value.records) {
|
||||
await admission.checkpoint(
|
||||
256 +
|
||||
4 *
|
||||
(record.evidence_id.length +
|
||||
record.evidence_links.reduce((sum, id) => sum + id.length, 0)),
|
||||
);
|
||||
if (!index.has(record.evidence_id))
|
||||
index.set(ownId(record.evidence_id), {
|
||||
file,
|
||||
links: record.evidence_links.map(ownId),
|
||||
});
|
||||
}
|
||||
last = { file, bundle: loaded.value };
|
||||
}
|
||||
const needed = new Set<string>(rootIds),
|
||||
files = new Set<number>();
|
||||
// Set iteration includes newly discovered nodes. Shared ancestors and cycles
|
||||
// are visited once, independent of source-file and record ordering.
|
||||
for (const id of needed) {
|
||||
await admission.checkpoint(128);
|
||||
const entry = index.get(id);
|
||||
if (entry === undefined)
|
||||
throw new EvidenceReferenceError(
|
||||
id,
|
||||
"missing",
|
||||
"Complete Evidence closure in the supplied bundles or inline inputs",
|
||||
null,
|
||||
);
|
||||
if (entry.file >= 0) files.add(entry.file);
|
||||
for (const linked of entry.links) needed.add(linked);
|
||||
}
|
||||
const retain = async (
|
||||
bundle: EvidenceBundle,
|
||||
file: number,
|
||||
): Promise<void> => {
|
||||
for (const record of bundle.records)
|
||||
if (
|
||||
needed.has(record.evidence_id) &&
|
||||
index.get(record.evidence_id)?.file === file
|
||||
) {
|
||||
await admission.admit(record);
|
||||
records.set(record.evidence_id, record);
|
||||
}
|
||||
};
|
||||
if (last !== undefined && files.has(last.file)) {
|
||||
await retain(last.bundle, last.file);
|
||||
files.delete(last.file);
|
||||
}
|
||||
last = undefined;
|
||||
for (const file of files) {
|
||||
const loaded = await readImmutableEvidenceBundle(
|
||||
paths[file]!,
|
||||
admission.signal,
|
||||
resources,
|
||||
);
|
||||
if (!loaded.ok) throw loaded.error;
|
||||
if ((await recordSnapshot(loaded.value, admission)) !== snapshots[file])
|
||||
throw new EvidenceIntegrityError(
|
||||
`Evidence bundle records changed during closure lookup: ${paths[file]}`,
|
||||
);
|
||||
await retain(loaded.value, file);
|
||||
}
|
||||
const left = rootIds[0]!,
|
||||
right = rootIds[1]!;
|
||||
const split = 2 + input.left_observations.length;
|
||||
return {
|
||||
input: {
|
||||
...input,
|
||||
left: reference(left),
|
||||
right: reference(right),
|
||||
left_observations: rootIds.slice(2, split).map(reference),
|
||||
right_observations: rootIds.slice(split).map(reference),
|
||||
},
|
||||
lookup: (id) => records.get(id),
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user