Architecture deepening plan¶
Status: living document. Candidate C1 is implemented (PR #395); the rest are proposals with concrete, sequenced plans. This document is the source of truth for the "deepen the architecture" effort and is updated as candidates land.
Why this document exists¶
abicheck is a healthy, mature codebase (≈158 modules, ≈77k LoC, 34 ADRs) with
several genuinely deep modules — a lot of behaviour sits behind small, stable
interfaces:
- the self-registering
@registry.detectorpattern (detector_registry.py), - the single-declaration
change_registry.py(one entry perChangeKind, with verdict + impact + addition flag colocated), - the import-time
ChangeKindpartition assertion inchecker_policy.py, service.render_output()as a clean format dispatcher.
The work below extends that same philosophy to the places that did not get it. The organising idea is module depth: increase the amount of behaviour behind an interface while shrinking the surface callers must understand. The test applied to every candidate is the deletion test — if the module or abstraction were removed, would its complexity concentrate (a sign the module was deep and worth having) or scatter across its callers (a sign the design was shallow and the knowledge was effectively inlined everywhere)?
Vocabulary¶
| Term | Meaning |
|---|---|
| Module | a unit with an interface and an implementation |
| Depth | leverage at the interface: lots of behaviour behind a small surface = deep |
| Seam | where an interface lives; a place behaviour can change without editing call sites in place |
| Adapter | a concrete implementation satisfying an interface at a seam |
| Locality | depth's payoff for maintainers: knowledge concentrated in one place |
Guard-rails for every candidate¶
Every change in this plan must keep the existing gates green and is reviewed against them:
ruff check abicheck/ tests/andruff format --checkmypy abicheck/— baseline 0 errors, must stay 0- the fast unit lane (
pytest -m "not integration and not libabigail and not abicc and not slow and not golden") python scripts/check_ai_readiness.py— 0 errors (warnings allowed)- where output or parity can change, the relevant
golden/abicc/libabigail/integrationmarkers are run as well
A refactor that changes visible behaviour (output text, verdict, exit code, ABICC parity) is called out explicitly below and is not treated as a pure refactor — it lands behind a golden-output review and, where it encodes a decision, an ADR.
Candidate catalogue¶
Each candidate states: the problem (with evidence), the goal (what depth we gain), the approach (concrete steps), the edge cases / risks found while grilling the design, and its place in the sequence.
C1 — Consolidate mangled-name classification (implemented — PR #395)¶
Problem. The Itanium-ABI prefix knowledge that answers "is this symbol an
RTTI artifact / a function-local RTTI / in an internal namespace?" was
re-encoded as private tuples in report_summary.py, diff_platform.py and
diff_symbols.py, and the copies had drifted (the reporting copy carried
the thunk prefixes _ZTc/_ZTh/_ZTv; the diff_platform local copy was the bare
_ZTV/_ZTI/_ZTS triple). There was no module to delete — the knowledge was
pre-scattered, which is itself the signature of a missing deep module.
Goal. One home for the prefix tables and the symbol_origin classifier, so
a new compiler convention is added once instead of hunted across the tree.
Approach (done). New abicheck/name_classification.py owns the tables and
classifiers. Crucially, semantically distinct concepts are kept as distinct,
clearly-named constants rather than merged — they are not interchangeable:
| Constant | Meaning |
|---|---|
ITANIUM_RTTI_PREFIXES |
generic RTTI artifacts (vtable/VTT/typeinfo obj+name/thunks) — origin classification |
RTTI_DATA_PREFIXES |
the size-owning data objects _ZTV/_ZTI/_ZTS only |
LOCAL_RTTI_PREFIXES |
RTTI for function-local (unnameable) types |
INTERNAL_NAMESPACE_COMPONENTS |
length-prefixed internal-namespace components |
Behaviour is unchanged (tuples moved verbatim).
report_summary.classify_symbol_origin is preserved as a re-export.
Deliberately out of scope (follow-up). The stdlib-/runtime-specific RTTI
skip sets in elf_symbol_filter.py, diff_elf_layout.py and elf_metadata.py
were left in place — their memberships genuinely differ and feed startswith
filters whose results would change if merged. Unifying them safely needs
per-call behaviour-equivalence checks. Tracked here as a sub-task of C10.
C2 — A report view-model behind one seam¶
Problem. There is no Reporter interface. Each of five output formats
re-walks DiffResult.changes independently: the line
changes = apply_show_only(list(result.changes), show_only, policy=…) is
copy-pasted verbatim into reporter.to_json, sarif.to_sarif,
junit.to_junit_xml and html_report.generate_html_report. Worse, each format
re-derives its own classification of the same changes — reporter.py via
checker_policy kind-sets, html_report.py via report_classifications.severity,
pr_comment.py via a private _SEVERITY_BUCKET dict, report_summary.py via a
third origin classifier. Three-plus competing notions of "how severe / what
category is this change" that can disagree across formats.
Goal. Build the classified, filtered, summarised view once; renderers
become thin, policy-free functions ReportModel → str/bytes. A new output
format becomes a ~30-line renderer instead of a re-implementation of the
pipeline tail. SARIF, HTML and the PR comment can no longer disagree about a
change's severity.
Approach.
1. Add report_model.py with ReportModel.from_diff_result(result, show_only)
— applies show_only once, classifies via C1's classifier + the policy
kind-sets, buckets, and summarises. Provide to_dict() / from_dict() so it
is serialisable.
2. Port reporter.to_json first (the canonical format) and snapshot its current
output as a golden baseline.
3. Migrate sarif, junit, html, pr_comment one at a time, diffing golden
output at each step.
4. Delete the per-renderer apply_show_only copies and the duplicate
classifiers.
Edge cases / risks.
- pr_comment.py consumes a JSON dict, not a DiffResult — hence the
serialisable model: it becomes the single feed for both in-process and
JSON-driven renderers.
- Some renderers read result.changes for unfiltered counts; audit each
before deleting its filter copy.
- Behaviour change: wherever renderers currently disagree on severity,
unifying changes that format's output. This is not a pure refactor — it needs
a golden-output review and a short ADR recording the canonical classification.
- Pairs naturally with C1 (origins/severity route through the deep classifier).
Risk: medium-high (user-visible output). Gate with -m golden.
C3 — Binary-format handler registry¶
Problem. dumper.dump() dispatches binary format via a hard-coded
if fmt=="macho" / elif "pe" / elif "elf" chain; magic-byte knowledge lives
both in _detect_format() and again as is_pe() / is_macho() in the metadata
modules. The three builders _dump_elf / _dump_macho / _dump_pe share ~70
lines of near-identical castxml-invocation + parser + AbiSnapshot-construction
boilerplate, and the --lang → profile conversion is a helper for ELF but
copy-pasted inline for the other two. Mach-O's leading-underscore symbol strip
is duplicated within _dump_macho itself.
Goal. Reuse the registry idiom the project already trusts for detectors.
Adding a future binary format becomes a new handler file, not edits to dump().
"Everything about Mach-O" lives in one place.
Approach.
1. Define a BinaryFormatHandler protocol: matches(magic) → bool,
parse_binary_metadata(path), attach(snapshot, meta),
normalize_symbol(name), plus an optional post-build hook.
2. Add a shared _build_snapshot_from_castxml() for the common body.
3. Extract ELF/PE/Mach-O handlers from the three _dump_* functions, keeping
each format's no-headers symbol-only path.
4. Replace dump()'s if/elif with registry.select(magic); unify
--lang → profile on the existing ELF helper.
Edge cases / risks.
- ELF alone calls _populate_elf_visibility and accepts debug_format — the
optional post-build hook keeps that without polluting the shared body.
- Magic detection must become single-source; keep is_pe()/is_macho() as thin
wrappers only if external callers exist.
Risk: medium. Requires the integration marker (castxml + gcc/g++) to verify.
C4 — Detector auto-discovery (drop the import manifest) (implemented — PR #395)¶
Problem. The detector registry's "no manual list" promise is undercut by
side-effect imports in checker.py tagged
# noqa: F401 — triggers detector registration. A new diff_* module that
forgets to get added to that block contributes zero detectors, with no error.
Goal. Adding a detector module requires no checker.py edit; a forgotten
module is impossible to skip silently.
What was implemented (order-preserving variant). Investigation surfaced two facts the original sketch missed:
- Most of
checker'sdiff_*imports are not pure side-effects — they also pull real symbols (diff_filteringhelpers,diff_platform/diff_typesfunctions, …), so the block cannot simply be deleted. - Registration order feeds post-processing dedup. An empirical check
confirmed importing all
diff_*modules registers exactly the current 49 detectors (no additions), but reordering them could change dedup outcomes.
So C4 was implemented as a safety net that preserves order:
registry.ensure_loaded() walks pkgutil.iter_modules(abicheck.__path__) for
the diff_ prefix (sorted) and imports each. It is called at the top of
compare(). Because checker's explicit imports already ran at module load
(fixing the canonical order), ensure_loaded() is a no-op for the existing
set — the modules are cached, so re-import does not re-register. A new
diff_* module is discovered automatically and appended after the existing
detectors, deterministically — no checker edit required, and zero
behaviour change today.
Verification gate is a test (tests/test_detector_discovery.py) rather than a
readiness check: it asserts every diff_* module is imported after
ensure_loaded() (the silent-skip footgun), that the call is idempotent and
order-stable, and a soft detector-count floor.
Risk: low; behaviour-preserving (verified: registered set + order unchanged).
C5 — Fold synthetic detectors onto the registry (deferred — not a clean win)¶
Re-grilled and deferred. Closer inspection showed the sketch
mis-characterised the target. The post-processing step is DetectCppPatterns,
which runs seven coupled sub-detectors, not three. Several
(detect_sycl_overload_set_removal, detect_cpu_dispatch_isa_dropped) return
(findings, suppressed_keys) tuples and drive grouped-child suppression
that mutates ctx.kept / ctx.suppressed by reference; one
(detect_inline_body_renamed_member) needs the in-flight changes list. They
fundamentally require post-filter context, which the registry's
(old, new) -> list[Change] contract does not carry. Splitting them onto a
registry phase buys marginal discoverability (detector_names listing them) at
the cost of either a second, ctx-aware detector contract or breaking up a
cohesive step. Net negative — left as-is. If discoverability is wanted later, a
cheaper move is to expose a read-only synthetic_detector_names list from
post_processing for docs/coverage tools, without touching the registry
contract.
Original problem (for reference). There are two detection orchestrators. The
synthetic detectors are invisible to registry.detector_names, so nothing that
introspects "all detectors" sees them.
Goal. One discovery point for "what detectors exist", without losing the post-filter ordering the synthetic detectors need.
Approach.
1. Add a phase: "primary" | "post_filter" attribute to @registry.detector
(default primary).
2. Register the two pure synthetic detectors as post_filter.
3. Replace their direct calls in AddSyntheticDetectorFindings with
registry.run_phase("post_filter", …).
Edge cases / risks.
- These run after surface scoping + dedup — the phase concept must preserve
that ordering; a naive move to the primary phase would run them too early.
- detect_sycl_overload_set_removal also suppresses redundant findings (it
returns a tuple, not just changes). The phase contract should either allow a
detector to return suppressions, or this one stays special-cased and
documented. Default: leave it special, migrate the two pure ones.
Risk: low-medium. Do after C4.
C6 — A Change factory for consistent findings (scheduled as its own PR)¶
Tracking decision: C6 is the widest-blast-radius candidate (~358 call sites) and is intentionally carved out into a separate, self-contained PR, done after the C1–C2 work in #395 merges. This section is the spec for that PR — detailed enough to start cold.
Increment 1 landed. The reusable core plus a broad first migration are in place:
ChangeKindMetagained an optionaldescription_templatefield (change_registry.py), anddiff_helpers.make_change(kind, *, symbol, name, old, new, detail, description, **kwargs)formats it (with explicitdescription=as the first-class bespoke override). EveryChange(...)constructor across thediff_*modules (~200 sites) now routes throughmake_change— bespoke findings keep their computeddescription=, and 167 regular kinds own adescription_templateso their wording lives in the registry (the wholediff_typestype/field/enum/union/typedef/qualifier family, thediff_symbolsfunction/variable/param/access kinds, and thediff_platformELF/PE/Mach-O symbol/version/dependency kinds). All byte-for-byte: the full detector suite plustests/test_change_factory.pylock the wording. The only descriptions left on the explicit path are those that genuinely cannot be a single template: divergent wording for one kind across call sites, a precomputeddescvariable, or capitalize/conditional text.Two deliberate deviations from the spec below: (1) the factory lives in
diff_helpersrather thanchange_registry—change_registryis a dependency-free leaf (checker_policy → change_registry), so importingChangethere would create achange_registry → checker_types → checker_policy → change_registrycycle the AI-readiness gate rejects;diff_helpersalready imports bothChangeand the registry. (2) The placeholder vocabulary adds{name}(the demangled declared name, distinct from the mangled{symbol}) because nearly every regular description interpolatesf_old.name, not the symbol field.
Problem. Change(kind=ChangeKind.XXX, description=f"…", old_value=…, new_value=…)
is hand-rolled across the diff_* modules, each call site inventing its own
description wording. Reporters then partly parse those free-text descriptions.
The impact text already lives in change_registry.py (one entry per
ChangeKind), but the per-finding description does not — so phrasing is
inconsistent, untestable, and the description is the only place some detail
(old→new) is encoded for machine consumers.
Goal. Uniform, templated descriptions owned next to each kind's
verdict/impact in change_registry; detectors pass structured fields, not prose;
machine consumers read fields, not scraped text.
Inventory / where to start.
- Enumerate the sites: rg -n "Change\(" abicheck/diff_*.py abicheck/checker.py
(was ~358 at time of writing — re-count first; treat the number as
approximate). Group by ChangeKind to see which kinds dominate.
- Classify each kind's description into regular (a fixed template +
symbol/old/new substitution, e.g. func_return_changed:
"Return type changed: {symbol}; old={old}, new={new}") vs bespoke
(embeds computed offsets, demangled signatures, vtable slot indices, counts —
e.g. type_field_offset_changed, vtable/RTTI size findings). Expect a long
tail of regular kinds and a small bespoke minority.
Approach.
1. Add an optional description_template: str | None field to each
change_registry entry (alongside impact). Use str.format-style named
placeholders drawn from a fixed vocabulary: {symbol} {old} {new} {detail}.
2. Add change_registry.make_change(kind, *, symbol, old=None, new=None,
detail=None, description=None, **change_kwargs) -> Change: formats from the
template when present, else requires an explicit description=. It stamps
kind and forwards the remaining Change fields (old_value, new_value,
caused_by_type, …). Keep it a thin wrapper over the Change dataclass.
3. Migrate the regular kinds to make_change(...), deleting their f-strings.
4. Leave bespoke kinds calling make_change(..., description=<computed>) —
do not force-fit a template; the override path is first-class.
5. Add a registry-completeness test: every ChangeKind that any detector emits
either has a description_template or is on an explicit BESPOKE_DESCRIPTION
allowlist (so a new kind can't silently regress to an ad-hoc f-string).
Sequencing within the PR. Land the factory + templates + a handful of migrated kinds first (small, reviewable), then migrate the rest in a second commit. Mechanical, so keep commits per-module to ease review.
Edge cases / risks.
- ~358 call sites — highest churn-to-benefit ratio; that is exactly why it is a
standalone PR, not bundled with semantic refactors.
- Descriptions are user-visible and some are golden-snapshotted: run the
golden lane; any wording change is intentional and regenerated deliberately.
- Some descriptions are parsed downstream (e.g. PR comment / appcompat). Audit
for .description string parsing before changing wording; prefer adding a
structured field over changing the prose those consumers read.
- Depends on C2 only loosely: C2's model already centralises classification;
C6 centralises description text. They are independent but both reduce
reporter string-scraping, so do C6 after C2 to land on the model.
Risk: mechanical but wide. Standalone PR, golden-gated.
C7 — Push business logic out of the CLI into the service layer (exit-code unification done; command-body extraction is a sized follow-up)¶
Problem. cli.py's compare_cmd (~200 lines of a file at the size cap) does
result post-processing, GitHub-annotation emission and exit-code mapping inline —
business logic, not presentation. The same leak appears in
cli_compare_release, cli_appcompat and cli_stack. Most contractually
dangerous: the verdict→exit-code mapping was duplicated inline in compare
(cli._exit_with_severity_or_verdict) and compare-release
(cli_compare_release._exit_compare_release), so the two flows could drift.
Done (this PR). The exit-code contract is unified:
- severity.legacy_exit_code(verdict) is the single home for the
non-severity-aware mapping (BREAKING→4, API_BREAK→2, compatible→0), sitting
next to the existing severity-aware compute_exit_code.
- Both compare and compare-release route their legacy branch through it;
flow-specific floors (compare-release's operational ERROR→4 and
removed-library→8) are applied on top, documented at the call site.
- tests/test_exit_code_integrity.py locks the contract and asserts the two
flows produce identical codes per verdict, and that the compat scheme
(BREAKING→1, API_BREAK→2, 3–11 errors) is deliberately distinct and not
accidentally unified.
Remaining (follow-up, can be its own PR). Extract the rest of the
command-body business logic into service.py:
1. A service.run_compare_to_report(...) (or similar) returning a structured
result + the resolved exit code, so post-processing, annotation emission and
exit decisions live in the library layer.
2. Reduce compare_cmd / compare_release_cmd / appcompat_cmd /
stack_check_cmd to arg-parse → service call → sys.exit.
3. Extract shared Click option stacks into cli_options.py where they duplicate
(--suppress, --policy, severity flags).
This is what finally drops cli.py below the size cap.
Edge cases / risks.
- Exit-code semantics are contractual (documented in /CLAUDE.md); the
unification preserved the legacy and severity-aware mappings exactly (covered
by the new integrity tests). The follow-up extraction must keep them.
- GitHub-annotation emission has side effects (writes workflow commands); keep it
behind the same flag and test with output capture.
Risk: exit-code unification — low (behaviour-preserving, tested). Command-body extraction — medium (wide, exit-code- and annotation-sensitive).
C8 — Make the ABICC-compat CLI a thin adapter¶
Problem. compat/cli.py (~1581 lines) is a parallel pipeline: it
re-implements dump, check, suppression-building (_build_skip_suppression) and
verdict computation, rather than wrapping the main pipeline. It already reuses
the output modules (html_report), so it is only half-wrapped.
Goal. ABICC compatibility becomes a translation layer: ABICC flags →
service.run_compare, then verdict → ABICC exit codes. One pipeline, two front
ends.
Approach.
1. Map ABICC -skip-* flags onto the existing suppression machinery instead
of a bespoke builder.
2. Route dump/check through service.resolve_input / run_dump /
run_compare.
3. Keep only the ABICC-specific argument parsing and the exit-code translation
in compat/cli.py.
Edge cases / risks.
- This is the drop-in ABICC replacement — parity is contractual. Land behind
the abicc marker (abi-compliance-checker + gcc/g++) and the golden lane.
- ABICC error exit codes (3–11) must be preserved exactly
(_classify_compat_error_exit_code).
Risk: high (parity-sensitive). Sequence after C2/C7 stabilise the shared layer.
C9 — Relocate confidence computation (implemented — PR #395)¶
Problem. _compute_confidence() and its three helpers lived in
diff_filtering.py but are pure orchestration: they consume detector_results
(the registry's output) and the snapshots' available metadata, and are called
from checker.compare(). Following the orchestration flow meant a cross-file
hop into a filtering module.
What was implemented. Moved the four functions
(_detect_evidence_tiers, _determine_evidence_tier,
_determine_confidence_level, and the public compute_confidence) into a new
dedicated abicheck/confidence.py. A new module (rather than folding into
checker.py) avoids a checker ↔ diff_filtering import cycle: confidence.py
depends only on checker_policy, detectors and model, so it sits at the
bottom of the graph. checker and the two test modules now import from
confidence; the historical name _compute_confidence is kept as an alias.
diff_filtering.py is left to actual filtering and shrank by ~190 lines.
Risk: low; behaviour-preserving (verified: full fast lane + no new import cycle).
C10 — Split model.py: pure data vs name heuristics¶
Problem. model.py (imported by 56 modules) mixes the core data classes
(AbiSnapshot, Function, RecordType, …) with string heuristics —
is_compiler_internal_type, is_non_abi_surface_type, canonicalize_type_name,
cv-qualifier parsing. The type-name classification part is conceptually the
same family as C1's symbol-name classification.
Goal. model.py is the pure data model; all name/type classification lives
with C1's name_classification (or a sibling). This also gives the deferred C1
follow-up (unifying the stdlib-/runtime-RTTI skip sets) a natural home.
Approach.
1. Move the type-name classification helpers into name_classification.py,
keeping back-compat re-exports from model.py to avoid churning 56 importers
at once.
2. Migrate importers incrementally.
3. Fold the stdlib-/runtime-RTTI skip sets (from elf_symbol_filter,
diff_elf_layout, elf_metadata) in with behaviour-equivalence proofs per
call site (the deferred C1 sub-task).
Edge cases / risks.
- model.py's public surface is part of the Python API (/CLAUDE.md calls this
out) — re-exports must stay until importers migrate.
- Watch for import cycles (name_classification must stay dependency-free).
Risk: medium; staged via re-exports.
N-A — One HTML page seam for the three native renderers (implemented)¶
Problem. There was no HTML-page abstraction. appcompat_html.py and
stack_html.py reached into html_report.py for styling
(from .html_report import _CSS, _VERDICT_STYLE, _changes_table) but each
re-emitted the same <!DOCTYPE html> … </html> skeleton and the same
<footer>…abicheck/abicheck…</footer> by hand. A stylesheet, layout or
accessibility fix had to be made in up to three places, and the shared _CSS
constant lived inside a 950-line renderer rather than at a seam. This is
distinct from C2: C2 unifies what data/severity each renderer sees
(ReportModel); N-A unifies how the HTML renderers emit page chrome.
Goal. One module owns the page chrome; each renderer supplies only its domain content as the document body.
What was implemented. New abicheck/html_template.py owns _CSS,
_VERDICT_STYLE, render_document(*, title, body, css=_CSS) (the
DOCTYPE/head/stylesheet/body frame) and render_footer(subtitle). All three
native renderers — html_report.generate_html_report,
appcompat_html.appcompat_to_html, stack_html.stack_to_html — build their
body and call the shared seam. _CSS/_VERDICT_STYLE/render_* are
re-exported from html_report so the satellites' historical import paths keep
working. The ABICC-clone format (_COMPAT_CSS) is deliberately left
separate — it mirrors abi-compliance-checker's own markup, a different chrome.
Verification (behaviour-preserving). A characterization golden test
(tests/test_html_template_golden.py, marker golden, with references in
tests/golden/html_template/) locks the byte-for-byte output of all three
renderers; the references were captured from pre-refactor code and pass
unchanged after the extraction. Full fast lane + golden lane + ruff/mypy/
AI-readiness all green.
Risk: low; output verified byte-identical. Follow-up (N-A inc. 2): the
three modules still each hand-roll <table class='changes'> markup — a shared
render_change_table(changes, grouping) is the next deepening, gated the same
way.
Environment / verifiability constraints¶
Some candidates change behaviour that can only be safely verified with external tools or output snapshots. Status in the current dev environment:
| Lane | Tool | Present? | Blocks |
|---|---|---|---|
integration |
castxml + gcc | castxml missing | C3 (dump path) |
abicc |
abi-compliance-checker | missing | C8 (parity) |
libabigail |
abidiff | missing | parity cross-checks |
golden |
snapshot files | present | C2 (output) |
Implication: C3 and C8 must not be merged from an environment that cannot run their lanes — they are deferred to a context with castxml / ABICC, or to CI with those lanes green. C2/C7 change user-visible output / exit codes and need explicit sign-off plus the golden lane before merge.
Sequencing¶
Ordered by risk-adjusted payoff. Cheap, isolated wins first; output- and parity-changing work last.
C4 detector auto-discovery ✅ done (PR #395)
C9 relocate confidence ✅ done (PR #395)
C1 name classification ✅ done (PR #395)
C2 report view-model ✅ inc 1–2 done (model+ADR-036; maps unified; integrity tests)
C7 CLI → service ◐ exit-code unified + shared loader moved to cli_params leaf (body-extraction follow-up)
C3 binary-format registry ✅ done (#407; handler registry drives _detect_format + dump dispatch)
C10 split model.py ◐ stage-2 done (name predicates + type-name canonicalization moved)
C8 ABICC compat adapter (parity-sensitive)
C5 synthetic detectors → registry ⛔ deferred (entangled; net-negative)
C6 Change factory ◐ inc 1 done (factory + 167 templates; all ~200 diff_* sites route via make_change)
Rationale: C4 and C9 are mechanical and reversible — do them to build confidence. C1 (done) unblocks C2 and C10. C2 is the largest locality payoff but carries visible-output risk, so it lands behind golden review before the churn-heavy C6. C8 is sequenced last among the structural items because ABICC parity is contractual and benefits from a stabilised shared layer underneath it.
Tracking¶
| ID | Title | Status | PR |
|---|---|---|---|
| C1 | Name classification module | Done | #395 |
| C2 | Report view-model + canonical severity + cross-channel integrity tests (ADR-036) | Increment 1–2 done | #395 |
| C3 | Binary-format handler registry (_FormatHandler registry: magic recognition + dump() dispatch in one declarative table; signature-equivalence guard test) |
Done | #407 |
| C4 | Detector auto-discovery | Done | #395 |
| C5 | Synthetic detectors → registry | Deferred (not a clean win) | — |
| C6 | Change factory |
Increment 1 done (factory + description_template registry field; all ~200 diff_* constructor sites route via make_change; 167 regular kinds templated; bespoke long tail remains) |
— |
| C7 | CLI → service (exit-code unify + cross-flow integrity tests done; shared _load_suppression_and_policy relocated to cli_params leaf, importers repointed, cli.py 1993→1928; command-body extraction follow-up) |
Partial | #395 / #407 |
| C8 | ABICC compat adapter | Proposed | — |
| C9 | Relocate confidence computation | Done | #395 |
| C10 | Split model.py (stage-1: name predicates; stage-2: type-name canonicalization + cv-qualifier helpers moved to name_classification, re-exported) |
Stage-2 done | #395 / #407 |
| N-D | Unify stdlib/runtime RTTI prefix tables — canonical STDLIB_RTTI_PREFIXES (union superset) in name_classification; elf_symbol_filter + diff_elf_layout share it; guard test pins membership; behaviour-preserving (only effective delta: skipping toolchain-owned std::__cxx11 vtable/typeinfo, verified identical parity+golden failure set vs HEAD) |
Done | #407 |
| N-A | HTML page seam (html_template) — shared document chrome + footer for the three native renderers, byte-identical golden-locked |
Done | #407 |
| N-D | Unify stdlib/runtime RTTI prefix tables into one canonical STDLIB_RTTI_PREFIXES (verified: identical parity-corpus failure set; only effective delta is suppressing std::__cxx11 RTTI in L0 layout, which is toolchain-owned) |
Done | #407 |
| N-E | Structured Change value fields |
Not needed — already satisfied: make_change routes old=/new= into old_value/new_value (offsets included) and reporters read those fields; zero prose-scraping found |
— |