Files
ECC/scripts/lib/context-routing-index.js
T
haelyraandmontjeffrey cc48be3c52 fix(context): bind advisory receipts and apply suggestion evidence floors
Port the evidence floor from PR #3480 onto the canonical generation-bound index; preserve policy-v5 anchors and the source PR attribution. Harden PR #3453 receipt metadata and pointer byte bounds.

Co-authored-by: montjeffrey <montjeffrey@gmail.com>
2026-10-10 17:48:20 -04:00

188 lines
12 KiB
JavaScript

'use strict';
// A metadata-only routing index for prompt-time suggestions. Each index is
// named by the managed generation it was built for and bound to that state's
// receipt, so a store change retires it. It never authorizes loading: the
// resolver re-verifies canonical sources before returning any skill body.
const path = require('node:path');
const io = require('./context-profile-store-fs');
const { buildRetrievalIndex, searchRetrieval, sparseDense } = require('./context-retrieval');
const { hasSuggestionEvidence } = require('./context-selection');
const { DEFAULT_REPO_ROOT, digestObject, stableStringify } = require('./context-profile-support');
const SCHEMA = 'ecc.context-routing-index.v1';
const POINTER_SCHEMA = 'ecc.context-routing-pointer.v1';
const DIGEST = /^[a-f0-9]{64}$/;
const MAX_SUGGESTIONS = 3;
const MAX_DESCRIPTION = 120;
// A Full index is under 1 MiB; these bounds keep a prompt-time read proportional
// to a real registry even when the store holds something else.
const MAX_POINTER_BYTES = 4096;
const MAX_INDEX_BYTES = 4 * 1024 * 1024;
const MAX_ENTRIES = 2048;
const SKILL_ID = /^skill:[a-z0-9]+(?:-[a-z0-9]+)*$/;
const ENTRY_KEYS = new Set(['id', 'name', 'description', 'ownerModuleId', 'packId', 'triggers', 'dense']);
function checkedRoot(stateRoot) {
if (typeof stateRoot !== 'string' || !path.isAbsolute(stateRoot) || path.resolve(stateRoot) !== stateRoot) {
throw new Error('stateRoot must be an absolute managed store directory');
}
return stateRoot;
}
// Reads only the ownership marker, state pointer and its receipt: enough to
// bind an index to the current generation without rehashing the payload.
function readBinding(stateRoot) {
const root = checkedRoot(stateRoot);
const marker = { schemaVersion: 'ecc.context-store.v1', destinationDigest: digestObject({ root }) };
const markerFile = path.join(root, 'store.json');
if (!io.inspect(markerFile, true).stat || stableStringify(io.readJson(markerFile)) !== stableStringify(marker)) {
throw new Error('Directory is not an owned ECC managed store');
}
const state = io.readJson(path.join(root, 'state.json'));
if (state.schemaVersion !== 'ecc.context-store-state.v1' || !DIGEST.test(state.receiptDigest) || !DIGEST.test(state.generationDigest)
|| !Number.isSafeInteger(state.revision) || state.revision < 1) {
throw new Error('Managed state integrity mismatch');
}
const receipt = io.readJson(path.join(root, 'receipts', `${state.receiptDigest}.json`));
if (receipt.schemaVersion !== 'ecc.context-store-receipt.v1' || digestObject(receipt) !== state.receiptDigest
|| receipt.generationDigest !== state.generationDigest || receipt.destinationDigest !== marker.destinationDigest
|| !Number.isSafeInteger(receipt.revision) || receipt.revision < 1 || receipt.revision !== state.revision
|| !['auto', 'manual', 'suggest'].includes(receipt.selection?.selectionMode)
|| stableStringify(receipt.selection) !== stableStringify(state.selection)) {
throw new Error('Managed receipt and state integrity mismatch');
}
return { root, generationDigest: state.generationDigest, receiptDigest: state.receiptDigest,
revision: receipt.revision, selectionMode: receipt.selection?.selectionMode || null };
}
// Stored vectors are little-endian uint16 dimensions and float64 weights in
// base64: half the size of JSON pairs and exact on any platform.
function encodeDense(pairs) {
const indexes = Buffer.alloc(pairs.length * 2);
const values = Buffer.alloc(pairs.length * 8);
pairs.forEach(([dimension, value], position) => {
indexes.writeUInt16LE(dimension, position * 2);
values.writeDoubleLE(value, position * 8);
});
return { indexes: indexes.toString('base64'), values: values.toString('base64') };
}
function decodeDense(dense) {
if (!dense || typeof dense.indexes !== 'string' || typeof dense.values !== 'string') return null;
const indexes = Buffer.from(dense.indexes, 'base64');
const values = Buffer.from(dense.values, 'base64');
if (indexes.length % 2 || values.length !== indexes.length * 4) return null;
const pairs = new Array(indexes.length / 2);
for (let position = 0; position < pairs.length; position++) {
pairs[position] = [indexes.readUInt16LE(position * 2), values.readDoubleLE(position * 8)];
}
return pairs;
}
// routing/<generation>.json is a small pointer bound to the state receipt; the
// entries live in routing/indexes/<sha256 of bytes>.json so readers verify them
// by hashing raw bytes instead of re-serializing a large object.
const pointerPath = (root, generationDigest) => path.join(root, 'routing', `${generationDigest}.json`);
const entriesPath = (root, digest) => path.join(root, 'routing', 'indexes', `${digest}.json`);
/** Build the index for the configured, recovered store. Reads canonical sources. */
function writeRoutingIndex({ stateRoot, repoRoot = DEFAULT_REPO_ROOT } = {}) {
const root = checkedRoot(stateRoot);
const status = require('./context-profile-store').getStoreStatus({ stateRoot: root });
if (!status.configured || status.recoveryRequired) throw new Error('Routing index requires a configured, recovered managed store');
const binding = readBinding(root);
if (binding.receiptDigest !== status.receiptDigest) throw new Error('Managed state changed during routing index build');
const carrier = require('./context-carriers').planContextCarrier({ repoRoot, profileId: status.profileId, target: status.target,
selectionMode: status.selectionMode, include: status.include, exclude: status.exclude });
if (carrier.carrierDigest !== status.carrierDigest) throw new Error('Stored profile source is stale; set the current generation before indexing');
const { routingEntries } = require('./context-selection');
const routing = routingEntries({ repoRoot, profileId: status.profileId, target: status.target,
selectionMode: status.selectionMode, include: status.include, exclude: status.exclude });
if (routing.planDigest !== carrier.planDigest || routing.registryDigest !== carrier.registryDigest) {
throw new Error('Skill sources changed during routing index build; run it again');
}
const index = { schemaVersion: SCHEMA, generationDigest: binding.generationDigest, receiptDigest: binding.receiptDigest,
...routing, entries: routing.entries.map(entry => ({ ...entry, dense: encodeDense(sparseDense(entry)) })) };
const bytes = io.jsonBytes(index);
const digest = io.hash(bytes);
const file = entriesPath(root, digest);
io.mkdir(path.join(root, 'routing')); io.mkdir(path.dirname(file));
// Temp file and rename, so an interrupted build never leaves partial bytes
// under a digest name; a file whose bytes do not hash to its name (left by
// an interrupted in-place write) is replaced rather than trusted.
if (!io.inspect(file, true).stat || io.hash(io.read(file)) !== digest) io.atomicJson(file, index);
if (readBinding(root).receiptDigest !== binding.receiptDigest) throw new Error('Managed state changed during routing index build');
const pointer = { schemaVersion: POINTER_SCHEMA, generationDigest: binding.generationDigest,
receiptDigest: binding.receiptDigest, indexDigest: digest, bytes: bytes.length };
io.atomicJson(pointerPath(root, binding.generationDigest), { ...pointer, pointerDigest: digestObject(pointer) });
return { status: 'written', path: file, generationDigest: binding.generationDigest, entries: routing.entries.length };
}
const optionalString = value => value === undefined || typeof value === 'string';
function validEntry(entry) {
return entry !== null && typeof entry === 'object' && !Array.isArray(entry)
&& Object.keys(entry).every(key => ENTRY_KEYS.has(key))
&& typeof entry.id === 'string' && SKILL_ID.test(entry.id) && typeof entry.description === 'string'
&& optionalString(entry.name) && optionalString(entry.ownerModuleId) && optionalString(entry.packId)
&& (entry.triggers === undefined || (Array.isArray(entry.triggers) && entry.triggers.every(trigger => typeof trigger === 'string')));
}
/** Return the index for the current generation, or null when none was built
* for this state. A pointer left by an earlier state of the same generation
* (after a rollback or mode change) is retired, not corrupt. */
function readRoutingIndex(stateRoot, binding = readBinding(stateRoot)) {
if (binding.root !== checkedRoot(stateRoot)) throw new Error('Routing binding belongs to a different store');
const file = pointerPath(binding.root, binding.generationDigest);
if (!io.inspect(path.dirname(file), true).stat || !io.inspect(file, true).stat) return null;
const invalid = () => new Error('Routing index integrity mismatch; rebuild it with ecc profile routing-index');
if (io.inspect(file).stat.size > MAX_POINTER_BYTES) throw invalid();
const pointerBytes = io.read(file);
if (pointerBytes.length > MAX_POINTER_BYTES) throw invalid();
const { pointerDigest, ...pointer } = JSON.parse(pointerBytes.toString('utf8'));
if (pointer.schemaVersion !== POINTER_SCHEMA || digestObject(pointer) !== pointerDigest || !DIGEST.test(pointer.indexDigest || '')
|| !DIGEST.test(pointer.receiptDigest || '') || pointer.generationDigest !== binding.generationDigest
|| !Number.isSafeInteger(pointer.bytes) || pointer.bytes <= 0 || pointer.bytes > MAX_INDEX_BYTES) throw invalid();
if (pointer.receiptDigest !== binding.receiptDigest) return null;
const entriesFile = entriesPath(binding.root, pointer.indexDigest);
if (io.inspect(entriesFile).stat.size !== pointer.bytes) throw invalid();
const bytes = io.read(entriesFile);
if (bytes.length !== pointer.bytes || io.hash(bytes) !== pointer.indexDigest) throw invalid();
const index = JSON.parse(bytes.toString('utf8'));
if (index.schemaVersion !== SCHEMA || index.generationDigest !== binding.generationDigest
|| index.receiptDigest !== binding.receiptDigest || !Array.isArray(index.entries) || index.entries.length > MAX_ENTRIES
|| !index.entries.every(validEntry)) throw invalid();
const entries = index.entries.map(entry => ({ ...entry, dense: decodeDense(entry.dense) }));
if (entries.some(entry => !entry.dense)) throw invalid();
return { ...index, entries };
}
function routingIndexStatus(stateRoot) {
const binding = readBinding(stateRoot);
const index = readRoutingIndex(stateRoot, binding);
return { status: index ? 'current' : 'missing', path: pointerPath(binding.root, binding.generationDigest),
generationDigest: binding.generationDigest, entries: index ? index.entries.length : 0 };
}
// eslint-disable-next-line no-control-regex
const CONTROL_CHARACTERS = new RegExp('[\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029]+', 'g');
const singleLine = text => String(text).replace(CONTROL_CHARACTERS, ' ').replace(/\s+/g, ' ').trim();
/** Rank index entries for a prompt. IDs and one-line descriptions only; like
* implicit admission, a suggestion needs a name or trigger anchor term. */
function suggestContext(index, prompt, { limit = MAX_SUGGESTIONS } = {}) {
// Filter before trimming so unanchored matches cannot crowd out anchored ones.
return searchRetrieval(buildRetrievalIndex(index.entries), prompt, { limit: index.entries.length })
.filter(candidate => SKILL_ID.test(candidate.id) && hasSuggestionEvidence(candidate)
&& (candidate.exact || candidate.anchorTerms.length > 0))
.slice(0, Math.min(limit, MAX_SUGGESTIONS))
.map(candidate => {
const description = singleLine(candidate.description);
return { id: candidate.id,
description: description.length > MAX_DESCRIPTION ? `${description.slice(0, MAX_DESCRIPTION - 1)}…` : description };
});
}
module.exports = { readBinding, readRoutingIndex, routingIndexStatus, suggestContext, writeRoutingIndex };