Skip to content

ADR-0027: Canonical Territory Skeleton

Accepted Cross-Project Universal

Date: 2026-05-27

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:

  • kendobackend/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-orchestratorbackend/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/.
  • ismsbackend/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 drifttests/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/
│       ├── 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.

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.
ublgenieNot StartedAdopt during next Structural Reform touching app/ or frontend/src/ root.
entreezuilNot StartedAdopt during next Structural Reform touching app/ or frontend/src/ root.
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.

Architecture documentation for contributors and collaborators.