From fa2c839833d7718484d661ad8bc65d446063fd2d Mon Sep 17 00:00:00 2001 From: Forge Date: Tue, 22 Sep 2026 17:07:05 +0000 Subject: [PATCH] test: observe audited semantic adapter in managed candidate path --- docs/design/semantic-dependency-shadow.md | 143 ++++++++++++++ docs/validation/dependency-shadow/adapter.cjs | 26 +++ .../validation/dependency-shadow/recorder.cjs | 170 +++++++++++++++++ docs/validation/dependency-shadow/run.cjs | 174 ++++++++++++++++++ 4 files changed, 513 insertions(+) create mode 100644 docs/design/semantic-dependency-shadow.md create mode 100644 docs/validation/dependency-shadow/adapter.cjs create mode 100644 docs/validation/dependency-shadow/recorder.cjs create mode 100644 docs/validation/dependency-shadow/run.cjs diff --git a/docs/design/semantic-dependency-shadow.md b/docs/design/semantic-dependency-shadow.md new file mode 100644 index 000000000..312c53aac --- /dev/null +++ b/docs/design/semantic-dependency-shadow.md @@ -0,0 +1,143 @@ +# Internal dependency shadow recorder + +This implements the bounded follow-on to the [contract review](semantic-dependency-contract.md) +as an **explicitly enabled validation host**, not a shipping engine option. The three +modules live under [docs/validation/dependency-shadow](../validation/dependency-shadow/). +No production source, schema, worker, public SDK or Drupal package is changed. + +## Observation point and admission + +The harness packs a small `shadow-binding` package whose entry points to the audited +host adapter. The real `ExtensionManager` installs/trusts its bytes; the normal loader +registers its pass and the existing candidate/synthesis pipeline runs it. The pass +receives the real `ResolutionContext`, reads the real candidate nodes and returns +ordinary validated edges through the real registry merge into SQLite. No pipeline or +context monkey patch is used. The validation package's absolute host-module path is +test-owned, local and nonportable; it is never a registry artifact or public opt-in. + +`recorder.enable(projectRoot, { expectedPackageDigest, owners })` explicitly creates +one process-local validation session. The session is not inferred from a plugin option +or manifest. The loader's actual resolved package digest must match the host's approval. +The host adapter/recorder/compiled engine digests are bound at module load and a later +disk change disables recording. This is an audited allowlist, **not arbitrary-plugin +containment**. Existing v1 code can access Node; a proxy cannot stop that. Without a +session, the adapter executes normally with no shadow callback. Other v1 hooks are +never double-invoked by the observer and normally make the generation ineligible. + +Two additional exact-digest fixture producers are admitted only in isolated tests: +a source-only node producer for stable-ID query-order changes, and a colliding edge +producer for ownership order. They always evaluate normally, once; their output is +never cached or shadow-executed. They are not a second reusable adapter or an API for +arbitrary upstream declarations. The legacy filesystem hook remains ineligible. + +The adapter is grounded in the generic binding pass in +[`__tests__/extension-sync.test.ts`](../../__tests__/extension-sync.test.ts): resolve +`dispatch` to the unique named Python function, with option override and a real failure +sentinel. `binding.txt` deliberately moves metadata outside the indexed source set; +`.events.yaml` registration membership supplies a fallback. Fixed optional probes +exercise an unsupported derived query and a caught missing operation. No option can +select an arbitrary function or arbitrary context operation. + +## Exact views, reads and invocation identity + +The audited evaluator has no filesystem, network, clock, environment or module reads. +Only the surrounding host observer performs bookkeeping. Its four captured operations +delegate to the actual context: `getAllFiles`, `fileExists`, `readFile`, `getNodesByName`. +Unknown operations are forwarded with sticky taint set **before** execution or an +exception, including when the audited callback catches that exception. The observer +does not reinterpret errors as an empty successful capture. + +Node results use a declared internal JSON projection: remove `updatedAt` and undefined +properties, preserve all remaining values and array order. This is a new internal view, +not transparent interception of arbitrary v1 callbacks. The evaluator cannot read the +omitted fields. Returned arrays/objects are detached copies. All empty, false and null +results are recorded, not just successful symbols. Each observation stores operation, +arguments, complete result SHA256, serialized value byte count and empty-result marker. +No previous returned member list substitutes for reexecuting a query predicate. + +Identity binds canonical project path, loaded adapter/observer/engine bytes, engine +version, view contract, post-resolution stage, options, full project config, ignore +rules, CODEGRAPH environment settings, all loaded package digests/options/replacements, +and the actual ordered synthesis registry (including builtins). The config includes +scan policies, enabled state and replacements. Read-set replay uses the new candidate's +context. A trace from another committed graph marker or identity is cold/ineligible. + +The first implementation is deliberately conservative and expensive: it replays complete +queries and hashes their results, reads identity files and inspects SQLite. No efficient +bucket index, missing-path watcher or general derived-query implementation is claimed. + +## Authoritative execution and commit discipline + +1. Always run the full audited callback on the real context. Its result is authoritative. + An actual callback error follows the existing loader/candidate failure path. +2. If explicitly admitted, replay the previous committed trace to predict `wouldReuse`. + Always run the separate capture execution of **only this audited callback**, and + compare its exact premerge output with the authoritative output. For a predicted hit, + also compare the previous owned output exactly with the newly evaluated result. +3. Return the original authoritative result regardless of observer error, mismatch, + budget or taint. Observer failure/mismatch disables the session's trace; unsupported + reads/legacy input discard that generation. No observed result controls the graph. +4. The validation owner wraps a real engine/manager operation in `session.operation`. + A pending trace is not retained as a valid generation unless the operation returns + successfully, the existing `codegraph.semantic.update` **committed** event occurred, + the actual SQLite `semantic_update` marker changed, observed metadata/config still + matches, and exact fixture-owner SQLite output matches the expected ordered merge. +5. Store the full owned output **before pair merge**, including an output shadowed by + another owner. The ownership test reverses registry order and removes the winner; + the previously shadowed contribution must appear. The private fixture oracle supplies + the known competing owner's output. This is not a general replacement for observing + all builtin/extension merge outputs in a production recorder. + +Failure, partial work, unknown commit path or missing observation drops the trace. The +existing transaction and recovery code is untouched; no new process-death mechanism +exists to validate. A fresh child process proves that observation state is absent while +the real graph remains correct. The validation owner closes the recorder with its graph. +Normal `CodeGraph.close()` has no new observer hook; lifecycle association is private +to this owner and would need an explicit internal hook before product integration. + +One trace is retained per explicitly registered project, bounded to 128 observations +and a 64 KiB serialized `{identity, trace, owned}` payload by default. Oversized captures +are declined without affecting the callback. Reporting history is capped at 256 rows +per session; it is diagnostic overhead, not an engine cache. The limits are serialized +payload bounds, not promises about V8 heap allocation. Closing removes the session, +trace and listener. No trace is written to the project or restored from disk. + +## Integrated boundaries discovered + +**Normal config-triggered sync can bypass candidate events.** It still runs the normal +engine and applies the changed options, but this observer cannot certify that path. +It discards observations. The options/scan identity cases therefore use the real explicit +candidate refresh, and a separate normal-sync case asserts this conservative refusal. +This is a finding about observation coverage, not a change to sync behavior. + +**Non-indexed metadata is not a certified filesystem snapshot.** The existing engine +source stamp does not cover arbitrary `binding.txt` reads. A deliberately timed edit +after candidate evaluation can leave the old observed binding committed. The observer +detects the changed direct-file observation and refuses to certify a trace; it cannot +retroactively undo or repair the authoritative graph. Explicit refresh converges. Even +auth/shadow equality can compare the same cached old file content, so equality alone is +not a freshness guarantee. Indexed-source mutation rejects the actual candidate; config +mutation during copy remains pending for the next sync and cannot certify the old trace. + +Only the main-thread resolver is instrumented in this validation. Worker dispatch lacks +the private session and must remain ineligible; no shared-process state or worker-channel +integration was added. Native worker and transaction evidence remains the unchanged +accepted evidence, not a new native shadow claim. + +## Decision + +**Pass for this bounded shadow experiment; no-go for production skipping yet.** It +predicts repeat/irrelevant-input reuse while preserving actual full evaluation and +detects the specified changes and failures. This cheap binding callback does not establish +an economic case: small-fixture observation overhead must be compared with the much +smaller callback time, not with the whole indexing operation. Predicted work avoided +is hypothetical; actual callback skips are zero. + +Before a selective-reuse implementation: provide real candidate generation and close/ +worker lifecycle hooks; close the non-indexed/negative-input observation and revalidation +gap; cover every eligible commit path; generalize premerge ownership comparison without +assuming the fixture schedule; measure a useful audited callback where replay costs less +than reevaluation. Retain legacy/global fallback and the current atomic graph transaction. +Do not roll this into public API-v1 admission, persistent cache, Drupal partitioning, +cyclic stage evaluation or graph storage deltas. Global extraction/resolution/candidate/ +database costs persist. See the [exact evidence report](../validation/extensions-dependency-shadow-20260922.md). diff --git a/docs/validation/dependency-shadow/adapter.cjs b/docs/validation/dependency-shadow/adapter.cjs new file mode 100644 index 000000000..35987e50a --- /dev/null +++ b/docs/validation/dependency-shadow/adapter.cjs @@ -0,0 +1,26 @@ +'use strict'; +// Audited validation adapter, grounded in extension-sync.test.ts's bindings pass. +// No ambient I/O inside evaluate(). This is NOT a public v1 purity declaration. +function evaluate(view, options) { + // Enumerate registrations even when absent; predicate membership is observable. + const registrations = view.getAllFiles().filter(f => f.endsWith('.events.yaml')); + let target = options.target; + if (!target) { + const exists = view.fileExists('binding.txt'); + target = exists ? (view.readFile('binding.txt') || '').trim() : ''; + if (!target && registrations.length === 1) target = (view.readFile(registrations[0]) || '').trim(); + } + // Fixed validation probes; arbitrary method names cannot be passed as options. + if (options.probe === 'aliases') view.getProjectAliases(); + if (options.probe === 'caught') { try { view.unknownReviewOperation(); } catch { /* test sticky taint */ } } + if (target === 'FAIL') throw new Error('audited binding failed'); + if (!target) return []; + const from = view.getNodesByName('dispatch').filter(n => n.kind === 'function' && n.language === 'python'); + const targets = view.getNodesByName(target).filter(n => n.kind === 'function' && n.language === 'python'); + if (from.length !== 1 || (!options.first && targets.length !== 1) || !targets.length) return []; + return [{ source: from[0].id, target: targets[0].id, kind: 'calls', line: 1, + metadata: { label: 'Audited binding', targetSignature: targets[0].signature || '' } }]; +} +exports.evaluate = evaluate; +exports.factory = ({ projectRoot, options }) => ({ synthPasses: [{ name: 'binding', languages: ['python'], + run: ctx => require('./recorder.cjs').invoke(projectRoot, ctx, options) }] }); diff --git a/docs/validation/dependency-shadow/recorder.cjs b/docs/validation/dependency-shadow/recorder.cjs new file mode 100644 index 000000000..870c7efbd --- /dev/null +++ b/docs/validation/dependency-shadow/recorder.cjs @@ -0,0 +1,170 @@ +'use strict'; +// Internal opt-in experiment. Never imported by production. No callback skips. +const fs = require('node:fs'), path = require('node:path'), crypto = require('node:crypto'); +const { channel } = require('node:diagnostics_channel'); +const { DatabaseSync } = require('node:sqlite'); +const { currentPlugins } = require('../../../dist/plugins/registry'); +const { getSynthPasses } = require('../../../dist/resolution/callback-synthesizer'); +const { getCodeGraphDir } = require('../../../dist/directory'); +const { evaluate } = require('./adapter.cjs'); +const { hash } = require('../dependency-review/model.cjs'); +const sessions = new Map(), events = channel('codegraph.semantic.update'); +const copy = value => structuredClone(value); +const clock = () => performance.now(); +const raw = file => { try { return fs.readFileSync(file, 'utf8'); } catch (e) { if (e.code === 'ENOENT') return null; throw e; } }; +const byteHash = file => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); +const loadedCode = { adapter: byteHash(path.join(__dirname, 'adapter.cjs')), observer: byteHash(__filename), + engineSource: byteHash(path.join(__dirname, '../../../dist/index.js')) }; +const normalize = edges => edges.map(e => ({ source: e.source, target: e.target, kind: e.kind, + line: e.line ?? null, metadata: e.metadata, provenance: e.provenance })); +const guardedOutput = edges => edges.map(e => ({ ...e, provenance: 'heuristic', metadata: { ...e.metadata, synthesizedBy: 'shadow-binding' } })); +const methods = new Set(['readFile', 'fileExists', 'getAllFiles', 'getNodesByName']); +function projected(value, method) { + // New internal view explicitly excludes wall-clock updatedAt. Every other + // observable Node field and query order is kept. No claim of transparent v1 capture. + return method === 'getNodesByName' ? JSON.parse(JSON.stringify(value.map(({ updatedAt, ...node }) => node))) : value; +} +function view(ctx, observer) { + return new Proxy({}, { get(_, method) { + return (...args) => { + // Unknown/caught operations taint before calling or throwing; authoritative + // semantics are forwarded unchanged. A proxy is not an I/O sandbox. + if (!methods.has(method)) observer?.taint('unsupported:' + String(method)); + const value = projected(ctx[method](...args), method); + observer?.read(method, args, value); + return copy(value); + }; + } }); +} +function dbState(root) { + const file = path.join(getCodeGraphDir(root), 'codegraph.db'); + if (!fs.existsSync(file)) return { marker: null, edges: [] }; + const db = new DatabaseSync(file, { readOnly: true }); + try { + const marker = db.prepare("SELECT value FROM project_metadata WHERE key='semantic_update'").get()?.value || null; + const edges = db.prepare('SELECT source,target,kind,line,metadata,provenance FROM edges').all() + .map(e => ({ ...e, metadata: e.metadata ? JSON.parse(e.metadata) : undefined })); + return { marker, edges }; + } finally { db.close(); } +} +function sourceIdentity(root, registry, options) { + if (loadedCode.adapter !== byteHash(path.join(__dirname, 'adapter.cjs')) || loadedCode.observer !== byteHash(__filename) || + loadedCode.engineSource !== byteHash(path.join(__dirname, '../../../dist/index.js'))) throw Error('loaded host code changed; restart required'); + return { project: fs.realpathSync(root), ...loadedCode, engine: require('../../../package.json').version, + queryContract: 'shadow-view-1-without-updatedAt', + stage: 'independent-post-resolution', options: copy(options), + config: raw(path.join(root, 'codegraph.json')), ignore: raw(path.join(root, '.gitignore')), + // Whole codegraph config includes scan policy, replacements and enabled order. + environment: Object.fromEntries(Object.entries(process.env).filter(([k]) => k.startsWith('CODEGRAPH_')).sort()), + registry: registry.resolved.map(p => [p.manifest.id, p.version, p.digest, p.options, p.replaces]), + mergeOrder: getSynthPasses().map(p => p.name) }; +} +function currentFileMatches(root, read) { + if (read.method === 'readFile') return hash(raw(path.join(root, read.args[0]))) === read.hash; + if (read.method === 'fileExists') return hash(fs.existsSync(path.join(root, read.args[0]))) === read.hash; + return true; +} +class Session { + constructor(root, options) { + this.root = fs.realpathSync(root); this.options = options; this.committed = null; this.active = null; + this.records = []; this.closed = false; this.disabled = false; + this.listener = e => { if (e.projectRoot === this.root && this.active) { + this.active.phases.push(e.phase); if (e.phase === 'committed') this.active.graphCommitted = true; + } }; + events.subscribe(this.listener); + } + async operation(name, fn, { fault } = {}) { + if (this.closed || this.active) throw Error('shadow session closed or operation active'); + const start = clock(); let previousMarker; + try { previousMarker = dbState(this.root).marker; } catch { this.disabled = true; this.committed = null; } + const op = this.active = { name, phases: [], pending: null, graphCommitted: false, fault, invocations: 0 }; + let error, result; + try { result = await fn(); } catch (e) { error = e; } + const finalizeStart = clock(); + // Never throw an observer failure into the successful engine operation. + const record = { name, success: !error, certified: false, phases: op.phases, ...op.pending?.metrics, + authoritativeInvocations: op.invocations, graphMarker: null, trace: op.pending?.trace || [], + owned: op.pending?.owned || [], reason: error ? 'operation-failed' : op.reason || 'not-observed' }; + try { + const state = dbState(this.root); record.graphMarker = state.marker; + if (!error && op.pending && !this.disabled && !op.reason && op.graphCommitted && state.marker !== previousMarker) { + const p = op.pending; + if (raw(path.join(this.root, 'codegraph.json')) !== p.identity.config || raw(path.join(this.root, '.gitignore')) !== p.identity.ignore || + !p.trace.every(r => currentFileMatches(this.root, r))) throw Error('observed source/config changed before certification'); + // Owned premerge output can be shadowed by another earlier pass. Compare + // the complete registered fixture-owner merge, never delete shadowed ownership. + const expected = this.options.expectedMerge ? this.options.expectedMerge(p.owned) : guardedOutput(p.owned); + const actual = state.edges.filter(e => this.options.owners.includes(e.metadata?.synthesizedBy)); + const sort = es => normalize(es).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))); + if (hash(sort(actual)) !== hash(sort(expected))) throw Error('committed SQLite/owned merge mismatch'); + this.committed = { identity: p.identity, trace: p.trace, owned: copy(p.owned), marker: state.marker, bytes: p.metrics.traceBytes }; + record.certified = true; record.reason = 'committed'; record.sqliteHash = hash(sort(actual)); + } else if (error || op.reason || !op.graphCommitted) this.committed = null; + } catch (e) { record.reason = 'observer:' + e.message; this.committed = null; this.disabled = true; } + record.finalizeMs = clock() - finalizeStart; + record.observerMs = (record.identityMs || 0) + (record.replayMs || 0) + (record.shadowMs || 0) + (record.compareMs || 0) + record.finalizeMs; + record.totalOperationMs = clock() - start; + this.records.push(record); if (this.records.length > 256) this.records.shift(); + this.active = null; + if (error) throw error; + return result; + } + close() { this.committed = null; this.active = null; this.closed = true; sessions.delete(this.root); events.unsubscribe(this.listener); } +} +function enable(root, options) { + const real = fs.realpathSync(root); + if (sessions.has(real) || !options.expectedPackageDigest || !options.owners?.includes('shadow-binding')) throw Error('explicit unique project/package admission required'); + const session = new Session(real, { maxReads: 128, maxBytes: 65536, ...options }); sessions.set(real, session); return session; +} +function invoke(root, ctx, options) { + // The full authoritative callback is unconditional. Observer code runs only + // afterward and never changes this value, including mismatches/over-budget. + const session = sessions.get(root), op = session?.active; + if (op) op.invocations++; + const authStart = clock(); const authoritative = evaluate(view(ctx), copy(options)); const authoritativeMs = clock() - authStart; + if (!session || !op || session.disabled) return authoritative; + try { + const identityStart = clock(), registry = currentPlugins(); + const own = registry?.resolved.find(p => p.manifest.id === 'shadow-binding'); + if (!own || own.digest !== session.options.expectedPackageDigest) { op.reason = 'not-admitted-package'; return authoritative; } + const identity = sourceIdentity(root, registry, options), identityMs = clock() - identityStart; + const legacy = registry.resolved.some(p => !session.options.auditedUpstreamDigests?.includes(p.digest) && p !== own); + let reason = legacy ? 'legacy-untracked-global-fallback' : null; + const old = session.committed, replayStart = clock(); + let wouldReuse = !reason && !!old && old.marker === dbState(root).marker && hash(old.identity) === hash(identity); + if (wouldReuse) for (const r of old.trace) { + if (hash(projected(ctx[r.method](...r.args), r.method)) !== r.hash) { wouldReuse = false; break; } + } + const replayMs = clock() - replayStart; + const trace = []; let traceBytes = 0, captureMs = 0; + const observer = { taint: why => { reason ||= why; }, read(method, args, value) { + const t = clock(); + if (op.fault === 'capture-error') throw Error('injected observer failure'); + if (methods.has(method) && !reason) { + const entry = { method, args: copy(args), hash: hash(value), valueBytes: Buffer.byteLength(JSON.stringify(value)), + empty: value === null || value === false || (Array.isArray(value) && value.length === 0) }; + traceBytes += Buffer.byteLength(JSON.stringify(entry)); + if (trace.length >= session.options.maxReads || traceBytes > session.options.maxBytes) reason = 'observation-budget'; + else trace.push(entry); + } + captureMs += clock() - t; + } }; + const shadowStart = clock(); const shadow = evaluate(view(ctx, observer), copy(options)); const shadowMs = clock() - shadowStart; + const compareStart = clock(); + if (op.fault === 'mismatch') shadow.push({ source: 'invalid', target: 'invalid', kind: 'calls' }); + if (hash(shadow) !== hash(authoritative) || (wouldReuse && hash(old.owned) !== hash(authoritative))) { + reason = 'prediction-or-shadow-mismatch'; session.disabled = true; + } + const compareMs = clock() - compareStart; + if (reason) { op.reason = reason; session.committed = null; } + op.pending = { identity, trace: reason ? [] : trace, owned: copy(authoritative), metrics: { + wouldReuse: wouldReuse && !reason, predictedCallbackMsAvoided: wouldReuse && !reason ? authoritativeMs : 0, + authoritativeMs, identityMs, captureMs, replayMs, compareMs, shadowMs, + traceBytes: reason ? 0 : Buffer.byteLength(JSON.stringify({ identity, trace, owned: authoritative })), + readCount: trace.length, negativeReads: trace.filter(r => r.empty).length, + actualCallbacksSkipped: 0, shadowExecutions: 1 } }; + if (op.pending.metrics.traceBytes > session.options.maxBytes) { op.reason = 'total-observation-budget'; op.pending.trace = []; session.committed = null; } + } catch (error) { session.committed = null; session.disabled = true; op.reason = 'observer:' + error.message; } + return authoritative; +} +module.exports = { enable, invoke, dbState, normalize, guardedOutput, hash }; diff --git a/docs/validation/dependency-shadow/run.cjs b/docs/validation/dependency-shadow/run.cjs new file mode 100644 index 000000000..1e10d4c8c --- /dev/null +++ b/docs/validation/dependency-shadow/run.cjs @@ -0,0 +1,174 @@ +'use strict'; +const fs = require('node:fs'), path = require('node:path'), os = require('node:os'); +const assert = require('node:assert/strict'), cp = require('node:child_process'); +const { channel } = require('node:diagnostics_channel'); +Object.assign(process.env, { CODEGRAPH_TELEMETRY: '0', CODEGRAPH_PARSE_WORKERS: '0', CODEGRAPH_RESOLVE_WORKERS: '0' }); +const { CodeGraph, packExtension } = require('../../../dist'); +const { ExtensionManager } = require('../../../dist/plugins/manager'); +const { packageDigest } = require('../../../dist/plugins/package'); +const { enable, dbState, hash, normalize, guardedOutput } = require('./recorder.cjs'); +const adapter = path.join(__dirname, 'adapter.cjs'); +const projectName = 'shadow-binding'; +const handlers = 'def dispatch():\n pass\ndef receipt():\n pass\ndef cancel():\n pass\n'; +const checks = [], commands = []; +let lab, out; +function write(root, file, value) { const p = path.join(root, file); if (value === null) { if (fs.existsSync(p)) fs.unlinkSync(p); } + else { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, value); } } +function config(root, mutate) { const p = path.join(root, 'codegraph.json'), c = JSON.parse(fs.readFileSync(p)); mutate(c); fs.writeFileSync(p, JSON.stringify(c, null, 2)); } +function nodeState(root) { + const { DatabaseSync } = require('node:sqlite'); const db = new DatabaseSync(path.join(root, '.codegraph/codegraph.db'), { readOnly: true }); + try { return db.prepare('SELECT id,name,file_path,start_line,signature FROM nodes ORDER BY id').all(); } finally { db.close(); } +} +function graphHash(root) { const s = dbState(root); return hash(normalize(s.edges).sort((a,b)=>JSON.stringify(a).localeCompare(JSON.stringify(b)))); } +function bindingEdges(root) { return dbState(root).edges.filter(e => ['shadow-binding', 'overlap'].includes(e.metadata?.synthesizedBy)); } +function targetName(root) { const ns = nodeState(root); return bindingEdges(root).map(e => ns.find(n => n.id === e.target)?.name); } +function makePackage(id, source, capabilities = ['synthPasses'], version = '0.1.0') { + const dir = path.join(lab, `author-${id}-${version}`); fs.mkdirSync(dir); + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: id, version, main: 'index.cjs', codegraph: { id, apiVersion: 1, capabilities } }, null, 2)); + fs.writeFileSync(path.join(dir, 'index.cjs'), source); + return { bytes: packExtension(dir), digest: packageDigest(dir), dir }; +} +function save() { if (out) fs.writeFileSync(path.join(out, 'progress.json'), JSON.stringify({ checks, commands }, null, 2)); } +function pass(name, record, extra = {}) { checks.push({ name, scope: 'real managed candidate/synthesis/SQLite', record, ...extra }); console.log('PASS', name); save(); } +async function main() { + out = path.resolve(process.argv[2] || '.qa/dependency-shadow'); fs.mkdirSync(out, { recursive: true }); + for (const name of ['adapter.cjs','recorder.cjs','run.cjs']) fs.copyFileSync(path.join(__dirname,name),path.join(out,'source-'+name)); + lab = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-shadow-')); + const pkg = makePackage(projectName, `module.exports=require(${JSON.stringify(adapter)}).factory;`); + const root = path.join(lab,'project');fs.mkdirSync(root);write(root,'handlers.py',handlers);write(root,'unrelated.py','def unrelated():\n pass\n'); + const manager = new ExtensionManager(root); let graph, s; + const settings = { expectedPackageDigest: pkg.digest, owners: ['shadow-binding'] }; + function start(overrides = {}) { s?.close(); s = enable(root, { ...settings, ...overrides }); return s; } + async function run(name, fn, expected, prediction, extras = {}) { + await s.operation(name, fn); const r = s.records.at(-1); + assert.deepEqual(targetName(root), expected, name); + if (prediction !== undefined) assert.equal(r.wouldReuse, prediction, name); + if (!extras.uncertified) assert.equal(r.certified,true, JSON.stringify(r)); + assert.equal(r.actualCallbacksSkipped ?? 0, 0); pass(name,r,extras); return r; + } + const refresh = () => graph.refreshPluginIndex(); + try { + // Ordinary installation before opt-in produces a real graph but no recorder. + await manager.install({bytes:pkg.bytes});graph=await CodeGraph.open(root); + assert.deepEqual(targetName(root),[]);pass('disabled by default: ordinary full install works',null); + start(); + const cold=await run('explicitly enabled cold trace with missing metadata',refresh,[],false); + assert.ok(cold.negativeReads>=1);assert.ok(cold.trace.some(r=>r.method==='fileExists'&&r.empty)); + await run('unchanged full evaluation predicts reuse',refresh,[],true); + write(root,'binding.txt','missing');await run('non-indexed metadata appears with missing symbol',refresh,[],false); + assert.ok(s.records.at(-1).trace.some(r=>r.method==='getNodesByName'&&r.empty)); + write(root,'missing.py','def missing():\n pass\n');await run('missing symbol appears in candidate query membership',()=>graph.sync(),['missing'],false); + write(root,'duplicate.py','def missing():\n pass\n');await run('duplicate matching symbol removes ambiguous edge',()=>graph.sync(),[],false); + write(root,'duplicate.py',null);await run('duplicate deletion restores unique target',()=>graph.sync(),['missing'],false); + fs.renameSync(path.join(root,'missing.py'),path.join(root,'renamed.py'));await run('target rename replaces endpoint identity',()=>graph.sync(),['missing'],false); + write(root,'renamed.py',null);await run('target deletion removes owned edge',()=>graph.sync(),[],false); + write(root,'binding.txt','receipt');await run('metadata retarget selects unchanged receipt endpoint',refresh,['receipt'],false); + write(root,'binding.txt','cancel');await run('metadata retarget selects unchanged cancel endpoint',refresh,['cancel'],false); + const beforeNodes=nodeState(root);write(root,'handlers.py',handlers.replace('cancel():','cancel(value=1):')); + await run('same-ID node signature field invalidates captured query',()=>graph.sync(),['cancel'],false); + const a=beforeNodes.find(n=>n.name==='cancel'),b=nodeState(root).find(n=>n.name==='cancel');assert.equal(a.id,b.id);assert.notEqual(a.signature,b.signature); + write(root,'unrelated.py','def unrelated():\n return 7\n');await run('irrelevant existing-file edit predicts reuse but still evaluates',()=>graph.sync(),['cancel'],true); + write(root,'binding.txt',null);await run('metadata deletion removes binding',refresh,[],false); + write(root,'registration.events.yaml','receipt');await run('registration enumeration membership enables fallback binding',()=>graph.sync(),['receipt'],false); + write(root,'registration.events.yaml',null);await run('registration deletion clears output',()=>graph.sync(),[],false); + write(root,'binding.txt','receipt');await run('restore metadata and rebuild trace',refresh,['receipt'],false); + config(root,c=>c.plugins[0].options={target:'cancel'});await run('loaded options identity invalidates',refresh,['cancel'],false); + config(root,c=>c.exclude=['unrelated.py']);await run('scan policy identity invalidates',refresh,['cancel'],false); + write(root,'.gitignore','unrelated.py\n');await run('ignore policy identity invalidates',refresh,['cancel'],false); + // Independent project with identical fixture package/options never shares traces. + const other=path.join(lab,'other');fs.mkdirSync(other);write(other,'handlers.py',handlers);write(other,'binding.txt','receipt'); + const osession=enable(other,settings);try{await osession.operation('project identity',()=>new ExtensionManager(other).install({bytes:pkg.bytes})); + assert.equal(osession.records.at(-1).wouldReuse,false);assert.equal(osession.records.at(-1).certified,true);pass('project identity isolates independent managed project',osession.records.at(-1)); + }finally{osession.close();} + const pkg2=makePackage(projectName,`// changed reviewed package bytes\nmodule.exports=require(${JSON.stringify(adapter)}).factory;`,['synthPasses'],'0.1.1'); + settings.expectedPackageDigest=pkg2.digest;s.options.expectedPackageDigest=pkg2.digest; + await run('adapter package content/version identity invalidates',()=>manager.install({bytes:pkg2.bytes}),['cancel'],false); + // A forged v1 declaration has no admission effect. Exact content is required. + const admitted=s.options.expectedPackageDigest;s.options.expectedPackageDigest='forged-purity-flag'; + await run('wrong package admission cannot opt into recording',refresh,['cancel'],undefined,{uncertified:true});assert.equal(s.records.at(-1).reason,'not-admitted-package'); + s.options.expectedPackageDigest=admitted; + // Real legacy hook reads another file without using ResolutionContext. + const legacy=makePackage('legacy-hook',`module.exports=({projectRoot})=>({frameworks:[{name:'legacy',languages:['python'],detect:()=>true,resolve:()=>null,extract(file){if(file!=='handlers.py')return {nodes:[],references:[]};const name=require('fs').readFileSync(require('path').join(projectRoot,'binding.txt'),'utf8');return {nodes:[{id:'plugin:legacy-hook:route',name,qualifiedName:'legacy:route',kind:'route',language:'python',filePath:file,startLine:1,endLine:1,startColumn:0,endColumn:0}],references:[]}}}],synthPasses:[{name:'count',run(){require('fs').appendFileSync(require('path').join(projectRoot,'legacy-calls.txt'),'1');return []}}]});`,['frameworks','synthPasses']); + await run('legacy fs hook is ineligible and still evaluated once',()=>manager.install({bytes:legacy.bytes}),['cancel'],false,{uncertified:true}); + assert.equal(s.records.at(-1).reason,'legacy-untracked-global-fallback');const count=fs.readFileSync(path.join(root,'legacy-calls.txt'),'utf8').length; + write(root,'binding.txt','cancel');await run('legacy cross-file edit changes actual node without capture admission',refresh,['cancel'],false,{uncertified:true}); + assert.equal(fs.readFileSync(path.join(root,'legacy-calls.txt'),'utf8').length,count+1);assert.ok(nodeState(root).some(n=>n.id==='plugin:legacy-hook:route'&&n.name==='cancel')); + await run('legacy removal returns to cold eligible trace',()=>manager.remove('legacy-hook'),['cancel'],false); + // Unsupported operation is performed only by the fixed audited probe branch. + for(const probe of ['aliases','caught']){ + config(root,c=>c.plugins.find(p=>p.name==='managed:shadow-binding').options={target:'receipt',probe}); + await run('unsupported '+probe+' read taints even when caught',refresh,['receipt'],false,{uncertified:true});assert.match(s.records.at(-1).reason,/unsupported/); + } + config(root,c=>c.plugins[0].options={});write(root,'binding.txt','receipt');start();await run('cold reset after unsupported traces',refresh,['receipt'],false); + // Failure originates in actual callback and existing loader/candidate guard. + write(root,'binding.txt','FAIL');const working=graphHash(root); + await assert.rejects(()=>s.operation('real callback failure',refresh),/audited binding failed/);assert.equal(graphHash(root),working);assert.equal(s.committed,null);pass('real callback failure preserves SQLite and certifies no partial trace',s.records.at(-1)); + write(root,'binding.txt','receipt');await run('actual failed callback retries cold',refresh,['receipt'],false); + for(const fault of ['capture-error','mismatch']){ + await s.operation('observer '+fault,refresh,{fault});assert.deepEqual(targetName(root),['receipt']);assert.equal(s.committed,null);assert.equal(s.disabled,true); + pass('observer '+fault+' disables trace and preserves authoritative SQLite',s.records.at(-1));start();await run('reset after '+fault,refresh,['receipt'],false); + } + s.committed.owned[0].metadata.label='injected stale prediction';await s.operation('prediction mismatch',refresh);assert.equal(s.disabled,true);assert.deepEqual(targetName(root),['receipt']);assert.equal(s.records.at(-1).reason,'prediction-or-shadow-mismatch');pass('incorrect predicted output disables trace, never replaces graph',s.records.at(-1));start(); + await run('reset before race tests',refresh,['receipt'],false); + let listener=e=>{if(e.projectRoot===root&&e.phase==='candidate_ready')write(root,'handlers.py',handlers+'# changed during candidate\n');}; + channel('codegraph.semantic.update').subscribe(listener);const beforeRace=graphHash(root); + try{await assert.rejects(()=>s.operation('indexed source race',refresh),/changed during semantic indexing/);}finally{channel('codegraph.semantic.update').unsubscribe(listener);} + assert.equal(graphHash(root),beforeRace);assert.equal(s.committed,null);pass('indexed source mutation rejects actual candidate and trace',s.records.at(-1)); + await run('indexed source race retries cold',refresh,['receipt'],false); + listener=e=>{if(e.projectRoot===root&&e.phase==='candidate_ready')write(root,'binding.txt','cancel');}; + channel('codegraph.semantic.update').subscribe(listener); + try{await s.operation('non-indexed source race',refresh);}finally{channel('codegraph.semantic.update').unsubscribe(listener);} + // The existing engine does NOT stamp arbitrary non-indexed metadata. The + // graph reflects its observed old value, but recorder refuses certification. + assert.deepEqual(targetName(root),['receipt']);assert.equal(s.committed,null);assert.match(s.records.at(-1).reason,/source\/config changed/); + pass('non-indexed race exposes existing snapshot limit and disables trace',s.records.at(-1),{limitation:'Engine commits old observed metadata; explicit refresh required'});start(); + await run('explicit refresh converges after metadata race',refresh,['cancel'],false); + listener=e=>{if(e.projectRoot===root&&e.phase==='copy_started')config(root,c=>c.plugins[0].options={target:'receipt'});}; + channel('codegraph.semantic.update').subscribe(listener); + try{await s.operation('config race',refresh);}finally{channel('codegraph.semantic.update').unsubscribe(listener);} + assert.deepEqual(targetName(root),['cancel']);assert.equal(s.committed,null);assert.equal(s.disabled,true);pass('configuration changed during commit cannot certify old trace',s.records.at(-1));start(); + await run('next normal config sync applies options but has no candidate certificate',()=>graph.sync(),['receipt'],false,{uncertified:true}); + assert.equal(s.records.at(-1).certified,false);assert.equal(s.committed,null); + + // Source-only framework fixture supplies fixed node IDs so query order can + // change independently of identity. It is a pinned test producer, never cached. + const provider=makePackage('target-provider',`module.exports=()=>({frameworks:[{name:'targets',languages:['yaml'],detect:()=>true,resolve:()=>null,extract(file,source){if(file!=='targets.yaml')return {nodes:[],references:[]};return {nodes:JSON.parse(source).map(n=>({id:'plugin:target-provider:'+n.id,name:'ordered',qualifiedName:'fixture:'+n.id,kind:'function',language:'python',filePath:file,startLine:n.line,endLine:n.line,startColumn:0,endColumn:1,signature:n.sig})),references:[]}}}]});`,['frameworks']); + settings.auditedUpstreamDigests=[provider.digest];start(); + write(root,'targets.yaml',JSON.stringify([{id:'a',line:1,sig:'a'},{id:'b',line:2,sig:'b'}])); + await s.operation('provider install',()=>manager.install({bytes:provider.bytes})); + config(root,c=>c.plugins.find(p=>p.name==='managed:shadow-binding').options={target:'ordered',first:true}); + await run('ordered membership baseline with pinned upstream producer',refresh,['ordered'],false);assert.equal(bindingEdges(root)[0].target,'plugin:target-provider:a'); + const ids=nodeState(root).filter(n=>n.name==='ordered').map(n=>n.id); + write(root,'targets.yaml',JSON.stringify([{id:'a',line:2,sig:'a'},{id:'b',line:1,sig:'b'}])); + await run('same-ID query order change selects correct first result',()=>graph.sync(),['ordered'],false); + assert.deepEqual(nodeState(root).filter(n=>n.name==='ordered').map(n=>n.id),ids);assert.equal(bindingEdges(root)[0].target,'plugin:target-provider:b'); + await s.operation('provider remove',()=>manager.remove('target-provider'));settings.auditedUpstreamDigests=[]; + config(root,c=>c.plugins[0].options={target:'receipt'});start();await run('restore ordinary audited binding',refresh,['receipt'],false); + const overlap=makePackage('overlap',`module.exports=()=>({synthPasses:[{name:'overlap',run(ctx){const a=ctx.getNodesByName('dispatch'),b=ctx.getNodesByName('receipt');return a.length===1&&b.length===1?[{source:a[0].id,target:b[0].id,kind:'calls',line:1,metadata:{label:'Overlap'}}]:[]}}]});`); + settings.owners=['shadow-binding','overlap'];settings.auditedUpstreamDigests=[overlap.digest]; + settings.expectedMerge=owned=>{const ordered=JSON.parse(fs.readFileSync(path.join(root,'codegraph.json'))).plugins.filter(p=>p.enabled!==false);const first=ordered[0].name; + return first==='managed:overlap'?owned.map(e=>({...e,provenance:'heuristic',metadata:{label:'Overlap',synthesizedBy:'overlap'}})):guardedOutput(owned);};start(); + await run('overlap keeps full premerge owned outputs',()=>manager.install({bytes:overlap.bytes}),['receipt'],false);assert.equal(s.committed.owned.length,1);assert.equal(bindingEdges(root)[0].metadata.synthesizedBy,'shadow-binding'); + config(root,c=>c.plugins.reverse());await run('ordered registry identity changes collision winner',refresh,['receipt'],false);assert.equal(bindingEdges(root)[0].metadata.synthesizedBy,'overlap');assert.equal(s.committed.owned.length,1); + await run('removing winning owner reveals shadowed output',()=>manager.remove('overlap'),['receipt'],false);assert.equal(bindingEdges(root)[0].metadata.synthesizedBy,'shadow-binding'); + settings.owners=['shadow-binding'];settings.auditedUpstreamDigests=[];delete settings.expectedMerge; + config(root,c=>delete c.exclude);write(root,'.gitignore',null); // keep repeat edits in the indexed set + start({maxBytes:64});await s.operation('bounded recorder',refresh);assert.deepEqual(targetName(root),['receipt']);assert.equal(s.committed,null);assert.match(s.records.at(-1).reason,/budget/);pass('bounded observations decline caching without changing graph',s.records.at(-1)); + start();await run('fresh session starts cold',refresh,['receipt'],false); + // Store exact observation breakdown, not an estimate of engine speedup. + for(let i=0;i<5;i++)await run('unchanged overhead sample '+(i+1),refresh,['receipt'],true,{sample:'unchanged'}); + for(let i=0;i<5;i++){write(root,'unrelated.py','def unrelated():\n return '+i+'\n');await run('irrelevant content overhead sample '+(i+1),refresh,['receipt'],true,{sample:'irrelevant-content-explicit-refresh'});} + const stable=graphHash(root);s.close();graph.close();graph=null; + const child=cp.spawnSync(process.execPath,[__filename,'--cold-child',root,settings.expectedPackageDigest],{encoding:'utf8',timeout:60000}); + fs.writeFileSync(path.join(out,'cold-child.log'),child.stdout+child.stderr);commands.push({command:[process.execPath,__filename,'--cold-child',root,settings.expectedPackageDigest],exit:child.status,signal:child.signal});assert.equal(child.status,0,child.stderr); + const last=JSON.parse(child.stdout.trim().split('\n').at(-1));assert.equal(last.wouldReuse,false);assert.equal(last.certified,true);assert.equal(graphHash(root),stable);pass('fresh OS process has no prior trace and preserves full graph',last); + // Cold full reference with recorder absent has exactly the same graph. + graph=await CodeGraph.open(root);await graph.refreshPluginIndex();assert.equal(graphHash(root),stable);pass('recorder-disabled full reference equals final SQLite graph',null); + fs.writeFileSync(path.join(out,'result.json'),JSON.stringify({success:true,revision:cp.execFileSync('git',['rev-parse','HEAD'],{encoding:'utf8'}).trim(),platform:process.platform,node:process.version, + checks,commands,productionChanges:false,callbackSkipping:false,workerCoverage:'main-thread resolver; production worker code unchanged',limits:['No arbitrary-plugin containment','Non-indexed metadata races are detected for trace but existing engine requires explicit refresh','Process-local validation owner must close the recorder with graph','No full suite/native rerun: no production source change']},null,2)); + }finally{s?.close();graph?.close();fs.rmSync(lab,{recursive:true,force:true});} +} +if(process.argv[2]==='--cold-child'){ + (async()=>{const root=process.argv[3],s=enable(root,{expectedPackageDigest:process.argv[4],owners:['shadow-binding']});const g=await CodeGraph.open(root); + try{await s.operation('cold process',()=>g.refreshPluginIndex());console.log(JSON.stringify(s.records.at(-1)));}finally{s.close();g.close();}})().catch(e=>{console.error(e.stack);process.exitCode=1}); +}else main().catch(e=>{save();fs.writeFileSync(path.join(out,'failure.json'),JSON.stringify({checks,commands,error:String(e.stack)},null,2));console.error(e.stack);process.exitCode=1;});