# Release notes

Changes per released version of the Architecture MCP server, newest first. Starts at 26.3.1;
earlier versions shipped without release notes.

---

## 26.5.1 — 2026-08-21

### Upgrading

- **`locate_fqn` now reports EVERY component declaring a name, and its reply shape changed.** The
  top-level `filterName` and `perFile` moved down one level into a new `components` array:

  ```jsonc
  // before                          // now
  { "fqn": "...",                    { "fqn": "...",
    "filterName": "Mod/A/Foo",         "components": [
    "perFile": [ ... ] }                 { "filterName": "Mod/A/Foo", "perFile": [ ... ] } ] }
  ```

  One name can legitimately belong to several components: a C# `partial` type is one type written
  across several files, and the halves can be assigned to *different* artifacts — one under
  `Domain/` and another under `Generated/` is normal. Answering with one of them was answering a
  different question. Expect a single entry for most types and do not assume exactly one.

- **`query_dependencies` replies with `components` (a list) instead of `component`**, for the same
  reason, and it no longer refuses a name owned by several. It answers for all of them as one: the
  results are the union of what the parts reach, and the parts are not reported as depending on
  each other. `trace_dependency` is unchanged in shape — it now searches every pair and reports the
  endpoints of the shortest path it found.

- **`duplicateFqns` no longer reports a C# `partial` type.** If you were filtering those out
  yourself, you can stop. On nhibernate-core this was 401 of 2429 top-level names, so for a C#
  project the signal was almost entirely noise. Java and Python are unchanged: two modules shipping
  one name really is a collision there, and is still reported.

### Added

- **C# is supported.** Set `"language": "csharp"` in `zugel.json`, or just call `generate_config` —
  it detects a solution and writes that configuration itself. Violations, cycles, the baseline
  ratchet and the dependency queries then run on a C# model exactly as they do for Java.

  **What you need on the machine is the .NET 10 SDK** — the SDK, not just the runtime, because
  solutions are opened through MSBuild. The parser itself ships inside the jar, so nothing is
  downloaded and it works offline. If the solution's NuGet packages have never been restored, the
  first scan restores them for you rather than telling you to.

  **A solution file is required**, and `generate_config` records which one it chose. Where a
  repository holds several — nhibernate-core has three — the choice is written to
  `project.solution` and reported, so pointing it at a different one is an edit rather than a
  mystery.

  **The one thing that surprises C# developers: `.arc` rules address directories, not namespaces.**
  A component is a source file and its package is the folder it sits in, relative to the project
  directory — the same rule as Java. C# does not require namespaces to follow folders, and where a
  codebase's don't, the rules follow the folders. So an artifact is written
  `include "MyApp/Services/**"`, not `include "MyApp.Services.**"`.

  **An external component is `External/<assembly>/<namespace>/<type>`** — for example
  `External/System.Collections/System/Collections/Generic/List<T>`. The assembly is part of the name
  because it is the unit of external identity in .NET and a namespace does not imply one:
  NHibernate's `Antlr.Runtime.*` types come from `Antlr3.Runtime.dll`. It is a single segment, so
  `External/System.Collections/**` names everything from one assembly. Generic parameters are part
  of the name — arity is part of a .NET type's identity, so `Task` and `Task<TResult>` are different
  components — and a nested type has none of its own: a dependency on `HashSet<T>.Enumerator` lands
  on `HashSet<T>`.

  **Six attribute retrievers**, the same ones Sonargraph offers and with the same semantics:
  `CSharpTypeOf`, `CSharpExtendsClass`, `CSharpImplementsInterface`, `CSharpIsClass`,
  `CSharpIsInterface` and `CSharpIsEnum`. They assign components by what a type *is* rather than by
  where it sits — `include "CSharpImplementsInterface: **.IRepository"` gathers every repository
  however each one is named. They match dotted type names, so a single `*` stops at a dot.

  Two more things worth knowing before you write rules. A project that targets several frameworks
  is analysed under exactly one of them — the newest, recorded in the configuration — and
  componentIds never mention it, so adding a target framework cannot invalidate a baseline. And a
  project that declares `IsTestProject` is not part of the model at all: its cycles and violations
  would be noise against what the production code owes, so no rule can govern the test-to-production
  direction.

---

## 26.4.1 — 2026-08-20

### Added

