enola/Changelog

Changelog

What's new.

What changed, why it matters, and whether you need to take action. Implementation details and pull requests remain available in each release.

v0.4.17 Latest

Python calls into a package's own __init__.py now resolve, and the dashboard opens with no snapshot

A gap in Python call resolution for symbols a package defines in its own __init__.py is fixed, and the dashboard now opens even before a snapshot exists, with clearer findings and dependency graphs.

  • FixedCalls to a symbol a Python package defines in its own __init__.py, rather than re-exporting from elsewhere, now resolve instead of dangling.
  • AddedThe dashboard now opens even with no snapshot yet generated, showing the exact command to run, and a Refresh control loads newer snapshots without disrupting the graph you're inspecting.
  • FixedFindings are prioritized, lifetime usage is separated from diagnostics, and confidence labels and suggested actions are clearer.
  • FixedThe dependency graph condenses cycles into clusters, renders edges more clearly, and gets a clear-selection control; browser launching on macOS is fixed and the docs are updated.

Action: re-pin baselines taken on a Python repository that uses this pattern — extraction caches from earlier releases are invalidated, so the first snapshot after upgrading re-extracts.

Implementation notes and pull requests

The re-export index mapped a package directory to the names its __init__.py imports from elsewhere, but not the names it defines itself. Both gaps produce the same dangling edge: from pkg import helper emits the target pkg.helper, whose prefix names a directory rather than a module, so absolute resolution failed and the call bound to nothing. Local definitions are now registered after the re-export pass and unconditionally — a name a package both defines and imports is shadowed by the local definition at runtime, which is the correct binding rather than an ambiguity to drop. PR #286.

Dashboard: snapshot loading now stays scoped to the selected repository unless an explicit cluster configuration is supplied, and graph data, snapshot metadata, and receipts are read from one consistent publication so a failed refresh preserves the page instead of blanking it. Findings are re-ordered by priority, lifetime-usage counts are split out from diagnostics, and confidence labels and suggested actions are clarified. The dependency graph condenses cycles into clusters, improves edge rendering, and adds a clear-selection control; browser launching on macOS is fixed, and dashboard and CLI documentation are updated. PR #283.

v0.4.16

Policy-as-code constraints now cover Go structs, and a silent gap in intent checking is reported

