Skip to content

feat(schema): repository-artifact layer — artifacts, dependencies, config keys - #103

Open
rahlk wants to merge 22 commits into
feat/issue-100-linker-propagationfrom
feat/issue-101-artifacts
Open

feat(schema): repository-artifact layer — artifacts, dependencies, config keys#103
rahlk wants to merge 22 commits into
feat/issue-100-linker-propagationfrom
feat/issue-101-artifacts

Conversation

@rahlk

@rahlk rahlk commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Closes #101. Stacked on #102. Spec: docs/design/specs/artifacts-and-dependencies.md.

Recalibrated 2026-08-27: the first cut anchored on 51ee29e — an unmerged python branch (feat/configuration-files). The ratified contract is python PR #160 (implementing spec PR #158); the final commit rebuilds the layer to it. Review the branch tip, not the first artifact commit.

What (python PR #160 parity)

  • application.artifacts{} — every RULES-matched non-code file as a flat node with a language-neutral id (can://artifact/<app>/<path> — sibling analyzers over one repo emit the SAME id for the same file): format, roles[] (dependency-manifest / tool-config / container-image / service-topology / ci / env / packaging / legal / docs / script / unknown), sha256, size_bytes, verbatim unbounded source, extraction status. Shebang scripts captured; unmatched files are not artifacts.
  • application.dependencies[] — flat, evidence-tagged records from every package.json (workspaces included): kinds runtime|dev|optional|build plus the coined additive peer; declared_in = the manifest's artifact id; provides_imports (@types/x also provides x); prov chains declared+lockfile when the sibling JSON-family lock (package-lock / npm-shrinkwrap / bun.lock JSONC) pins locked_version. Locks never create records; yarn.lock/pnpm-lock.yaml are inventory-only.
  • application.unresolved_imports[] — the dependency-hygiene signal: every non-relative, non-builtin specifier root with no accounting declaration. A VALUE import needs the runtime package; import type is satisfiable by @types/x alone; only-@types-for-a-value-import → partially bound (prov: ["heuristic"]). --resolve-installed (opt-in, default off) probes node_modules metadata (prov: ["installed-metadata"]); default runs read only repo files, byte-identical.
  • Neo4j contract 2.2.0 — language-neutral :Artifact / :Package (purl ids, scoped pkg:npm/%40scope/name): the deliberate, now-sanctioned exception to TS-prefixing so sibling analyzers MERGE onto the same nodes (the conformance gate allowlists exactly these). HAS_ARTIFACT / DECLARES_DEPENDENCY (_k=kind) / LOCKS (coarse fan from every lock artifact, python's documented rule) + the analyzer's own claims TS_PROVIDES / TS_UNRESOLVED_IMPORT into minted module-level :TSExternal ghosts — dependencies join the call graph rather than sit beside it.
  • The wire strip stayed structural (structuredClone) — it also removed the measured vscode-L4 stringify OOM (see the ledger).

Gates

Rebuilt fixture (root+workspace manifests, both JSON locks, yarn.lock inventory-only, .env, tsconfig, Dockerfile, CI workflow, LICENSE, an undeclared VALUE import, an import type satisfied by @types) + 13-test suite: neutral id grammar, every kind token incl. peer, prov chains, scoped purls, ghost edges, --resolve-installed, level-invariance -a 1-a 4, two-run byte-identical determinism. Count-parity gates taught the neutral rows + minted ghosts. Full suite 149 green; schema.neo4j.json regenerated at 2.2.0.

Release constraints

  • python-sdk models are extra="forbid" — the SDK gains the three families before its analyzer pin moves (python's PR #160 carries the same obligation).
  • Cross-analyzer artifact-id joins require pinned --app-name agreement (spec §2 precondition).
  • config_keys deliberately absent — python's unit 4 owns config extraction; the earlier TS parser is parked in the spec.

rahlk added 21 commits August 26, 2026 20:50
BREAKING: the released --call-graph-provider and --tsc-only flags are
removed (commander now rejects them); the tsc resolver is the one base
call graph, per the defuse-linker design
(docs/design/specs/defuse-linker-call-graph.md). Gone with them: the
union/jelly providers and selector, the __jelly multi-call binary mode
and CANTS_SELF_JELLY re-exec, the @cs-au-dk/jelly dependency, its patch,
and the --external @babel/preset-typescript bundling workaround (bundle:
998 → 455 modules). mergeCallGraphs stays — the defuse linker overlays
the tsc base through it. python-sdk's tsc_only kwarg keeps passing
--tsc-only until its tracked follow-up lands (spec, release plan).

Full suite + typecheck green; binary builds.
…#98)

walkBody only visited the body's CHILDREN unless the body was itself a
callable boundary, so `u => u.describe()` never recorded u.describe()
as a call site — an L1 gap the Jelly leg papered over with its own
approximated edge. Visiting the body node itself closes it: the site
lands in the anon's call_sites, the tsc resolver types it precisely
(users: User[] ⇒ u: User), and the body{} call node appears at L1.
Additive wire change (a real call site that was missing).
…base (#98)

The local pass that backfills what the resolver missed
(docs/design/specs/defuse-linker-call-graph.md; python's Jedi+defuse
architecture). Per-callable and bounded-round only, sorted iteration —
deterministic by construction, no whole-program fixpoint:

  T1 local value chase (alias chains via bounded symbol hops)
  T2 decorator invocations (method/accessor/parameter owners; edge-only)
  T3 external-callback rule (function value passed to an external or
     unresolved callee ⇒ enclosing→lambda; edge-only — the documented
     divergence from python)
  T4 bounded interprocedural votes (param-invoking sites resolve to the
     functions passed by resolved-internal callers, two rounds; factory
     returns through a unique returned function)
  T5 CHA-by-name fallback (bounded fan, skip-not-truncate over the cap)

Edges carry prov ["defuse"] and overlay the tsc base via
mergeCallGraphs; body-node resolutions return out-of-band and are
applied by backfillCallees — never persisted into callee_signature
(cache provenance rule).

Jelly-recovery on the fixtures: 6/6 jelly-only edges (3 decorator,
1 callback via T3, 2 upgraded to typed tsc resolutions by the
concise-arrow fix); edge counts equal the old union exactly
(57 / 22 / 2). Full suite + typecheck green.
…ual 0 (#98)

The reference-validation harness (scripts/joern: cpg call dump + the
signature-mapping comparator) and the audited exception ledger
(docs/design/specs/defuse-linker-joern-ledger.md). Corpus: the three
repo fixtures + nestjs-realworld-example-app @ c1c2cc4. Every Joern
real pair is covered; exclusions are classified per family (their
lowering synthetics, parameters-as-callees stubs, super-to-interface
fabrications, decorator-attribution variants) — python's ledger
discipline. A/B paired runs byte-identical on all four apps.
`inline\`url(...)\`` never recorded a call site — walkBody, the callee
resolver, and the call-expression index all matched Call/New only, while
the L3 exception model already treated tagged templates as calls. Found
by the vscode Joern ledger (432-strong same-file residual family).
Additive wire change: real call nodes that were missing.
…lable (#98)

`createCSSRule(sel, style = getSharedStyleSheet())` executes the default
in the callee's activation, but walkBody only covered getBody() — the
call (and any nested arrow) was invisible. Found by the vscode ledger
(domStylesheets family). Additive wire change + regression test.
…ied (#98)

Corpus gate holds at residual 0 (74/74). vscode @ a3c9dc6 (8,735 files,
1.15M LOC): 54,703/54,947 single-candidate Joern real pairs covered
after four ledger-driven fix rounds; the 244 residual is classified
(escaped closure-locals — python #150's staged tier, vendored marked,
the static/instance signature-collision finding, accessor tails).
Scale table: cants L2 5m52s/24.4GB for 1.02M edges vs Joern parse
9m06s/30.3GB. Comparator gained fan-row and misresolution-variant
rules (their multi-candidate rows are enumeration, not resolution).
…ain (#98)

Two residual families from the vscode ledger:

- .js/.jsx/.mjs/.cjs sources were never DISCOVERED (SOURCE_EXTS was
  ts-only) — vscode's vendored marked.js was analyzed through its
  bodiless .d.ts, so every internal edge was missing. JS files are now
  first-class, with a compiled-sibling guard (a .js next to its same-
  prefix .ts is build output, skipped — the compiler's own allowJs
  duplicate rule).
- T4c: `this.field(...)` where the field's value arrives through the
  constructor (parameter property or this.f = param) resolves to the
  function values passed at the class's `new` sites — with ONE bounded
  parameter hop (register(key, cb) → new Migration(key, cb)) and
  module-scope resolved calls now feeding the vote rounds. The
  registered callbacks' own param-invoking sites then resolve in the
  existing T4 rounds: the full migrateOptions chain
  (apply → callback → apply.write) lands, no fixpoint anywhere.
#98)

The compiled-sibling guard registered marked.d.ts's prefix and skipped
marked.js — inverted for the hand-written-declarations pattern. Sibling
rules are now two-way: .js beside a REAL .ts source is build output
(skipped); .d.ts beside an analyzed .js is its declaration file
(skipped as a module; the checker still reads it from disk).
…#100)

- Class property initializers execute in the constructor: their call
  sites now attribute to the ctor (explicit or implicit), and an
  initializer ARROW materializes as a class-scoped positional anon —
  signature ↔ containment stays aligned, and the property-arrow
  known-gap closes. Instance-prop sites leave the module sweeps
  (vscode's Registry.as-in-field-initializer family).
- T4a property form: 'template.onChange(...)' where the receiver is a
  parameter votes over object-literal properties (incl. method
  shorthand) passed at that position.
- T4b chained return summaries: 'return makeInner()' follows one
  resolved-internal level, memoized and cycle-guarded.
- Joern comparator: methods' parameter tables ride the dump; a residual
  whose target leaf-name is a parameter of the Joern caller is their
  parameters-as-callees fabrication wearing a real name — proven by
  their own table, classified, never gated.

Corpus gate: 74/74, residual 0. vscode re-audit deferred until the L4
benchmark run frees the box.
…, config keys (#101)

Python-parity port of codeanalyzer-python 51ee29e:

- application.artifacts{}: every non-source file as a first-class node
  (can://…/@artifact/<path>, dotfiles keep their dot), classified by the
  shipped rules table (closed artifact_kind enum, catch-all other),
  content_hash + size always, text under the capture policy
  (--no-artifact-text / --artifact-text-max-bytes, 256 KiB default —
  parsing is independent of capture).
- Contained TSDependency children from every package.json (workspace
  members included): npm sections map to the shared scope vocabulary
  plus the coined additive 'peer' token; the JSON lockfile family
  (package-lock / npm-shrinkwrap / bun.lock JSONC) backfills
  resolved_version on the OWNING manifest's declared records only —
  lockfiles never create records, yarn/pnpm are inventory-only.
- Contained TSConfigKey children: .env-family flat keys (namespace env,
  placeholder refs) and JSON dotted keys.
- assignIds stamps all three families per run; level-free and identical
  at every -a (parity gates taught the three containment families).
- The wire strip is now STRUCTURAL (structuredClone + targeted deletes):
  artifact content_hash is wire payload where the module trio is
  internal — and the old stringify-roundtrip clone OOM'd at vscode-L4
  scale (measured on the L4 benchmark; this removes that ceiling).
- Neo4j contract 2.2.0: :TSArtifact/:TSDependency/:TSConfigKey +
  TS_HAS_ARTIFACT/TS_DECLARES_DEPENDENCY/TS_DEFINES_CONFIG, constraints
  derived, schema.neo4j.json regenerated; text stays off the graph.
- SDK note: python-sdk models are extra=forbid — they must gain these
  families before the SDK's analyzer pin moves (recorded in spec+issue).

Fixture artifacts-app + 15-test gate suite; full suite 151 green.
@rahlk
rahlk force-pushed the feat/issue-101-artifacts branch from 5ab3dfc to e954070 Compare August 27, 2026 11:00
@rahlk
rahlk force-pushed the feat/issue-100-linker-propagation branch from fbda332 to b520db3 Compare August 27, 2026 13:27
The first cut anchored on 51ee29e — an UNMERGED python branch. The
ratified contract (python PR #160, spec PR #158) differs materially;
this rebuilds the layer to it:

- Artifact ids are LANGUAGE-NEUTRAL (can://artifact/<app>/<path>): the
  first can:// segment is a namespace, so sibling analyzers over one
  repo emit the same id for the same file.
- Artifacts are flat nodes: format, roles[] (union across rule matches),
  sha256, size_bytes, verbatim UNBOUNDED source (python's decision),
  extraction status. Capture is rules-matched only (shipped glob table +
  shebang scripts); text-cap flags removed. config_keys dropped —
  python's unit 4 owns config extraction.
- Dependencies are FLAT evidence rows on the application (no node ids):
  kind runtime|dev|optional|build + the coined additive 'peer',
  declared_in (artifact id, re-stamped per run), provides_imports
  (@types/x also provides x, DT scope-mangling unmangled), prov
  declared/lockfile; JSON-lock family backfills locked_version on the
  sibling manifest's records.
- unresolved_imports: every non-relative non-builtin specifier root; a
  VALUE import needs the runtime package, import type is satisfiable by
  @types alone; --resolve-installed (opt-in) probes node_modules
  metadata (prov installed-metadata).
- Neo4j 2.2.0: NEUTRAL :Artifact/:Package (purl pkg:npm/..., scoped
  %40-encoded) — the sanctioned prefix-gate exception so siblings MERGE
  onto the same nodes — HAS_ARTIFACT / DECLARES_DEPENDENCY(_k=kind) /
  LOCKS (coarse fan from every lock artifact) + the analyzer's own
  TS_PROVIDES / TS_UNRESOLVED_IMPORT into minted module-level
  :TSExternal ghosts. Count-parity gates taught the neutral rows.

Fixture + 13-test suite rebuilt to the new contract; full suite 149
green; spec + SCHEMA_DECISIONS record the anchor correction.
@rahlk

rahlk commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Recalibrated to python PR #160 (the ratified contract) — the original anchor (51ee29e) turned out to be an unmerged branch. Delta and decisions recorded in the spec; branch tip is the reviewable state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant