02 · System Architecture · src/cupid/ · ~19.6k lines of Python ·
60+ test files
System architecture: the code-level panorama
The upper band is the scan & decision engine (runs on every scan);
the lower band is the evidence & calibration engine (offline; produces the weights
and the evidence). Every card lists real modules and measured facts, all verifiable in
the repository.
✦ Click any card → the layer's concept
and modules pop up; press “Locate in the code map” → the source is circled
and the engineering details expand
A · Scan & Decision Engine · runs on every scan · five stations, each removing a class of generic-tool mistakes
1 · Ingest & Inventory
Intake, classification, the ledger: coverage is explainable
Every file is recorded as analysed or skipped, with the reason. Coverage is explainable; nothing is silently dropped.The first stop of every scan.
language.py decides which frontend each file goes to.
report/analyze.py ScanInventory records analysed or skipped, with the reason, so coverage is explainable.
path_context.py weights path roles (production 1.0 · fixture 0.1) so test fixtures cannot drown the real source.
vcs_risk.py reads commit metadata only and adds the bus-factor governance lens.
2 · Frontends
The only home of language syntax
Every language difference is confined to its own frontend. Adding a language means adding one frontend; the rules never change.python.py uses the stdlib ast; C and C++ use tree-sitter and share one CFamilySpec walker, so dialect differences live in a spec instead of a copied parser.
clang_index.py is the optional libclang precision tier: it sees the real malloc/free after macro expansion and degrades gracefully without compile_commands.json.
Adding a language means adding one file here; nothing downstream changes.
3 · IR Fact Contract
Language-neutral: the hub of the whole system
Every language lowers into one neutral fact model. A rule never asks what language this is; it asks whether the facts it needs exist.Every language lowers into one neutral fact model: function shape (FunctionFacts) plus site-level facts (loop, call, alloc, deref, membership, ORM query…).
A frontend declares which capabilities it fills, a signal declares which it needs; the gate is one line.
Inapplicable rules never run, and a half-built frontend is still safe to use.
4 · Analysis Engines
File rules × project graphs × external proof
Cheap rules find candidates and deep tools provide proof; beyond single-file rules sit the project-wide import and call graphs.signals/ holds 28 capability-gated rules in six families.
graph/ adds the cross-file view: graded import cycles, call-graph hubs, static test linkage.
tools/ brings semgrep, clang-tidy and friends in as second opinions.
code_context.py lets the same rule reach different verdicts on a sanitizer versus a parallel variant: the "do not split" judgment comes from this layer.
5 · Decision & Delivery
One schema → ranked → shipped
Every source converges on one finding schema, ranked by three explainable axes; every threshold knows its percentile in the reference corpus.Every source (native rules, graphs, external tools, history) converges on one Finding.
Severity is calibrated by the corpus percentiles in norms/; score = severity × (0.5 + 0.5·leverage) × removability, all three axes explainable.
report/ renders an 862KB zero-CDN single-file HTML; serve.py is the stdlib web front door, and the settings panel shows each threshold's corpus percentile as you tune it.
⤴ The AUC weights, removability verdicts and corpus
percentiles produced by band B all feed back into A5's ranking
the evidence engine calibrates the decision engine
B · Evidence & Calibration Engine · offline · deterministic · zero LLM · it does not assume its rules are right; it measures them
1 · Corpus & History
Real cross-version history, acquired
vcs.py takes history with blobless clones (--filter=blob:none), pins HTTP/1.1 against mid-transfer drops on large repos and never touches the working tree.
corpus/ reconstructs whole before/after files.
dataset/scan.py snapshots every release with git archive: at most 60 tags per repo, resumable.
2 · Trajectory & Labels
Functions tracked across releases
trajectory.py classifies each function's transition across releases as new / renamed / fixed / improved / unchanged, detecting renames by body-hash first.
labels.py uses a fix-commit regex and hunk line numbers to locate each fix back to the function it belonged to in that commit.
release_transitions.py emits the training table.
3 · Ground Truth
Deterministic refactor confirmation: no LLM
What counts as a real refactor is decided by an algorithm: not a model's label, not a feeling.confirm_refactors.py decides algorithmically what a real Extract-Method is: cognitive drops by ≥15 and ≥50% of the old statement lines reappear in new sibling helpers in the same file (aligned with Tsantalis et al., ICSE 2018).
On datasette it even caught a case RefactoringMiner missed.
removability.py converges the historical verdicts into stuck / confirmed / rising / unknown, feeding the ranking directly.
4 · Validation & De-risk
Measure predictive power, veto bad directions
Which metrics actually predict later refactoring is measured and written into the weights; every smell family can cite a real grew → fixed → stayed case.multivariate.py runs k-fold CV-AUC with a pure-Python logistic regression (zero dependencies), measuring cognitive / cyclomatic at 0.78–0.79.
evidence.py builds the grew → fixed → stayed arcs and the build_degraded() long-term-debt query.
The de-risk series records the negative results: DL defect prediction, fan-in ranking and ownership ranking were all measured here and vetoed.
5 · Quality Loop
False positives become regression assets
Every finding is tried as a suspect: the verifier's starting position is that the finding is wrong.Every finding is tried as a suspect: three-class verdicts (CONFIRMED / FALSE_POSITIVE / NOT_ACTIONABLE) with the verifier starting from "this finding is wrong".
Every false positive must name the responsible location in CUPID's own source.
After a fix the eval set replays in both directions: false positives must disappear, confirmed findings must survive.
Human overrides outrank every automated verdict, and engine-vs-doc disagreements are DOC_CONFLICTs that need a ruling, never an automatic verdict.
The loop drains its own backlog: one gated merge per item, 20 verified correction classes now catalogued in the Precision view.
Interfaces
--findings-jsonl (machine-readable, feeds the verification loop)
--combined-html · --norm-profile
--threshold k=v · --compile-commands
Commercial boundary
rule-first · token-free verdicts
customer code is never executed · Apache/MIT embeds only
excluded: GPL (cppcheck) · CodeQL · Coverity
Engineering discipline
450-line file cap · strict layered dependencies
ruff + strict mypy + pytest (60+ files)
frontend → IR → signals → graph → report