A new policy-as-code example demonstrates GDPR and PCI-DSS constraints end to end; require_defines now matches Go/Rust/C++/C# structs as well as classes, and a stale-citation check that was silently skipping unmeasured repositories now says so.

  • AddedA new examples/policy-as-code example walks through declaring GDPR and PCI-DSS cardholder-data constraints and checking a codebase against them.
  • Fixedrequire_defines now matches struct-kind types (Go, Rust, C++, C#) as well as classes, so a rule like "every store defines Erase" no longer verdicts nothing on a Go codebase.
  • FixedA page whose anchors name a repository the current snapshot doesn't carry now raises an "Anchors not checked" notice instead of silently passing as if nothing were wrong.
  • Fixedconstraints lint now resolves component membership through compiled policy pages, matching what the constraint check itself resolves.
  • FixedA yarn.lock dependency descriptor that itself contains an @ (an npm alias or a git+ssh URL) now resolves correctly, so a lockfile-pinned dependency is no longer reported as unpinned.

Action: re-pin baselines on projects with yarn.lock aliases or git-protocol dependency ranges — extraction caches from earlier releases are invalidated, so the first snapshot after upgrading re-extracts.

Implementation notes and pull requests

New example: examples/policy-as-code declares GDPR personal-data and PCI-DSS cardholder-data policies over a small checkout/gateway/vault codebase, wired through governed_by and require_governed constraints. PR #281.

Class-kind now means class or struct, since the owner of a method isn't spelled the same way in every language: Ruby, Python, TypeScript, Java, Kotlin and PHP write class; Go, Rust, C++ and C# write struct. While the gate admitted class alone, a rule written over Go structs resolved its members, verdicted nothing, and raised no advisory — vacuous compliance. Separately, a page whose anchors name a repository absent from the snapshot now raises one 0.4 anchors not checked notice per page rather than looking like a page whose citations all held; governed_by and require_governed are unaffected since with one repository loaded they drop the anchor's label rather than comparing it. constraints lint also now resolves a governed_by component through the compiled policy pages instead of a store that excluded them, matching the explainer. PR #282.

The name/range separator in yarn.lock parsing was read as the last @ in the descriptor, so an npm alias such as @typescript/native@npm:typescript@7.0.2 was misparsed and matched no manifest entry, reporting a dependency the lockfile pins to one version as unpinned. cacheVersion moves to v265. PR #282.

v0.4.15

PyPI wheels now install cleanly on RHEL and Rocky Linux 8

pip install enola failed on RHEL and Rocky Linux 8 with "no matching distribution found"; Linux wheels now carry an extra tag so older pip versions on those distros can see them.

  • FixedLinux PyPI wheels now carry both the PEP 600 tag and the older manylinux2014 alias, so pip 20.2.4 as shipped on RHEL/Rocky Linux 8 can find and install them.

Action: if pip install enola failed on RHEL or Rocky Linux 8, upgrading to 0.4.15 resolves it; 0.4.14's wheel cannot be fixed in place since PyPI won't accept a replacement file for a version it already has.

Implementation notes and pull requests

Installing 0.4.14 on Rocky Linux 8 failed with "no matching distribution found" despite its glibc 2.28 sitting well above the 2.17 the wheel actually needs: pip only learned the PEP 600 manylinux_<major>_<minor> tag spelling in 20.3, and RHEL and Rocky 8 ship pip 20.2.4, so the wheel that could run there was the one their pip couldn't see. Upgrading pip inside the container installed the identical file, which is what identified the cause. Linux legs now emit a compressed tag set — one file carrying both spellings in its filename and a Tag line for each in WHEEL, the same thing auditwheel does for the same reason. Verified by installing a dual-tagged wheel with stock pip 20.2.4 on Rocky 8. PR #280.

v0.4.14

Enola ships on PyPI as a prebuilt wheel

pip install enola now works directly, with prebuilt wheels for Linux, macOS, and Windows published automatically from tagged releases; the installer also adds support for opencode.

  • AddedEnola now publishes to PyPI as prebuilt wheels for Linux (x86_64/aarch64), macOS, and Windows — pip install enola — built automatically from tagged GitHub releases.
  • AddedThe installer supports opencode as a target, writing its plugin hook and a strictly-validated MCP config entry.
  • FixedCodex installation instructions in the README described the wrong MCP server setup.

Action: none — pip install enola is a new, optional install path alongside the existing ones. RHEL/Rocky Linux 8 users hitting "no matching distribution found" should install 0.4.15 instead; see below.

Implementation notes and pull requests

Linux wheels build inside manylinux_2_28 and are tagged manylinux_2_17, so the resulting binary requires only glibc 2.17 instead of whatever a generic Ubuntu runner links against — reaching RHEL and Rocky 8, Amazon Linux 2, Ubuntu 20.04, and Debian 11 that a glibc-2.34 build would exclude. The Go toolchain is mounted into the container from the runner rather than downloaded, so the compiler stays exactly what go.mod pins. Publishing runs only on a tag push through PyPI Trusted Publishing, downstream of and gated on the GitHub release succeeding, so a PyPI failure can't block or unpublish it; a workflow_dispatch path rehearses the full build against TestPyPI without touching GitHub releases or production PyPI. PR #279.

opencode reads none of the files the other install targets write. Its hooks are registered as a plugin, since opencode has no hook configuration of that shape, and its MCP entry is identified by shape rather than a marker field — opencode validates its config strictly, so enola writes no unknown keys and never touches an entry it didn't write. PR #278.

Documentation: corrected the Codex installation instructions in README.md, which described the wrong global MCP server setup. PR #277.

v0.4.13

A new explainer measures what importing your Python package actually costs

A new import-closure explainer reports what importing a Python package executes, several Python and C++ call-resolution bugs are fixed, and three Go extractor passes get faster.

  • AddedA new import-closure explainer reports what import yourpackage actually executes in Python, including the package __init__.py files no import statement names but Python runs anyway, and flags the ones that dominate the closure.
  • FixedC++ call resolution now uses include visibility, qualified names, unique project-wide matches, and explicit receiver types to resolve more calls instead of relying on speculative matches.
  • FixedPython absolute imports resolve to the exact module they name rather than the enclosing package directory, and forty-two standard-library modules missing from Enola's own list no longer misreport as third-party dependencies.
  • PerformanceThree Go extractor passes now match tree-sitter queries directly and share a single pass instead of each walking the syntax tree separately.

Action: re-pin baselines taken on a C++ or Python repository — extraction caches from earlier releases are invalidated, so the first snapshot after upgrading re-extracts.

Implementation notes and pull requests

C++ calls on explicitly typed parameter and local-variable receivers now resolve to a uniquely declared method on that type; auto-deduced, template-wrapped, unknown, and ambiguous receivers stay unresolved rather than guessed, and a receiver call binds only when the method actually exists. cacheVersion moves to v260. PR #273.

A Python absolute import now binds to the module it names rather than the package directory that also matches one segment shorter, and a same-package absolute import resolves to its sibling file instead of collapsing to the shared parent directory; inheritance bound through a relative import, latent until now, also resolves. Import facts carry Props["deferred"]=true when the import does not run at module-import time — a function- or class-body import, or an if TYPE_CHECKING: block — distinguishing what's actually on the import path from what's merely visible. The Python stdlib set is completed against sys.stdlib_module_names: forty-two modules were missing, including atexit, binascii, colorsys, optparse, and curses, and a stdlib name absent from that set was classified as a third-party dependency. The new import-closure explainer reads a graph built at file granularity from these same dependency facts, because the module graph other explainers use resolves both ends of an edge up to the enclosing package and can't say which modules beneath one were paid for; per top-level package it reports modules loaded, share of the package, depth, and third-party packages on the path, and at severity 0.7 the package __init__.py files that dominate the closure — the barrel worth splitting. cacheVersion moves to v264. PR #274.

Three recently added Go extractor passes each walked a syntax tree to find a handful of nodes; go-tree-sitter allocates a heap object for every node it returns, so each pass paid an allocation for every node stepped over on the way to the few it wanted. The three passes now share a single match of the tree instead of walking it three times. Extractor output is unchanged — the corpus sweep is byte-identical in facts on every comparable repository — so no cacheVersion moves. PR #275.

v0.4.12

GraphQL clients and servers now connect through the same route

Enola now reads GraphQL across TypeScript/JavaScript, Vue/Nuxt, Ruby, and Hasura, matching client operations to the server root fields they call and flagging schema operations nothing calls.

  • AddedGraphQL extraction across a wide range of servers, including Apollo Server, GraphQL Yoga, Mercurius, NestJS GraphQL, TypeGraphQL, Nexus, Pothos, graphql-ruby, and Hasura metadata.
  • AddedGraphQL extraction across clients, including Apollo Client, Relay, urql, graphql-request, Nuxt Apollo, and plain fetch-based requests.
  • AddedClient operations and server root fields now share a canonical route name such as Query.books or Mutation.addBook, so Enola can connect a client to the schema it consumes and report GraphQL operations nothing calls.

Action: none.

Implementation notes and pull requests

Framework-aware GraphQL extraction spans TypeScript/JavaScript, Vue/Nuxt, Ruby, Hasura, and standalone .graphql/.gql documents. Server coverage includes Apollo Server, GraphQL Yoga, Mercurius, express-graphql, graphql-http, GraphQL Tools and GraphQL.js's buildSchema, NestJS GraphQL, TypeGraphQL, Nexus, Pothos, graphql-ruby, Hasura metadata, and schema-first documents with server provenance, covering Query, Mutation, and Subscription root fields including SDL type extensions and common code-first aliases. Client coverage includes Apollo Client and gql tags, Nuxt Apollo inside <script> and <script setup>, Relay graphql tags, urql, graphql-request, plain fetch requests, standalone operation documents, and Ruby operation strings, including anonymous operations, aliases, directives, fragments, variables, and subscriptions. Client operations and server root fields resolve to a canonical route carrying type=graphql, a role of client or server, a registered GraphQL source, and a root-field name; the cross-repository signal matches client operations to server root fields on that exact name and contributes GraphQL coverage and consumption verdicts without mixing GraphQL routes into HTTP matching. Validated against NestJS's official GraphQL example, GraphQL.js, Nhost, and Relay patterns alongside framework-specific unit and regression fixtures; the full Go test suite, go vet, gofmt, and golangci-lint pass. Server coverage for JVM, Python, .NET, PHP, and Go GraphQL frameworks is a known follow-up. PR #272.

v0.4.11

A Vue component's template is code, and enola now reads it

Vue and Nuxt extraction no longer stops at the <script> block: a component or handler used only from a template no longer reads as an orphan.

  • AddedVue and Nuxt template interpolations and directive values resolve against the SFC's own declarations and component tags, so references from a template become calls edges seen by traverse, find_path, impact_analysis, and orphan analysis.
  • Added<script setup> compiler macros (defineProps, defineEmits, defineSlots, defineModel, defineExpose, defineOptions) are recorded as the component's public surface.
  • AddedNuxt file-based routing covers more page extensions and drops route-group and named-view/client/server suffixes from the URL; literal Vue Router records compose into page routes with handled_by component edges.
  • ChangedExtraction caches from earlier releases are invalidated for Vue and Nuxt repositories, so the first snapshot after upgrading re-extracts.

Action: re-pin baselines taken on a Vue or Nuxt repository — the new template edges can resolve orphan findings and surface coupling that was previously invisible.

Implementation notes and pull requests

A Vue Single File Component's template is where a handler is wired to a button and a child component is placed, so a component used only from a template read as an orphan and a @click handler read as dead. Interpolations and directive values are now resolved against the SFC's own declarations, and component tags through default imports, named imports, local aliases, and unambiguous Nuxt components/ auto-import conventions; resolution is narrow where it cannot be sure, so HTML text, native tags and CSS tokens are not read as code, and a tag two Nuxt layers could both supply stays unresolved rather than binding to whichever was walked first. <script setup> macros land in vue_macros with a boolean per macro; statically declared prop, emit, slot, model, and exposed names land in the matching vue_*_names lists, with generic declaration text kept in vue_contract_types, and macro-looking text inside a comment or string is ignored. Nuxt file-based routes now cover every supported page extension rather than .vue alone, omit route-group directories from the URL, and drop named-view/client/server suffixes; Vue Router createRouter({ routes }) records with literal paths emit page routes, nested children compose onto their parent, and a statically or lazily imported component becomes a handled_by edge, while the configuration file keeps its router_config marker fact. A Nuxt auto-imported useXxx() call rebinds to its unique exported declaration under composables/, left unresolved when two layers export the name. PR #270.

cacheVersion moves to v258: every Vue and Nuxt repository gains facts and existing ones change shape, so extraction caches from earlier releases are invalidated and the first snapshot after upgrading re-extracts. PR #271.

v0.4.10

Facts and evidence get stable ids, and the file formats are documented

Every fact and every piece of insight evidence can now carry a stable id computed at write time, two findings that cited names nothing in the snapshot carried are fixed, and the facts/insights/receipt file formats have a written reference.

  • AddedFacts and insight-evidence citations can carry a stable id, computed when facts.jsonl and insights.json are written rather than stored on the fact itself.
  • FixedTwo findings cited names no fact actually carried; both now cite the real fact and its location.
  • FixedThe served insights.json emitted null where the written file emitted [].
  • AddedA new docs/schema/ reference documents the facts, insights, and receipt file formats, plus an INTEGRATING.md guide for building on Enola's output.
Implementation notes and pull requests

Ids are computed when facts.jsonl is written and never stored on a Fact: the struct has 418 dependents, the serializer has 3, and an id no internal reader can see cannot change an internal answer. A relation carries target_id only when its target name resolves to exactly one fact. Also corrects three documented claims the emitted data contradicted: repo is set on every fact including single-repo snapshots, declares points from the declared fact to its module, and the identity triple is a convention rather than a uniqueness guarantee. PR #267.

A citation that resolves to nothing is not a failure, so fact_id is omitted rather than forced: some findings are about absence, such as a route naming a handler not defined here, and that is the finding being true. Two findings previously cited names nothing carried: an outbound-integrations finding cited a display label with the HTTP method glued onto the path, and a layer-violation finding cited its import edge without the file it came from, so it could not be told apart from the same import elsewhere. Both now cite the fact and its location. PR #268.

Documentation: docs/schema/ documents the facts, insights, and receipt formats against what the code actually emits, correcting comments and doc claims in internal/facts/model.go that no longer matched the wire format. INTEGRATING.md is a new guide for consuming Enola's output directly. PR #265, PR #269.

v0.4.9

The dashboard runs in the background, and changes route to who owns what they touch

enola dashboard now starts detached by default, and a new --reviewers flag routes a change to the owner of what it touched using the import graph rather than just the commit log.

  • Addedenola dashboard starts detached in the background by default and adds dashboard status and dashboard stop to manage it; --generate, --refresh, and check --write print a one-line hint pointing at it.
  • Added--reviewers reports, per module a change touched, who owns it and whether the author is a stranger to something they own an import of.
  • FixedA repository-scoped fact's directory label was read as a file the change touched, wrongly opening routing with a note about the root module on changes that only moved coverage counters.
  • FixedA wrapper binary built on Enola reported "dev" in doctor, its SARIF driver, and its dashboard instance record instead of its own version.
  • ChangedDocumentation reorganized across the CLI, constraints, clusters, providers, intent, and glossary references, with new doc-lint checks enforcing structure.

Action: enola dashboard now returns immediately instead of blocking; pass --foreground if you relied on the old behavior.

Implementation notes and pull requests

dashboard gains status/stop subflags and --foreground, detaching by re-executing the same binary in the background. pkg/cli.ShowDashboardHint is a single shared hint builder used by both --generate/--refresh and check --write so the wording can't drift between them, suppressed in CI via ENOLA_NO_PROMPTS/TTY detection. The Activity tab reorganizes active sessions above collapsed runtime details, and the extraction-quality panel adds a prefilled "Report extraction issue" link when parse errors are present. Follow-up work distinguishes normal shutdowns in server error handling and adds unit tests for shutdown and dashboard instance-tracking behavior. PR #260, PR #263.

--reviewers pairs a module's owner against its importers using the major-minor-dependency relationship from Bird, Nagappan, Murphy, Gall and Devanbu, "Don't Touch My Code!" (ESEC/FSE 2011), which needs the import graph rather than just the commit log to spot. It is never graded and not part of the snapshot: ownership is read from git history, so it moves while the code stands still, and a history is per-machine, so the same commit can grade differently on a laptop holding a local record than in CI. Off by default; without the flag no author name is read. PR #261.

An extraction fact carries a repository's directory name in File rather than a path, and changedFiles handed it to callers that read File as something somebody edited, so routing opened with a line about the root module on any change that moved an extractor's coverage counters. Filtered in changedFiles rather than at the source, since File is part of a fact's identity in every snapshot and diff and AttachGuidance reads the same list. PR #262.

pkg/command read internal/version, which only Enola's own release stamps set, so a wrapper binary reported "dev" in doctor, the SARIF driver it uploads under, and its dashboard instance record simultaneously. cli.Binary carries the version now, and whether a binary can upgrade itself is derived from the subcommands it dispatches for itself rather than a new field. PR #264.

Documentation reorganized across README.md, docs/CLI.md, docs/CLUSTERS.md, docs/CONSTRAINTS.md, docs/FIRST-CHANGE.md, docs/BLIND-SPOTS.md, docs/EXPLAINERS.md, docs/GLOSSARY.md, docs/INTENT.md, and docs/PROVIDERS.md, with new internal/docslint tests enforcing command and count consistency. PR #258, PR #259.

v0.4.8

Declared dependencies join the constraint vocabulary, and every excuse gets counted

A new rule can require every external dependency to be pinned to a version, and enola constraints ledger reports how much of the declared architecture is being excused rather than obeyed.

  • AddedA dependency member kind and a shipped supply-chain recipe can require every declared external dependency to be pinned to a version.
  • Addedenola constraints ledger reports, across the whole rule set, what share of breaches are excused, the oldest excuse nobody has revisited, and which suppressions or exemptions matched nothing in the current snapshot.
Implementation notes and pull requests

supply-chain binds to nothing the repository has to name: its role selects kind: dependency with where: {type: package, pinned: false} over the package facts the manifest extractor measures, and the law is a forbid_fact over that selection — no new rule form, since "this set must be empty" already existed. A repository with no manifest facts gets a loud 1.0 "selector cannot be evaluated" finding rather than silent compliance. cacheVersion moves to v257. PR #257.

constraints ledger reads the same signatures every suppression and exemption already required — owner, reason, date — and turns them into a ratio instead of a pile of individually reasonable-looking exceptions. A rule most of whose breaches carry a signature is a rule to reconsider; an excuse untouched for months is a reason that has likely stopped being true; and a suppression that matched nothing in the current snapshot is marked inline and counted on the summary line, giving the coverage census's existing unused suppression and exemption matching nothing facts a denominator. PR #257.

v0.4.7

Built-in providers respect what you told the engine to ignore, and layer rules stop flagging wiring code

A built-in fact provider now skips files your ignore rules already excluded instead of building facts for them and throwing the result away, and the layers explainer adds a neutral classification for dependency-injection and wiring code that was never meant to sit in a dependency order.

  • PerformanceA built-in fact provider now receives the engine's ignore-glob decisions and skips an excluded file before reading it, instead of building the fact and discarding it afterward. A Rails monolith snapshot dropped from 440s to 193s.
  • AddedLayer definitions gain a neutral classification for code that is classified but never ordered, such as dependency-injection wiring — one wiring package had been responsible for most of a repository's reported layer violations.
  • AddedNuxt, SvelteKit, and PHP layer taxonomies.
  • FixedThe Android clean-architecture taxonomy now orders data below domain, matching Android's own documented guidance; the previous ordering flagged legitimate domain-to-repository calls as violations.

Action: re-pin baselines if your project uses the layers explainer against Android, Nuxt, SvelteKit, or PHP code — violation counts will change.

Implementation notes and pull requests

The Rubydex provider's remaining cost after the previous release's walk fix was work done only to be thrown away: on a Rails monolith it built 2,155,664 facts and the repository's own ignore globs rejected 1,520,163 of them, 71%. Because a built-in provider runs in the engine's own address space, Collect now takes the engine's Ignored set directly, skipping an excluded document before its definitions are read and an excluded reference before its fact is built; the surviving 1,800,829 facts are byte-for-byte identical to before. Indexing stays untouched — vendored gems still feed the indexer, which is what lets a workspace constant resolve outside it — only emission is filtered. PR #255.

A Hilt di package or a Spring @Configuration package is referenced by, and references, every layer it wires together — that is its whole job — so any level it is given turns half of those edges into a violation; giving one Android taxonomy's di package a level had produced 61 of thingsboard's 75 findings and all 7 of dubbo's. The new Neutral flag classifies such packages without ordering them, applied to Android's di, iOS's config, Nuxt's server and wiring directories, and PHP's listeners, subscribers, and service providers. The Android layout itself now places data below domain: nowinandroid's own architecture guide states the data layer sits at the bottom, and every use case in it imports a repository, so the previous ordering had reported three of those imports as violations on Google's own reference application. PR #256.

v0.4.6

A Rubydex reference could not stop referencing itself

The Rubydex Ruby fact provider could hang indefinitely on certain code shapes. A single self-referencing constant in a real Rails monolith reproduced it every time.

  • FixedRubydex's declaration-path walk could loop forever when a reference's end column, plus its separator, equaled its own start column, making the reference its own predecessor. A candidate now qualifies only when it is a different reference whose end line matches the line being walked.

Action: a Ruby snapshot that previously hung or timed out should be retried.

Implementation notes and pull requests

The Rubydex provider could stop returning entirely: on a Rails monolith it was killed at 26 minutes holding 6.7GB with nothing written, on a tree the engine itself indexes in 24 seconds, and a second Rails application hung the same way. One reference caused it on that monolith — <LibDDWAF> in the libddwaf gem, spanning six lines — because the path walk compared a candidate's end column plus separator against the current segment's start column alone, with no check that the candidate was a different reference on the line being walked. The regression test constructs the reference shape directly rather than depending on a workspace, and fails against the prior code by never returning within its deadline; facts are byte-for-byte identical on repositories where both versions complete. The graph underneath was never the slow part: Rubydex indexes the same monolith in 13.5 seconds through its own CLI, and the fixed provider's index and resolve phases cost 9 seconds. PR #254.

v0.4.5

Detection stops re-walking the tree, and constraint verdicts speak CI's language

Every extractor's file detection now reads from the same walked file list the engine already built, instead of re-scanning the tree with its own depth limit, recovering files that previously sat past an old scan boundary. The constraints program also gained CI-native output formats and richer explain results.

  • FixedLanguage detection no longer re-walks the tree with a per-extractor bounded scan; it now reads membership over the file list the engine already walked, so a marker or source file counts wherever in the tree it sits.
  • AddedA vendored-candidates explainer reports directories that look like an in-tree copy of another project — a licensed subdirectory under a conventional dependency folder — informational only and never gating.
  • Fixedconstraints init and constraints explain no longer report a successful run as a failure. Both used to return without exiting, so the outer argument parser mistook the completed run for an unknown command.
  • AddedThe constraints program writes SARIF and CI annotations alongside its existing text and JSON formats, constraints explain reports the blast radius of moving a file out of its part, and every verdict now carries a "could not see" line naming what the provider census could not resolve.
  • ChangedDocumentation reorganized, including a dedicated Rails guide and a repository CHANGELOG.md mirroring this page.

Action: detection behavior changed across every language — regenerate snapshots and re-pin baselines.

Implementation notes and pull requests

Detection used to be a bounded re-walk inside every extractor, and every bound was a cliff: dotnet/runtime carries 3,270 C/C++ sources with none inside the three levels the C++ detector scanned, so the language was absent from the graph entirely. plugin.FileListDetector answers instead from the names the engine's own walk already collected, pruning only node_modules, vendor, testdata, and dot-directories — shorter than the engine's configurable ignore globs, since this decides what a language is spelled by rather than what gets indexed. Each migrated extractor keeps exactly one decision, its file-name predicate, so its bounded Detect and its unbounded walk cannot drift apart. cacheVersion moves to v253. PR #250.

Both constraints init and constraints explain returned on success rather than exiting, so main's flag loop re-read constraints as an unknown argument and printed "did you mean constraints?" before exiting 1 on a run that had already produced a correct report. The exit now happens at the dispatch site, where every other case in that switch already exits; lint and mine are unaffected. PR #251.

Eight additions to the constraints program follow from one property: the verdict has four writers — text, json, sarif, annotations — and all four read the same computed verdict, so nothing is recomputed per writer. SARIF carries one rule per declared rule id; annotations place every positioned finding on its file and line. constraints explain now answers incoming edges and reports the blast radius of moving a file out of its part. Provider facts are cached, keyed by content for per-file providers and by library version, file-set digest, and lockfile for the built-in Rubydex provider, with the receipt reporting how much was reused. since and growth read git blame per witness file, cached by blob hash. Two providers spelling the same receiver differently now produce one edge instead of two. Every verdict carries one "could not see" line and a census of what each provider contributed. A 281-case benchmark suite passes at 281 with no case regressed; the provider cache was measured against a Rails monolith of roughly 1.7 million facts, where providers previously cost 249 of a 292-second run. cacheVersion moves to v256. PR #252.

Documentation reorganized: a repository-root CHANGELOG.md now mirrors this page at the resolution a reader of the repository needs, and constraints, provider, and Rails documentation split into dedicated files. PR #253.

v0.4.4

Laws written in Ruby, Angular, and the rule forms only a graph can state

Architecture constraints can now be written as Ruby sentences, parsed and never executed. The rule vocabulary reaches 21 forms, including the ones that need the whole graph. Angular is read the way the framework works — decorators, dependency injection, composed routes and templates. Convention recipes ship with the binary, so a repository adopts them in one command.

  • AddedConstraints may be written in Ruby. Files ending .rb under enola/constraints/ are parsed with the same grammar the extractor uses and never executed, compiling to exactly what the YAML loader produces — same merge order, same evaluator, same lint surface. Nineteen verbs cover the rule forms.
  • AddedAngular extraction: @Component/@Directive/@Pipe/@Injectable/@NgModule classes and the role the container gives them, constructor and inject() dependency injection as edges, routes composed outward from forRoot/provideRouter including lazy loadChildren, templates joined to the component that owns them, @NgModule composition arrays, and requests made through an injected HttpClient.
  • AddedRule forms only a graph can state: storage_stays_home, cap_runtime, require_consumer, unique_across, require_governed, forbid_cycles and independent — taking the vocabulary to 21 forms.
  • AddedConvention recipes ship with the binary. enola constraints init binds every recipe whose roles resolve to directories the repository has — rails-conventions, rails-strict, vanilla-rails, layered, ports-and-adapters and more — naming what it could not bind and guessing nothing.
  • AddedA component may select by ancestry (ancestor:), read transitively over resolved inheritance. Rubydex joins Prism as a Ruby fact provider carried by the binary itself.
  • Addedsince and growth put a time dimension on a rule, and every edge or cycle verdict names its smallest cut.
  • FixedA snapshot no longer depends on whether a previous snapshot exists: markdown links resolve against the walked file list rather than the filesystem, and the output directory is ignored at any depth rather than only at the repository root.
v0.4.3

Rails route parity, fewer query-loop false positives, and utoipa routes

Rails route derivation matched Rails across a 3,500-route application. This release also adds dead-method detection and utoipa routes, improves query-loop analysis, and cuts a 22-repository cluster rerun from 891s to 224s.

  • AddedRails routes now derive the way Rails derives them — plural/singular naming, scope module:/controller: composition, leading-slash escapes, and the hash-rocket mount form — checked against ActionDispatch::Routing::RouteSet line for line on a 3,500-route application.
  • Addeddead-methods reports Ruby methods that no call edge in the graph reaches.
  • Fixedquery-loops now follows a relation back to where it was built and accounts for declared preloads, removing two classes of false positive.
  • AddedExternal fact providers can contribute lint findings through the same seam as other facts. A reference ESLint provider ships as a worked example, and constraints mine --scaffold-eslint writes a mined convention out as an ESLint rule.
  • AddedRust services using #[utoipa::path] (utoipa_axum) now have their routes extracted. These routes were previously invisible because there is no .route(path, …) call to read.
  • PerformanceCluster generation across multiple repositories does each shared step once instead of once per repository. A 22-repository, 1.56M-fact cluster went from 891s to 224s on a rerun.

Action: extractors changed, so regenerate snapshots and re-pin baselines.

Implementation notes and pull requests

Rails expectations ran through ActionDispatch::Routing::RouteSet on actionpack 8.1.3 and 8.1.1. Handlers naming an existing controller rose from 3,482 to 3,536, with no route assigned to the wrong controller. The Ruby extractor also adds JSON:API resource classes, symbol-based method references, and receiver chains in block bindings. dead-methods stays limited to surfaces whose callers the graph can see; every result is a candidate, not a verdict. Cache versions v216 through v223. PR #244.

utoipa_axum::routes!(find_team) registers a handler without repeating the path stored in #[utoipa::path(...)]. Before this change, crates.io's API stored 8 routes where it serves 59. The extractor handles multiple verbs and context_path, and skips non-literal paths rather than guessing. OpenApiRouter::nest() prefixes are not yet supported. Cache version v224. PR #245.

v0.4.2

Fact paths fixed on Windows; docs checked against the code

A declared layer order could silently match zero modules on Windows. Every fact path is now forced to forward slashes regardless of host, and a new CI check catches documentation that has drifted from what the code actually does.

  • FixedFact paths — module names, layer classification, ignore globs, cross-repo linking — always use forward slashes now, even when generated on Windows. The host's backslash paths previously stopped matching everywhere paths get compared, so a declared layer order could report itself valid while classifying zero modules.
  • FixedThe query_insights MCP tool's explainer= filter description named eleven explainers while the engine ran sixteen. Five — intent, constraints, domain, query-loops, entry-points — were invisible to an agent deciding what it could ask for, even though their findings already existed.
  • FixedSeveral docs pages had explainer, tool, and rule-form counts that had silently drifted from the code, and two links pointed at files that had been renamed. A new CI check (docslint) now catches this going forward instead of publishing it.

Action: none for Unix users. Windows users should regenerate snapshots and re-check declared layer orders — matches that were silently empty before now report correctly.

Implementation notes and pull requests

path/filepath is the host filesystem's dialect, and its output carries backslashes on Windows — filepath.Dir("src/lib/x.ts") returns src\lib there, because Clean ends in FromSlash. Every downstream consumer that splits fact paths on "/" silently stopped matching, which is how a declared layer order came to classify 0 modules on Windows while reporting itself valid (issue #242). A new internal/factpath package covers the operations that build a path — Dir, Clean, Join, Split, Match — normalising to forward slashes on every host, with a contract test enforcing the split from plain path/filepath use. Cache version v215 to v216. PR #243.

docslint checks prose against the code it describes: that a number in the docs equals a number derived from config.KnownExplainers, cli.OSSTools(), intent.RuleForms and layers.TaxonomyNames(), that a linked path exists, and that a page carries the sections its own index promises. docs/EXPLAINERS.md had claimed both "fifteen explainers" and "sixteen explainers" three paragraphs apart. A frozen number can be waived by phrase, with a reason, so a historical benchmark figure is not flagged as wrong because the code moved on. Runs in milliseconds with no engine or CGO, so it sits in .githooks/pre-push as well as CI. PR #241.

v0.4.1

AsyncAPI channels and Kafka call sites, cross-service

Messaging contracts declared in AsyncAPI now join the graph the same way HTTP routes do, so a missing consumer or an undeclared producer shows up as a finding instead of staying invisible.

  • AddedAsyncAPI spec files (asyncapi.yaml/.yml) parse into declared channels, messages, and their schemas.
  • AddedKafka producer and consumer call sites extracted in Go, TypeScript, and Scala, matched to declared channels by topic.
  • AddedA new messaging-coverage explainer reports channels nobody produces or consumes, and messaging code with no declared contract — the messaging counterpart to unused-routes.
  • AddedWorks across repositories: a producer in one service and its consumer in another link through the same multi-repo snapshot that already joins HTTP routes to their clients.

Action: none. Existing snapshots pick up messaging facts on the next generate_snapshot.

Implementation notes and pull requests

A messagingcontract binder links declared AsyncAPI operations to the producer and consumer call sites the extractors find, the same shape as the HTTP route binder. messaging-coverage turns those binding verdicts into three finding kinds, available through query_insights(explainer="messaging-coverage"): a declared channel with no producer, a declared channel with no consumer, and messaging code with no declared contract at all. This is the sixteenth explainer added to config.KnownExplainers. The cross-repo fixture pairs a Go producer service (svc-billing) with a Go consumer service (svc-orders) sharing one Kafka topic across two repositories. PR #239.

v0.4.0

Declared architectural law

Architecture rules you write down are now verdicted against the measured graph, checked before an edit lands, and gateable in CI. Tools Enola does not ship can contribute facts through one seam, and the recorded history moves between machines.

  • AddedA constraint vocabulary: components select facts, and rules state what may not reach what. Declarations live in enola/constraints/*.yaml beside the code they govern, so CODEOWNERS can route each file to the team that owns it.
  • Addedenola constraints lint reports every validation problem with its file context and resolves each component against the snapshot, so a selector that matches nothing is caught while you are writing it rather than by a rule that passes vacuously.
  • Addedenola constraints mine proposes candidate rules from regularities the graph already shows, with their evidence and their exceptions. Proposals for review, never self-adopting law.
  • Addedenola plan answers before the edit: which rules govern the change, its blast radius, and — for a patch — the verdicts that would appear, evaluated over a scratch copy. Nothing is written.
  • Addedenola endpoint walks a URL to what changing it reaches: the controller serving it, the models it touches, the models associated with those, and the tables behind them.
  • Addedenola history push|pull|verify|gc shares the recorded architecture history between machines through a plain directory store — a Git repository, a shared mount, an S3-synced folder. Content-addressed and tamper-evident; verify walks every chain and names gaps and tampering.
  • AddedA fact-provider seam: an external tool can contribute facts in the store's own schema. Fail-closed end to end — one invalid line discards that provider's whole output, every fact must declare how it was resolved, and the seam stamps provenance itself.
  • AddedThree MCP tools: constraints_for, plan_check and endpoint_impact. Four explainers: constraints, domain, query-loops and entry-points.
  • AddedRuby reads db/schema.rb and db/structure.sql, Stimulus and Turbo bindings declared in markup, and model associations. TypeScript adds constructors, decorators, accessors and extends bases; Go adds interface methods.
  • Changedhotspots now reports its top 20 per repository. It was around 80% of every finding Enola produced, which buried the other explainers; corpus findings fall from 29,633 to 9,131.
  • Changed--fail-on refuses a name no explainer has. It previously accepted anything and enforced nothing, so a typo in a CI config was indistinguishable from a passing build.
  • FixedA plain file named enola at a repository root no longer aborts the snapshot — building a binary by that name in your own repository was enough to make generation fail.

Action: extractors changed, so regenerate snapshots and re-pin baselines. Two things may need attention: if a --fail-on value in CI is misspelled, that job will now fail rather than silently enforcing nothing — which is the point. And a service whose label came from its directory name may be renamed, since labels derive from the Git remote at a repository root.

Implementation notes and pull requests

Constraint rules are verdicted against measured facts rather than file paths: a component resolves to the facts its match patterns select, and a rule states one of twelve enforceable forms over components — forbid, the transitive forbid_reach, allow-only, protect, private and the rest. A breach is set membership over measured edges, so it carries confidence 1.0 and --fail-on=constraints can gate on it. PR #234, PR #235.

plan --patch applies a unified diff to a scratch copy, re-extracts, and reports the constraint verdicts the patch would introduce and resolve, with a witness path for each. The working tree is never written and a malformed diff is an error rather than a partial application. constraints mine ranks candidates by confidence multiplied by support and renders each as a rule you can paste, deliberately in advisory mode. PR #236.

The provider seam runs an executable once with --version and once with the repository path, reading facts as JSONL. Every fact must carry a resolution_level from a closed vocabulary — the provider's own account of how it resolved what it emitted — and runtime-observed must name its observation channel while declared must name the signature file that claimed it. Provider facts may not collide with an extractor fact's identity, and each run lands in the receipt's census including providers that contributed nothing and why, so a delta whose two snapshots ran different provider sets is never graded as a full verdict. PR #237.

The shared history store is plain files, content-addressed, with each revision carrying the machine that recorded it. pull imports what other machines pushed, so blame on a second clone attributes facts it never generated itself; verify walks every chain and names gaps and tampering; gc prints its retention plan and deletes only with --apply, on record. Working revisions — snapshots of uncommitted trees — are skipped unless --all asks for them. PR #238.

Seventeen extractor changes, cacheVersion v198 to v215. Ruby: db/structure.sql and db/schema.rb read as the database's own account of the schema and folded onto the model that reads each table; Stimulus data-controller/data-action and literal Turbo frame ids recorded at markup-declared; a namespace's table_name_prefix applied to derived table names only; a declared self.table_name now corrects the model's own fact instead of emitting a second one; an abstract class no longer claims a table it does not have; importmap-rails apps detected as JavaScript projects. TypeScript: constructors, member decorators, get accessors, self-assigned fields, parameter presence, extends bases and CommonJS export assignments. Go: interface methods declare symbols. association joins the fact kinds, which is what lets endpoint reach the tables behind a URL.

hotspots also stops counting has_method containment edges as calls, so a large class no longer reads as a pinch point for being large. The --fail-on accepted set, its help text and the default explainer list now derive from one list, which is how four gateable explainers came to go unadvertised in the first place. PR #240.

v0.3.21

Nothing fails until you name it

The check gate no longer ships an opinion about which findings should break a build, and repository labels come from the Git remote so a second clone compares against its own baseline.

  • Changedenola check defaulted to failing on cycles. It now fails on nothing until --fail-on names it — a cycle is exactly measurable and still contested, so shipping it as a default asserted a rule about a codebase Enola had not looked at.
  • AddedA run that enforced nothing now says so, rather than printing a bare pass. A silent green is indistinguishable from a broken gate.
  • ChangedA repository's label comes from its Git remote when the indexed directory is that repository's root, and from the directory name otherwise.
  • FixedA label mismatch between a snapshot and its baseline is now blocking rather than graded, so a worktree no longer reports its entire graph as added and removed.

Action: if you relied on the old default, add --fail-on=cycles to your CI job explicitly. Service names taken from a directory may change where the Git remote differs.

Implementation notes and pull requests

The default policy was cycles — exact, but an opinion: Go's compiler already forbids package-level cycles, and a Rails application resolves most of its graph at runtime and does not read them as defects. Two consequences follow that the diff does not show. An unenforced run has to state that it enforced nothing, so the session hook now reports the exact findings no policy covered and leaves the verdict to the reader. And informational findings are never gradeable: layers emits an exact finding announcing a declared order, so without that rule the very commit declaring one would fail on its own description. Enola now declares its own layers and gates its CI on them rather than on cycles.

The repository label is part of every fact's diff key, so a worktree or a second clone under another name shared no keys with its baseline and the delta reported the whole graph as added and removed — while comparability, which prefers the remote, waved it through as the same repository. The label now comes from the remote too, but only at a repository root: a remote identifies a root and nothing under it, and labelling sub-directories with the parent's name merged separate graphs, since nodes are name-keyed. PR #233.

v0.3.20

Express sub-router mounts compose across files

A router declared in one file and mounted in another now reports its routes at the path they actually serve, which is the layout most Express services use.

  • FixedRoutes from a router declared in routes/*.js and mounted in index.js were dropped entirely, because a per-file pass cannot resolve the mount. A repository-wide pass now propagates mount prefixes from the application roots.
  • FixedA same-file mount onto an unmounted parent emitted a fragment path; it now emits nothing. An extensioned ./x.js specifier now resolves to the x.ts it names.
  • Fixedinstall and uninstall printed the full plan twice when --yes left no prompt between the preview and the result.

Action: cacheVersion moves to v198, so cached TypeScript snapshots re-extract on the next run.

Implementation notes and pull requests

A repository-wide fixpoint propagates mount prefixes from the application roots, the same shape the Go and Axum extractors already use for their own mounts. It covers ESM and CommonJS, renamed named exports, factory-returned routers, transitive nesting and a router mounted twice. A non-literal prefix, an external module or an unmounted router still emits nothing: the pass can correct a path but never invent one. PR #232.

v0.3.19

Lower peak memory, and a verdict that says why nothing failed

Cold-run peak heap drops by a quarter to two fifths on large repositories, and advisory findings now name the reason they did not fail the build.

  • PerfCold peak heap falls 24% on a large C# repository and 40% on the Linux kernel; the --explain report falls 41% and 43%. Output is unchanged — the determinism suite is the proof.
  • ChangedThe extractor cache streams to disk instead of being buffered whole, and unsaved cache writes are skipped.
  • FixedAn advisory finding now says why it did not fail: below the confidence floor, or from an explainer no policy named. One line could not cover both, and the old text asserted "Confidence < 1.00" over findings printing exactly 1.00.

Action: none. Peak memory is a property of the run, not of the output.

Implementation notes and pull requests

Peak has to be sampled to be seen at all: the engine's end-of-snapshot log line runs after FreeOSMemory and describes the survivor, and Darwin keeps freed pages resident so RSS reports neither. On a large repository the peak is roughly six times the steady state. Three independent changes bring it down — skipping unsaved cache writes, pacing the collector for the duration of a snapshot, and dropping copies — none of which alters output. The GC pacing is scoped to the call rather than set at startup, because it is process-global and importing the engine must not re-pace a host process's collector; an explicit setting in the environment wins. The warm peak does not move, which is itself the finding: it is live retained bytes rather than collector headroom. PR #230.

Advisory findings now name their own reason for not failing. PR #231.

v0.3.18

Full Rails route tables, plus Grape

Rails routes are now read from every routes file, not just the top-level one, and Grape APIs are detected automatically.

  • AddedRoutes now come from every config/routes.rb and every .rb file under config/routes/ at any depth, with mount resolved to the mounted engine's own route file.
  • AddedRoutes carry a handled_by edge to their controller action, and Rails classes carry a rails_component prop.
  • AddedGrape API classes are now detected automatically by transitive inheritance, without needing a route file of their own.
  • FixedThe Ember util layer no longer claims Ruby lib directories in a mixed Rails/Ember repo — layer matching is by path segment, and lib isn't a real Ember layer.

Action: regenerate snapshots for Rails repositories to pick up the full route table and Grape detection.

Implementation notes and pull requests

Routes now come from every config/routes.rb and every .rb under config/routes/ at any depth, each parsed under the prefix that draws or mounts it, with mount resolved to the mounted engine's own route file. Routes carry a handled_by edge to their controller action, the walk descends into Ruby control flow, and classes carry a rails_component prop. Grape is identified by transitive inheritance rather than by a route file, computed from the superclass props the AST pass already emits: a codebase typically has one class inheriting Grape directly and hundreds inheriting that, and deriving the set from facts that already exist costs no extra I/O where there is no Grape. Also drops lib from the Ember util layer — it's wrong on Ember's own terms, and since layer matching is by path segment, it was claiming Ruby lib directories in a mixed repo and turning almost every edge out of them into a violation. PR #229.

README now documents how to use the GitHub Actions Marketplace CI gate directly. PR #228.

v0.3.17

Five grammar updates and a protocol bump

Java, Ruby, C, C++, and PHP move to newer tree-sitter grammars, the bundled MCP SDK updates to the latest protocol, and CI gets hermetic tests and pinned tool versions.

  • ChangedJava, Ruby, C, C++, and PHP tree-sitter grammars all updated to their latest upstream versions.
  • ChangedThe bundled MCP SDK updates to v1.7.0, tracking the 2026-07-28 protocol revision.
  • FixedGit-dependent tests no longer pick up the machine's own git config, so results stay the same in CI and locally.
  • ChangedCI now runs a dedicated probe test for every supported tree-sitter grammar, plus actionlint and pinned action versions.

Action: none. Regenerate a snapshot if you want facts extracted with the newer grammars.

Implementation notes and pull requests

Java grammar to v0.23.5 and Ruby to v0.23.1 (cacheVersion v195); C to v0.23.6, C++ to v0.23.4, and PHP to v0.23.12 (cacheVersion v196) — a stale cacheVersion is what forces re-extraction on upgrade rather than serving facts parsed under the old grammar. PR #221, PR #225.

The bundled MCP go-sdk moves to v1.7.0, tracking protocol revision 2026-07-28. PR #227.

Git-dependent tests previously inherited the host's ambient git config (user name, signing key, aliases), so a contributor's local setup could pass or fail a test CI would not; tests now run against a hermetic, test-local git config. PR #208.

CI now runs a probe test for every supported tree-sitter grammar rather than relying on the extractor test suite to notice a broken parser, adds actionlint, and pins tool versions instead of floating on a version range. PR #226. The repository's own CI now runs on enola's GitHub Action instead of hand-rolled steps. PR #220. Dependabot is configured for gomod and github-actions, PR #207, with ignore rules added to cut the churn it would otherwise generate, PR #222: actions/checkout to v7 (PR #214), actions/setup-go to v7 (PR #212), golangci-lint-action to v9 (PR #211), actions/upload-artifact to v7 (PR #210), softprops/action-gh-release to v3 (PR #215), and golang.org/x/sys to v0.47.0 (PR #217).

v0.3.16

Vendored OpenAPI specs no longer read as your own server routes

A mobile app vendoring the OpenAPI spec of a backend it calls was indexed as if it served those routes itself, manufacturing inbound edges and unused-route findings that don't exist.

  • FixedOpenAPI specs vendored by a native mobile client in a bare openapi/ directory were classified as server routes; they're now demoted to client routes when the repo looks like a native app and declares no server route of its own.
  • AddedA Next.js loading.tsx under a single dynamic segment now extracts as a route, matching any one-segment client call with a path parameter.

Action: regenerate snapshots for repositories that vendor OpenAPI specs to drop the phantom server routes and findings.

Implementation notes and pull requests

The OpenAPI extractor defaults every spec to role=server, demoting to client only for the openapi/client/ directory convention. Mobile apps vendor the specs of services they call in a bare openapi/ dir, so those were indexed as served endpoints: an Android app acquired inbound edges from three backends and 28 unused-route findings. A pre-link binder now demotes a repo's OpenAPI routes to role=client when the repo ships a native-app HTTP client (retrofit, urlsession, swift-endpoint, or dart) and declares no server route outside a spec. The native-app marker is positive evidence, so spec-first backends serving only from api/openapi/*.yml are untouched. Rewritten facts carry vendored_spec=true, keeping the change auditable and reversible. Also adds "loading" to uiRouteTypes: a Next.js loading.tsx under a single dynamic segment extracts as /{}, matching any one-segment client call with a path parameter. PR #206.

v0.3.15

The update-available notice actually gets written now

The check for a newer enola release only ever ran from two entry points, so most installs never got the cache file it depends on. It now refreshes from any command.

  • FixedThe update-available cache only refreshed from the session-start hook and the MCP server's boot; an install using neither never wrote it and silently never notified.
  • FixedA command that exits fatally with "snapshot produced no facts" now also prints the update notice, since that failure is often actually caused by running a build that's behind.

Action: none. The refresh still runs in a detached background process, so no command anyone waits on touches the network.

Implementation notes and pull requests

Refresh previously ran only in the session-start hook and the MCP server's boot goroutine, so an install using neither path never wrote ~/.enola/update.json and every reading surface read an empty file forever. Commands now spawn a detached child to perform the refresh, keeping the network off any path a person waits on. cmdFatal now prints the notice too: when the extractors have moved on, an old build detecting no language is what produces "snapshot produced no facts," so the failure most likely to actually be caused by running a stale build was the one path that stayed silent about it. PR #205.

v0.3.14

Uninstall leaves nothing behind

enola uninstall now removes the empty files and directories it leaves behind, instead of stripping its own configuration out and abandoning the shell.

  • FixedA hook config emptied by uninstall used to leave a stray {} file and its directory behind; both are now removed when a mutation empties them.
  • FixedThe heartbeat's output directory is now removed on uninstall too, when nothing else is left inside it.

Action: none. Existing installs are unaffected until the next enola uninstall.

Implementation notes and pull requests

Stripping enola's keys from a hook config left {} and its directory behind; a config the mutation empties is now removed outright, and all three removal paths prune what they emptied. Pruning stops at any directory whose existence gates the install: enola never creates those and only reads them to decide whether the tool is present, so removing an empty one would reverse a state enola never caused. PR #204.

v0.3.13

Dart and Flutter extraction, gin routes, and update notices

Enola now extracts Dart and Flutter apps — widgets, navigation, storage, and outbound calls — plus gin routes in Go, and tells you when a newer enola is available.

  • AddedDart and Flutter are now extracted: symbols, imports, the call graph, and complexity metrics, plus Flutter widget roles, navigation routes, outbound HTTP calls, and local/remote stores.
  • Addedgin routes are now extracted from the Go extractor, including Group prefixes and handlers registered via Any or the generic Handle.
  • AddedEnola now checks for a newer release at most every 12 hours in the background and prints a notice — never on a command a person is waiting on.
  • FixedA Dart project's pubspec.yaml was silently excluded by the default ignore globs, so every legal Dart import cycle was misreported and Flutter projects went undetected.
  • FixedA Dart call resolving to a same-named constant (not just callables) could misattribute high fan-in to the constant instead of the real function.

Action: regenerate snapshots for Dart, Flutter, and Go/gin repositories to pick up the new facts. No configuration change is required for the update notice.

Implementation notes and pull requests

Every Dart framework pass is gated on the file's own imports, which Dart makes a language guarantee rather than a heuristic — imports are mandatory and there's no ambient namespace, so a file that never imported go_router cannot be calling it, which is what makes it safe to match on short names like go and get. Flutter navigation routes carry type="page" so they stay out of the cross-repo server-route index: a screen's /users/:id is not an endpoint anything can call, and indexing it beside a real backend route of the same shape would manufacture an edge in the wrong direction. Dart permits circular imports between libraries outright, so module facts now carry pub_package and a Dart cycle is treated as a coupling signal rather than a build-order defect. Generated Dart (.g.dart, .freezed.dart, .mocks.dart) produces nothing, since it's the majority of files in a build_runner project and none of it is navigated. PR #202.

gin's Group("/prefix") mounts are joined onto their routes rather than concatenated: gin's own no-prefix idiom is Group("/"), which real servers lean on — ente's opens seven — and naive concatenation turns every route beneath one into a doubled-separator path nothing serves. Group is recognized by its argument being a string literal rather than by name, since chi declares a same-named Group that takes a function and mounts nothing entirely different in meaning. Verified on ente's server: 359 registrations, 359 routes extracted, and its Flutter client's 167 previously-unresolved call sites all resolve. The Dart package index itself is read directly from disk, the same deliberate bypass the OpenAPI and Symfony route readers already make, since **/*.yaml sits in the default ignore globs and a pubspec would otherwise never reach an extractor. Extractor cache versions v190–v193. PR #202.

The update check is cached in ~/.enola/update.json and refreshed at most every 12 hours from a detached session-start child and the server's own boot goroutine, so no command a person waits on ever touches the network. The notice shown to an agent deliberately names no upgrade command: an agent told to run enola upgrade mid-task would do so regardless of context, and it wouldn't affect a server process already running from the old binary's inode. The human-facing notice keeps the imperative instruction instead. PR #203.

v0.3.12

Scala extraction and native Codex hooks

Enola now extracts Scala — call graph, routes, storage, and complexity metrics tuned for its effect-typed idioms — and enola install --hooks wires real session hooks for Codex.

  • AddedScala is now extracted: symbols, the call graph, complexity metrics, and dependency injection across a fourth JVM language sharing packages with Java and Kotlin.
  • AddedPlay Framework routes (including conf/routes), Pekko/Akka HTTP, and http4s route trees are extracted, along with Scala storage facts and outbound clients.
  • Addedenola install --hooks now wires real SessionStart/Stop hooks for Codex, matching its native hook schema.
  • FixedScala anonymous class bodies (new T { ... }) are now walked instead of skipped, recovering thousands of declarations and calls that were producing false dead-code positives.
  • FixedA parenless Scala method reference — its uniform access principle drops the parentheses — now counts as a call edge instead of being silently dropped.

Action: regenerate snapshots for Scala repositories to pick up the new extractor. Codex users should run enola install --hooks and approve the new hook once via /hooks.

Implementation notes and pull requests

Scala's extractor assumes it does not own the repository: apache/spark holds 1,355 .java beside 6,275 .scala in the same packages, so the shared JVM package index now reads Scala too and the Java/Kotlin extractors invalidate on it. The grammar is pinned to tree-sitter-scala v0.24.1, the newest ABI-14 release, since v0.25.0+ are ABI 15 and the vendored runtime rejects them silently. Complexity metrics discriminate a loop from an effect-typed for comprehension by the yield keyword — measured at 60.4% effect-typed for for ... yield versus 9.7% for bare for across 8,119 production files — so sequencing three fetches doesn't report as O(n) per-iteration I/O. Call resolution binds a receiver whose type the source declares (constructor and method parameters), which lets performs_io cross the constructor-injection boundary common in Scala services. PR #200.

Play's conf/routes (and included *.routes files) is read directly from disk, since it has no extension no glob would admit; a sub-router's mount prefix composes onto its paths unless they already carry it segment-wise, which blind composition once turned into /team/team. Pekko/Akka HTTP and http4s route trees are read from the AST, gated on the file importing the framework — an unscoped pass once turned a metrics timer's path(result).record(nanos) into a phantom route and a forum's closeTopic/hideTopic into seven phantom Kafka topics. A trait is only counted as an abstraction when it declares something abstract, since Scala traits routinely carry full implementations; a case class carries the data_holder marker the same way a Kotlin data class or C# record does. PR #200.

The anonymous-class walker fix alone recovered 5,673 declarations and 9,637 calls across 1,817 previously-unwalked bodies — Scala's idiomatic home for implementations, and the largest single cause of false dead-code positives found in the corpus. A combinator applied to an Option (xs.find(p).foreach) is now demoted rather than flagged as an unbounded loop, since the receiver — not the combinator name — determines whether it can actually repeat. Scala module facts now carry the build module they compile into, so the cycles explainer can tell a legal within-module cycle from one that a real build tool (sbt, Maven) would actually reject. Extractor cache versions v181–v189. PR #200.

Codex hook installation writes .codex/hooks.json (local, unconditional) or ~/.codex/hooks.json (global, only if ~/.codex already exists), matching Codex's matcher-group schema including its required async field. Codex requires a one-time /hooks approval before a newly written hook runs, which enola cannot grant on the user's behalf. PR #201.

v0.3.11

Understand the architecture of an entire .NET solution

Enola now reads every language in a .NET solution, not just C#, and understands how the pieces are wired together — project references, EF Core storage, outbound HTTP calls, and dependency injection.

  • AddedF#, VB.NET, Razor/Blazor, and XAML (WPF, WinUI, MAUI, Avalonia) are now extracted into the same fact set as C#, so cross-language references resolve.
  • AddedMSBuild project and solution files are parsed, so .NET dependencies and cycles are judged at the real assembly boundary instead of by directory.
  • AddedEF Core, Dapper, and MongoDB storage facts are extracted, along with outbound HTTP client calls and conventional MVC routing.
  • FixedClasses registered only through dependency injection (services.AddScoped<IFoo, Foo>()) no longer read as dead code.
  • Fixed.NET test directories named Test/ or Tests/ were missed by case-sensitive globs and indexed as production code; this is now fixed.

Action: regenerate snapshots for .NET repositories to pick up the new languages, storage facts, and dependency edges. No configuration change is required.

Implementation notes and pull requests

MSBuild project files are parsed rather than read by path: a ProjectReference becomes a module depends_on edge, giving cycles and package-metrics the assembly as their real unit instead of the directory, and a PackageReference becomes a nuget dependency fact. Project files of every .NET language are read (.fsproj, .vbproj), which is what stops a solution like Giraffe — a .slnx with no .cs — from matching the extractor and emitting zero facts. Razor components merge with their .razor.cs code-behind, carrying @onclick handlers, @bind targets and @inject types as references; MudBlazor reported 5,749 orphans of 9,287 symbols before this. XAML documents merge the same way with their code-behind, covering WPF, WinUI/UWP, MAUI, and Avalonia's .axaml. VB.NET is read line-oriented, since it has no maintained tree-sitter grammar, into the same type index as C# — Roslyn's own VB compiler is 3,652 files, and 6,644 of its C# orphans had callers only there. F# adds module-level free functions, a construct no other .NET language has. Extractor cache versions v172–v176. PR #199.

storage facts were zero across all fourteen .NET repositories in the benchmark corpus, EF Core products included: a DbContext now becomes a storage fact, with DbSet<T>/IEntityTypeConfiguration<T> types as entities named for the resolved symbol rather than the directory, plus Dapper's generic query methods and IMongoCollection<T>. Outbound HTTP calls (HttpClient verb calls, Refit attributes) become client routes, with the request path resolved through the same per-member literal environment the conventional-routing pass uses; an absolute URL under Aspire keeps only its path, since the host is a service name rather than a hostname. Conventional MVC routing reads MapControllerRoute/MapAreaControllerRoute registrations directly — OrchardCore declares 288 verb attributes across 114 controllers and only 7 carry a [Route] — and a template still containing an unsubstituted {controller}/{action}/{area} is left unemitted rather than guessed. DI registrations (services.AddScoped<IFoo, Foo>()) are read as references, recovering 441 of bitwarden-server's 1,661 orphan classes (27%) and 59 of eShop's 202. Extractor cache versions v177–v180. PR #199.

v0.3.10

C# extraction

Enola now extracts C#: types and members, dependency injection, and both of ASP.NET Core's routing mechanisms.

  • AddedC# types, members, using dependencies, base lists, and constructor/primary-constructor injection are now extracted, with a partial class's several files merged into one symbol.
  • AddedASP.NET Core routes are extracted from attribute routing ([Route]/[HttpGet], including inherited templates) and minimal APIs (MapGroup/MapGet).
  • FixedA C# call through an interface — the common dependency-injection shape — now produces an inbound edge instead of reading as dead code.
  • FixedA qualified reference like VideoRange.HDR now emits edges to both the member and its declaring type, so enum members and static fields used without a call stop reading as unreferenced.
  • ChangedA foreach or LINQ loop's iterable is now walked in the enclosing scope, fixing spurious O(n²) complexity readings on the most common C# iteration idiom.

Action: regenerate snapshots to pick up C# facts. If your mcp-arch.yaml overrides the extractors: list, add csharp to it — a config written before this release silently disables the new extractor.

Implementation notes and pull requests

A partial class split across files merges into one symbol carrying the union of every half's edges — 7,740 files in the benchmark corpus declare one, and left unmerged the type's edges scatter across facts that each look thinly connected. Only public and protected fields and properties become symbols, since a BCL-scale repository's private state would otherwise dominate the fact set. A bare type reference resolves against a project-wide index rather than the file's own imports, because a C# using opens a namespace and names no type in particular; an ambiguous simple name resolves to nothing rather than a guess. Attribute routing composes a class-level [Route] with a method-level [HttpGet] after the whole fact set exists, walking resolved inheritance edges to find the nearest template — 40 of jellyfin's 64 controllers inherit theirs from a shared base. A controller with no [Route] anywhere in its hierarchy is conventional routing, whose template lives in Program.cs, and now emits nothing rather than the wrong path composed from what's visible. Minimal-API groups (app.MapGroup("api/orders")) are scoped to one method body; an unresolved, non-literal group prefix — a library mounting its surface at a caller-supplied pattern — marks its routes unresolvable rather than publishing a path the library doesn't actually serve. Measured on jellyfin: 422 attribute routes, all 388 distinct handlers resolving; on eShop: 0 → 30 minimal-API routes. Extractor cache versions v164–v171. PR #198.

v0.3.9

Declared architectural intent, five new extraction kinds, and a lower memory footprint

Repos and knowledge pages can now declare what the architecture is supposed to be, graded against every snapshot. Coverage extends to five new kinds, Ember support is complete, and large graphs use substantially less memory.

  • AddedRepos and knowledge pages can declare intended architecture in enola-intent.yaml, graded against every snapshot through a new governing_intent tool.
  • AddedNew extractors cover GraphQL, Terraform/HCL, Ansible, and React Navigation, plus Sequel storage facts alongside existing ActiveRecord support.
  • AddedEmber and Glimmer support is complete: navigation edges, full resolver joins, engines, and monorepo-safe ignores.
  • PerformanceThe fact store's memory footprint on large graphs is substantially lower, using an interned, compressed-sparse-row graph index.
  • FixedSequel's call-form superclass declaration now correctly emits a storage fact instead of being silently dropped.

Action: regenerate snapshots to pick up the new route and storage kinds. Declaring architectural intent is opt-in via enola-intent.yaml; see docs/INTENT.md.

Implementation notes and pull requests

Architectural intent compiles into the graph as a fourteenth tool. A repo's enola-intent.yaml, or a cluster-level override, declares what the architecture is supposed to be; markdown knowledge pages opt in with an enola_intent: block naming typed relations, origin channels, and file anchors. Every snapshot verdicts the measured graph against declared intent, and a genuinely unasked question (an absent repo, an unmeasured file kind, foreign vocabulary) is never conflated with a dangling anchor. governing_intent answers the reverse query in both directions, and impact_analysis/show_symbol now carry the governing trail. Calibrated at estate scale: 590 false intent findings before counterparty corrections, 3 genuine after. PR #197.

Deterministic coverage extends to GraphQL (server root fields via the graphql-ruby DSL, client operations from gql tags and .graphql documents, joined cross-repo), a new HCL/Terraform extractor (blocks as addressed symbols, literal references as depends_on edges), a new Ansible extractor (plays, roles, by-name role references, never rendering a template), React Navigation (screen registrations as page routes, navigate() calls as navigation edges), and Sequel storage facts. A shared literal-derivation helper (bounded: single-assignment constants, wrapper-call path literals, interpolation-headed template tails) now backs the HTTP-client scans; the Ember fold map runs through it byte-identically. PR #194.

Ember: Router.map honors resetNamespace, routes gain handled_by edges to their route classes, and <LinkTo>/literal transitionTo/replaceWith become navigation edges. The resolver joins @service fields and service: lookups, ember-data relationships and @attr transforms, adapters/serializers/transforms, contextual components, addon re-exports, engines, and both pods and classic layouts. Container-resolved classes are marked framework_registered so live singletons stop reading as dead code. Heavy-directory ignores are now any-depth, since a monorepo sub-app's nested node_modules previously entered the graph (~880k facts on one production monolith). Validated at 90% invocation resolution with zero parse errors on a large production app. PR #193.

On the Linux kernel's 1.89M-fact graph, the adjacency index cost 854 MiB as two map[string][]Edge-shaped structures, and Props/Relations maps cost a further 858 MiB across 16.8M live objects (44% of retained heap), from the same fact shape being allocated fresh 178,043 times over. The graph index is now CSR (compressed sparse row); File and Relation.Target strings are interned on the way in; and Store.Freeze() deduplicates structurally identical Props maps and Relations slices onto shared instances at the single publication point, since the published bundle is never written again. PR #195.

Sequel's call-form superclass (Sequel::Model(:customers)) was dropped by the superclass reader, so the dataset idiom emitted no storage fact at all; the golden fixture had pinned the miss while the corresponding helper's unit test passed against a narrower case. The superclass is now read whole and stripped to its base name. New extraction docs cover HCL, Ansible, and the new Ruby fact classes. Extractor cache versions: v149–v154 (Ember, coverage extractors), v157–v163 (declared intent). PR #196.

v0.3.8

Ember and Glimmer extraction

Enola now resolves routes, templates, services, and data-model relationships in Ember applications.

  • Added.gts, .gjs, and classic .hbs templates now contribute symbol references without changing source line numbers.
  • AddedThe Ember resolver links template invocations, @service injections, and ember-data relationships to declarations under app/. Ambiguous matches remain unresolved and are counted in ember_unresolved.
  • AddedRouter.map declarations produce composed page routes. Route templates attach to route classes, with controllers used as a fallback. Ember-data models produce storage companions.

Action: regenerate snapshots for Ember repositories. No configuration change is required; Ember detection requires ember-source.

Validation and implementation notes

Template tags are replaced with same-length parseable text while preserving newlines. The extractor produced 93,438 facts with zero parse errors on a roughly 6,900-file production application; repeated runs were byte-identical, and 89% of template invocations resolved. Extractor cache version: v148. PR #192.

v0.3.7

Incomplete binary releases are no longer published

A release remains a GitHub draft until binaries and checksums exist for every supported platform.

  • FixedThe release workflow now verifies all five expected binary and checksum pairs before publishing. A failed or unscheduled platform build leaves the release in draft.
  • Fixedinstall.sh now identifies the missing OS/architecture artifact and links to the release downloads instead of returning a generic curl error.

Action: none.

Implementation notes

Platform jobs upload tar.gz and sha256 assets to a draft release. A separate publish job compares the expected asset set with the release before changing its state. PR #191.

v0.3.6
Aug 3, 2026

Architecture history — enola log, show, diff, blame, and backfill

  • AddedEvery prior surface describes the tree as it is now, diff_snapshot included, which compares two nows. enola log answers a question a snapshot structurally can't: when did this happen. Recording is on by default: every generate_snapshot appends a revision to an append-only history kept under ~/.enola/graphs/<workspace>/history/, outside the repository, so running it against a checkout you don't own is safe and clearing .enola doesn't take the history with it. enola show <rev> explains one revision, enola diff <a>..<b> compares any two, and agents reach the same two questions through new architecture_history and architecture_blame MCP tools. #190
  • Addedenola blame <pattern> matches a module or symbol name, a file path, or both endpoints of an edge, and reports the revision where it first appeared; --findings searches recorded findings instead ("which snapshot introduced this cycle?"), --first stops at the introduction. A revision whose stored contents have aged out is reported as unsearched, never as absent, since "not found" and "not found in what I could read" are different answers and only one means the pattern truly never occurred. #190
  • AddedA repository enola has never analyzed still gets a past: enola log --backfill --sample=daily (also all/merges/tags, plus --since/-n/--dry-run) walks existing commit history, extracting each selected commit's tree to a temporary directory and snapshotting it with today's enola and today's config, never the config that happened to be committed alongside that commit, which would make every historical config edit look like an architectural event. It reads the repository and writes nothing into it, and re-running resumes rather than repeating. #190
  • AddedA delta is only a description of somebody's change when both sides were produced the same way. Each revision is stamped with an epoch, a fingerprint of enola's version, extraction behaviour, effective config, and plugin sets, and a revision straddling a changed epoch is marked incomparable in enola log rather than reported as an ordinary architectural change — elapsed time alone never earns that mark. #190
  • AddedHistory entries are keyed by facts.RepoIdentity (the normalized git remote), never an absolute path, so two machines' histories of one repository describe the same subject and are mergeable by union-and-dedup; each revision is stored as a patch against the previous one, with a full copy periodically, at roughly 600 bytes per revision header (measured: ~600B–4KB with contents on a ~4k-fact repo, ~600B–34KB on a ~23k-fact repo). enola gc reports what's stored and reclaims what isn't needed; --thin-older-than drops old contents while keeping the timeline, --prune-working discards uncommitted-tree snapshots. #190
  • ChangedThe history package is deliberately read-only and imports only pkg/facts and the standard library, staying buildable with CGO_ENABLED=0 so a viewer that reads a history doesn't have to link ten tree-sitter grammars. Nothing that judges the present, check, diff_snapshot, freshness, or drift, reads the history: deleting it changes no verdict and no snapshot_id, per the same authority rule docs/SNAPSHOTS.md already holds everything else to. Experimental: the commands and the on-disk format may still change. #190
v0.3.5
Aug 3, 2026

Cross-repo linking becomes a plugin architecture

  • ChangedExtractor→linker prop values (client sources, route kinds) were spelled out independently on both sides, so the Java extractor's java-http-client and feign sources were never in the linker's hand-written set — Java call sites linked as via="http" instead of via="http-client", silently. Both sides now share internal/facts/contract.go, guarded by tests that reject an unregistered route source or a literal spelled outside the contract; 24 files migrated to constants. Java call sites now correctly link as via="http-client", pinned by a new multi-repo fixture. #188 #189
  • ChangedThe engine resolved gRPC and HTTP routes to handlers itself, as four methods in a fixed sequence — httpbind tested language == "go", grpcbind carried a regex per code generator — so a new language meant editing the engine. Binders are now plugin.Binder implementations under internal/linkers/binders, each declaring the stage it runs in; the HTTP binder keys on the structural http_handler prop instead of a language name. Order-independence is enforced by a test that serializes the store after running binders forward and reversed. #188 #189
  • ChangedAll four cross-repo signals — HTTP calls, imports, Kafka topics, shared code — lived in one 1,823-line file with a dedicated field and materializer block each, so a fifth signal meant editing both. Signals are now plugin.CrossRepoSignal implementations under signals/, each declaring a phase and reporting evidence into a sink; crossrepo.go is down to 78 lines of orchestration. A new routeindex package holds the path-matching rules the HTTP signal and both unmatched-route passes must agree on. #188 #189
  • AddedCross-repo linking vocabulary — the word lists deciding which names are too generic to link on, and the thresholds signals compare against — is now configurable via a linking: block in enola.yaml, overlaid under internal/linkers/vocab. List fields are add/remove rather than replace, so one added base class can't silently discard every default; an out-of-range threshold is a config error rather than a silent clamp. The vocabulary is now part of computeConfigHash, since it changes which edges are drawn. #188 #189
  • FixThe bundled mcp-arch.yaml ignores **/*.json, so collectPackageNames — which read package.json off the engine's glob-filtered file list — never saw one under the shipped config. No module carried package_name, so the cross-repo linker's own-@scope guard had nothing to fire on: a repo importing its own published package was reported as depending on whatever other repo happened to share that scope name. package.json is now read directly from disk, the same principle the OpenAPI and Symfony-config extractors already apply. cacheVersion v145 → v146. #188 #189
  • FixThe Axum route rewrite emitted one route copy per mount prefix using nf := f, which copies the struct but not the map behind Props — a router nested at two mount points produced two facts sharing one props map, so a per-route binder verdict written onto one copy landed on both. Cache-dependent: cached facts round-trip through json.Unmarshal and come back with independent maps, so it only bit on a cache miss. Fixed with facts.Fact.CloneProps(). #188 #189
  • FixThe layers explainer ranked pkg/ below internal/ and fired on any import from the lower rank into the higher one — flagging essentially every Go repository with both directories (15 false findings on enola's own snapshot), since publishing an API over a private implementation is the standard layout, not a violation. internal/, pkg/, and api/ are now collapsed to one tier; the only ordering the layout actually expresses — nothing should import into cmd/ — still fires. #189
  • ChangedNew docs/EXTENDING.md documents the plugin surface this release created: which plugin kind fits a given problem, adding a binder or a cross-repo signal, the linking: vocabulary, and the prop-vocabulary contract. Its config example is asserted by a test, TestLinkingExample_FromDocsParsesAndApplies, so it can't silently stop matching what it claims to show. #188 #189
v0.3.4
Aug 2, 2026

Scope conformance, pluggable measurements & a shared command surface

  • AddedA structural diff reports what changed and nothing about what was meant to change. New internal/conformance takes a declared target (or the packages a caller expected to touch), runs reverse-dependency impact analysis on the pre-change graph, and reports any package the change reached outside that radius as spillover — the coupling case where a package gains an outbound dependency without any of its own facts moving. With nothing declared, the edited packages are taken as the statement of intent. #186
  • AddedConformance is now exposed as target/expected_packages on diff_snapshot and as --target/--expected on enola check, reported as a policy measurement rather than graded in place — it only fails a build under --max-spillover, so passing --target for the first time can't silently break a build that never opted in. #187
  • AddedA caller running its own analysis over two snapshots had no way to feed the result into the verdict, so it had to grade itself. New Measurement, Threshold, and Breach types let pkg/check's Evaluate take arbitrary caller-reported counts and decide what they mean against policy; a verdict with no configured thresholds stays byte-identical to before. #183
  • AddedAn analyzer computing a continuous per-module value had nowhere to publish it — one insight per module is noise, publishing only outliers discards the rest, so the value never left a single snapshot's analysis. A new Annotator interface lets an explainer write derived values onto facts after linking and before every Explain call, so no explainer's insights depend on registration order; values are rounded before storing, since facts are hashed into the snapshot ID. The fact diff now names which prop moved, as key: before → after, instead of only showing that a fact changed. #184
  • Changedcheck, coverage, doctor, baseline, install/uninstall, and hook lived in cmd/enola, unreachable from a wrapper binary built on internal packages that don't cross a module boundary. Moved to pkg/command behind a Runner holding the binary name, so usage lines and error prefixes name whichever binary is actually running; --help output is unchanged. #180
  • FixModuleDir stripped the repo label unconditionally, so a top-level source directory sharing the repository's own name resolved to a name no module had — edges out of that tree hung off a phantom node, taking cycle and layer detection with them. New ModuleDirCandidates returns both the raw and repo-stripped directory, and callers try exact matches against both before walking up. #178
  • AddedcompareMeta compared extractor sets between snapshots but never explainer sets, so an explainer present on only one side made its entire finding set read as new or resolved with no warning and Comparable: true. New WarnExplainerSet adds the same symmetric check, registered in the verdict's advisory summary. #179
  • ChangedThe working-tree drift warning was computed inline inside the diff_snapshot handler, so any other consumer building its own delta had to reimplement it — and the wording had already begun to diverge. Shared via new internal/drift, exposed out-of-module as pkg/bootstrap.AddDriftWarning. Also pins the diff's attribution contract: a cycle closed between two modules that both pre-date a change attributes through the added edge's endpoints, while a finding citing nothing the change touched stays incidental. #182
  • ChangedResolveBaselineDir — the shared contract behind what diff_snapshot, compare_receipts, and enola check mean by "previous" — is now re-exported from pkg/bootstrap, so an out-of-module consumer no longer has to spell the baseline subdirectories out locally and risk disagreeing silently once the rotation changes. #181
  • ChangedREADME and docs/BENCHMARKS.md refreshed against re-measured numbers. #177
v0.3.3
Aug 01, 2026

Python inheritance edges, and explainer findings that attribute to the right fact

  • AddedThe Python extractor now resolves implements edges for inheritance relations, taking import context into account — previously the only extractor missing this relation entirely. #175
  • FixLayer-violation findings cited only the importing file, not the underlying dependency fact or raw import target; since the diff engine matches fact names and edge endpoints, file-only evidence never attributed, so every new violation landed in the incidental bucket the gate doesn't grade. Findings now key on the actual fact (layer violations, unused routes), and a new common.ModuleDir resolves cross-repo file paths back to modules so a composed multi-repo graph doesn't resolve to phantom nodes with undetected cycles. god-class and the layers pattern now clamp at common.MaxHeuristicConfidence rather than 1.0, since 1.0 is what the receipt, dashboard, and query_insights(min_confidence=) read as a structural fact, and a cycle should be the only finding that reaches it. layers and complexity-outliers now also exclude test code from their populations, matching every other explainer. #176
v0.3.2
Jul 31, 2026

Config resolution, agent-hook reliability, and reproducibility fixes across explainers and extractors

  • FixConfig resolution could silently analyze the wrong config: lookup fell back to the binary's own directory, and a list-valued key like extractors: replaced the built-in list rather than extending it — an eleven-extractor config sitting beside a go build output disabled Rust for every repository that binary touched, from any directory without its own config, with zero facts and zero error. bootstrap.ResolveConfig now returns and prints the resolved path on every command; the engine detects extractors disabled by an incomplete extractors: list and records them as shadowed_extractors in the receipt. Shipped configs no longer declare extractor lists at all — full.yaml had claimed to enable all languages while omitting grpc, openapi, and python. #166
  • Fixinstall --hooks's Stop hook was registered as a flat list, but Claude Code expects every hook event as matcher groups — it parsed, and never fired. The correctly-shaped SessionStart hook beside it didn't fire either, since one malformed entry invalidates the whole hooks block: install --hooks configured nothing while reporting success. Every event is now written grouped, with migration for existing installs (enola's legacy flat entry is dropped, foreign entries preserved) and an opt-in end-to-end test that installs the hooks, closes a real dependency cycle in a headless session, and asserts the hook actually ran. #167
  • FixA hardcoded .enola/** ignore-glob literal only matched the default output location by coincidence — pointing output.dir anywhere else made each snapshot walk the previous one's own artifacts, so an unchanged tree produced a different snapshot every run, breaking the reproducibility the baseline diff depends on. The ignore glob is now derived from output.dir itself, which must now name a subdirectory of the repo (absolute paths and .. escapes rejected); a config that exists but is invalid now errors instead of silently falling back to defaults that analyze the working directory. #168
  • FixMeanStdDev's float reduction ran over the fact store's insertion order, which varies between runs of an unchanged tree due to concurrent extraction — and god-class is the only explainer that puts a threshold into its output (confidence) rather than using it only to select. Across 90 runs of 30 repositories, 11 of 25 insights.json findings drifted by ~1.9e-15, invisible at the two decimals anyone reads but enough to break insights.json's output-hash reproducibility. MeanStdDev now sorts a copy before reducing. Also fixes insights.json serializing as null instead of [] for a repository with no findings. #169
  • AddedA hook that never fires and a hook that fires and finds nothing look identical from inside a session — the Stop-hook registration defect above shipped silently for exactly that reason. Every hook invocation now stamps <output.dir>/hooks.json, including no-op runs; enola doctor reports when each hook last ran and what it concluded (always exits 0, since a diagnostic can't borrow the non-zero-means-regression convention); and a pre-push git hook runs the agent-hook end-to-end test when a push touches the installer, since CI runners can't run real agent sessions. #170
  • FixThree extraction gaps surfaced by widening the benchmark corpus to languages it had no prior coverage for: Kotlin/Retrofit only matched positional @GET("...") arguments, missing the named-argument form (@GET(value = "...")) real Android code uses, so no mobile-to-backend edges existed for it at all; Axum route chains terminated at the first non-verb method (e.g. .layer(mw)), dropping routes with the verb in plain sight; and TypeScript path-alias resolution picked the first matching prefix by map-iteration order instead of the longest, the only reproducibility failure found in the corpus. cacheVersion bumped to v144, v145. #171
  • Fixexamples/multi-repo.yaml claimed in its own header to enable all extractors “regardless of which languages each repository uses,” then pinned five of twelve — a cluster containing PHP, Rust, Python, Java, or C++ indexed those repos as unsupported. A multi-repo cluster config is the one place the language mix can't be known in advance, so it no longer pins a plugin list; per-language example configs keep theirs. Also republishes docs/BENCHMARKS.md (38 repositories, 4,211,113 facts, zero parse errors, 38/38 byte-identical across 114 runs) and widens the regression ratchet to 12 repositories and 8 languages. #172
  • FixThe Stop hook only emitted when a diff found a regression, so a baseline that couldn't be compared at all looked identical to a clean session — nothing was ever graded. shouldAutoPin now refreshes an auto-pinned baseline that's blocking-incomparable, not just one whose tree moved; the hook emits on incomparability too, naming the cause and remedy from the same table enola check prints from; and doctor reports baseline usability before a session starts rather than after. #173
  • ChangedAdded docs/SNAPSHOTS.md, documenting the design argument for why enola's graph is a computed snapshot rather than a continuously maintained one: a verdict is a function of two states, and updating a graph in place would destroy the earlier state with the very edit being graded. Corrects a benchmark citation (30 repositories vs. the actual 38) and a mischaracterized cost comparison in docs/BENCHMARKS.md, and trims retrospective narration from ARCHITECTURE.md and docs/extraction/python.md down to present-tense statements of each rule. #174
v0.3.1
Jul 30, 2026

See which cross-repo edges resolved, from the CLI

  • Addedenola coverage reports which cross-repo edges actually resolved, now callable from the CLI instead of only through an MCP tool call — extracted into pkg/coverage so both surfaces compute the same report. The unresolved list is unconditional, not a flag: a report showing only successes would be marketing, and each miss is labeled as a repo you haven't loaded, a third-party endpoint, or a genuine blind spot. Exits 0 whenever it ran (2 on a usage error) — it's a report, not a gate; check owns the meaning of a non-zero exit. Also adds an examples/cross-repo pair whose link depends on a prefix composed across a function boundary, plus one deliberately dynamic call that stays unresolved, with a test that fails if the mount is removed. #165
v0.3.0
Jul 30, 2026

enola check: grade architectural regressions from the CLI or CI, wired into agent sessions

  • AddedGrading an architectural diff previously required an agent to remember to call an MCP tool. A pure Evaluate(*diff.SnapshotDiff, Policy) Verdict now backs a new enola check subcommand, exiting 0 clean / 1 regression / 2 could-not-run / 3 declined — 3 is deliberately distinct from 1, since a delta built over different inputs describes how the snapshots were produced, not what was edited, and grading that as a failure would be a lie. Comparability gains a typed Kind set instead of a single bool, so a stale baseline now warns and still grades rather than hard-refusing. Policy gates on the explainer (cycles by default), not on raw confidence, since god-class and layers findings both use 1.0/0.4 for reasons unrelated to certainty. Also adds baseline pin|show|clear and rejects unrecognized arguments instead of silently absorbing them as config paths. #161
  • AddedA baseline pinned on one machine can now be graded on another. Comparability previously compared absolute RepoPath, so a baseline pinned in a CI runner's workspace and restored on a developer's machine always read as a different repository — the delta was correct, only the verdict was wrong. Repository identity now comes from the git remote, normalized (host/path, with scheme, credentials, port and a trailing .git stripped) so an HTTPS clone with an injected CI token and an SSH clone of the same repo compare equal, falling back to checkout directory name when neither side has a remote. CI gains an advisory architecture job that grades every PR against a published baseline without ever failing the build. #162
  • Addedenola install --hooks wires the check loop into an agent session automatically: a baseline pins when a session starts, and the architectural delta is reported when it ends, only if the change introduced a regression. The stop hook is advisory by default (emits context, always exits 0), and the session-start snapshot runs detached so a large repo's index time never stalls session start — measured at the binary's floor (~17ms) on a repo that takes roughly ten seconds to index. Concurrent sessions are single-flighted through a non-blocking file lock, and a deliberately pinned baseline is never overwritten by the hook's own auto-pins. Two engine fixes this depended on: copyArtifacts now publishes a baseline atomically (a failed pin no longer destroys the previous one), and the extractor cache now records the binary that wrote it, after a missed cacheVersion bump was observed serving 568 phantom facts from a stale binary. #163
  • ChangedRewrote the README and CLI docs to lead with architectural regression testing — the check/--hooks loop shipped in this release — instead of cross-repo linking as the primary pitch, and added CLI reference docs and two agent-session screenshots. #164
v0.2.9
Jul 30, 2026

Traversal cap fixed to bound output, not the walk, and staleness checks scoped to the loaded graph

  • FixtraverseFrom's maxNodes cap stopped the BFS frontier outright instead of bounding only the returned set, so every node reachable only through a capped-out node was silently missed — and NodesVisited/MaxDepthReached, computed over that truncated walk, were reported as properties of the whole graph. The frontier now queues a node even after the cap is hit, matching the existing node-kind-filter contract; maxNodes bounds the returned set only. Also fixes a fused impact-summary statistic: the dependent count came from an uncapped reachableCount while the depth printed beside it came from the cut walk. Measured on a 1.9M-fact graph: worst-case traversal 6ms → 22ms; extraction output is unchanged, no cacheVersion bump. #157
  • FixStaleness checks pulled both the age signal and the per-repo VCS check from the machine-wide graph receipt, which every server process on the machine rewrites — so it reported drift for repos absent from the loaded graph and missed drift in the ones actually loaded. Receipt entries are now filtered to repos present in the loaded graph, and age comes from the loaded snapshot's own generated_at, with the receipt as a fallback only when it describes a loaded repo. #158
  • FixgitInfo flagged a repo dirty on any git status --porcelain output, which lists untracked paths — and the snapshot creates <repo>/.enola before the receipt reads git state, so any repo that doesn't gitignore it recorded dirty:true for a fully committed checkout, disabling the staleness check's VCS arm (which only fires when the recorded state was clean). The status query now excludes the configured output dir via a pathspec, defined once in gitInfo so the snapshot-time capture and the live check share one definition of dirty. #159
  • Fixdiff_snapshot and validate_architecture_change both labelled one side “current” without checking it still matched the working tree; the only age check was relative (baseline vs. current), so two equally stale snapshots compared clean, and an inverted pair was silent because a non-positive gap shared its signal with “no timestamp recorded”. Engine.Drift re-walks under the current config and re-hashes against the snapshot's recorded file hashes, and both tools now surface a comparability warning naming the drifted files — a filesystem comparison, not a VCS one, so it also works for repos that aren't git working trees. Known limit, documented: in append mode, drift checks only the most-recently-indexed repo and reports “cannot verify” for the others. #160
v0.2.8
Jul 29, 2026

TypeScript server routes, fixed client-call detection, and multi-repo clusters outside MCP sessions

  • FixThe optional type-argument group in TypeScript client-call patterns was <[^>]*>, which can't span a nested type argument — fetch<ApiResponse<Foo>>(...) matched one level of generics but not two, including on the openapi-fetch shape the pattern was written for. Lowercase verb calls (axios.get('/path'), http.post('/path')) matched nothing at all, excluded to avoid colliding with map.get()/cache.delete(); admitting them required gating client-route extraction on facts.IsTestPath, since a supertest e2e call is byte-identical in shape to production traffic — one API's own test suite had produced 500+ phantom client routes ungated, promoting an isolated service to connected and fabricating a cross-repo dependency edge out of test traffic. cacheVersion bumped to v141. #154
  • AddedTypeScript emitted server routes only for file-based routers (Next.js, Nuxt, SvelteKit); a decorator- or call-routed backend contributed zero routes, so every client call against it stayed unresolved and the backend itself was classified isolated. Two new passes cover NestJS/InversifyJS @Controller classes and Express/Fastify/Hono/Koa <recv>.<verb>('/path', handler) calls, separated from client-call extraction by receiver binding and gated on facts.IsTestPath so an e2e fixture can't mint routes nothing calls. Also folds four duplicated route-path composition implementations (Axum, FastAPI, Spring, Symfony) into one facts.JoinRoutePath. cacheVersion bumped to v142, v143. #155
  • AddedCross-repo linking was reachable only from an MCP session — append is a generate_snapshot tool parameter that both CLIs hardcoded to false, so a CI job or a developer not driving an agent saw only the single-repo subset: no service nodes, no cross-repo edges, no coverage_report. A config can now name a repo cluster (repos: [../api, ../web, ../sdk]), indexed with one --generate run — first repo fresh, the rest appended, resolved against the config file's own directory so a checked-in cluster config means the same thing on a laptop and in CI. --explain now also accepts a cluster config instead of always treating its argument as a repository path. #156
  • AddedA live MCP Toplist rank badge in the README. #153
v0.2.7
Jul 27, 2026

Query credit scaled by the corpus searched, and seeded across restarts

  • FixQuery credit was a flat per-tool weight with no corpus term, so the same query_insights call earned an identical figure over a 1.9M-fact graph and over a few hundred facts — finding 51 cycles across 55,399 files priced the same as finding them across thirty. A query tool's ordinal weight now scales by the loaded graph's corpus, logarithmically and capped, so a haystack 100x larger costs a few more rounds of greps, not 100x, and a query can never earn more than reading the graph it searched outright would have cost. #152
  • FixAutoLoadSnapshot restores a graph after a restart without re-snapshotting, but the corpus pool wasn't seeded on that path, so every query against a restored graph priced unscaled — measured at 23,669 tokens for a call worth 189,836. Each repo's parsed-source size (GraphRepoEntry.SourceBytes) is now carried in the graph receipt and published via Server.SeedCorpus before serving, behind an atomic pointer since it's now read on every concurrent tool call. #152
v0.2.6
Jul 27, 2026

Value model re-anchored on measured corpus instead of flat call weights

  • FixThe estimate behind --status and the dashboard was a flat per-tool weight and two conversion constants, so a 72K-token service and a 33M-token monorepo priced identically, a failed call still earned full credit, and the output_mode ladder — the main token-saving lever — never moved the number. Re-anchored on the counterfactual it claims to measure: what an agent would have had to ingest with grep and file reads to reach the same answer. Store.SourceBytesWithFacts now sums exactly the files that produced facts (summing the walked-file list instead pulls in images, media, and vendored databases — a 39x difference on one repository); snapshot credit derives from that corpus plus a cross-repo premium and reduced refresh tiers; credit is capped at a repo's own corpus so cumulative credit can never exceed the code it was computed over; and accounting moved into middleware that runs after the handler, so a failed call earns nothing. #151
  • AddedCorpora exceeding a context window are now flagged instead of priced — for those the counterfactual isn't expensive, it's impossible, and a token figure would understate it. #151
v0.2.5
Jul 26, 2026

Python test-file isolation, package re-export resolution & generated-code dead-code signals

  • FixThe default ignore globs covered Go, TypeScript, and Ruby test conventions but not Python's: a pytest fixture mounting routers on a throwaway app is a route-mount fact, and the repo-wide FastAPI prefix fixpoint folds every mount it sees, so test-only prefixes rewrote production routes (six phantom endpoints in one repo, ~35% of its facts). Adds **/conftest.py, **/test_*.py, **/tests/**/*.py, and **/test/**/*.py to Ignore and TestGlobs — deliberately not a bare **/*_test.py suffix, which would repeat an earlier Ruby hazard of swallowing production code. #147
  • FixThe bundled mcp-arch.yaml users are told to curl had drifted behind config.Default().Ignore — missing two Ruby spec globs plus dist/, coverage/, .nuxt/, .svelte-kit/, and target/ — so every adopter of the shipped file silently stopped ignoring those paths. Added a containment guard test. #148
  • AddedA production Python symbol exercised only by a pytest file read as dead code (253 of 1,927 orphans in one corpus). New TestRefExtractor plugin credits these test-only references without polluting the production route graph — it emits only KindTestRef facts, so re-reading test files the ignore globs exclude can't reintroduce a fixture's include_router prefix. cacheVersion bumped to v137. #148
  • FixA Python package is a directory, so from pkg.sub import thing left a call target pkg.sub.thing whose prefix matched no module file: 674 dangling targets carrying 4,660 call edges in one corpus, including every router factory an API composition root mounts, so find_path from that file reached none of them. resolveCallTargets now indexes package __init__ re-exports the walker already recorded; exact module resolution still runs first, so this is purely additive. #148
  • FixTwo more Python call-target resolution bugs, both producing a wrong target rather than merely an unresolved one. Suffix matching and the internal-root gate accepted any directory name at any depth, so an internal directory sharing a name with a third-party package captured its imports (~500 third-party call edges misclassified as first-party in one repo); both now apply Python's actual package rule — a directory is a top-level package only if its parent isn't one. Separately, resolveAbsolute dropped trailing segments on failure, which is correct for an import but wrong for a call target: pkg.mod.Cls.method resolved to pkg/mod and kept only the last segment, yielding pkg/mod.method — a plausible-looking but wrong edge, not a dangling one. Resolution now walks the module/symbol split point leftward with an exact module lookup; an unconfirmed multi-segment candidate stays dotted rather than minting a wrong edge. cacheVersion bumped to v138. #149
  • AddedTwo new signals let the dead-code detector drop findings it can never act on. Facts from a file carrying a codegen banner (DO NOT EDIT, generated by, @generated, matched case-insensitively against the leading comment block only, so it works for any generator and any language) now gain generated=true, since a generator's unreferenced half is guaranteed noise. Python functions registered with a framework by decorator — FastAPI @app.exception_handler/@app.middleware/@app.on_event/@router.websocket, Modal @app.local_entrypoint and (gated on the file importing modal) @app.function/@app.cls — now gain framework_registered=true, the same treatment Click/Typer commands already had. Facts are flagged, never dropped, so callers of generated code still resolve. cacheVersion bumped to v139. #149
  • FiximportableRoots stopped scanning a path at the first package parent, but a directory without an __init__.py breaks the package chain and starts a new source root, so a segment below a package can itself be importable — classifying a real internal import as external made a whole analysis tree read as dead. Now judged per position, matching buildSuffixIndex, which already did (v138 continued). Separately, a function handed to a decorator as a bare value (@override_run_tasks(handler)) is a real use — the decorator-argument walk looked only for nested calls, so a bare identifier slipped past and the referenced function had no incoming edge. #150
v0.2.4
Jul 25, 2026

Python FastAPI route composition, safer install script & cross-repo single-segment route linking

  • Changedinstall.sh now uses install -m 755 instead of cp when placing the extracted binary, guaranteeing rwxr-xr-x permissions regardless of umask or archive attributes, and unlinking the existing binary before replacing it to avoid a Text file busy error when updating enola while it's running. #144
  • AddedRoute decorators on defs nested inside a function body — the router-factory idiom — are now extracted; module-level statement walking never reached them. A repo-wide fixpoint folds include_router mount prefixes onto the bare decorator path, so a route is stored at the path it actually serves: the Python analog of the Go (v125) and Axum (v130) subrouter composers, keyed by the decorator's receiver rather than by line span. Unresolved mounts keep the bare path. #145
  • FixTypeScript module facts now carry package_name (from the nearest package.json), and the http-client verb is read from the call's own options-object literal instead of a flat forward byte window that let a later call's method: bleed backwards onto an earlier one. The cross-repo linker also no longer binds a client call to an API-compatible reimplementation elsewhere when the calling repo serves that route itself, and a scoped import under a namespace the consumer itself publishes no longer draws a false cross-repo edge. #145
  • FixSingle-segment endpoints could never form a cross-repo HTTP edge, from two compounding bugs: isGenericPath rejected any path under two segments, sweeping up named endpoints alongside /health and /status; and pathSuffixes yields nothing below minSharedSegments, so single-segment server routes were indexed under no key at all. The first is now a vocabulary test, with linkHTTP requiring an unambiguous provider for a one-segment match; the second gets a new pathMatchKeys whole-path fallback used by every server-index builder. Also excludes **/testdata/** from the OpenAPI and gRPC extractors, which walk the repo themselves and previously attributed fixture repos' routes to the host repo. cacheVersion bumped to v135. #146
v0.2.3
Jul 23, 2026

Running-server instance registry fixes cross-process dashboard and restart state bugs

  • FixOne MCP server is started per agent session, so several run at once against a shared ~/.enola, but nothing modelled a running instance, conflating per-process and shared state: the dashboard's PID/uptime/session cards were filled from the cross-process aggregate (a page served by one process could announce another process's PID), the graph panel read the machine-wide receipt while other panels read the live store (describing different codebases on one page), per-repo counter files were overwritten rather than merged (two servers on one repo silently discarded each other's increments), and a restart could come back holding whatever repos another session had snapshotted last. New instance registry under ~/.enola/instances/, written by the process it describes and reaped once that PID is gone; counter writes now merge only the unflushed delta under a cross-process lock; graph receipts are additionally written per workspace and preferred on restart. The dashboard now binds a fixed port and lists every live server. #143
v0.2.2
Jul 22, 2026

Kotlin-on-Maven detection, programmatic servlet routes & Kafka topic cross-repo linking

  • AddedKotlin detection only recognized Gradle, so a Kotlin service built with Maven was skipped entirely — no symbols, routes, or client calls extracted. Detection now also reads pom.xml (kotlin plugin/dependency, src/main/kotlin sourceDir) and falls back to a src/main/kotlin source root. Also adds route extraction for the fluent .addServlet("/path", handler) registration DSL, emitted with a new facts.MethodAny ("*") method that the cross-repo HTTP linker treats as a wildcard matched by any concrete-verb client call. #140
  • AddedAsync coupling via message topics was invisible — a service consuming another's events showed no dependency. New topic fact (KindStorage, storage_kind=topic) extracted from an envconfig default: tag on a *Topic field or an env.Get("..._TOPIC", "default") call, plus a linkKafka cross-repo signal binding consumer to producer via the topic name's owning-service prefix. #141
  • ChangedDocumented the Kafka topic signal in ARCHITECTURE.md's fact model, linker section, and pipeline stage 4 (previously shipped undocumented), backfilled the one-sentence Go entry in Supported languages, and added the golden fixture covering the producer/consumer topic-name join that unit tests of each half individually had missed. cacheVersion bumped to v132 for changelog convention only — Go facts are never cached. #142
v0.2.1
Jul 21, 2026

Cross-repo shared-symbol linking overhaul: verified matches replace name-only guesses

  • FixCross-repo shared-symbol edges were materialized in both directions even when an import or HTTP call had already fixed the direction, so a library whose types a consumer also declares came out depending on that consumer; edges now annotate the existing directional edge instead, staying symmetric only when nothing else fixed a direction. Separately, graph nodes are keyed by name, so same-named facts from different kinds merged and the earliest-stored one supplied the label — meaning node_kinds filtering on that label could drop a node from a service-filtered traversal. nodeFor now picks the fact whose kind matches the relation that reached it. #136
  • FixGenerated stubs (.gen/.pb/_pb2/.generated, /mocks/) and unqualified type names shared across more than half of loaded repos were feeding the shared_symbols cross-repo signal, so services generating clients for the same API, or independently modelling the same domain vocabulary, fabricated a cross-repo dependency. Real HTTP/import edges are unaffected. #137
  • FixTwo repos declaring the same distinctive type names were linked with a traversable depends_on edge even though neither imports nor calls the other and the linker has no way to substantiate the claim from names alone — measured across real identities behind these edges, about 45% were entirely unrelated code sharing a name, and depends_on also composes across hops while shared code does not, so a caller of one side of a copy-paste pair appeared to transitively reach the other. Such a pair now yields one symmetric, non-relational cross_repo_shared_code fact (queryable but never in the traversable graph) plus a separate lower-confidence insight; shared symbols still annotate an edge an import or call already established. Also fixed the direction gate, which enumerated directional via kinds and omitted grpc — it now tests for any via except shared_symbols. #138
  • AddedA shared type name only ever meant "both repos declare this," which could equally be copied code or two teams independently naming the same concept alike. The linker now optionally reads source and, for each name-matched candidate, compares the two declaring files by token-set overlap (calibrated against a character diff: line-set comparison disagreed on 4 of 13 sampled declarations, all false negatives, versus token-set agreeing on all 13). Only verified names count toward the link threshold, and the resulting fact keeps both symbol_count and name_match_count so the gap between shared code and shared vocabulary stays visible; the insight moves to 0.8 confidence when verified. #139
v0.2.0
Jul 21, 2026

CLI gains --help, --list & --status, plus a read-only live dashboard

  • AddedNew pkg/cli renders the tool catalogue behind --list and a composable help spec behind --help (usage, flags, config path, examples, MCP configuration, build), documenting the previously-undocumented upgrade command. New pkg/status records per-tool usage under ~/.enola/usage/, keyed by absolute repo path so counters survive both a restart and deleting .enola/, letting --status report uptime, session and lifetime call counts, and an estimate of time/context saved; --status --all breaks this down per repo. #134
  • AddedStarting the MCP server now also serves an auto-refreshing read-only dashboard on a free loopback port (--no-dashboard to opt out; --status prints its URL). Shows the same activity/value data as --status plus snapshot and graph receipts, the service and cross-repo-edge lists with a node-link diagram, insights grouped by explainer and filterable by confidence, and the extraction-quality proof (coverage, unresolved routes, skipped-file and parse-error samples). Every request reads through the same concurrency-safe accessors the MCP tools use, and each source degrades to an explanatory note rather than an error page. #135
v0.1.40
Jul 20, 2026

Cross-repo linker stops false-linking on Rails boilerplate, Swift XcodeGen app targets sub-divided, graph restore-on-restart

  • FixTwo repos declaring the same Rails framework-convention classes (ApplicationController, ApplicationRecord, ApplicationJob, ApplicationMailer, ApplicationHelper, ApplicationCable::Connection/::Channel, CanCanCan's Ability) or auto-generated Rails migration classes under db/migrate/ were linked as sharing code by the cross-repo shared_symbols signal, even though every app of that framework declares these identically and every app that ran the same migration gets a coinciding class name. Both are now excluded from the shared-symbol identity check. #131
  • AddedThe Swift extractor mapped every file under an XcodeGen application/app-extension target's source root to one module identity, so a target spanning many directories collapsed the whole app into a single module in package_metrics (hundreds of types, no internal structure) — only SPM-style Sources/<Target> layouts appeared split. Application and app-extension targets are now sub-divided by leaf directory (matching Go/Ruby), with the type-reference pass run intra-target so per-directory packages get real directory-to-directory coupling; framework and SPM targets stay whole. cacheVersion bumped v130 → v131. #132
  • AddedThe MCP server dropped its in-memory graph on restart: AutoLoadSnapshot reloaded only the configured repo's facts.jsonl, skipping insights and snapshot metadata, so multi-repo graphs had to be regenerated every restart with no generated_at to judge freshness against. Restore now rebuilds the whole graph from disk with no extractor runs, via a graph-wide registry (~/.enola/receipt.json) read by engine.LoadGlobalReceipt and republished by engine.RestoreFromDir; adds warn-only staleness surfacing (graph older than 24h, or a repo's git HEAD moved / working tree turned dirty since its snapshot) prepended to read-tool results. #133
v0.1.39
Jul 20, 2026

TypeScript HTTP-client precision fixes & Axum nested-router prefix composition

  • FixFour precision fixes in the TypeScript http-client extractor: a left word-boundary on the fetch()/makeRequest() matcher so a call merely ending in "fetch" (router.prefetch(), query.refetch()) is no longer captured as an outbound HTTP call; an options-object url: must now carry a real HTTP verb or request-payload key, not just a non-verb type: (SEO metadata, JSON-LD); a required leading "/" in cleanTSPath so non-path string literals don't become phantom routes; and a stripped query-string placeholder fused to a path's final segment. cacheVersion bumped to v128. #128
  • FixA client call written as ${this.basePath}/calculate, where basePath is a "/"-rooted string literal in the same file, previously lost its base — cleanTSPath stripped the leading ${...} token, leaving a single-segment suffix the cross-repo matcher skips. A per-file identifier-to-literal base map now reconstructs the full path so these calls resolve to their server route; conflicting bindings are left unresolved. cacheVersion bumped to v129. #129
  • FixThe Rust Axum extractor emitted routes at their bare in-router path and skipped .nest(...), so a router built by one function and mounted by a parent via .nest("/api/v1/datasets", module::router()) in another file was stored as /status instead of the true runtime /api/v1/datasets/status, breaking cross-repo client-route matching. A crate-wide fixpoint now resolves each nest to its callee builder and propagates mount prefixes from root builders, mirroring the Go gorilla/mux+chi subrouter composition shipped in v0.1.38. cacheVersion bumped to v130. #130
v0.1.38
Jul 19, 2026

Go route-prefix composition, struct-usage edges & Rails route-extraction fixes

  • AddedNew optional config.Config.ChangeVerifyHint (set programmatically before server construction, not read from YAML) appended to both the server's Instructions block and the post-snapshot loopHint nudge, giving a host or wrapper that registers extra change-verification tools a way to surface them from the workflow that pulls agents to diff_snapshot. Empty by default, so OSS behavior is unchanged. #123
  • FixThe Go route extractor composed gorilla/mux and chi subrouter PathPrefix mounts only within a single function, so a subrouter created in one place and passed into a per-file/per-package registration function or method that registers bare leaf paths on it started with an empty prefix map — storing a route as /things instead of the true runtime /api/things, skewing cross-repo client-to-route matching. New buildRoutePrefixIndex module-wide fixpoint propagates each router argument's prefix to the callee's router parameter, also handling receiver-method registrations via the extractor's shared type resolver. cacheVersion bumped v124 → v125. #124
  • FixThree further Go-extractor accuracy fixes. A collection-root route registered as sub.HandleFunc("", h) was dropped because the empty path was checked before composing the subrouter prefix; an empty string literal is now distinguished from a dynamic arg, composing to the prefix instead of being discarded. A struct used as a composite literal or as another struct's field type now emits a RelInstantiates edge, so an internal type referenced only that way no longer reads as dead code. scaling_loop_depth now discounts hierarchical nested loops (a loop reached through an enclosing loop variable, e.g. range pkg.Files) so a tree/AST walk is no longer over-reported as O(n²+). cacheVersion bumped v125 → v126. #125
  • FixThe new RelInstantiates struct-usage edges (above) kept ubiquitous data structs out of the dead-code report, but a data struct built at many sites is not a god class or call-graph hotspot, and those edges started surfacing ubiquitous data types as false-positive high-fan-in findings. RelInstantiates is now filtered out of Graph.ArchitecturalReverse, the shared fan-in helper used only by the god-class and hotspots explainers; traversal, impact_analysis, find_path, and the dead-code detector are unaffected. Explainer-only change, no cache impact. #126
  • FixThree Rails route-extraction fixes. A nested plural resources with shallow: true now emits its member routes (show/edit/update/destroy) at the shallow path while collection routes (index/create/new) stay nested, matching Rails; an optional path segment like foo(/:bar) now expands to both paths it actually serves instead of one literal that matched nothing; and the hash-rocket route form get 'path' => 'ctrl#action' is now extracted, where previously the path was skipped entirely. cacheVersion bumped v126 → v127. #127
v0.1.37
Jul 18, 2026

I/O metadata for Rust, PHP & C++, module-graph fixes for nested layouts, SvelteKit route entry points & concurrency-safe snapshots

  • Addedpkg/facts re-exports NewStore, NewGraph, and the Graph/ImpactResult/TraversalNode type aliases, so out-of-module consumers can reconstruct an in-memory store and dependency graph from a loaded snapshot (e.g. facts read back from a persisted baseline) without importing the internal package. #112
  • ChangedScrubbed a real customer repository's name from source comments and test fixtures across the test-path, god-class, surface, and diff explainers, replacing it with anonymized examples. No functional change. #113
  • FixBuildModuleGraph derived a dependency edge's source module from the file's leaf directory. In nested module layouts — an Xcode/SPM target with files under Sources/<Target>/<Sub>/ — the leaf directory is not a module, so source nodes never matched module-name targets and no strongly-connected component could form, silently zeroing cycle detection (and skewing dependency-depth) on such projects. New nearestModule helper walks a file's directory up to its enclosing production/test module for the edge source; a no-op on flat layouts (Go packages, directory-based Ruby/TypeScript modules). #114
  • FixCompanion to the edge-source fix: BuildModuleGraph and the layers explainer matched an import edge's target against an exact module name, but extractors emit targets at differing granularity — a bare module dir (Go/Ruby), a module dir plus imported symbol (Kotlin/Java import a.b.Ca/b/C), or a file stem (TypeScript) — so an exact match dropped every class- or file-suffixed target; on a Kotlin codebase this hid all Kotlin-sourced layer violations and dependency cycles. Target resolution now walks up to the nearest enclosing module in both BuildModuleGraph and the layers explainer's detectViolations. #115
  • FixThe Svelte extractor only ever fed a SFC's <script> block to the parser — the template/markup was discarded, so any handler, action, or binding wired solely from markup (on:click={fn}, {fn(x)}, bind:x={fn}, use:action) had zero incoming edges and was mis-reported as dead code by find_orphans. New extractSvelteMarkupRefs scans the markup for such identifiers and folds them into a KindFileRef fact, mirroring the TypeScript extractor's JSX file-ref pass. cacheVersion bumped to v120. #116
  • AddedThe Rust extractor emitted only cyclomatic complexity, unlike every other extractor, so downstream performance analysis produced nothing for Rust regardless of loop nesting or I/O. The AST walker now tracks syntactic loop nesting with a constant-trip-vs-scaling distinction, records loop_count/calls_in_loop/calls_in_scaling_loop, flags recursive_self, and detects direct filesystem/DB/HTTP primitives as io_direct, with a transitive performs_io fixpoint over the call graph ported from the Python extractor. #117
  • AddedThe PHP extractor emitted loop metadata but no I/O signal, so an in-loop call to a DB/HTTP wrapper was invisible to N+1 detection — only a keyword name match could fire, under-detecting wrappers and over-firing on in-memory helpers. The walker now flags io_direct on direct filesystem/DB/HTTP primitives (file_get_contents/fopen/curl_exec/mysqli_*/wp_remote_*, and distinctive $wpdb->get_results/get_row/get_var/get_col, PDO/mysqli fetch/query/execute calls) and runs the same transitive performs_io fixpoint as the Python/Rust extractors. #118
  • ChangedRemoved developer-machine-only C++ integration tests that referenced absolute local paths and would never run in CI, and anonymized a further customer name reference in a Python extractor test comment. No functional change. #119
  • AddedThe C++ extractor emitted loop metadata but no I/O signal, so an in-loop file/socket call was invisible to N+1 detection. The walker now flags io_direct on a deliberately narrow set of direct file/socket data-transfer primitives called as free/namespaced functions (fopen/freopen/fread/fwrite/socket/recvfrom/sendto) and runs the same transitive performs_io fixpoint; console/logging calls and ambiguous socket verbs (bind, connect, send) are excluded to avoid mass false positives, and <fstream> stream I/O via member calls is not detectable. #120
  • FixSvelteKit's file/export-name-convention entry points — load in +page.ts/+layout.ts/+page.server.ts/+layout.server.ts under routes/, HTTP-method exports in +server.ts, and handle/handleError/handleFetch in hooks.server.ts — got no route fact and no symbol classification, since detectSvelteKitRoute only ever ran for .svelte SFCs; these plain-.ts framework entry points had zero incoming edges and their exports read as find_orphans false positives. cacheVersion bumped to v124. #121
  • FixTool handlers dispatch on their own goroutines, unserialized, so multiple handlers could run against the one shared engine at once. GenerateSnapshot mutated the fact store in place (Clear → Add → BuildGraph) while readers took no lock, racing the snapshot pointer and repoPaths map — and snapshot.Facts aliases the store's live slice, so a reader could iterate an array being reallocated mid-regeneration. Snapshot state is now published as an immutable bundle behind an atomic.Pointer; GenerateSnapshot builds a new store off to the side and swaps the pointer once at the end. Added -race stress tests covering overlapping generates against lock-free readers. #122
