ADR-0078: fleet cruises each repository as it is and evaluates the union; declared edges attach there, and an OpenAPI operation declares an edge only through the handler its document records
- Status: Proposed (2026-10-11)
- Date: 2026-10-11
- Derives from: plan 0004, Steps 11 and 12, its § 1.6 (the frozen
fleet.yamlandedges.yaml) and § 1.7 (the union path prefix; OpenAPI edges by operation id); ADR-0014 (declared edges are marked, never inferred); ADR-0004 (additions are additive and--strict-schemastrips them); ADR-0031 (the exit code from the result alone) - Constrains:
crates/rb-cli/src/cmd/fleet.rs,crates/rb-ingest/src/declared.rs,crates/rb-config/src/fleet.rs,config lint --fleet,schema/v1.json,schema/fleet-v1.json,schema/edges-v1.json - Requirements: FR-REACH-03
Context
Plan 0004 freezes the two file formats and two of the decisions: the union prefixes each module path with the repository's name and /, leaving module identity inside a repository unchanged so its rules and cache are reused as they are; and an OpenAPI edge is read only from a client folder edges.yaml names, matching operation ids to the server's endpoint handlers by the operation id the server's own document records, with no edge and a reason in the receipt when the document has no operation ids. It leaves open where declared edges attach, what fields a declared dependency carries beyond declared and dependencyKind, what a per-repository finding becomes in the fleet's report, what the fleet rules may contain, and, for OpenAPI, how a document records a handler and what counts as a client calling an operation. Each of these touches the "never inferred" line of ADR-0014 or the field contract of ADR-0004, so they are recorded here rather than left in code.
Decision
- Declared edges attach in
fleetonly.fleetreadsedges.yamlbeside the manifest, or the file--edgesnames, and adds its edges to the union before the fleet rules are evaluated.cruisedoes not readedges.yaml: its cache keys do not cover the file or the documents it names, and a mixed-language repository is a fleet of one (repos: [{ name: app, path: . }]). Paths inedges.yamlare the union's module paths, prefixed (web/src/api/client.ts). - A declared edge joins two modules of the graph. An entry whose
fromortois not a module of the union is not added; it is listed insummary.declared.skippedwith the reason and printed as a warning.config lint --fleetreports afrom,toor OpenAPIserverthat no longer exists asstale-declared-edge: against the modules of--graphwhen given, else against the repositories' files. - The fields of a declared dependency.
declared: true,dependencyKind: declared(a new value of the vocabulary),declaredKind(the entry'skind;httpfor an OpenAPI edge),comment(the entry's comment; for an OpenAPI edge the operation id, method, path and document),module(thetopath; the operation id for an OpenAPI edge), nodependencyTypes,followable: false, and themoduleSystemof thefrommodule's language (es6for TypeScript and JavaScript,clrfor .NET,pyfor Python), since the upstream field is required and has no value for an edge no import formed.declaredKindandcommentare additive, absent on every detected edge, and--strict-schemastrips them withdeclaredanddependencyKind, as it stripssidecarfrom a sidecar edge: the edge stays and validates (plan § 1.8), and the strict document says only what dependency-cruiser's schema can say. - An OpenAPI operation declares an edge only through what its documents record. The server document (OpenAPI 3 or Swagger 2, JSON or YAML) records a handler as the operation's
x-rulebearing-handlerextension: a file path relative to the repository the document is in, sosrc/Web/Endpoints/Orders.csinapi/openapi.jsonbecomesapi/src/Web/Endpoints/Orders.cs. A client module calls an operation when it is under the named client folder and the code layer its extractor wrote records a function, class or member declared in it named exactly as the operation id (a generated client declares one per operation; no case folding, no renaming a generator might do). The client side is read from the extractor's output, never from source text, so no source file is read outside an extractor (ADR-0010); the server document is read as a declaration, asedges.yamlis. Each such module declares one edge to the handler. An operation without anoperationId, or without a handler, or that no client module declares, declares nothing;summary.declared.openapiSources[]counts each case per pair and, when a pair declares nothing, says why. A path item given by$refis not followed. - Each repository is cruised as it is. In its own folder, with the configuration the manifest names (or the one
cruisewould find),-T json, and its own cache: the configuration'soptions.cachewhen it sets one, else--cacheat the configuration's default folder;fleet --no-cacheturns it off for every repository. Repositories run in parallel on a pool as wide as the machine's parallelism. A repository whose folder is missing, or whose own run exits 2 or 3, stops the fleet with that exit code and the repository named. - The union. Every module path, dependency
resolved, andcycleandviaentry, and thefromandtoof every violation over modules or folders, is prefixed with<name>/(a project, element or slice violation names projects or types, and keeps its names), except a core module and an unresolved specifier, which are not paths of the repository and are merged into one module when several repositories reach them. The code layer is not carried: its type names are not paths and may repeat across repositories. - The fleet rules are dependency rules. The manifest's
rulestakedependencies,layersandindependencein the native format and are evaluated byrb-rulesunchanged, with liveness strict.elements,slices,diagramsandratchetsare refused (exit 3): the first three read the code layer, and a ratchet's budget belongs to a repository. - One report. Each repository's own findings are folded into the evaluated union, with their paths prefixed and their stable ids recomputed from the prefixed paths (ADR-0015), together with their counts, expired entries, ratchets and vacuous rules, and their rules are added to
summary.ruleSetUsed, where a reporter finds each finding's comment andfix(the fleet'sallowedSeveritystays the fleet's). The document is then reported byfmt's path, so every reporter is available and the exit code is computed from the document alone, as ADR-0031 does: a gating reporter exits with the fleet's and the repositories' error count.summary.fleetrecords each repository's own counts, receipt and cache verdict;summary.declaredrecords the declared edges by source.
Consequences
- A team declares a cross-service edge once, and a fleet rule over two languages matches through it; without it the rule matches nothing, which
config lint --fleet --graphreports asnever-matches. - A second fleet run reuses every repository's cache, and
summary.fleet.perRepo.<name>.cache.hitshows it. - A team whose server framework cannot write
x-rulebearing-handlergets no OpenAPI edges and a reason in the receipt; it can declare the edges inedges.yamlinstead. - The violation ids of a repository's findings differ between its own run and the fleet's, because the paths differ; a repository's known-violations file keeps applying to its own run.
- Three schemas are published from this change:
schema/v1.json(regenerated),schema/fleet-v1.jsonandschema/edges-v1.json. The docs site serves them from the first release that contains them (ADR-0072).
Alternatives considered
- Read
edges.yamlincruisetoo. Deferred: the cache would have to key on the file, on every server document and on the client folders' text, and a fleet of one already covers a mixed-language repository. - Find an OpenAPI handler from the server's code (an ASP.NET action named after the operation, a FastAPI function, Connexion's
x-openapi-router-controller). Rejected: each is a per-framework guess at a name, the inference ADR-0014 rules out. - Match operation ids case-insensitively, or as a generator would rename them. Rejected for the same reason; an id the client does not declare as the document spells it yields no edge and is counted as
uncalled. - Search the client files' text for the operation id. Rejected: it reads source outside an extractor, and a string in a comment or a test would declare an edge.
- Strip declared edges, not only their marks, under
--strict-schema. Rejected: the summary's counts,dependents, orphans and cycles were computed with the edge, and removing it after the fact leaves a document that contradicts itself. - Report the fleet rules' findings only, with the repositories' counts in the receipt. Rejected: the design asks for one report across the repositories with per-repository and fleet-wide rules.