Architecture Decision Records
Architecture Decision Records (ADRs) document the significant architectural choices made across our projects. Each record captures the context, options considered, the decision, and its consequences.
Why We Write These
When you join a project or review a pull request, you often ask "why is it done this way?" These records answer that question. They capture not just what was decided, but why — including what we considered and rejected.
How to Read an ADR
Each ADR follows the same structure:
- Status —
Proposed(under discussion),Accepted(implemented or being implemented),Superseded(replaced by a newer decision) - Context — The problem or situation that prompted the decision
- Options Considered — Alternatives we evaluated, with reasons for rejection
- Decision — What we chose and how it works
- Consequences — The trade-offs we accepted, both positive and negative
Current Status of Every Decision
The table below indexes the decisions themselves. For where each one actually stands — which territories have adopted it, what shipped, what is still in flight, and what each amendment changed — see the ADR Status Index. It covers all 46 decisions including the five that are war-room-internal and have no page here.
Read the status index before acting on any ADR's current state: a decision page tells you what was decided, not how far it has travelled.
Decision Index
Kendo
| # | Decision | Status | Summary |
|---|---|---|---|
| 1 | Audit Logging System | Accepted | Per-entity audit tables with hash chains for ISO 27001 compliance |
| 6 | Two-Tier Authorization | Accepted | Route-level model permissions + action-level interaction permissions |
| 3 | AI Interaction Logging | Accepted | Three-channel logging for outbound AI calls, MCP tools, and AI runs |
| 8 | Multi-Tenancy | Accepted | DIY database-per-tenant for 3 fixed companies |
| 13 | Adapter-Store Pattern | Accepted | Custom reactive store factory over Pinia for 15+ domain stores |
| 25 | GitHub Integration Split | Accepted | Split GithubService into OAuth handshake + bearer-token-parametrized API client; per-call token argument |
Brick Inventory
| # | Decision | Status | Summary |
|---|---|---|---|
| 4 | Import Atomicity | Accepted | Save-what-you-can with honest reporting for API imports |
Emmie
| # | Decision | Status | Summary |
|---|---|---|---|
| 22 | Schedule End Date Inclusive | Accepted | Schedule.end_date is the last active day; canonical predicates use inclusive <= / >= |
| 23 | Schedule Mutation Chokepoint | Accepted | Single Action owns Schedule date mutation; wrapping normalizer, schema unchanged |
| 31 | Datetime Instant-vs-Wall-Clock Classification | Accepted | Every datetime field declared instant or wall-clock; wall-clock serialized naive (self-describing wire); backend WallClock* casts + arch test, frontend Temporal helper (polyfilled) |
Town-crier
| # | Decision | Status | Summary |
|---|---|---|---|
| 36 | Cross-Harness Producer Ingress | Accepted | One bus-hosted GitHub App replaces 14 per-repo announce-pr.yml; HMAC-verified pull_request/pull_request_review ingress dispatching in-process to announce/resolve/consensus with the internally-minted github-action identity; first bus outbound HTTP (timeout-bound, installation_id-keyed token cache); pull-first /github/health observability for the silent per-org failure mode |
Cross-Project
| # | Decision | Status | Summary |
|---|---|---|---|
| 2 | Cascade Deletion & Soft Deletes | Accepted | Model declares, Action executes, tests verify. No ON DELETE CASCADE. |
| 9 | Unified ResourceData Pattern | Accepted (amended 2026-07-03) | Custom ResourceData base class replaces Laravel's JsonResource; loud-failure eager-load gate + Hydrator supply side |
| 11 | Action Class Architecture | Accepted | final readonly Actions with single execute() method, explicit DI |
| 12 | FormRequest → DTO Flow | Accepted | Type-safe pipeline from HTTP validation to business logic |
| 14 | Domain-Driven Frontend Structure | Accepted | Vertical slices by business domain, not technical layers |
| 16 | Config Attribute Injection | Accepted | #[Config] attribute for all config access, config() helper prohibited |
| 17 | Page Integration Tests | Accepted | Mount domain pages with real components, mocked services, separate coverage accounting |
| 19 | Explicit Model Hydration | Accepted | Ban $fillable/$guarded, require explicit property assignment in Actions |
| 15 | ADR Governance | Accepted | Single source of truth at adrs.script.nl |
| 18 | ISMS Information System | Accepted | Laravel + Vue ISMS with policies as markdown, operational data in PostgreSQL |
| 20 | Input/Result DTO Split | Accepted | Split DTOs by usage direction at the Action boundary — arch tests enforce |
| 21 | PHPStan Rules Package | Accepted | Canonical war-room PHPStan rules distributed as script-development/phpstan-warroom-rules Composer package |
| 24 | Automated External Provisioning | Accepted (amended 2026-05-20) | Async + provider-abstracted + rollback-on-failure + audit-mandatory + flag-gated for tenant-resource provisioning |
| 26 | Decrement Action Symmetry | Accepted | Caller-side symmetry discipline + Pest arch test for counter-mutation decrement Actions; no internal floor guard at Action layer |
| 27 | Canonical Territory Skeleton | Accepted | BIO-aligned baseline + Audit/ for app/; Architecture/ for tests; apps/<role>/ + shared/ for frontend; trip-point arch tests from day 1 |
| 28 | Canonical Git Hooks (v1) | Accepted | Plain core.hooksPath + .githooks/ shell scripts; fast pre-commit + heavy pre-push; per-territory copy from /templates/githooks/; lefthook + distributed package as documented future iterations |
| 29 | Audit Row Durability Contract | Accepted | Audited Actions' DB::transaction(...) closure contains only DB writes; throws exit via sentinel-return; non-transactional state mutates post-commit |
| 32 | Cached-Store Hash-Bumping Protocol | Accepted | Subscribe-via-header: SPA declares cached keys per request; middleware authorizes (per-key Tier-1 policy) + stamps the entitled subset, one-resolve-per-distinct-parent (N+1 arch-tested) |
| 33 | AVG Erasure by Anonymization-in-Place | Accepted | Erasure = anonymize-in-place (not hard-delete): per-column scrub contract across the user aggregate + framework tables, S3 delete, emmie revoke-only + OpenAI persist-then-delete residuals, audit-trail identity-PII accepted as lawful record; completeness Feature+arch tests |
| 34 | Ambulatory Financing Care-Type / Rate-Interval Coupling | Accepted | Emmie territory-specific: ambulatory registration ↔ AMBULATORY financing ↔ HOUR/MINUTE interval three-way coupling; interval-keyed Invariant A (revenue correctness) enforced at the registration chokepoint + arch test; care-type-keyed Invariant B (hygiene) held for prod breach-check |
| 35 | Appointment-Slot Inverted-Window DB CHECK Backstop | Accepted | wijs territory-specific: end_date >= start_date enforced by a DB CHECK on appointment_slots (MySQL 8.0.45) as a write-path-agnostic Level-1 backstop below the shipped Action/validation guards; two-step clean-then-constrain (repoint 9 cancelled-appointment occupations → delete 576 inverted rows → ADD CHECK) |
| 37 | fs-http Middleware Guarding by Default | Accepted (shipped 0.6.0 2026-07-11) | fs-http register*Middleware auto-wrap the body in guarded() (loud-swallow) by default; per-call {guard:false} opt-out + service-level onMiddlewareError; supersedes the "every consumer MUST guard" obligation of Principle #8. Loud-swallow is the third option unavailable in 2026-05-13 (serves "be loud" without corrupting the request); WR-0290 proved opt-in doesn't hold. Consumer obligation stands until the fs-http minor ships |
| 38 | Daymate Data-Encryption & Privacy Doctrine | Proposed | Daymate territory-specific: three-axis frame (confidentiality→encryption / read-authz→consent / integrity→tamper-evidence); server per-column app-level encryption scoped explicitly to DB-only compromise (key co-resident with DB_PASSWORD, no KMS — does NOT cover app-compromise/secrets-bundle); scope drawn on free-text-vs-structured axis + widened; mood-plaintext needs a named future aggregation; email blind-index escape hatch; framework-table (jobs/sessions) caveat. Pending expert review + 3 operational confirmations |
| 39 | Cross-Harness Merge Gate | Accepted | Town-crier cross-harness contract: review findings become review_findings rows (enum tier/confidence/provenance); merge-gate state derives on the model — BLOCKED (Confirmed Blocker) / HELD (Confirmed Major or Unconfirmed Blocker) / CLEAR — never stored, binds bus machinery only (suppresses consensus auto-resolve, never forces a verdict — R1); findings persist across heads until dispositioned (fixed/follow_up/declined/refuted) through one FindingDisposer chokepoint with a no-self-verification guard (R4); the App emits a town-crier/gate check run per head (per-repo opt-in requiredness, fail-open, lock-anchored atomic emit — the teeth). Built + prod-live (TC-0041, #133/#134/#135, Fly v44); gate_mode (TC-0054) pending |
| 40 | Cross-Harness Review Thread & Turn-Lock Protocol | Accepted | Town-crier coordination substrate: reviews serialize into a THREAD (announce-once keyed on pr_url → transient turn-lock claim returning prior reviews → submit a stance {confirm/deny/independent} releasing the lock → next harness); no-two-turns-in-a-row (decorrelation); ends via any-harness resolve or hard cap (TOWN_CRIER_REVIEW_CAP=5); stale lock (>TOWN_CRIER_LOCK_TTL_MIN=15m) stealable (no deadlock); turn-lock = single conditional UPDATE (TOCTOU-free), submit re-checks holder under lockForUpdate(); identity server-stamped never input; roster invite-based (unique slug on join, no squat); reviews[] full history vs per-round review_count. ClaimRaceTest pins 5 invariants. Prod-live 4-way |
| 41 | Cross-Harness Severity Ladder | Accepted | Town-crier shared review vocabulary (TC-0036): 🛑 Blocker / 🔴 Major / 🟡 Minor / ⚪ Nit first-match tree (ship/edge/objective-quality/taste tests) + Praise outside it; one ladder all categories (severity=consequence not type); as-if-true (uncertainty → confidence tag, never a lower tier); tags = confidence (Confirmed/Unconfirmed — a 2nd harness turn promotes/kills) + provenance (introduced/adjacent/pre-existing, strict — only introduced blocks); tie-break round-down except security/data-integrity/acceptance-criterion/regression round-up (R3); riders R1 (verdict-binding, superseded by ADR-0039 gate) / R2 (invisible criteria N/A) / R4 (approve-with-residuals → disposition). Enum-backed on the wire (TC-0040 → ADR-0039). Ratified + skills migrated |
| 42 | Per-Repo Read Authorization, Derived from GitHub Access | Proposed | Town-crier cross-harness contract: retires D1 ("any bus member reads all") for a per-repo read scope resolved from the member's GitHub login via the App's installation credentials, cached off the hot path, fail-closed; scopes index/show/batch + the write verbs and folds in the enact-verb authz question. Awaiting socialization with the co-owner (Jasper) — rewrites the shared read-authz contract; not yet ratified |
| 43 | Headless + Themeable Shared Vue UI Components | Accepted | Cross-project: shared Vue input components as a HEADLESS behavioural core (select/combobox/field — the 4×-reinvented controls) + a STRUCTURAL CSS-var theme contract (34 --ui-* props: palette AND border-width/radius/shadow-shape/label-transform — a colours-only contract fails, spike-proven kendo→BIO) + error-as-prop (never imports a territory error service); lives as a new ui-* family in the fs-packages monorepo (NOT fs- — that's frontend service), pkg @script-development/ui-inputs, peers on fs-theme; graduate to its own repo if it grows. 0.2.0 published; BIO pilot (WR-0431 / PR #284) validated the token + error-prop contract end-to-end → Accepted 2026-07-16. Follow-ups: Number/Date/Textarea atoms + candidate bg-state vars = WR-0460, combobox AT-focus = WR-0441. Phase-1 UnoCSS-core; date pickers deferred. kendo=donor, BIO=first live consumer |
| 44 | Collection Response Shape — Bare Arrays vs Cursor Pagination | Accepted | Cross-project: list-response convention. BOUNDED sets (known ceiling) return a BARE JSON array + client-side pagination (kendo/emmie withoutWrapping, isms array-backed Data); UNBOUNDED DB-backed registers return a Laravel CursorPaginator envelope ({data,next_cursor,prev_cursor,per_page}) — cursor MANDATED over offset (stable under inserts, BIO precedent, per_page cap 100). Tier is a design-time per-endpoint call keyed on unbounded-growth potential; the two shapes coexist and are never retrofitted. Accepted trade-off: a bounded list that unexpectedly outgrows FE pagination takes a one-time breaking bump |
| 45 | The War Tent — Commander's Field Command Post | Accepted | War-room territory-specific: found war-tent (Goosterhof/war-tent, private; submodule territories/war-tent) — a Windows-native Tauri v2 + Vue 3 desktop command post bridging into WSL2. Substrate inherited from the lab's Mezzanine (AD-1 pty-wrap of claude via wsl.exe, POSIX path hygiene, Ascent updater pattern), soul not (own metaphor/voice/semantics). Rulings: name war-tent (war-table collides with a lab codename, command center too generic); SIX FIXED seats for Generals (count is config — Workbench precedent distinguished: all Generals target one war-room root); spy Score: N/10 memory stamps forward (all 8 SOPs amended — territories tab averages as stamps accumulate, tolerates absence); design = a named lab campaign (Artisan/Illusionist) before Phase 1 visuals; updater keys Commander-gated. Phase 0 landed 2026-07-16 |