v0.1.36
Jul 17, 2026

Fixed dead-code false positives from macro-defined C/C++ functions, Python closures & guarded imports

  • FixA C/C++ function defined through a name-carrying macro the grammar can't expand parsed either as an errored statement with a detached top-level body, or as a clean function_definition whose declarator lacked a function_declarator — both cases dropped the body's calls, so a static helper reached only from it had zero incoming references and was flagged dead code. New handleDetachedBody credits the body's calls to the module owner, the same mechanism already used for #define replacement lists; no symbol is emitted for the macro-defined function itself. cacheVersion bumped to v116. #109
  • FixPython's walkForCalls hard-stopped at nested class_definition/function_definition/decorated_definition nodes, so a closure's body — every call, decorator, lazy import, and reference inside it — was never walked. Any module-level helper reached only from a closure (router factories, decorator factories, event-listener registrations) had zero incoming references, the single largest false-positive bucket in the Python corpus (~25%). New walkNestedScope walks nested definitions in a shadowed scope that credits references to the enclosing symbol, with metrics suppressed and bound names extended both directions to prevent fabricated edges. cacheVersion bumped to v117. #110
  • FixPython imports nested inside a module-level try/if block — the try/except ImportError dual-import pattern, if __name__ == "__main__": imports, if TYPE_CHECKING: imports — registered no binding and no dependency fact, so calls through the guarded name were unresolvable and mis-reported as dead code. These statements now route through registerBodyImports, with an importFallback flag so an except-branch import can't clobber the try-branch binding. Conditionally-guarded definitions are deliberately still not emitted as symbols, to avoid manufacturing a dead-code false positive for an intentional shim. cacheVersion bumped to v119. #111