- **Python is supported.** Set `"language": "python"` in `zugel.json`, or just call
  `generate_config` — it detects a Python project and writes that configuration itself. Everything
  else works as it does for Java: violations, cycles, the baseline ratchet, and the dependency
  queries all run on a Python model.

  **There is no build system to ask, so the structure comes from the layout.** A virtualenv tells
  you where packages are *installed* and never where the sources are, so poetry, uv, pdm and conda
  have nothing to contribute here. Every `pyproject.toml` is one module — named after the
  distribution it declares — with its source roots taken from setuptools, hatch or poetry metadata,
  or from src-vs-flat convention when the metadata says nothing. A project with no `pyproject.toml`
  at all is still configured from its directories.

  **Nothing has to be installed first.** An import names its own target, so a freshly cloned
  repository with no environment set up yields a complete, governable model. What you do need is a
  Python 3 interpreter the server can find — it looks for `python3`, then `python`, and on Windows
  the `py` launcher first. Prefer 3.10 or newer: Python's own parser only understands the syntax of
  its own release, so an older interpreter reports a project's modern syntax as errors in your
  source. To pin a specific one, add `"interpreter"` to the `project` section.

  **Writing `.arc` rules for Python.** A componentId is `module/path/to/file` — the source file's
  location under its source root, with no extension. `locate_fqn` works as it does for Java, and
  resolves three kinds of name to a component: a class, a module-level function, and the module
  itself — `pkg.archive` names the file, which is what an import writes.

  Python has **two attribute retrievers of its own**. `PythonTypeOf` matches any direct or indirect
  base class, so `include "PythonTypeOf: pydantic.BaseModel"` gathers a DTO layer, and
  `PythonTypeOf: airflow.sdk.bases.operator.BaseOperator` an operator layer — roles no naming
  convention identifies. Where Java needs four hierarchy retrievers, Python needs one: it has no
  class/interface split, so an ABC or a `Protocol` is simply another base.
  `PythonHasDecorator` matches any decorator on a class **or a function** — function decorators
  being where the signal usually is — and matches either spelling: the written `app.get`, whose
  head is an instance and resolves to nothing, or the resolved `airflow.decorators.task`, which
  keeps matching after `import task as t`. The Java retrievers do not apply, and naming one in a
  Python project fails at rule-compile time rather than silently matching nothing.

  One limitation: the hierarchy walk stops at the project's edge, since installed packages are not
  parsed. Deriving from `BaseModel` is seen; deriving from a third-party class that itself derives
  from `BaseModel` is not.

  **Generated code is declared by pattern rather than by root.** Java points
  `generatedSourceRoots` at a directory; Python has no such directory, because a module's dotted
  name *is* its path from the source root — anything importable as `pkg.api.models` has to live at
  `pkg/api/models.py`, beside the hand-written code. So declare
  `"generatedPatterns": ["**/*_pb2.py"]` in the `project` section instead. Matching files get the
  same treatment generated Java gets: cycles among them, and violations originating in them, are
  excused. `generate_config` seeds the protobuf globs when your project actually has such files.

  One configuration covers one language. A repository that is genuinely both needs a server per
  language, each with its own `--config_dir`.

### Changed

- **The launcher now checks for a new version on every start, and every four hours after that**
  (launcher 1.0.3). The four-hour interval used to *suppress* the startup check instead of repeating
  it, so restarting right after a release — the one moment anybody is waiting for an update — read a
  manifest fetched *before* that release existed, found nothing, and left `server_version` reporting
  "no update staged or pending" when what it really meant was "we have not looked since before that
  version was published". The startup check is now obligatory, and the interval does what it was
  wanted for: keeping a session that stays open for days from missing a release. Updating is
  otherwise unchanged: the download runs in the background and the new version activates on the
  *next* restart, and `--launcher.no_update_check` still switches the whole thing off.

  Getting it needs the new launcher, which does not update itself — re-download
  `zugel-launcher.jar` if you want this.

---

## 26.3.6 — 2026-08-18

### Added

