The L5 source graph's own node identities are never renumbered alongside the flat snapshot's closure markers (Codex review on PR #868, fresh evidence)¶
Index: Known gaps
Status (2026-10-31): FIXED for every snapshot written from now on. storage/closure_graph_renumber.py folds the graph's node/edge strings into the same strip-and-renumber pass (closure_identity._renumber_collected), on the dump path and on a stored graph's first decode. A graph-only closure is numbered after its (marker, header) group's flat ordinals, as a hybrid conflict-only marker is. Tested over generated graphs mixing flat-visible and graph-only, raw and stripped closures (tests/unit/storage/test_graph_closure_renumbering.py; oracle computed from the generated coordinates, lazy equal to eager). Residual: a baseline written before this fix with flat #N spellings and a :line:col graph cannot be reconciled; its graph closures are numbered as graph-only. The history below is kept for the record.
renumber_anonymous_closure_identities rewrites a
closure's :<line>:<col> discriminator to a stable #N ordinal across
AbiSnapshot.functions/variables/types/enums/typedefs/
constants/fact_provenance (_LAMBDA_IDENTITY_FIELDS), but
service_header_graph_attach._attach_header_graph's embedded
build_source.source_graph is built by a genuinely separate clang
parse (buildsource.header_graph), whose node ids
(graph_facts._decl_node_id/_type_node_id) are derived directly from
the raw, un-renumbered identity string -- confirmed by reading both
functions, which apply no renumbering at all. A closure-parameterized
declaration therefore reads as Foo<(lambda:file.h#1)> in the flat
snapshot but Foo<(lambda:file.h:20:5)> in its own source-graph node, so any
consumer trying to correlate the two (e.g. matching a flat finding back
to its graph neighborhood) sees two different spellings for the same
entity. Not fixed here: a correct fix needs the ordinal map
collect_anonymous_type_ordinals computes from the flat fields to also
be applied to every graph node id/name and every edge's src/dst
reference to it -- and the source graph can name a closure the flat
ABI-surface fields never mention at all (an internal-linkage helper
visible only in the L5 graph), which the flat-only ordinal map has no
entry for, so naively reusing it risks leaving some graph-only closures
unrenumbered while their flat-visible siblings are. A correct fix likely
needs the ordinal collection widened to scan the graph's own node/edge
strings too, verified against a case that actually mixes flat-visible
and graph-only closures in one header -- a real, cross-cutting change
to two independently-evolving modules, not a same-PR reactive patch.
Same gap, also reachable from the load path (Codex review, fresh
evidence). storage.snapshot_load_normalization.normalize_anonymous_
type_spellings_on_load (added to close a sibling bug: a raw pre-strip
on-disk baseline was left completely unrenumbered on load, see this
file's own git history) rewrites the identical flat
_LAMBDA_IDENTITY_FIELDS only, called after snapshot_from_dict has
already decoded a schema-v29+ document's AbiSnapshot.surface_graph --
so a loaded raw-marker baseline's attached graph keeps its own
un-stripped node/edge identities even once the flat fields are
normalized. Not fixed here, for the same reason as above: it needs the
same graph-aware widening this entry already calls for, not a second,
independent patch on the load side.
-
~~Neither
scan's dry-run report validates--abi3-floorapplicability before reporting success~~ Fixed (CLI cleanup phase two, PR 5 follow-up). Bothfrontends.cli.scan_dry_run.render_scan_dry_run(single-binary) andfrontends.cli.artifact_set_dry_run.render_artifact_set_dry_run(--artifact-set) now run a cheap, binary-only extension probe (python_ext.detect_python_extension_from_binary-- container-only ELF/ PE/Mach-O read, no DWARF/AST parse, the same "binary export table parse" already priced under the L0_binary dry-run row) and route a non-qualifying candidate throughDryRunResult.block()(exit 1), matching the real run'sEVIDENCE_CONTRACT_ERROR. The message text is shared withscan_engine._run_abi3_audit's real precondition failure viapython_ext.abi3_precondition_message()so the three callers cannot independently drift. Seetests/test_scan_dry_run_abi3.py. -
~~A pinned depth backed only by a query-declaring
--config(no--sources/--build-info) prices L3/L4/L5 at zero TUs/zero cost~~ Fixed (CLI cleanup phase two, PR 5 follow-up)._estimate_total_tusgained aquery_onlybranch: whenreq.build_configdeclares a realbuild.queryand no--sources/--build-info/compile DB is given, the L3 note is flagged[UNKNOWN: build.query declared, ...](the same "annotate honestly rather than fold a floor into the summed total" shape_UNSCOPED_TU_NOTE_SUFFIXalready uses for the sibling--build-targetundercount case) instead of the confident-looking0every other "nothing given" case reports._source_layer_estimatescarries the same marker onto the derivedL4_source_abi/L5_source_graphnotes too (Codex review, fresh evidence: an earlier revision of this fix only flagged L3's own row, so--depth source/--depth graphstill priced the derived layers as a confident zero), andestimate_artifact_set's 4th return value (unknown_layers) lets the--artifact-setaggregate renderer apply the same per-layer treatment rather than a single project-wide flag hardcoded toL3_build. Reaches both dry-run paths uniformly, since both callestimate_scan(). Seetests/test_scan_dry_run_abi3.py::test_estimate_total_tus_query_only_config_marks_count_unknownand itstest_estimate_scan_propagates_unknown_tu_count_to_l4_and_l5/test_estimate_artifact_set_reports_unknown_layers_per_layersiblings.