v0.1.35
Jul 15, 2026

Rust language extractor & Java SPI dead-code fix

  • AddedFirst Rust extractor — tree-sitter-based symbols for fn/struct/enum/trait/type/const/static with impl/trait and nested mod {} qualification, impl Trait for Type as an implements edge, use-based dependency facts classified internal/external/stdlib with cross-crate and submodule resolution for Cargo workspaces, calls/instantiates edges, per-function cyclomatic complexity, and Axum route detection (including chained verbs like get(a).post(b)). Tested on dbt-core and Axum. #85
  • FixJava classes loaded only by name — SPI implementations registered in a service file, or classes discovered by classpath scanning for a plugin annotation — produced neither a call edge nor a prop and were reported as dead code. Service files are now read and resolved to their in-repo canonical type, emitting a KindFileRef fact the orphan detector already folds in as a reference; classpath-scanned plugin classes are now tagged scanned_plugin as entry points. #108
v0.1.34
Jul 13, 2026

Test-path gating for architecture explainers, findings-diff dedup & TypeScript ORM storage

  • Fixgod-class and hotspots ranked symbols by fan-in with no notion of a test path, so a test-scaffolding assertion helper could outrank real findings. A shared facts.IsTestPath now gates the candidate (not the edges) across Swift, Kotlin/Java, Python, and C/C++ — languages the prior reference-only-kind filter never covered. #102
  • Fixenola-enterprise's analyze_performance could summarize the unfiltered result set instead of the filtered one — a package="androidTest" filter printed the repo-wide total above an empty table. New mcputil.Scoped[T] carries the filtered set plus its population size so a renderer has nothing else to count; traverse's node-count stat is fixed the same way. #103
  • Fixcycles and dependency-depth only excluded test modules via module_role == ModuleRoleTest, a prop only Java, Kotlin, Ruby, and Swift emit — Go, Python, TypeScript, PHP, and C/C++ test trees walked straight into the coupling graph. Both explainers, and exported-surface's public-API count, now fall back to facts.IsTestPath where the build-file prop is absent. #104
  • Fixenola-enterprise's performance analyzer carried its own weaker glob matcher that silently matched nothing against any ** pattern. The engine's directory-scoped **-capable matcher is now exported as pkg/facts.MatchGlob/MatchAnyGlob so both sides share one implementation. #105
  • Fixdiff_snapshot could report "no facts, edges, or findings altered" while insights.json's hash had changed — a findings key collapsed distinct findings (e.g. "5621 more" vs. "3662 more" dead-code candidates) into one bucket with no changed-content case to land in, and 78 same-titled layer violations shared a map key so all but the last were silently dropped from the diff. Findings are now grouped and paired positionally, matching the facts side; a pinned baseline older than 3 days now warns. #106
  • AddedTypeScript now emits storage facts — the only backend language in enola that modelled no tables. Covers TypeORM (@Entity()), Drizzle (pgTable(...)), and Prisma (schema.prisma models), gated on the package.json dependency. performs_io now also propagates through repository-wrapper methods around ORM query calls, so a per-iteration call to a wrapper is caught as a candidate N+1. #107
  • AddedCode of Conduct and contributing guidelines. #101