- **`generate_config` bootstraps Bazel workspaces.** Maven, Gradle and Bazel are all detected now,
  and each build system present is tried in turn until one answers. Bazel is tried last on purpose:
  where a repository carries both a `MODULE.bazel` and a Gradle build, the Gradle build is usually
  the authoritative one, and a Bazel-only workspace has nothing ahead of it in the list anyway.

  **A Bazel module is one SOURCE ROOT, not one target.** Bazel has no unit corresponding to a Maven
  module — its unit is the target, at whatever granularity the build author found convenient — so a
  workspace compiling 325 targets out of three directories would otherwise be modelled as 325 modules
  nobody on that project would recognise. Source roots are derived from each file's declared package
  rather than from directory names, because Bazel workspaces do not follow the `src/main/java`
  convention and often have no `src` anywhere.

  Generated java comes along: the source jars produced by `genrule`, `java_proto_library`,
  `java_grpc_library` and the like are unpacked under `<config dir>/.zugel-bazel/generated-sources/`,
  which wants a `.gitignore` entry — the tool says so. Those components are flagged generated, so
  they are already excused from cycles and violations.

  **One caveat, and `generate_config` warns about it: a Bazel-generated `zugel.json` is
  machine-specific.** Every jar Bazel reports lives under `bazel-out/<platform>-<mode>/…`, so a
  configuration generated on macOS does not resolve on Linux, and `bazel clean` deletes the entire
  tree it points at. Regenerate it rather than committing it — that is cheap once Bazel's cache is
  warm. If you do scan against a stale one, the classpath warning below now tells you instead of
  letting the model quietly shrink.

  Verified on Windows as well as macOS, on every route Bazel installs by there — Bazelisk via npm,
  winget, Chocolatey and Scoop. One Windows prerequisite is not ours to fix and worth knowing: a
  workspace with Maven dependencies needs `BAZEL_SH` pointing at a bash (Git Bash will do), or
  `rules_jvm_external`'s fetch fails before we ever see it.

### Fixed

- **`unrestricted` did nothing when the artifact was declared last — the position it is usually
  written in.** An `unrestricted` artifact may depend on any of its siblings, above it or below it.
  The connectors that grant this were generated only for artifacts that had another sibling after
  them, so the *last* artifact in a body got none at all and every dependency it had was reported as
  a violation. That is the catch-all shape the modifier exists for: a Spring-style
  `unrestricted public artifact Other { include "**" }` closing the file reported violations on
  everything it touched. The guard belonged to `strict` alone, which reaches only the next sibling
  down; `relaxed` and `unrestricted` are unaffected in every other position, and neither
  `strict` nor `relaxed` changes behaviour at all.

  **These were false violations, so your next scan may show fewer.** If you changed code, or
  loosened an `.arc` file, to satisfy one of them, that change can be reverted.

- **A classpath entry that no longer exists is now reported, instead of quietly shrinking your
  model.** Every scan (`rescan_sources`, `reload_all`) now names in `configWarnings` any module whose
  configured classpath entries are not on disk, with how many are missing and what to do about it.
  This used to be completely silent: the sources still parse, so there was no warning and no error —
  and the only casualty was every dependency into those libraries, which means violations and cycles
  running through them could not be seen either. A `mvn clean` over a sibling module's jar, a
  checked-in `libs/` directory that moved, or a hand-edited configuration are the ways in. Re-running
  `generate_config` restores them. A project whose classpath is intact is unaffected.

- **`query_dependencies` now tells the truth about how complete its answer is.** `completeness` was
  wrong in both directions. It counted the language's own constructs as unresolved — a type variable
  (`T`, `RespT`) and the supertype of every `new Something() { … }`, though both resolve fine and
  neither names a file — so answers were reported as incomplete when nothing was missing. And it
  missed references it had nothing to report them *by*: a static call on a type whose jar is gone
  (`StringUtils.capitalize(x)`) or an unresolvable import left no trace at all, so answers that
  really were missing dependencies claimed to be complete. Both are fixed, and an unresolvable import
  is reported by its whole name rather than the package prefix the compiler gives.

  The same gap dropped dependency edges: `new SomeProjectClass() { … }` recorded no `NEW` edge, so
  that coupling was invisible to your architecture rules. It is visible now, so **a violation this
  was hiding may appear on your next scan** — the rule catching something real, not a new
  restriction.

- **`server_version` no longer announces an update that was never downloaded.** It counted a
  version as staged when the launcher's cache merely held a *directory* for it — which the launcher
  creates as soon as it fetches that version's release notes, or begins staging the download. A
  restart was therefore advised for a build that did not exist, and the launcher, which checks for
  the jar itself, went on correctly running the previous version. A version now counts only when a
  verified jar is present. Release notes are unaffected: they are deliberately cached for versions
  whose jar will never be fetched, so that a pinned or out-of-maintenance user can still find out
  what an update contains.

- **A failed update check is now reported instead of vanishing.** The launcher's explanation goes
  to stderr, and MCP clients stop capturing stderr once the handshake completes — so the reason a
  download never landed reached nobody, and the symptom was a version that silently never changed.
  Launcher 1.0.2 records the failure to a file; `server_version` reports it as
  `update.lastCheckFailed`. Older launchers do not write it, which reads as nothing to report.

---

## 26.3.5 — 2026-08-17

### Added

