Skip to content

ADR-0027: Canonical Territory Skeleton ​

Accepted Cross-Project Universal

Date: 2026-05-27 · Amended: 2026-07-13 (Frontend Test Layout & Canonical Vitest Config — see §Amendment) · 2026-07-14 (Canonical Arch-Test Skeleton — see §Amendment) · 2026-10-01 (App-level components/ slot — see §Amendment)

Context ​

War-room doctrine prescribes patterns (Actions, DTOs, domain-driven frontend, audit logging) but not the directory layout that hosts them. Each territory has improvised its own skeleton:

  • kendo — backend/app/ has 24 top-level directories (Actions/, Audit/, Broadcasting/, Console/, DataTransferObjects/, Enums/, Events/, Exceptions/, Features/, Helpers/, Http/, Jobs/, Listeners/, Mail/, Mcp/, Models/, Policies/, Providers/, Rules/, Services/, Tenancy/, …). Tests at tests/Arch/. Frontend src/apps/{central,mcp,tenant}/ + src/shared/.
  • brick-inventory-orchestrator — backend/app/ has 13 top-level directories (Actions/, Console/, Contracts/, DataTransferObjects/, Enums/, Exceptions/, Http/, Jobs/, Mail/, Models/, Policies/, Providers/, Services/). Tests at tests/Architecture/. Frontend src/apps/{admin,families,showcase}/ + src/shared/.
  • isms — backend/app/ has 3 directories (Http/, Models/, Providers/) — Laravel stock scaffold from slice A (2026-05-26). Tests at tests/Architecture/. Frontend src/{pages,router}/ flat — no apps/ split.

The shape converges on backend/ + frontend/ and apps/ + shared/ on the frontend side. The divergences are real:

  • Cargo cult risk — kendo's 24 dirs include territory-specific concerns (Tenancy/ for multi-tenancy, Mcp/ for its MCP server, Broadcasting//Events//Listeners/ for its real-time stack, Agent/ for AI-specific work, PHPStan//Rules/ for its self-published custom rules). Adopting the full kendo set into every territory would create twenty empty directories that pretend to be doctrine.
  • Naming drift — tests/Arch/ (kendo) vs tests/Architecture/ (BIO + ISMS). Both work; the inconsistency is friction at zero benefit.
  • Where DTO conventions live — ADR-0020 split is App\DataTransferObjects\Input\<Domain>\* / App\DataTransferObjects\Result\<Domain>\*. Both kendo and BIO host them under DataTransferObjects/ but the in-tree shape is convention-by-grep, not directory-explicit.

The problem for AI agents (and for "apply everywhere") ​

Without an explicit canonical skeleton, soldiers reaching for "where do I put this" pattern-match the territory's existing tree — and accumulate the territory's idiosyncratic dirs. Greenfield work (ISMS) reverts to Laravel-stock layout. Cross-territory migrations (e.g. promoting a service from one territory to another) hit naming friction.

The Commander has stated the goal: codify a baseline that ISMS can scaffold to today, and that the other territories can converge toward as natural opportunities arise. This ADR is the doctrine for that baseline.

Decision ​

The canonical territory skeleton is BIO's structure, plus Audit/, with the kendo-specific dirs explicitly out of the baseline (territories may still add them when warranted — they are not banned, only not mandatory).

Backend — app/ ​

backend/app/
├── Actions/                       final readonly + execute() (ADR-0011)
├── Audit/                         hash-chain audit infrastructure (ADR-0001)
├── Console/                       artisan commands
├── Contracts/                     service interfaces
├── DataTransferObjects/
│   ├── Input/                     Action inputs (ADR-0020)
│   └── Result/                    Action returns (ADR-0020)
├── Enums/
├── Exceptions/
├── Http/
│   ├── Controllers/
│   ├── Middleware/
│   ├── Requests/                  FormRequests with toDto() (ADR-0012)
│   └── Resources/                 ResourceData subclasses (ADR-0009)
├── Jobs/
├── Mail/
├── Models/
├── Policies/
├── Providers/
└── Services/

Not in the baseline (allowed when warranted; not required from day 1):