v0.1.33
Jul 12, 2026

Loop-depth parity for Java/C/C++/PHP, Swift override & #if fixes, Flask route detection

  • ChangedJava (v104), C/C++ (v105), and PHP (v106) now emit scaling_loop_depth and calls_in_scaling_loop via the same three-valued loop classifier (constant/infinite/scaling) as Go/Python/TS/Kotlin — a literal-bounded loop no longer inflates the reported Big-O exponent. #95
  • Fixquery_insights, query_facts(kind=service), and the llm_context renderer reported "cross-repo explainer did not run" identically to "found nothing," and misattributed which explainers actually produced an insight. The no-match case now distinguishes a single-repo snapshot (no cross-repo linker output) from a genuine no-match, and lists sources from Insight.Source instead of the ran-without-error set. #96
  • FixSwift methods carrying the override modifier are now tagged override:true (v107), matching the existing Kotlin prop — an override is dispatched by its supertype (UIKit/SwiftUI lifecycle callbacks, XCTestCase hooks) and was being flagged dead by the orphan detector, which already consumed this prop for Kotlin but Swift never set it. #97
  • FixA Swift type declared in both branches of a #if/#else block produced two same-name symbol facts. Branch symbols are now tagged conditional so counting analyzers collapse the duplicate while keeping genuine overloads intact. #98
  • FixPython route detection matched only FastAPI verb decorators and hardcoded framework:"fastapi" on every match — @app.route/@bp.route and Flask-AppBuilder @expose produced zero route facts (0 of 293 on python/superset). Flask idioms are now detected and tagged framework:"flask" (v109), with the verb-shorthand label derived from the detected framework instead of a literal. #99
  • AddedJava now emits io_direct/performs_io (v110) from high-precision, type-level signals — every method of a @FeignClient interface or Spring Data repository, any @Query/@Modifying/@Procedure method, and Room ops inside a @Dao interface — so a per-iteration repository call can be recognized as a confirmed N+1 instead of ranked on a keyword guess. Deliberately excludes bare @GET/@GetMapping, which are inbound handlers, not outbound I/O. #100