- **Attribute retrievers in `.arc` patterns.** An `include`/`exclude` can now match something other
  than a component's name by naming a retriever first — `include "JavaImplementsInterface:
  **.Controller"` gathers every class whose ancestry reaches that interface, wherever it is declared
  and whatever it is called. This is the case naming conventions cannot express: generated or
  framework-driven code that carries its role in an annotation or a contract rather than in its
  name. Seven are available for Java:

  | Retriever | Matches |
  |---|---|
  | `JavaIsClass` | any type that is a class |
  | `JavaIsInterface` | any type that is an interface |
  | `JavaExtendsClass` | any direct or indirect base class of a non-interface type |
  | `JavaImplementsInterface` | any interface implemented by a non-interface type, transitively |
  | `JavaExtendsImplementsInterface` | as above, but interfaces extending interfaces count too |
  | `JavaTypeOf` | any direct or indirect base type, class or interface |
  | `JavaHasAnnotation` | any annotation on the type |

  Wildcards work on fully qualified names, so `*` stops at a dot and `**` does not: `**.Controller`
  matches the interface in any package, `*.Controller` in one package level only. A type may carry
  several values (a class implements many interfaces) and matching any one of them matches.

  As in Sonargraph, only the component's **main type** is considered — the one named like the file —
  and external components match nothing. The walk up a hierarchy stops at the first type outside the
  analysed sources: its name still counts, but nothing above it does. A retriever name the
  configured language does not provide is a rule-compile error rather than a pattern that silently
  matches nothing.

  Two Sonargraph retrievers are deliberately absent: `JavaHasAnnotationValue`, which needs
  annotation property values, and `JavaBelongsToAggregateRoot`, which needs generic type arguments.
  `JavaHasAnnotation` differs from Sonargraph's in one way on purpose: it sees annotations on the
  type itself, not on its fields and methods, so a detail of one member cannot decide the whole
  component's artifact.

  `explain_architecture_dsl` covers them, so an agent asked to express a grouping that no name
  pattern can capture will find them without being told they exist.

### Fixed

- **Nested types now resolve in `locate_fqn`, `query_dependencies` and `trace_dependency`.**
  Passing `com.example.Outer.Inner` previously missed the FQN index and fell through to reporting
  the type as `External/com/example/Outer/Inner` with status `UNASSIGNED_UNKNOWN` — an internal,
  assigned type described as an unassigned external one, which an agent could reasonably read as
  "no artifact governs this target". It now resolves to the component declaring the enclosing type,
  since a component is a file. Pass nested names source-level with dots; the binary `Outer$Inner`
  form is still not recognised.

  A component's `contains` list consequently holds every declared type rather than only the
  top-level ones. Duplicate-FQN reporting is unaffected on purpose: nested types are excluded from
  it, because no dependency edge ever resolves to a nested name (edge targets are normalized to the
  enclosing top-level type), and a nested FQN can only collide when its enclosing type already has.

- `generate_config` no longer gives up when the first build system fails. On a project carrying
  both a Maven and a Gradle build — a Tycho `pom.xml` beside a Gradle build, for instance — a Maven
  failure now falls back to Gradle instead of aborting. Maven is still preferred when both are
  present and healthy. The fallback is reported in `warnings`, so a Gradle-derived module structure
  never appears without an explanation, and if nothing can configure the project the error names
  every build system tried rather than only the last.

### Upgrading

- **`locate_fqn` answers differently for a nested type.** It used to report
  `com.example.Outer.Inner` as `External/com/example/Outer/Inner` with status
  `UNASSIGNED_UNKNOWN`; it now resolves to the component declaring `Outer`. If you built anything
  on the old answer — stripping the nested part before asking, or treating that status as "not
  ours" — it can go.

---

## 26.3.4 — 2026-08-11

### Changed

- The product is now called **Zügel**. The configuration file is `zugel.json`; the server jar is
  `Zugel-<version>.jar`; the MCP server reports itself as `zugel`.
- **Existing projects keep working.** The server reads the previous `architecture-mcp.json` if no
  `zugel.json` is present, and the launcher renames the file for you the first time it starts a
  26.3.4 or newer server. It never renames when a `zugel.json` already exists, and never when the
  server it is about to start is older than 26.3.4 and could not read the new name.
- Update the server key in `.mcp.json` to `zugel` if you want the tools to appear as
  `mcp__zugel__*`; the old key keeps working.

---

## 26.3.3 — 2026-08-10

### Added

- **`release_notes`** — the server's own release notes, per version, read from the copy inside the
  running jar. It pairs with `server_version`: that answers which build you are talking to, this
  answers what is in it. Call it with no arguments for the running version, `since` for everything
  released after a version you were on, or `version` for one in particular.
