Skip to content

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:

  • StatusProposed (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

#DecisionStatusSummary
1Audit Logging SystemAcceptedPer-entity audit tables with hash chains for ISO 27001 compliance
6Two-Tier AuthorizationAcceptedRoute-level model permissions + action-level interaction permissions
3AI Interaction LoggingAcceptedThree-channel logging for outbound AI calls, MCP tools, and AI runs
8Multi-TenancyAcceptedDIY database-per-tenant for 3 fixed companies
13Adapter-Store PatternAcceptedCustom reactive store factory over Pinia for 15+ domain stores
25GitHub Integration SplitAcceptedSplit GithubService into OAuth handshake + bearer-token-parametrized API client; per-call token argument

Brick Inventory

#DecisionStatusSummary
4Import AtomicityAcceptedSave-what-you-can with honest reporting for API imports

Emmie

#DecisionStatusSummary
22Schedule End Date InclusiveAcceptedSchedule.end_date is the last active day; canonical predicates use inclusive <= / >=
23Schedule Mutation ChokepointAcceptedSingle Action owns Schedule date mutation; wrapping normalizer, schema unchanged
31Datetime Instant-vs-Wall-Clock ClassificationAcceptedEvery 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

#DecisionStatusSummary
36Cross-Harness Producer IngressAcceptedOne 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

#DecisionStatusSummary
2Cascade Deletion & Soft DeletesAcceptedModel declares, Action executes, tests verify. No ON DELETE CASCADE.
9Unified ResourceData PatternAccepted (amended 2026-07-03)Custom ResourceData base class replaces Laravel's JsonResource; loud-failure eager-load gate + Hydrator supply side
11Action Class ArchitectureAcceptedfinal readonly Actions with single execute() method, explicit DI
12FormRequest → DTO FlowAcceptedType-safe pipeline from HTTP validation to business logic
14Domain-Driven Frontend StructureAcceptedVertical slices by business domain, not technical layers
16Config Attribute InjectionAccepted#[Config] attribute for all config access, config() helper prohibited
17Page Integration TestsAcceptedMount domain pages with real components, mocked services, separate coverage accounting
19Explicit Model HydrationAcceptedBan $fillable/$guarded, require explicit property assignment in Actions
15ADR GovernanceAcceptedSingle source of truth at adrs.script.nl
18ISMS Information SystemAcceptedLaravel + Vue ISMS with policies as markdown, operational data in PostgreSQL
20Input/Result DTO SplitAcceptedSplit DTOs by usage direction at the Action boundary — arch tests enforce
21PHPStan Rules PackageAcceptedCanonical war-room PHPStan rules distributed as script-development/phpstan-warroom-rules Composer package
24Automated External ProvisioningAccepted (amended 2026-05-20)Async + provider-abstracted + rollback-on-failure + audit-mandatory + flag-gated for tenant-resource provisioning
26Decrement Action SymmetryAcceptedCaller-side symmetry discipline + Pest arch test for counter-mutation decrement Actions; no internal floor guard at Action layer
27Canonical Territory SkeletonAcceptedBIO-aligned baseline + Audit/ for app/; Architecture/ for tests; apps/<role>/ + shared/ for frontend; trip-point arch tests from day 1
28Canonical Git Hooks (v1)AcceptedPlain core.hooksPath + .githooks/ shell scripts; fast pre-commit + heavy pre-push; per-territory copy from /templates/githooks/; lefthook + distributed package as documented future iterations
29Audit Row Durability ContractAcceptedAudited Actions' DB::transaction(...) closure contains only DB writes; throws exit via sentinel-return; non-transactional state mutates post-commit
32Cached-Store Hash-Bumping ProtocolAcceptedSubscribe-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)
33AVG Erasure by Anonymization-in-PlaceAcceptedErasure = 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
34Ambulatory Financing Care-Type / Rate-Interval CouplingAcceptedEmmie 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
35Appointment-Slot Inverted-Window DB CHECK BackstopAcceptedwijs 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)
37fs-http Middleware Guarding by DefaultAccepted (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
38Daymate Data-Encryption & Privacy DoctrineProposedDaymate 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
39Cross-Harness Merge GateAcceptedTown-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
40Cross-Harness Review Thread & Turn-Lock ProtocolAcceptedTown-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
41Cross-Harness Severity LadderAcceptedTown-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
42Per-Repo Read Authorization, Derived from GitHub AccessProposedTown-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
43Headless + Themeable Shared Vue UI ComponentsAcceptedCross-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
44Collection Response Shape — Bare Arrays vs Cursor PaginationAcceptedCross-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
45The War Tent — Commander's Field Command PostAcceptedWar-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

Architecture documentation for contributors and collaborators.