v0.1.32
Jul 10, 2026

Test-ref parity for Go & TypeScript, Python shadow-guard fix & LLM-context repair

  • Fixllm_context.md's Extraction Quality block only reported unresolved-edge counts when coverage_gaps > 0, hiding real signal on snapshots with many unresolved calls but zero gaps; the Repository Map section could also consume the entire token budget on multi-repo snapshots and drop the quality preface entirely. Sections now reserve budget ahead of layout, and a byte- vs. rune-boundary truncation bug that could emit invalid UTF-8 is fixed. #87
  • FixPython call resolution checked same-class method/property matches before the local-binding shadow guard, so the common self.x = x constructor idiom produced a false calls edge whenever the class also defined a property literally named x (e.g. a SQLAlchemy synonym property). The shadow guard now gates every resolution branch, not just the same-module fallback. #86
  • ChangedKotlin now emits scaling_loop_depth and calls_in_scaling_loop alongside loop_depth, joining the Go/Python/TypeScript convention — a constant-count loop no longer inflates the reported Big-O exponent. The same fix separates "grows with input" from "has a non-constant trip count" across all four languages, so an infinite loop with a chain walk inside it is now caught as an N+1 candidate instead of masked. #88
  • FixGo functions called only from their own _test.go file were reported as high-confidence dead code — test files are now scanned for reference-only test_ref facts, matching the existing Ruby handling. #89
  • FixA Go baseURL + "/path" HTTP-client pattern discarded the host, causing third-party API calls to be counted as unresolved internal edges and flip isolated backends to a false coverage_gap. The base identifier now resolves through a package-scoped string-literal index and is tagged external when every binding is an absolute http(s) URL. #90
  • FixA Swift type reached only through its typealias name was reported as dead code — handleTypeAlias now folds the aliased type in as an instantiates edge. #91
  • Fixtest_ref and file_ref nodes were indexed into the coupling graph, so their edges landed in the reverse adjacency map and inflated fan-in and centrality for the god-class and hotspots explainers — drifting their outlier thresholds and dropping genuine findings below the cut. Both explainers now read from a filtered reverse index that excludes reference-only edges. #92
  • FixTypeScript was the last extractor without test_ref support — a production symbol called only from its .test.ts(x)/.spec.ts(x) file was reported dead. Closed to parity with Go and Ruby. #93
  • ChangedDoc comments referenced an unmatched_reason value (no_match) the cross-repo linker never actually emits; the four reason strings are now exported crossrepo.Reason* constants so the list is defined once. #94