DirectoryWhen to add
Agent/, Mcp/Territory ships an AI/MCP surface
Broadcasting/, Events/, Listeners/Real-time stack adopted
Features/Pennant-style feature-flag namespace established
Helpers/Standalone helper namespace beyond app/Support/
PHPStan/, Rules/Territory publishes custom static-analysis rules
Tenancy/Multi-tenancy beyond the package-default surface (ADR-0008)
ValueObjects/A type genuinely earns first-class VO treatment (encapsulated invariants, equality semantics, used across multiple Actions). ublgenie has the directory; kendo and BIO route the same concerns through DTOs without a separate VO namespace. Add when a real VO lands, not pre-emptively.

Backend — tests/ ​

backend/tests/
├── Architecture/                  Pest arch tests
├── Feature/
├── Unit/
├── Pest.php
└── TestCase.php

Tests directory naming is Architecture/ (long form), not Arch/. Self-documenting; converges with BIO and ISMS; kendo carries the short form as a known deviation to converge over time, not a forcing function for an immediate rename.

Frontend — src/ ​

frontend/src/
├── apps/
│   └── <app>/                     one entry-point per audience/role
│       ├── App.vue
│       ├── main.ts
│       ├── index.html
│       ├── router/
│       ├── components/            app-level chrome shared by several of THIS app's domains (Amendment 2026-10-01)
│       ├── domains/               vertical slices (ADR-0014)
│       │   └── <feature>/
│       │       ├── components/
│       │       └── pages/
│       └── services/              app-local services
└── shared/                        cross-app scaffolding
    ├── components/
    ├── composables/
    ├── errors/
    ├── helpers/
    ├── services/
    └── types/

One apps/<name>/ per audience or auth boundary, not per feature. The split decision is "who logs in here" (anonymous public, authenticated tenant, ops/admin, role-scoped), not "what does this page do." Features live under domains/ inside an app.

Additional optional dirs at the app level (modals/, stores/, types/, middleware/, enums/, constants/, __mocks__/) are allowed but not mandatory — they emerge from the territory's domain shape.

Frontend specs live in frontend/tests/ (a sibling of src/, not under it) and the canonical Vitest runner config — see the 2026-07-13 amendment.

Trip-point arch tests (Level 1 enforcement) ​

Each territory's tests/Architecture/ must include — from day 1 of skeleton adoption — these arch tests:

