You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
explainers: make findings attributable, and keep confidence 1.0 structural
- Layer violations cite the dependency fact and the raw import target, not
only the importing file. The diff matches fact names and edge endpoints, so
file-only evidence never attributed and every new violation landed in the
incidental bucket, which the gate does not grade.
- Unused-route findings key on the route fact name and carry the file, so
they attribute and can be scoped by repository.
- common.ModuleDir resolves a repo-prefixed file back to its module, used by
layers and the shared module-graph builder. Without it a composed graph
resolves to phantom nodes and cycles go undetected. No-op for single-repo.
- god-class and the layers pattern clamp at common.MaxHeuristicConfidence.
Both compute a saturating score, and 1.0 is what the receipt, the dashboard
and query_insights(min_confidence=) read as a structural fact. A cycle is
the only claim computed with certainty and the only one that reaches it.
- layers and complexity-outliers exclude test code, the last two explainers
that did not. layers gates on the importing file, since resolution walks up
and would attribute a nested mock to the production layer, and drops test
modules from the pattern population. complexity-outliers drops test symbols
from the distribution too: a symbol's cyclomatic value is its own, so
narrowing the population falsifies nothing retained.
Benchmarks re-measured and republished.
Copy file name to clipboardExpand all lines: ARCHITECTURE.md
+3-5Lines changed: 3 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -174,7 +174,7 @@ Three plugin roles drive the middle of the pipeline — **extractors** (source
174
174
Explainers turn raw facts into architectural observations. Each insight carries a **confidence** score: `1.0` means it's a structural fact, below `1.0` means it's a heuristic. Every insight is also tagged with the explainer that produced it (`Insight.Source`), and the whole set is retrievable through the **`query_insights`** tool — filter by `explainer`, `repo`, or `min_confidence` — so an agent fetches a finding directly instead of re-deriving it from raw facts or scraping it out of `explore depth=2` / `.enola/insights.json`.
175
175
176
176
-**Cycles** ([`internal/explainers/cycles`](internal/explainers/cycles/cycles.go)) — finds cyclic module dependencies using **Tarjan's strongly-connected-components algorithm**. A cycle either exists in the import graph or it doesn't, so these land at confidence `1.0`, with every module in the cycle listed as evidence.
177
-
-**Layers** ([`internal/explainers/layers`](internal/explainers/layers/layers.go)) — recognizes common architectural shapes by matching module paths against known patterns: **hexagonal** (application / port / adapter / domain / …), **Next.js** (pages / components / hooks / lib / api / …), and **Go-standard** (cmd / internal / pkg / api). Confidence is computed from how much of the codebase matches. It also flags **layer violations** — an inner layer importing an outer one — as lower-confidence heuristic warnings.
177
+
- **Layers** ([`internal/explainers/layers`](internal/explainers/layers/layers.go)) — recognizes common architectural shapes by matching module paths against eight known taxonomies: **hexagonal** (application / port / adapter / domain / …), **Go-standard** (cmd / internal / pkg / api), **Next.js** (pages / components / hooks / lib / api / …), **rails-mvc**, **django**, **spring-layered**, **android-clean** and **ios-clean**. Most are gated on a detected framework or language, so only one is ever reported: the most specific match wins, and ties break on confidence. Confidence is computed from how much of the codebase matches — capped below `1.0`, because a directory-name match is a well-supported guess and never a proof. Test modules are excluded from that measurement (a build file's `module_role` outranks what a path looks like), so a test source set cannot vote on the architecture. It also flags **layer violations** — an inner layer importing an outer one — as lower-confidence heuristic warnings.
178
178
-**Cross-repo** ([`internal/explainers/crossrepo`](internal/explainers/crossrepo/crossrepo.go)) — summarizes the cross-repo edges found by the linker. Returns nothing for a single-repo snapshot.
179
179
-**Coverage** ([`internal/explainers/coverage`](internal/explainers/coverage/coverage.go)) — turns the per-service `edge_coverage` counts the linker records into **coverage-gap** insights: a service with no resolved outbound edges but unresolved outbound call sites is flagged as a blind spot ("appears isolated but…"), distinct from one that is genuinely a leaf. Distinguishes absence of edges from a gap in coverage. Returns nothing for a single-repo snapshot. Surfaced programmatically by the `coverage_report` tool.
180
180
-**Unused-routes** ([`internal/explainers/unusedroutes`](internal/explainers/unusedroutes/unusedroutes.go)) — the **server-side inverse** of the cross-repo HTTP linker: it rolls up the `route` facts that *no loaded client calls* (tagged `unmatched_by_clients` during linking — see [Finding unused endpoints](#finding-unused-endpoints)) into one candidate-cleanup insight per service. Deliberately conservative: it only considers repos that actually serve a cross-repo client (an HTTP *provider* — never a frontend's own page routes), skips low-signal generic paths (`/health`, single-segment), and biases toward false negatives. Each insight carries the mandatory caveat that candidates are unused *by the loaded clients only* — consumers outside the snapshot (admin scripts, cron, webhooks, third-party clients, deep links) don't appear, so verify before deleting. Confidence `0.6` (a candidate to review, not a verdict). Returns nothing for a single-repo snapshot.
@@ -642,11 +642,9 @@ Both absolute paths stay in the warning text, and it names which signal decided,
642
642
643
643
`Status.ExitCode()` is the contract with CI: `0` clean · `1` regression · `2` usage error · `3` incomparable. Precedence is **blocking → usage error → regression**; blocking comes first because when the snapshots were built over different inputs, the inverted-pair remedy ("re-generate") would send the caller down the wrong path. Nothing is hidden by the ordering — every warning is reported regardless of which decided the status.
644
644
645
-
**Why the policy keys on the explainer rather than on confidence.**The obvious design is "fail at confidence `1.0`, because [Insights](#insights-explainers)says `1.0` is a structural fact and anything below is a flagged heuristic". That does not survive contact with the explainers: `godclass` computes confidence from a fan-in ratio and **clamps it to `1.0`**, so a statistical outlier at twice the threshold presents as a certainty; and `layers` emits an informational `Architecture pattern: <name>` finding whose confidence is the share of the codebase matching the pattern, which can also reach `1.0`. A gate keyed on the number alone would fail builds for a new statistical outlier and for a re-detected pattern after a reorganization.
645
+
**Why the policy keys on the explainer rather than on confidence.**Confidence alone is not enough to name what should break a build, even though [Insights](#insights-explainers)guarantees that `1.0` is a structural fact: it says how strong a claim is, not what kind of claim it is. A statistical outlier and a proven cycle are different objects, and only the second is a defect by construction. So the **explainer is the primary filter** (`DefaultFailExplainers = ["cycles"]`) and confidence is a floor applied within it (`DefaultMinConfidence = 1.0`). The floor does real work: `cycles` emits both a true cycle at `1.0` and a "highly coupled module cluster" at `0.4` whose own description calls it "a coupling-density signal, not a defect to break".
646
646
647
-
So the **explainer is the primary filter** (`DefaultFailExplainers = ["cycles"]`) and confidence is a floor applied within it (`DefaultMinConfidence = 1.0`). The floor still does real work: `cycles` emits both a true cycle at `1.0` and a "highly coupled module cluster" at `0.4` whose own description calls it "a coupling-density signal, not a defect to break".
648
-
649
-
> The confidence-invariant violation above is a **real inconsistency between the docs and the explainers**, worked around here rather than fixed. Capping `god-class` below `1.0` and reclassifying the `layers` pattern finding as informational would let the gate key on confidence directly — but it changes insight output, so it needs golden regeneration.
647
+
> **The two-part filter is what keeps `1.0` meaningful in both directions.** Every explainer that computes rather than proves its confidence clamps at `common.MaxHeuristicConfidence`, strictly below `1.0` — `god-class`, whose score is a fan-in ratio against a statistical threshold, and `layers`, whose pattern confidence is a coverage share. Both saturate on real repositories, and letting them reach `1.0` would have published a statistical outlier as a structural fact to everything downstream that reads the number: the receipt's heuristic-insight count, the dashboard's structural/candidate split, and `query_insights(min_confidence=…)`. A cycle is the only claim enola computes with certainty, and it is the only one that reaches `1.0`.
650
648
651
649
**New coupling is reported, never failed.**`diff.Edge` is name-level, so `EdgesAdded` is populated by virtually any change — adding a function that calls another adds edges. A gate firing on that would be switched off within a day. Only module-level and cross-repo coupling deltas are worth escalating, and that needs an edge filter that does not exist yet.
Copy file name to clipboardExpand all lines: README.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -57,7 +57,7 @@ New coupling (5):
57
57
invoice.Retry --declares--> invoice
58
58
```
59
59
60
-
Two packages here, for a readable example. On a 68,000-fact repository carrying 335 pre-existing findings it behaves identically: it reported the one thing the change introduced, and none of the 335.
60
+
Two packages here, for a readable example. On a 68,000-fact repository carrying 268 pre-existing findings it behaves identically: it reported the one thing the change introduced, and none of the 268.
0 commit comments