v0.1.31
Jul 09, 2026

Cross-repo linking, diff-pairing determinism & skip-accounting fixes

  • Fixdiff_snapshot no longer drops or misreports facts that share an identical key (e.g. Swift types declared under mutually exclusive #if/#else branches, overloaded methods) — colliding facts are now bucketed, sorted deterministically, and paired positionally instead of last-write-wins, fixing both silently-dropped deletions and spurious changed-fact reports on byte-identical fact sets. #81
  • FixCross-repo shared-symbol linking no longer treats nested type names (e.g. Kotlin/Swift Outer.Inner) as namespace-qualified — only :: now counts. Dotted nested names were bypassing the same-language guard and producing spurious bidirectional depends_on edges between unrelated repos that merely reused a type name. #82
  • FixCoverage-gap detection consolidated behind a single rule — the snapshot receipt was flagging any unresolved outbound call as a gap, while coverage_report and the coverage explainer required zero resolved edges, so the receipt's gap count saturated and lost its signal on healthy repos. All three now classify through one shared function. #83
  • FixSnapshot receipts now count pruned directories — a directory dropped by an ignore glob (e.g. node_modules/) previously left its files uncounted entirely, understating files_skipped by orders of magnitude on trees with large ignored directories. A new dirs_skipped field is added, and each skip sample now names the glob that matched it. #84
v0.1.30
Jul 09, 2026

Python value-ref resolution, relation-kind breakdown & Ruby test-file scoping

  • FixPython call resolution now emits a value-ref edge when a function is passed on via assignment or return (cb = handler; return cb) — a common factory/registry/DI-by-return pattern that previously left the referenced function with no incoming edge and misreported it as dead code. #79
  • FixThe Python call-resolution shadow guard now covers every name bound anywhere in a function's scope — not just parameters — so a loop variable, local assignment, with ... as alias, or walrus binding that happens to share a name with an unrelated top-level function no longer gets credited as a call to it. #79
  • Added--explain now reports a relation-kind count breakdown (declares/imports/calls/.../has_method) for visibility into a snapshot's edge mix. #79
  • FixRuby test-file matching now requires both a _spec.rb/_test.rb filename and a spec/ or test/ directory segment — a filename-only rule was silently excluding production files that merely end in the token _test (e.g. an ab_test.rb A/B-test job) from indexing entirely. #80
v0.1.29
Jul 08, 2026

Cross-repo coverage triage, Swift/Rails route fixes & Rails explainer tuning

  • AddedCross-repo coverage now buckets calls to hardcoded third-party hosts as external, separate from genuine unresolved internal gaps; unresolved client calls are tagged with a reason (method_mismatch, path_unknown, generic_path) so the residual can be triaged from data. #77
  • FixSwift HTTP method inference widened — multi-line case-label lists and single-value method properties no longer silently default to GET; URLSession verb detection now scans a symmetric window and recognizes enum/.rawValue forms. #77
  • FixRails route extraction now covers PUT on resource updates, symbol-path routes (get :cities_by_zip), bare scope symbol prefixes, and resource(s) path: overrides — on a 3-repo test graph, unresolved client calls dropped from 97 to 59. #77
  • ChangedRuby/Rails architecture explainers tuned to cut false positives — two-tier layering (delivery vs. domain), deduplicated god-class/hotspot candidates, oversized dependency clusters reported as one low-confidence note instead of a cyclic-dependency alarm, and framework scaffolding excluded from god-class and hotspot findings. #78
v0.1.28
Jul 08, 2026

Graph-wide receipt for multi-repo snapshots

  • AddedGlobal receipt written to ~/.enola/receipt.json — describes the current multi-repo graph of graphs: composing repositories, each repo's git commit/ref/dirty state, membership duration, and graph-wide stats (fact/insight counts, cross-repo services and edges, coverage). Complements the existing per-repo receipt.json. #76
v0.1.27
Jul 08, 2026

Self-update via enola upgrade

  • Addedenola upgrade command — resolves the latest GitHub release, downloads the matching platform artifact, verifies its checksum, and atomically replaces the running binary. #75
v0.1.26
Jul 08, 2026

Python gRPC support & call-resolution fixes

  • AddedFirst-class Python gRPC support — servicer-to-route binding via the Servicer subclass convention and client detection from stub.Method() calls (including positional rebinding of a stub variable across services), bringing Python to parity with the existing Go and TypeScript gRPC support. #74
  • FixPython resolveCall no longer fabricates same-module call edges for callable parameters, locals, and loop variables — removes spurious "used" signals, inflated fan-out, and misleading impact analysis. #74
  • FixModule-graph construction no longer misclassifies single-segment internal modules (e.g. top-level cmd, config) as external, which was silently dropping real dependency edges. #74
v0.1.25
Jul 07, 2026

Explainer correctness & determinism fixes

  • FixMetric parsing in pkg/explain corrected — digits inside symbol or module names (e.g. Sha256Hash, oauth2) no longer corrupt parsed fan-in/fan-out and exported-surface percentages. #73
  • FixCycle detection made deterministic — cycle paths, evidence order, and insight order no longer depend on map iteration order, via a shared strongly-connected-components pass. #73
  • FixDependency-depth explainer no longer under-counts chains hidden behind a cycle — depth is now computed over the SCC condensation instead of a globally memoized, back-edge-truncated walk. #73
  • FixCross-repo explainer no longer drops the via detail after a facts.jsonl round-trip. #73
  • FixLayer explainer hardened — deterministic pattern-insight evidence ordering, layer violations deduplicated by source/target module, and relative import targets (./x, ../y) resolved before layer matching. #73
v0.1.24
Jul 07, 2026

Python dead-code false-positive fixes & complexity signal improvements

  • FixPython dead-code false positives reduced — absolute intra-project imports, function-local (lazy) imports, decorator applications and arguments, parameter defaults, and functions passed by name are now all tracked as call/reference edges. #72
  • Addedscaling_loop_depth and calls_in_scaling_loop signals — loops over constant/bounded ranges no longer inflate scaling-complexity or N+1 findings the way input-scaling loops do. #72
  • AddedPython performs_io propagation — direct DB/network/file calls now propagate transitively across the call graph, matching the existing TypeScript pass. #72
  • AddedPython extractor now classifies enums, data classes (@dataclass, attrs, Pydantic models, NamedTuple, TypedDict), and duck-typed abstract classes (methods that only raise NotImplementedError). #72
  • FixPython virtualenvs and installed dependencies (.venv, site-packages, .tox, and similar) are no longer walked, parsed, or cached, cutting snapshot time on repos with a local virtualenv. #72
  • ChangedGenerated/vendored path filtering widened to cover vendor/, openapi-gen/, third_party/, __generated__, and common codegen suffixes, keeping them out of performance analysis. #72
v0.1.23
Jul 05, 2026

Gemfile-less Ruby repo detection

  • AddedGemfile-less Ruby repositories now detected, with extensionless Ruby executables indexed via shebang detection. #71
v0.1.22
Jul 05, 2026

gRPC support & expanded cacheVersion test coverage

  • AddedgRPC support — proto RPCs modeled as routes, with Go server handlers bound via handled_by edges; grpc-go, connect-go, and struct-field-injected Go clients now detected as client-role routes. #70
  • AddedTypeScript grpc-web and connect-es client call sites now recognized alongside Go clients. #70
  • ChangedTest coverage extended — every cacheVersion case now covered by unit and golden tests, with an enforced coverage guard. #69
v0.1.21
Jul 05, 2026

CLI version flag

  • Added--version flag on the enola binary, printing the ldflags-stamped build version. #68
v0.1.20
Jul 04, 2026

Snapshot receipts

  • AddedSnapshot receipts — receipt.json now captures provenance (version, git ref, content fingerprint, config hash, output hash) and extraction-quality metrics; new snapshot_receipt and compare_receipts MCP tools added, with quality signals surfaced in llm_context. #67
  • Addeddiff_snapshot comparability guard validates snapshot compatibility before comparison. #67
  • FixSnapshot generation now requires an explicit fresh=true parameter, preventing silent data pollution when switching between projects. #67
v0.1.19
Jul 04, 2026

Expanded cross-repo HTTP-client detection

  • ChangedHTTP-client detection expanded — TypeScript options-object and openapi-fetch patterns, Swift endpoint enums with prefix resolution (plus request-wrapper and stored-method endpoint resolution), and Rails draw() scope prefixes now recognized. #66
  • FixRails nested singular/plural resource route path handling corrected; import/shared-symbol false positives removed from cross-repo HTTP-client detection. #66
v0.1.18
Jul 03, 2026

TypeScript & Kotlin extractor accuracy improvements

  • FixTypeScript dead-code false positives reduced — JSX, import, and this-member references now tracked; framework-dispatched symbols correctly excluded from unused-code findings. #65
  • PerfMinified bundle files now skipped during performance analysis — transitive performs_io propagation added to cut false positives. #65
  • FixTypeScript interfaces now classified as data shapes and excluded from package metrics, at parity with type aliases; abstract classes properly tagged. #65
  • FixKotlin dead-code false positives reduced — method kinds and call edge types (receiver, property, callable-reference) now emitted; framework, DI, override, and test entry points excluded from orphan detection. #64
  • FixKotlin cross-module Groovy-namespace imports now resolved correctly in multi-module projects. #64
v0.1.17
Jul 03, 2026

TypeScript monorepo aliases, Swift accuracy & Ruby on Rails graph

  • FixTypeScript monorepo path aliases now resolve correctly — a root tsconfig.json without aliases no longer blocks alias discovery in nested packages; ~/ and #/ imports resolve against the correct package config. #61
  • FixBarrel re-exports (export * from, export { X } from) now tracked as dependency facts — code accessible only through index files no longer appears unused. #61
  • AddedPlain JavaScript framework detection — Vue, React, Svelte, Next.js, and Nuxt projects without TypeScript are now correctly identified. #61
  • FixSwift extractor accuracy improved — false positive count in performance analysis reduced from 259 to 81 via bounded loop detection and recursion awareness; dead-code detector no longer flags methods in flattened types. #62
  • ChangedSwift modules now tagged with roles (production / test / tooling); test bundles collapsed and excluded from the dependency graph. #62
  • ChangedRuby on Rails graph improved — dead-code coverage extended to delegate, ivar, and string-dispatch call sites; mixin vs namespace module classification added with namespace-aware coupling resolution. #59
v0.1.16
Jul 01, 2026

C/C++ macro parsing & Ruby dynamic dispatch

  • ChangedC and C++ extractors improved — macro parsing, function pointer detection, field assignments, compound literals, and token pasting now handled; false positive rate reduced significantly. #57
  • ChangedRuby extractor extended — callbacks, dynamic dispatch, chained no-arg calls, .rake file indexing, and predicate (method?) and bang (method!) style methods now captured. #58
v0.1.15
Jun 29, 2026

Diff tool, C support & extractor improvements

  • AddedArchitecture diff tool — compare two snapshots to detect structural changes between commits or branches. #56
  • AddedC language extractor — first-class support for C source files alongside the existing C++ extractor. #56
  • ChangedRuby on Rails graph improved — more accurate dependency extraction across Rails projects. #56
  • FixC++ extractor bug fixed and snapshot comparison hardened. #56
v0.1.14
Jun 28, 2026

PHP support

  • AddedPHP extractor — classes, functions, and dependency edges indexed from PHP source, with WordPress framework detection. #55
  • ChangedPHP support brought to parity with other language extractors, including framework-specific detection. #55
v0.1.13
Jun 28, 2026

Svelte support, shared MCP result helpers & snapshot refresh fix

  • AddedSvelte framework detection — Svelte components and SvelteKit routing now extracted and linked in the architectural graph; the $lib alias resolves to src/lib/ for correct internal dependency edges. #51
  • ChangedMCP result-builder and output-cap helpers (jsonResult, textResult, token-budget capping) extracted to pkg/mcputil so out-of-module tools share one implementation instead of duplicating it. #52
  • Fixquery_insights' repo filter matched by loose substring against the insight title, so filtering by repo="golf" could also return insights from unrelated sibling repos that merely share the token (golf-ui, my-golf-journal-ios). The filter now matches against the evidence path's repo segment on multi-repo snapshots, falling back to the title-substring heuristic only when a snapshot has no repo prefixes to match against. #53
  • FixAuto-loaded facts no longer overwritten on snapshot refresh — append flag now handled correctly during cleanup. #54
v0.1.12
Jun 27, 2026

JS/JSX support, query_insights tool & doc fixes

  • Addedquery_insights tool — surfaces architectural insights including dead-route detection directly from queries. #50
  • AddedTypeScript extractor now recognises .js and .jsx files — mixed JavaScript/TypeScript codebases are fully indexed. #49
  • FixDocumentation corrections across tool descriptions and cross-repo capability wording. #50
v0.1.11
Jun 26, 2026

Unused route detection in cross-repo environments & extended testing

  • ChangedTesting strategy extended with broader coverage and linting fixes applied across the codebase. #47
  • AddedServer-side inverse computation — unused routes are now identified across multi-repository graphs, surfacing dead endpoints that no client calls. #48
  • ChangedCross-repo capability descriptions updated to reflect the expanded route analysis. #48
v0.1.10
Jun 25, 2026

Coverage insights, HTTP client extraction & extended testing

  • AddedHTTP outbound client extraction for Ruby (Net::HTTP, Faraday, HTTParty) — outbound API calls now appear as graph edges in Ruby projects. #45
  • FixGo HTTP client extraction improved — more call patterns captured for outbound API edges. #45
  • AddedCoverage insights — unresolved dependency counts now tracked for Swift, Kotlin, Go, and TypeScript extractors. #46
  • FixJava and Python HTTP client extraction hardened with improved exception handling. #46
  • FixCI configuration corrected — coverage explainer source files no longer excluded by .gitignore. #46
v0.1.9
Jun 24, 2026

Extraction pipeline performance pass

  • PerfScanning performance improved — first pass of optimisations across the extraction pipeline. #44
v0.1.8
Jun 23, 2026

Security patch — CVE-2026-27896

  • FixUpgraded modelcontextprotocol/go-sdk to 1.3.1 to remediate CVE-2026-27896 (improper handling of case sensitivity). #43
v0.1.7
Jun 23, 2026

Architectural smell detection

  • AddedFive new explainers — god classes, call-graph hotspots, deep dependency chains, large public surfaces, and complexity outliers. #42
  • Changed--explain now includes architectural smell findings alongside the existing cycle and layer analysis. #42
v0.1.6
Jun 21, 2026

Vue & Nuxt support, per-function complexity metadata & Python polyglot detection

  • AddedVue and Nuxt framework detection — Vue SFC script block extraction, composable detection, and automatic Nuxt file-based route inference (e.g. pages/users/[id].vue/users/[id]). #40
  • AddedPer-function metadata — complexity and performance metrics now tracked for Go, Python, Ruby, Swift, Kotlin, TypeScript, and Java functions. #41
  • FixKeyword matching and nested closure handling corrected across multiple extractors. #41
  • FixDeep-nested project detection extended to Python repositories — pyproject.toml, setup.py, setup.cfg, and requirements.txt now trigger deeper TypeScript root search in polyglot repos. #39
v0.1.5
Jun 19, 2026

Adaptive TypeScript project detection & Python symbol indexing

  • ChangedTypeScript root detection now adapts search depth to repository structure — simple repos search 2 levels, Java/Gradle polyglot repos search up to 8 levels to find nested frontends (e.g. src/main/resources/ui). #38
  • AddedJava project detection (pom.xml, build.gradle, build.gradle.kts) to trigger deeper TypeScript search in polyglot repositories. #38
  • FixCommon build and dependency directories (node_modules, dist, .next, build, out, target, vendor) now skipped during TypeScript root search — reduces false positives and improves performance. #38
  • ChangedPython extractor enhanced with two-pass symbol indexing — cross-file type resolution, typed parameter inference, and abstract-to-concrete implementation tracking. #35
v0.1.4
Jun 18, 2026

--explain flag & multi-language fixes

  • Added--explain flag — asks enola to explain the architectural reasoning behind query results. #34
  • FixRuby dependency resolution improved — require and include statements now resolve correctly across modules. #37
  • FixJava static import detection gap closed. #34
  • FixSwift & Kotlin edge dependency resolution corrected; Kotlin internal module coupling now resolves accurately. #34
  • ChangedArchitecture detection improved for more accurate pattern recognition. #36
v0.1.3
Jun 16, 2026

C++, Java & Ruby tree-sitter rewrite

  • AddedC++ extractor — classes, functions, and cross-repository edge detection. #31
  • AddedJava extractor following the established multi-language extraction pattern. #32
  • ChangedRuby extractor migrated from regex to a tree-sitter AST parser — more accurate class, method, and call-site extraction across Rails and plain Ruby projects. #33
v0.1.2
Jun 16, 2026

Python tree-sitter rewrite

  • AddedPython extractor rewritten on tree-sitter AST — adds Django support (models, class-based views, DRF serializers, URL patterns), decorator recognition, and improved call/instantiation edge detection. #28
v0.1.1
Jun 15, 2026

TypeScript improvements, token economy & release tooling hardening

  • ChangedTypeScript extractor improved with React component, hook, and route handler semantic classification. #27
  • PerfToken economy improved across all tools — output budgets enforced, cross-repo summaries tightened. #29
  • Fixfind_path improved — ambiguous node names handled correctly, cross-repo dependency traversal corrected. #29
  • ChangedDocumentation restructured — a new ARCHITECTURE.md added, describing enola's fact/relation graph model and its core determinism invariant: every fact comes from a real parser or a deterministic algorithm, never inferred by a language model. #30
  • FixRelease workflow and install script hardened — native ARM64 and Intel GitHub Actions runners replace cross-compilation on shared runners, workflow permissions scoped explicitly, and install.sh now falls back to shasum -a 256 when sha256sum isn't available (stock macOS). #26
v0.1.0
Jun 12, 2026

First official release — Swift AST parser, HTTP client extraction & install script

  • AddedSwift extractor upgraded from regex to a C-based AST parser (tree-sitter) — more accurate symbol extraction across SPM and Xcode projects. #24
  • AddedHTTP client extraction for Kotlin (Retrofit), Swift (URLSession), and TypeScript (fetch/makeRequest) — outbound API calls now appear as graph edges. #25
  • FixCall graph resolution improved across Go, Kotlin, and TypeScript — typed variable method calls and cross-module imports now resolve correctly. #23
  • FixInstall script added with checksum verification and OS/arch auto-detection for Linux, macOS, and Windows. #25
v0.0.8 Pre-release
Jun 09, 2026

Cross-repo resolution fixes & org migration

  • FixCross-repository graph resolution corrected — edges between repositories now resolve accurately across all reference patterns. #21
  • ChangedArchitectural node descriptions improved for clearer, more consistent output across graph types. #22
  • ChangedTop-level inclusion refactored — modules can now be included at the project root without additional configuration. #20
  • ChangedReferences migrated to the enola-labs GitHub organisation — all import paths, links, and identifiers updated. #19
v0.0.7 Pre-release
Jun 03, 2026

Graph of graphs & first-response latency

  • AddedGraph of graphs — multiple repository snapshots now connect into a single traversable graph. Cross-repo edges resolve correctly across module and service boundaries. #18
  • PerfFirst-response latency reduced — top-level resolution object rebuilt to surface the most relevant context on the initial query without a second round-trip. #17
v0.0.6 Pre-release
May 29, 2026

Rebrand to enola

  • ChangedProject renamed from archmcp to enola. All identifiers, config keys, and MCP server registration names updated. Existing archmcp configs require a one-line rename. #11
v0.0.5 Pre-release
May 14, 2026

Kotlin DI detection

  • ChangedImproved dependency-injection detection in Kotlin — constructor, field, and setter injection are now captured as first-class graph edges. #10
v0.0.4 Pre-release
Mar 06, 2026

OpenAPI integration

  • AddedOpenAPI spec integration — routes and schemas from openapi.yaml are merged into the architectural graph. #9
v0.0.3 Pre-release
Feb 26, 2026

Ruby extractor & memory improvements

  • ChangedRuby extractor now captures method calls in addition to definitions — call-site edges appear in snapshots. #7
  • PerfMemory handling overhauled — peak RSS reduced significantly on large repositories during indexing. #8
v0.0.2 Pre-release
Feb 22, 2026

In-memory graph, query performance & Rails support

  • AddedIn-memory knowledge graph — the full architectural graph is now held in memory for zero-latency snapshot queries. #6
  • AddedRuby on Rails support — routes, controllers, and models are extracted and linked in the graph. #5
  • PerfQuery efficiency improvements — snapshot generation is substantially faster on deep dependency graphs. #4
v0.0.1 Pre-release
Feb 11, 2026

First public release

  • AddedKotlin extractor — classes, functions, and dependency edges indexed from Kotlin source. #1
  • AddedSwift extractor — types, protocols, and call-site edges indexed from Swift source. #2
  • FixedTest coverage added across extractors; edge-case bugs in symbol resolution corrected. #3