TestAsserts
ActionPatternTestApp\Actions\* are final readonly, single execute(), no facades (ADR-0011)
DtoSplitTestApp\DataTransferObjects\Input\* do not depend on App\Models\*; Result/* allowed (ADR-0020)
EnumPatternTestApp\Enums\* are PHP backed enums
ControllerCurrentUserTestControllers use #[CurrentUser] container attribute, not Auth::user() / Request::user() / auth()->user() (war-room principle 9; companion to EnforceCurrentUserAttributeRule in phpstan-warroom-rules)
ServiceFinalTestApp\Services\* are final

When a baseline dir holds no production class yet, the arch test passes vacuously — that is the intended state. The tests exist as trip points: the moment the first class lands in the dir, structural compliance is checked at CI time.

Options Considered ​

OptionVerdictReason
BIO-aligned baseline + Audit/ (chosen)AcceptedLean enough to be defensible everywhere; adds the one dir ISMS demonstrably needs (compliance audit infrastructure under the highest compliance burden in the war room). Kendo-specific dirs explicitly allowed but not required.
BIO + Audit/ + ValueObjects/RejectedNeither model territory (kendo, BIO) has ValueObjects/; only ublgenie does. Scaffolding the directory empty would be exactly the "dir pretending to be doctrine" failure mode this ADR warns against in the §Risks section. When a real value object lands, the territory adds the directory then — zero scaffolding cost deferred to the moment of actual need.
Strict BIO baseline (13 dirs)RejectedForces Audit/ to be invented per-territory; ISMS would need to add it immediately under the highest compliance burden in the war room. Better to codify once.
Full kendo baseline (24 dirs)RejectedCargo cult — would force Tenancy/, Mcp/, Broadcasting/, PHPStan/ directories into territories that have no such concern. Empty dirs masquerading as doctrine.
Per-territory freedom (status quo)RejectedThe Commander explicitly requested a canonical that "applies everywhere eventually." Freedom is what produced the kendo/BIO/ISMS three-way divergence in the first place.
Generate skeleton via a Composer scaffolding packageRejectedPremature tooling. The skeleton is ~20 directories with .gitkeep files plus 5 arch tests — well below the scaffolding-package break-even. Revisit if a fifth Laravel territory needs adoption.

Consequences ​

Positive ​

  • One layout to learn. Cross-territory navigation, agent dispatches, and rotating maintainers all benefit from convergent shape.
  • Doctrine becomes scaffoldable. A new Laravel territory starts at the canonical baseline by copy-paste; ADR-0027 is its checklist.
  • Trip-point arch tests catch the first violation, not the fifth. Empty dirs with passing arch tests are cheap and outlive the human who set them up.
  • DTO split (ADR-0020) gets a physical home that's identical everywhere. No more grep-to-find-the-convention.

Negative ​

  • kendo carries an Arch/ ↔ Architecture/ deviation indefinitely until a future rename mission. Renaming costs CI history continuity (job names, coverage reports) — acceptable to defer.
  • Existing territories will not be retrofit in one campaign. Each territory adopts the baseline opportunistically (when a Structural Reform mission touches the relevant layer). Convergence is on order of months, not weeks.
  • Some baseline dirs will sit empty for a long time in some territories (e.g. Mail/ in ISMS until the first notification ships). The cost is one .gitkeep per dir — accepted in exchange for the trip-point arch test being live.

Risks ​

  • Risk: territories accumulate the baseline + their pre-existing extras, producing trees larger than either kendo or BIO today. — Mitigation: the "Not in the baseline" table is the explicit exclusion list. The Adjutant's quarterly registry sweep flags territories whose app/ tree adds dirs outside both the baseline and the allowed extras.
  • Risk: the worker / colleague two-app split for ISMS proves to be the wrong cleavage (e.g. colleagues turn out to need write actions later). — Mitigation: Vite multi-app config is reversible. Merging two apps back into one is a one-day Engineer dispatch; the cost of being wrong is bounded.

Enforcement ​

WhatMechanismScope
Action pattern (final readonly + single execute + no facades)ActionPatternTest (Pest arch)App\Actions\*
DTO split (Input not depending on Models)DtoSplitTest (Pest arch)App\DataTransferObjects\Input\*
Enum shape (backed enums)EnumPatternTest (Pest arch)App\Enums\*
Controller user resolutionControllerCurrentUserTest (Pest arch) + EnforceCurrentUserAttributeRule (PHPStan)App\Http\Controllers\*
Service finalityServiceFinalTest (Pest arch)App\Services\*
Baseline dir presenceAdjutant registry sweep (Level 6 passive monitoring)per-territory app/ and frontend/src/ tree

Directory presence itself is not gated at CI — the trip-point arch tests handle the actual structural compliance. A territory with a missing baseline dir is surfaced by Adjutant on its next registry pass.

Implementation ​

TerritoryStateNotes
ismsIn ProgressFirst territory to adopt the canonical at scaffold time (greenfield; this ADR is the trigger for the scaffold).
brick-inventory-orchestratorPartialBackend at 13 of 14 baseline dirs (missing Audit/). Frontend already at apps/ + shared/. Tests already at Architecture/. Adopt Audit/ when first audit-trail work lands.
kendoPartialBackend has all baseline dirs plus extras (allowed). Tests at Arch/ (deviation; rename on next Architecture-test mission). Frontend already at apps/ + shared/.
ublgeniePartialBackend has the baseline dirs plus ValueObjects/ (allowed-when-warranted example). Adopt remaining baseline alignment on next Structural Reform touching app/.
emmieNot StartedAdopt during next Structural Reform touching app/ or frontend/src/ root.
entreezuilCompleteFull adoption campaign 2026-06-26, merged 2026-06-30 (campaigns/entreezuil/2026-06-25-canonical-skeleton-restructure-adr0027.md). R1 skeleton (backend/+frontend/) merged (#249); R2 apps-split admin/kiosk landed via #270 (#262 superseded/closed); R3 doctrine (#263); R4 backend baseline + backend/tests/Architecture/ rename + 5 mandated arch tests (#264) — all on development. Allowed extras: Rules/, Support/, Http/Notification/. Two ratified exemptions: CMSmsUser non-final (ServiceFinalTest, tracked WR-0229).
codebookNot StartedAdopt during next Structural Reform touching app/ or frontend/src/ root.
daymate-apiNot StartedLaravel 9 / PHP 8.2 — confirm canonical compatibility with older Laravel scaffold defaults before adopting.

Amendment (2026-07-13): Frontend Test Layout & Canonical Vitest Config ​

The original ADR codified the backend tests/ layout (Architecture//Feature//Unit/) and the frontend src/ tree, but left the frontend test side unspecified — where Vue specs live, and what runs them. A 2026-07-13 fleet survey found the structure already convergent (Vitest 4.x everywhere, .spec.ts under tests/, no test globals) but two substantive knobs — DOM environment and coverage provider — split near-evenly, and the config shape diverged three ways (standalone / folded-into-vite.config / mergeConfig). This amendment ratifies the convergent baseline and gives it a canonical artifact. Reasoning: the survey + fork resolution in this session; the artifact lives at templates/vitest.config.ts with full override commentary in templates/README.md.

Frontend — tests/ ​

frontend/tests/
└── **/*.spec.ts                   Vue specs, mirroring the src/ tree they cover

