Skip to content

Code Analysis Call Evidence

Tu Shaokun edited this page Nov 6, 2026 · 6 revisions

JS/TS lexical call evidence

This is the producer slice of the local Code Analysis experiment, prepared as Draft PR 1225 on top of #1220. It records callsites and conservative lexical targets in raw-facts.json. It does not add a call graph, question CLI, paired investigation or UI. The work remains outside Labs/dev until reviewed and merged.

Candidate: bfb7a5d56986db5222291630ad4f1159c77db317. Comparison base: 06dd7340c44fc2bebafbbb897b86f9f789d303a1. Both use TypeScript 5.9.3.

What the source checks establish

Fixed source scope Source question and observed result
Archify Labs bf0a29e7, full root The call in schema.mjs:23 points to the directly exported semanticErrors at semantics.mjs:3. The entry contains 14 captured CallExpressions.
EventEmitter3 a6505491, full root Registration calls at 228 and 241 point to addListener at 58. All 14 calls inside emit remain unresolved: 12 invoke stored listeners with .fn.call/.fn.apply, and 2 invoke a removal method. Captured coordinates do not identify which callback object executes. See the independent dispatch investigation.
Redux c9e06506, src only Compatibility control: all pre-existing import, file and function records remain unchanged.
dependency-cruiser e6517db0, full root Same preservation control. Ten test-fixture parse errors are unchanged; this full-root scope is broader than the earlier source-only resolver comparison.

Across these scopes, the parent and candidate preserve all existing facts except extraction identity and the new call evidence. The normal CLI and unpacked archive agree byte for byte. The counts below describe captured output, not independently checked binding accuracy across every call.

Scope Files Imports CallExpressions Resolved lexical targets Unresolved sites
Archify 607 3,447 112,145 27,593 84,552
EventEmitter3 16 87 1,085 6 1,079
Redux src 17 39 87 27 60
dependency-cruiser 1,157 1,972 12,876 1,406 11,470

Contract and cost

callModel: "js-ts-lexical-v1" identifies call collection independently of adapter revision 8. A local, unpublished question consumer previously inferred capability from revision numbers. An import-only revision 6 producer was consequently described as having available call coverage with zero records. That consumer has a separate local fix requiring an explicit model; the producer PR does not publish the question workflow.

The model collects CallExpressions from parse-valid, non-declaration files. It binds ordinary declarations, direct const function initializers and relative named imports to direct captured exports. Other dispatch remains explicit unknown. Constructors and tagged templates are excluded. A target is a lexical declaration hint, not proof of execution, reachability or complete impact.

Old empty arrays remain valid in the updated schema. Missing/unknown models do not establish capture capability. Older strict schemas reject the new marker. Consumers must also account for parse errors, excluded files and declaration files.

This evidence has a storage cost: the Archify raw-facts output grew from 15,476,327 to 42,493,249 bytes. No speedup or reader-benefit claim is made. The parser, snapshot host and resolver are reused; no dependency was added.

Eleven of 13 call tests fail on the exact parent; two negative controls already pass. All 24 focused tests and 246 module tests pass on the candidate. The archive was built twice with official Node 22.23.2 and has SHA256 ded08e6909ee9fcee3242396b54d182698edd10f0c2ccba385219b746043cc04. Remote CI is recorded on the Draft; local results do not establish merge readiness.

Reproduce the selected source checks

Fresh public-fetch reproduction passed with Node 22.23.2. Requires Git, unzip and Node 22. The script fetches fixed public commits and installs locked dependencies only in temporary checkouts. It checks the two source questions plus parent preservation and source/archive parity. It does not execute the analyzed applications.

#!/usr/bin/env bash
set -euo pipefail
w=$(mktemp -d)
printf '%s\n' "$w"
pin() {
  git init -q "$w/$1"
  git -c http.version=HTTP/1.1 -C "$w/$1" fetch -q --no-tags --depth=1 "https://github.com/$2.git" "$3"
  git -C "$w/$1" checkout -q --detach FETCH_HEAD
}
pin analyzer tt-a1i/archify bfb7a5d56986db5222291630ad4f1159c77db317
pin parent tt-a1i/archify 06dd7340c44fc2bebafbbb897b86f9f789d303a1
pin source tt-a1i/archify bf0a29e7664f52973d06a8e6a608df2db1ecbd39
pin eventemitter3 primus/eventemitter3 a650549176ded0d0dcc52e27b03ae5669d7dcd7c
unzip -q "$w/analyzer/archify.zip" -d "$w/unpacked"
for tool in analyzer parent unpacked; do
  (cd "$w/$tool/archify/modules/code-analysis" && npm ci --ignore-scripts --no-audit --no-fund)
  for project in source eventemitter3; do
    node "$w/$tool/archify/bin/archify.mjs" code-analysis check "$w/$project" \
      --language ts --fail-on never --out "$w/$tool-$project-out" --json > "$w/$tool-$project-receipt.json"
  done
done
node - "$w" <<'JS'
const assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path');
const w=process.argv[2];
const read=(tool,project)=>JSON.parse(fs.readFileSync(path.join(w,tool+'-'+project+'-out','raw-facts.json')));
const preserved=({calls,callModel,repository:{adapterRevision,snapshotId,...repository},...facts})=>({repository,...facts});
for(const project of ['source','eventemitter3']) {
  const current=read('analyzer',project),parent=read('parent',project),packaged=read('unpacked',project);
  assert.deepEqual(current,packaged);
  assert.deepEqual(preserved(current),preserved(parent));
  assert.equal(current.callModel,'js-ts-lexical-v1');
  assert.equal(current.repository.adapterRevision,8);
  if(project==='source') {
    const calls=current.calls.filter(c=>c.file==='archify/modules/code-analysis/extract/shared/schema.mjs');
    assert.equal(calls.length,14);
    const call=calls.find(c=>c.line===23&&c.target?.name==='semanticErrors');
    assert.equal(call.resolution,'imported-function');
    assert.deepEqual(call.target,{file:'archify/modules/code-analysis/extract/shared/semantics.mjs',name:'semanticErrors',line:3,column:1});
  } else {
    const registration=current.calls.filter(c=>c.file==='index.js'&&[228,241].includes(c.line));
    assert.equal(registration.length,2);
    assert.ok(registration.every(c=>c.resolution==='same-file-function'&&c.target.name==='addListener'&&c.target.line===58));
    const dispatch=current.calls.filter(c=>c.file==='index.js'&&c.line>=166&&c.line<=216);
    assert.equal(dispatch.length,14);assert.ok(dispatch.every(c=>c.resolution==='unresolved'));
    const lines=fs.readFileSync(path.join(w,'eventemitter3','index.js'),'utf8').split('\n');
    assert.equal(dispatch.filter(c=>/\.fn\.(call|apply)\(/.test(lines[c.line-1])).length,12);
  }
  console.log(project,{files:current.files.length,imports:current.imports.length,calls:current.calls.length,previousFactsUnchanged:true,archiveParity:true});
}
JS

Clone this wiki locally