- Each section carries an **`upgrading`** field when that release has one: what it does TO you —
  a forced re-parse, a changed reply shape, a moved configuration key — as opposed to what it
  offers. It stays in the section text as well, so nothing is lost by reading only `notes`.
- The notes are bundled at build time, so they work with no network and cannot describe a version
  newer than the server you are asking. Naming one is an error that lists what is available.

---

## 26.3.2 — 2026-08-10

### Improved

- `generate_config` writes JSON arrays with one entry per line, instead of putting a module's
  entire `classpath` on one line. Existing files keep working; re-run `generate_config` to
  reflow the current one.

---

## 26.3.1 — 2026-08-05

### Upgrading

- **The first scan after upgrading is a full re-parse.** The parse cache is now keyed to the
  build that produced it, so older cached models are refused. Minutes once on a large reactor;
  warm again afterwards.
- **Re-run `generate_config`** to pick up `moduleRoot`. Older configurations keep working
  without it.
- **Reply shape change:** `remove_baseline` returns `removed` as an array of names in both
  modes, previously a bare string. Everything else is additive.

### Added

- **`moduleFilter`** in `zugel.json` — `includes`/`excludes` patterns selecting which
  modules `generate_config` writes into the `project` section, replacing the flat exclusion
  list. Patterns use the product's wildcard syntax (`**`, `*` within one dot-separated segment,
  `?`) against the whole module name. `generate_config` gains `includeModules`,
  `excludeModules` and `removeModulePatterns`, all additive against what is persisted.
- **`moduleRoot`** per module — the module's own directory. Lets the loader settle a source root
  two modules claim, by the same rule the generators use.
- **`configWarnings`** on every scan: source root that is not a directory, `dependsOn` naming no
  configured module, source root claimed by two modules, module that indexed nothing. Warnings
  only — no scan fails and no configuration is rewritten.
- **`architectureWarnings`** on every scan: non-`optional` artifacts that matched no component,
  and deprecated artifacts that still hold components.
- **`baselineDrift`** on scans, at startup, and on all four baseline tools — baselines that have
  outlived the configuration they were captured under. While the active baseline has drifted,
  `diff.sinceSessionBaseline` is withheld. Adding a module does not drift a baseline; removing
  one or moving its source roots does.
- Baselines written before this release are reported with `status: UNVERIFIABLE` — no recorded
  scope to check. Their diffs are still served, and they are not deleted by the bulk cleanup.
- **`remove_baseline {"drifted": true}`** deletes every drifted baseline in one call. The active
  baseline is skipped rather than failing the call.
- **`server_version`** — reports the running build. The same version is now used for the startup
  log line and for `serverInfo.version` in the handshake (previously a hardcoded `1.0.0`).
- **`generate_config` reply carries a `rescan` object** with the same payload `rescan_sources`
  returns, plus an `arcFilesLoaded` count. `generate_config` was always a reload; a following
  `reload_all` is unnecessary.

### Changed

- **`diff.addedDependencies` is now `diff.problematicAddedDependencies`** and lists only new
  couplings that break a rule, sit inside a flagged cycle, or sit inside a newly created
  tolerated cycle. Ordered violations first, then cycle members, capped at 50, with
  `problematicAddedDependenciesTotal` giving the real count. Edges of pre-existing tolerated
  cycles and ordinary new dependencies are no longer listed. `addedViolations` is never capped.

### Fixed

- Member references are attributed to the receiver's static type rather than the declaring type,
  which differ when a subclass re-exposes an inherited API. Removes false violations. Unqualified
  and statically imported calls are distinguished.
- `list_artifact_components` returns a declared-but-empty artifact instead of `Artifact not
  found`; "matched nothing" and "never declared" are now separate states.
- External components are assigned in the standing index, so an artifact capturing a third-party
  library reports its contents. `list_reachable_components` still excludes externals.
- A source root claimed by two modules is settled at load time when `moduleRoot` is recorded,
  instead of by scan order. Without that evidence the collision is reported, as before.
- `generate_config` no longer reports "no .arc files configured (cycles-only mode)" when the
  project has `.arc` files.
- A source root that is not on disk is dropped and reported by name, instead of reaching ecj and
  surfacing as a compiler bug.
- The parse cache is keyed to the build that produced it, so a stale model is no longer reused
  after an upgrade.

### Known issues

ecj does not close the classpath jars it opens. The open set does not accumulate across rescans
(macOS and Windows). On Windows those jars stay locked while the server idles until a garbage
collection releases them. Generated configurations put only immutable cache jars under `~/.m2`
and `~/.gradle` on the parse classpath, and class directories hold no handle, so `mvn clean` is
unaffected. Restarting the server releases everything.