Specs are a sibling of src/ (frontend/tests/), not nested under it — parallel to the backend tests/ sibling of app/. The tests/js/** layout (kendo's) is a subset of tests/** and resolves identically under the canonical include glob. .spec.ts is the naming (isms's .test.ts is the lone outlier, to converge opportunistically).

Canonical runner config — templates/vitest.config.ts ​

A base you extend, not a copy-paste drop-in (unlike oxlintrc.json): plugins and resolve.alias are inherently territory-specific — each repo mirrors its own vite.config + tsconfig paths. What the canonical fixes is the four invariants every app territory keeps:

InvariantValueRationale
Standalone configvitest.config.ts imports from vitest/config; vite.config stays build-onlyProduction build never pulls the test runner into the bundle (town-crier's rationale). mergeConfig(viteConfig, …) (scripthub) is an allowed deviation that trades bundle-purity for zero alias duplication.
DOM environmenthappy-domLighter/faster than jsdom; fleet-leading (17 vs 11 at survey).
Coverage providerv8Native, zero-instrumentation; fleet-leading (19 vs 17).
Test globalsoff — explicit import {describe, it, expect, vi}Keeps the strict tsconfig free of an injected vitest/globals type surface.

Documented territory overrides expected (full detail in templates/README.md): plugins/alias (always); environment: 'jsdom' (specs needing jsdom's fuller browser API); coverage.provider: 'istanbul' (a territory at a hard 100% coverage gate — wijs — buys istanbul's more accurate branch accounting); coverage.include = resources/js/** for Laravel-inlined frontends vs src/** for standalone; and coverage.thresholds (the base ships none — territories that gate add the floor and document it).

Out of scope: fs-packages (per-package config + vitest.workspace.ts — a library-monorepo shape, not app shape) and the-laboratory gadgets (sovereign posture). The canonical targets app territories (Vue frontends).

Enforcement reality ​

This amendment is Level 4 (territory doctrine) + a template, not Level 1. The four invariants are conventions a copying maintainer or soldier reads — nothing at CI catches a territory that drifts to jsdom or istanbul against the baseline. That gap is deliberate for now: the two overrides are legitimate per-territory choices, so a hard gate would fight the documented exception list. The path to teeth is a /canon-check axis that flags off-baseline environment/provider/globals as a drift row (visibility, not a block) — noted as a follow-up, not built here. Until then, convergence is opportunistic (a Structural Reform or Fortification mission touching a territory's frontend test setup adopts the base) and surfaced by the Adjutant registry sweep alongside the backend-skeleton presence check.

Implementation ​

ScopeStateNotes
Fleet baselineEstablishedVitest 4.x, .spec.ts under tests/, no globals — already universal at 2026-07-13 survey.
happy-dom / v8PartialFleet-leading but not universal; territories on jsdom/istanbul either override deliberately (wijs istanbul) or converge opportunistically.
templates/vitest.config.tsCompleteCanonical base authored 2026-07-13; README override commentary shipped alongside.

Amendment (2026-07-14): Canonical Arch-Test Skeleton ​

The original ADR mandated five trip-point arch tests ("§Trip-point arch tests (Level 1 enforcement)": ActionPatternTest, DtoSplitTest, EnumPatternTest, ControllerCurrentUserTest, ServiceFinalTest) and named them in §Enforcement — but shipped no canonical file. The result is the failure mode this ADR warns about applied to itself: every adopting territory re-derived the five from scratch, and they drifted — three directory names (tests/Architecture/, kendo's tests/Arch/, wijs's tests/Unit/Architecture/), divergent BypassFinals handling, and per-territory reinvention of the file-scanning helper. A mandated test with no artifact is Level-4 doctrine wearing a Level-1 badge. This amendment gives the mandate its artifact.

The canonical artifact ​

templates/tests-architecture/ — a base to adapt, not a byte-identical drop-in (the App\* namespaces are universal; the BypassFinals whitelist, external-service roster, and identity block are territory-shaped). It realizes the five mandated tests as concrete, proven files (extracted from town-crier's tests/Arch/, the cleanest full-ladder realization in the fleet), plus the shared Support/ArchTestHelper.php (copy verbatim) and a Pest.php.fragment. Full override commentary lives in templates/README.md.

A whole Pest.php is deliberately NOT shipped — a whole-file Pest template was rejected as cargo-cult (war-room memory reference_bypass_finals_arch_scoping); only the load-bearing BypassFinals suite-wiring fragment is templated.

New: the 6th mandated arch test (conditional) — ExternalHttpTimeoutTest ​

The original five did not cover Principle #8 (explicit timeouts on external HTTP), whose Level-1 enforcement is an arch test that greps every outbound-HTTP class for a per-call ->timeout(). This amendment adds ExternalHttpTimeoutTest as a conditionally-mandated sixth: required from the day a territory makes its first outbound HTTP call, omitted (not shipped empty) before then. town-crier's ExternalHttpTimeoutTest (its first outbound-HTTP gate, TC-0014) is the exemplar.

The BypassFinals invariant (load-bearing) ​

The single most important thing this skeleton encodes: the Architecture suite must never enable BypassFinals. BypassFinals strips the final keyword at autoload so unit tests can mock final classes — but if it is active when the arch suite loads, toBeFinal() silently passes on a codebase with zero final keywords. The gate goes blind, not red. The discipline: isolate the arch suite in its own ->in('Architecture') binding that never calls enable() (robust — unlike the argv-sniffing guard, which a bare php artisan test defeats), always setWhitelist([...]) your own namespaces (a bare enable() FATALs on vendor readonly), and optionally plant a portable canary (a genuinely-final fixture asserted isFinal()) so a future accidental process-wide enable turns a canary red instead of the gate green.

Enforcement reality ​

Unlike the vitest amendment, this is Level 1 — the arch tests run in CI (composer test:arch, a backend-lint matrix gate in the canonical CI base) and fail the required ci-passed rollup on violation. The residual Level-6 gap is presence: nothing at CI catches a territory that never adopts the skeleton at all (directory presence is not itself gated, per the original §Enforcement). That stays an Adjutant registry-sweep + /canon-check arch-axis concern (both already check arch-dir presence). The trip-point design means an adopted-but-empty skeleton is correct and cheap — the tests go live the moment the first class lands.

Implementation ​

ScopeStateNotes
templates/tests-architecture/CompleteCanonical skeleton authored 2026-07-14 (6 test files + Support/ArchTestHelper.php + Pest.php.fragment); README override commentary shipped alongside.
town-crierComplete (source)The extraction source — full ladder, tests/Arch/.
entreezuilComplete5 mandated tests + tests/Architecture/ rename landed in the ADR-0027 R4 campaign (#264, 2026-06-30); the CMSmsUser ServiceFinalTest exemption is the ratified-exemption exemplar.
ublgenie / codebook / wijsPartialCarry arch tests (tests/Arch/, tests/Unit/Architecture/) — converge on the canonical file set + directory name on the next arch-test mission.
kendoPartialtests/Arch/ short-form deviation (per the original ADR); converge opportunistically.
emmie / ismsOpportunisticAdopt the skeleton on the next Structural Reform touching app/.

Amendment (2026-10-01): App-Level components/ Slot ​

The skeleton tree previously showed only router/, domains/ and services/ inside apps/<app>/. That left nowhere to put chrome that is shared by several domains of one app but by no other app: the shell, a classification mark, an aside. Such a component belongs neither in a single domain (other domains of the same app need it) nor in cross-app shared/ (no other app does).

Decision: apps/<app>/components/ is a canonical slot for exactly that chrome. The placement rule has three rungs:

Used byLives in
one domainapps/<app>/domains/<feature>/components/
several domains of one appapps/<app>/components/
more than one appshared/components/

A component moves up a rung when its second consumer appears, not before.

Evidence (n=2 in one territory, the gate the deferral named): isms built apps/colleague/components/ in its Phase 3 frontend campaign (2026-07-02, the Samizdat chrome), then apps/worker/components/ for the worker app's own chrome. Both are at origin/development.

Enforcement: Level 4 (this text). No arch test: placement by consumer count is a review judgment, not a shape a scan can decide.

Architecture documentation for contributors and collaborators.