Skip to content

ADR Index

Architectural Decision Records for the Dazzle project. Agent-scannable: each line is a decision that prevents a wrong proposal.

  • 0052Accepted (2026-07-07; #1541). scope: all on tenant-kind entities compiles to the partition-root subtree. Tenant kinds (tenant_host: anchors, usually entities_excluded → NO fence) previously got Tautology from all = every row of every tenant (cross-trust leak reproduced live). Now READ/LIST all on a tenant kind compiles to id = current_user.tenant_id OR <parent> = … OR <parent>.<parent> = … walking the entity's own tenant_host.parent chain — the inverse of ADR-0036's self-or-ancestor expansion, bound by the same depth cap, fail-closed on broken/cyclic chains (partial legs = narrower, never broader; membership-less sessions hit the deny sentinel). current_user.tenant_id resolves to the #1463 membership partition root. Write-verb all keeps Tautology (subtree writes need the #1455 probe machinery — follow-up). Non-kind entities unchanged. Shipped with the #1541 observability fixes (scope/permit denials + swallowed fetch errors logged; errored fetch renders distinct copy).
  • 0051Accepted (2026-07-02; #1525). Retire the orphaned ops platform. Delete the 12-module (~5,000 LOC) founder-cockpit cluster (ops_integration / analytics_collector / ops_database / health_aggregator / api_tracker / api_middleware / email_templates / spec_versioning / system_entity_store / admin_api_routes / deploy_history / rollback_manager) + static/ops/ UI + the OPS_DB artifact class + the always-404 super-admin deploy/rollback buttons. mount_ops_platform() was never wired anywhere in history; its only consumer (the Founder Console) died in #703 when the admin workspace (#686) won; ADR-0050 already salvaged the one thesis-relevant capability (usage capture) by deliberate narrow rebuild. Every other capability has a modern substitute (dazzle perf, /health+Prometheus, admin workspace, http/email/, git+api-surface baselines, #1004 virtual-entity path). Kept: sse_stream/sse_wiring (live), device_registry (live). Salvage filed separately: narrow LLM-cost tracking on the ADR-0050 pattern (the one un-substituted capability). Follow-up: the DeployHistory admin entity now has no writer — removing it touches the admin-entity suite fleet-wide, deferred. Rejected: revive opt-in (registration+hardening cost for duplicated capabilities); keep-health-only (Prometheus covers it); tombstone (how it got lost the first time).
  • 0001 — MkDocs Material for docs. No wiki, no Docusaurus.
  • 0002 — MCP = stateless reads. CLI = process/writes. Don't block conversation with long ops.
  • 0003 — No backward compat before v1.0. Delete freely, never create shims or wrappers.
  • 0004 — DSL optimized for AI agents. Precision and formal correctness over human ergonomics.
  • 0005 — RuntimeServices dataclass on app.state. No new module-level singletons.
  • 0006 — IR is immutable frozen Pydantic. Never mutate after parse.
  • 0007 — RBAC: static matrix + dynamic conformance + audit trail. Three independent layers.
  • 0008 — PostgreSQL is the sole database. No SQLite code paths.
  • 0009 — Scope rules compile to 6-type predicate algebra. Validated against FK graph at link time.
  • 0010 — permit = role gate (RBAC). scope = row filter (ABAC). Never mix.
  • 0011 — Server-side HTML rendering + HTMX. No SPA frameworks. Originally Jinja2; rendering pipeline is now typed Fragments — see ADR-0023, post-#1042.
  • 0012 — Alembic for schema migrations. No hand-rolled migration planners.
  • 0013 — One per-project KG with TOML seed. No separate knowledge systems.
  • 0014 — No from __future__ import annotations in FastAPI route files. Breaks OpenAPI.
  • 0015 — TigerBeetle for double-entry ledgers. Optional dependency, DSL ledger/transaction constructs.
  • 0016 — API Packs for vendor integration. TOML-driven mocks, webhook testing, DSL generation.
  • 0017 — All schema changes via Alembic, including framework entities. No raw DDL at startup.
  • 0018 — All file writes go to the project directory. Never write to the Python package directory.
  • 0019 — (Entity, Surface, Persona) triple is the atomic unit of verifiable behaviour.
  • 0020 — Lifecycle evidence predicates are orthogonal to state machines.
  • 0021 — Marketing pages via sitespec.yaml. No # dazzle:route-override on public paths.
  • 0022 — Don't put Alpine bindings on idiomorph-morphed elements. Server-render or use x-init helpers with direct DOM manipulation.
  • 0023 — Two-pattern template-emission model post-jinja2. Pattern A (framework writes HTML) uses f-strings + dazzle.render.html.esc; Pattern B (framework executes user-authored templates) uses string.Template. Choice is mechanical: who writes the template.
  • 0024 — No regex for DSL grammar. A regex parsing DSL is a signal for missing grammar, not an end solution. Lexical-shape regex (identifiers, numerics) is fine; matching call shapes / keywords / sub-expressions is not.
  • 0025 — Authorization is entity-level only. Field-level auth is not added; it would break the enumerable role×entity×operation security surface. A field with its own lifecycle is its own entity. Field sensitivity uses classify/pii(), not permit:.
  • 0026 — Subtype polymorphism uses TPT (table-per-type), flat hierarchy, immutable discriminator. Complex, potentially brittle — only justified when all three conditions hold: true IS-A, subtype-specific NOT NULL fields, polymorphic queries genuinely needed.
  • 0027 — No polymorphic_ref: keyword, now or planned. Rails-era ORM idiosyncrasy that breaks referential integrity, scope composition, and JOIN-based queries. Every classic use case (comments, attachments, tags, audit log, notifications, likes) fails a four-question interrogation that routes to per-target refs, event-stream entities, per-pair junctions, or TPT (ADR-0026). Union sugar ref X | Y | Z rejected outright. Amended by ADR-0042 (#1448): the pre-committed escape hatch has now fired — a typed poly_ref [T1, T2] field + field[Type].path scope selector for the ≈5% of cases that survive the interrogation. The closure becomes "closed except via typed poly_ref"; the two hard rejections (no ref union sugar; discriminator stays a visible queryable column) still stand.
  • 0028 — Guarded transactional actions (multi-hop FK-path write boundary; multi-step atomic mutation) are a composition, not a new action: primitive. Don't add action: (duplicates atomic #1228, overloads the keyword) and don't put guards in the handler (regresses the predicate algebra, ADR-0009). Build via #1124 v2 (FK-path create-scope, shipped v0.80.63), update-destination revalidation (shipped v0.80.64), and extending atomic (#1313, pending — now scoped by ADR-0029). Normative rules: derive scope keys from the principal; validate every touched entity (source + destination); one transaction; fail closed. Denormalize-for-create-scope is a spoofable anti-pattern.
  • 0029Proposed. atomic is the schema-local transactional-intent substrate: n=1 CRUD is the degenerate case, n=k a guarded multi-entity mutation; one analyzable model for rbac//testing//specs/ via a shared (entity, op, scope_predicate) projection. Unify the model, specialise the runtime (don't merge CRUD into the multi-step engine). Schema-local boundary (writes + guard-reads in the AppSpec; one DB transaction) — not a workflow/saga/command system; compensation is definitionally absent. Adopts TigerBeetle's invariant-owning-primitive philosophy but not schema elimination, so guarantees hold at the flow boundary, not storage. Boundaries: conserved-quantity/double-entry → ADR-0015 (ledger); transition-with-effects seam with ADR-0020 open. #1313 re-decomposed: per-step scope routing + audit + analysis-surface wiring first; derived FK ordering, scope-parent share-lock, and the cross-step aggregate invariant (needs an ADR-0009 AggregateCheck extension) are deferred follow-ups.
  • 0030Accepted. PostgreSQL capability admission rubric (durable) + scored catalogue (living). Single-platform (ADR-0008) is a constraint to exploit: emit Postgres-specific declarative constructs unconditionally. Core-admissible only if it passes four independent criteria — A1 IR-derived & forward-projectable (the analysis surfaces read the IR, not the SQL; Dazzle has no SQL→IR re-extraction and needs none), A2 effect-deterministic (across plans/collations/float-vs-exact/clock/replay — the determinism gotchas are the test fodder), A3 transactionally contained, A4 migration-evolvable (what ADR-0017 actually implies; demotes Postgres ENUM→Tier B) — plus mechanical sub-checks N (non-Turing-complete) and P (provenance, for the enforcement obligation: conformance flags hand-rolled equivalents). Corrects two framing errors from the external-reviewer draft: ADR-0017 governs the migration channel, not DB-side-logic (that prohibition is ADR-0009/0029-inv-8) — so the old "Gate G" was redundant; and SSI+retry is Tier C, struck per #1316 (FOR SHARE is the invariant-4 mechanism). Tier A is ~half net-new (CHECK/EXCLUDE/DEFERRABLE/ranges/data-modifying-CTE) — admissibility is a design property; an Impl column tracks what ships. Spun off bug #1321 (DECIMAL/MONEY→float). Conservation → ADR-0015 (ledger) or ADR-0031 (invariant:), never a trigger.
  • 0031Accepted. Flow-level aggregate invariants (sum/count over a set, compared to a bound) are a distinct invariant: construct on the atomic block — NOT a 7th ADR-0009 predicate (the row-scoped algebra stays closed, #1306). Set defined by an ADR-0009 row-filter (where) anchored to a flow row; RHS is a literal or an anchor-row field (sum(Posting.amount where transaction=input.txn)=0, <= input.budget.total, count(...)>=2). Enforced in-transaction after the steps, before commit, fail-closed → rollback. Concurrency: SELECT … FOR UPDATE the anchor row before the aggregate (reuses #1316 lock philosophy; SERIALIZABLE stays struck). Boundary: true double-entry conservation stays in ledger/transaction (ADR-0015); this is the bypassable non-ledger remainder (caps/quorums/cardinality). Deferred: min/max/avg, expr RHS, unanchored/global aggregates (rejected at validate time — no lockable anchor). Accepted; implementation tracked by #1318.
  • 0032Accepted (implemented, #1319). A lifecycle transition invokes a named atomic flow in the transition's own transaction (synchronous, shared-tx): the status write + the flow's effect steps commit/roll-back together. Binding = a transition invoke fulfil_order(order: self, …) effect with an explicit input map (self = transitioning row; validator checks every required input bound). Division: the process owns the transition + guard (ADR-0020); the atomic carries the effect, each step scope-enforced (#1313); a flow scope denial rolls the whole transition back (fail-closed). Principal = triggering user; a no-user (scheduled) transition invoking a scope-enforced flow is a validate-time error (no unscoped write path) — service-principal story deferred. Replaces the post-commit on_transition effect path for invoke-bound flows; hless event stays the path for genuinely-decoupled (non-atomic) downstream effects. Confirm jointly with ADR-0020 before implementation.
  • 0033Accepted (implemented Phases 1–3, #1337). CSRF is an auth-class-derived disposition, not a per-path token dance. CSRF is a control on ambient authority (the session cookie); a request authenticated by a caller-presented credential (Bearer / HMAC signature / OAuth state) is structurally immune. Two pure functions in back/runtime/csrf.py: csrf_disposition(...) classifies each state-changing request (NA_BEARER/NA_SIGNATURE/NA_PREAUTH/PROTECTED_SESSION, default-deny), csrf_admits(...) admits (origin-primary Sec-Fetch-Site+Origin gate with session-bound double-submit token fallback for PROTECTED_SESSION; admit for NA_*). The token is the server-side session's own csrf_secret (Phase 1, rotates on login/logout, survives swaps). Per-request Origin==Host makes tenant_host work configless. The disposition policy is surfaced in the RBAC compliance report (render_csrf_policy) so exemptions + rationale are auditable, not inferred from absence. Deliberately deferred (recorded so the absence is a decision): the <body hx-headers> transport switch (dropped — dz-csrf.js is a simpler central script), the guarded-action seam (redundant with the always-on middleware), the test-harness refactor (motivation moot post-Phase-1), and ESCAPE_HATCH/UNAUTH_MUTATING runtime classification + the DSL escape hatch (rare / a security-sensitive behavior change). Design: docs/superpowers/specs/2026-06-03-declarative-csrf-design.md.
  • 0035Accepted (implemented, #1339). QA-auth containment invariant (RLS Phase E.2). A secret-gated (QA_AUTH_SECRET, self-disabling — the router isn't mounted without it), HMAC-signed (NA_SIGNATURE per ADR-0033, ~60s constant-time window) mint may scope a session ONLY into a qa--namespaced, is_test=true, run-matched organizations row the target user has an active membership in — resolved from the DB via the signed run_id, never request-supplied (no confused-deputy). is_test is a column, not a slug heuristic (unforgeable from the request). Defence in depth: namespace ∧ is_test ∧ run-match ∧ membership; any one failing → one opaque 403 (no client oracle). The QA secret can never reach a real tenant; the minted session's active_membership.tenant_id makes cross-tenant access structurally impossible at the RLS layer (Phase B). On the auth Plan 1a–1c membership model; teardown via the E.1 excise_tenant primitive. Closes #1339 / completes RLS Phase E. (ADR-0034 reserved for the RLS-tenancy capstone.)
  • 0036Accepted (2026-06-16; not yet implemented, #1394 Layer 2). Tenant hierarchy data model (#1394 Layer 2 / #1393 Phase C). The hierarchy lives on the domain tenant_host entities (declared parent: FK edge, FK-graph-validated) — NOT on the public.tenants schema registry; current_tenant stays bound to the host ResolvedTenant (Layer 1). Aggregate-vs-single is compiled from the resolved host kind vs the source entity's kind (direct equality when equal, FK-path predicate when host is a proper ancestor, deny otherwise) — no new ADR-0009 predicate. Load-bearing reconciliation: the hierarchy lives strictly within one RLS isolation boundary — the RLS tenant_id partition must sit at/above the hierarchy root; current_tenant selects a sub-tree view within the fence and never aggregates across RLS tenants (link-time enforced). Rejected: parent column on public.tenants, pure FK-graph inference, RLS-bypassing aggregation. Acceptance decisions: membership×hierarchy resolved by ADR-0037 (root membership, derived reachability); aggregate (ancestor) host views are read-only across descendants (writes descend to the row's own kind); order:-vs-parent: consistency is an impl-time linker check.
  • 0038Accepted (2026-06-17); implemented; layer naming updated by ADR-0041. Rendering layer boundary: render/ is pure and acyclic. The runtime is a four-layer stack http → page → render → core (named back → ui → … at the time of writing; renamed in ADR-0041, 2026-06-20); render/ and ui/ must never import back/ (top-level or lazy in-function). Completes ADR-0011/0023 — fixes where rendering code lives, not what/how. ~3,700 LOC of pure rendering (region_adapter/, workspace_card_bodies.py) was mis-homed in the HTTP package back/, forcing the pure render/ layer to reach up via 5 cycle-dodging lazy imports. Decision: (D1) render/ imports only render/core/stdlib; (D2) relocate the misplaced subsystems into render/fragment/region/, flip the back-edges to downward imports, clean break (ADR-0003); (D3) enforce with a live AST drift-test (test_layering_drift.py, zero new deps, matches the *_drift.py idiom) — the rule was documented but not enforced, so it regressed; (D4) keep the ui/runtime/*_renderer.py page-orchestration sub-seam (it coordinates route/context and calls down into render/). Shrinks the htmx 4 migration surface (one rendering path, one home for the htmx contract). Rejected: leaving pure rendering in back/; laundering layer violations with lazy imports; import-linter (new dep). Origin: docs/evaluation/back-ui-render-boundary.md.
  • 0037Accepted (2026-06-16; not yet implemented, #1393 Phase C). Declarative membership relation (#1393 Phase C; sibling of ADR-0036). The framework already owns membership as a first-class runtime model (Plan 1a–1d: Identity/Organization/Membership/Session, memberships table, hash-chained events, IDOR-checked activation); Phase C adds a declared binding, not a new table. A membership: block on the tenant-root kind (per-tenant role source; principal is always the framework User in v1) makes the previously-inferred tenant-root match explicit and link-validated. The root-kind row is the dazzle.tenant_id tenant root. Descendant-host reachability is derived from one root membership via ADR-0036's parent: edges — no per-leaf membership rows (this answers ADR-0036's open membership×hierarchy question). Validated invariant (D5): membership root == RLS partition_key root == hierarchy root, else dazzle validate error (kills the silent empty-session footgun). Rejected: app-owned junction entity (forks Plan 1a), per-host membership rows, root divergence, status-quo inference. Phases B (apex discovery) + D (email-domain routing) out of scope.
  • 0039Accepted (2026-06-19; not yet implemented, #778). Auth-identity ↔ domain-User bridge via a declared binding. The principle that keeps the auth model intact: never repoint session.user_id (it's the auth-store identity — load-bearing for sessions/membership/RLS/audit); bridge auth→domain at the injection layer (which id lands in a ref User FK) and the provisioning layer (ensure the domain row exists), never the session layer. Replaces two implicit conventions (#774 injects the auth id into ref User, silently assuming domain User.id == auth id and that the row exists → FK violation on the real /auth/register path, #778) with a declared, validated auth_identity: binding on the User entity (link_via: email + field map/defaults). When declared: (a) provision/mirror the domain row on real registration via a shared helper both the production path and the #1398 test-route mirror call, and (b) stop special-casing User — inject the domain row's id resolved by link_via, reusing the cycle-249 backed_by/link_via path already used for Tester/Agent. Undeclared ⇒ status quo (backward-compatible). Rejected: repointing session.user_id (the auth-model breaker), a bespoke principal: construct (forks backed_by), test-only + "app owns the upsert" (the _seed_domain_user workaround this removes). Ratified 2026-06-19: A1 = validate-time error on any unresolvable required column / fenced-User binding (never swallow at runtime; D6); A2 = opt-in declaration (D7).
  • 0040Accepted (2026-06-19; not yet implemented, #1420 Slice 3). Conformant custom routes: the security model travels with the handler. Closes the #1420 invariant for custom routes (Slices 1–2 governed generated routes): no domain-touching route escapes its entity's permit/scope, and every domain route is an RBAC-matrix row. Reuses the existing static # dazzle:implements Entity.op via <param> route-override header (#1126) — it already opts a handler into check_entity_op via _wrap_with_policy_gate (fail-closed 403/404 before the body). The header is statically scannable (not a runtime decorator → more analyzable), so it's the conformant binding. Slice 3 makes conformance structural, not opt-in: a domain-touching override with no binding is a violation (D2); every domain route contributes a matrix row and an unbound one fails a HARD CI security gate (D3); a counter-prior + test_no_* gate flags the residue (raw SQL / direct repo for an undeclared entity in routes/*.py) at authoring time (D4). Rejected: a new DSL route: construct (custom routes need a Python body; interfaces: is spec-generation, not route-mounting), a runtime @dazzle_route decorator (binds at import, not statically scannable), advisory gates (an agent routes around warnings). No new IR/keyword — runtime + tooling only.
  • 0041Accepted (2026-06-20). Layer rename: back/http/, ui/page/. Names-only capstone of the back/ui-vestige consolidation. The runtime is a four-layer stack (ADR-0038) whose names predate SSR and mislead — a FastAPI router lived at ui/runtime/page_routes.py, HTML was produced under back/runtime/. After the v0.83.32–34 cleanups (naming sweep, route_generator cycle break, server-runtime relocation), the names were the last vestige. Decision: rename the two layer packages so they match the topology — http → page → render → core — via one word-bounded tree-wide rewrite (~1000 .py files + configs + package-data + mypy overrides + import-linter contracts + regenerated drift baselines). No behaviour/dependency/API-surface change; public package (dazzle-dsl) and import root (dazzle) unchanged; clean break (no back/ui shims). The import-linter contracts enforce the same rules under the new names. Rejected: keep back/ui (names lie, standing comprehension tax); merge the two layers (the boundary is a sound seam — the "split is the coupling cause" hypothesis was disproven); defer (pure churn that only grows). Origin: docs/evaluation/back-ui-render-boundary.md + the 2026-06-20 four-agent investigation.
  • 0042Accepted (#1448). poly_ref: the realized polymorphic-ref escape hatch.
  • 0046Accepted (shipped; scope narrowed to in-process introspection). Introspection boots the app's real entrypoint. inspect --runtime / perf trace boot the app's declared ASGI entrypoint (dazzle.toml [serve] app = "server:app"), not the framework-default create_app — so they introspect exactly what prod runs. Opt-in (falls back to create_app with a loud note); validate stays static. Closes the inspect-class false-negative (#1485 renderers, latent primitives/routes). ux verify is OUT of scope (#1486 closed): its live oracle needs framework test seams the prod entrypoint lacks — that case stays solved by the page_auth_context bridge (#1401, NOT superseded). Supersedes the per-subsystem app_init hook approach only for the inspect-class.
  • 0045Accepted. The snapshot-diff engine is the sole migration generator. Make the #1431 DSL-snapshot diff engine the one generator across db revision/db baseline/db migrate, and delete the legacy Alembic metadata-vs-live-DB autogenerate path plus everything propping it up: the #1427 additive-only scoping, the #1460 cyclic-FK inline hoist, the --legacy-autogenerate flag, and DAZZLE_ALEMBIC_ALLOW_DESTRUCTIVE. Rationale: the legacy path added no expressiveness (both project the same load_target_metadata()) — its only distinct trait was diffing against the live DB, which is the destructive-churn source #1427 fought; the engine emits FKs/indexes/uniques as separate post-create ops so cyclic/self-ref FKs + all new-table constraints are correct by construction (no hoist). Engine-inexpressible schema → hand-author with --no-autogenerate + op.execute; live-DB drift → db verify/stamp, not auto-diff. db baseline = engine vs empty prev (framework tables excluded, full snapshot embedded → no follow-up snapshot-baseline); db migrate = revision+upgrade (drops live-DB drift diffing; #1390 autostamp retained as an upgrade-time concern). Correctness pinned by a create_all parity oracle on real PG (db baseline ≡ create_all, name-insensitive, over inline corpus + example apps) + a round-trip property, all under the migration_engine marker — built because op-tree tests are blind to "valid op-tree, wrong schema" (the class that hid #1460 + a latent drop of ALL FKs/indexes/uniques on new tables, both fixed here). Bare alembic --autogenerate (no dazzle Config) is unsupported (suppressed with a directing warning). Supersedes-in-part #1431's keep-legacy decision. Builds on ADR-0017/0003, #1431/#1390.
  • 0044Accepted. Complete, CI-managed framework migration baseline. Collapse 19 dev-churn framework Alembic migrations (a partial, divergent mirror — only params/auth/process; ~10 other live framework tables had no Alembic rep) into ONE squashed baseline at the stable head 0019_process_runtime_tables (down_revision=None) that is the complete, provably-faithful mirror of the framework schema the runtime builds. One orchestrator ensure_framework_schema(conn) is the single schema source (18 method-based table creators extracted to shared ensure_* fns called by both store _init_db AND the orchestrator); the baseline's upgrade() calls the orchestrator's shared no-commit core _ensure_framework_schema_ddl(cur) (baseline ≡ orchestrator by code, not copy); a committed readable FRAMEWORK_SCHEMA_SNAPSHOT (rich index format: name/unique/predicate) declares it; a real-PG three-way parity gate asserts alembic-head ≡ orchestrator ≡ snapshot over all 30 in-scope tables + a no-unlisted-table guard, with a readable diff on mismatch. Plus a chain-cleanliness gate (single baseline at stable head) and a dazzle db reframework-baseline regeneration command (ruff-formats → byte-idempotent). Behavior change: ~10 previously-lazy framework tables now eagerly created for every app at boot (usage-audited 2026-06-23, all live). Excluded (can't live in one app-DB baseline): ops_database (separate DB), event-bus {prefix} tables, tenant registry. Widened dual-write rule: new framework table → orchestrator + regenerate baseline, not a per-table migration. Consumer runbook (3 states) for the shared-base squash. Builds on ADR-0017/0008/0005/0003, #1431/#1390/#1309. Known follow-ons: 4 tables still inlined (vs delegated) in the orchestrator; ensure_process_tables dead in prod; redundant atomic-audit boot call — all parity-gate-covered (non-divergent today).
  • 0047Accepted (2026-06-27). The DB-artifact registry is the single source of truth for DB-artifact metadata. One pure-data registry (dazzle.db.artifact_registry) declares every framework database artifact's class / creator / boot_entry / owner / RLS posture / baseline membership / boot-DDL gating; IN_SCOPE_TABLES is now registry-derived (in_baseline_tables()), collapsing a triplicated hand-synced list. An executable contract (tests/unit/test_db_artifact_contract.py, static) enforces: every registered boot_entry structurally self-gates with if skip_boot_schema_ddl(): return/raise (the #1495 catcher); a completeness sweep asserts every framework function issuing app-DB CREATE TABLE/INDEX is registered or in a function-level reasoned allowlist (the excluded-classes list made executable) — so a new ungated boot-DDL path is un-shippable; the framework never RLS-fences its own tables; refs resolve. Surfaces: dazzle inspect db-artifacts + docs/reference/db-artifacts.md. A known_ungated_issue field records currently-ungated paths as tracked debt. Origin #1495; building the contract surfaced four siblings (#1496/#1497/#1498/#1499, the last also missing from the ADR-0044 baseline). Amends ADR-0044: 0044 keeps the baseline construction + parity mechanism; 0047 owns which artifacts exist + their metadata. Rejected: a descriptive snapshot+diff baseline (proves no invariant); a read-only code-inferring lens (fragile, leaves the triplication); folding into ADR-0044 (rewrites an immutable record).
  • 0049Accepted (2026-06-29; direction committed, phased build pending). The typed Fragment substrate is the universal render path; the legacy direct-template layer is retired mode-by-mode. Production runs 100% on a legacy per-mode render path (page/runtime/{table,detail,form}_renderer.py, ~2,322 LOC) — a Jinja-era shape fossil (f-strings since #1042 but the dual-path fork, skeleton table, hand-built <dt>/<dd>, wrapper-less form remain); the typed substrate (ADR-0023) is built but unadopted (0 examples set render:). The fork is page_routes.py::_maybe_dispatch_inner_html on surface.render is None. Decision: make the substrate the single render model (AppSpec → Fragment → HTML), delete the legacy renderers mode-by-mode (list → view → create/edit; CUSTOM already substrate-only). Load-bearing: visual (not byte) parity — substrate DOM is canonical, goldens re-baselined + verified via ux verify/card-safety/a11y; list keeps skeleton+hydrate so render_data_row (ADR-0048) stays the sole row source and only chrome needs parity; dzTable mounts on the list Region; no silent legacy fallback post-delete (migration-only crutch); default flips per mode. Completes ADR-0038/0023/#1042 at the render-path-shape level; builds on ADR-0048 (row engine already converged). Rejected: keep both paths (fossil ossifies, permanent dual-model tax); byte-match substrate to legacy (imports the fossil shape); big-bang flip (un-reviewable, no fallback at the riskiest moment). Design: docs/superpowers/specs/2026-06-29-substrate-universal-render-path-design.md.
  • 0048Accepted (2026-06-28; #1505, Phase 1+2 shipped). The render/ substrate is the single source of list-row HTML; http/ is transport-only. The rich dz-tr-row data-table row renders solely via render/fragment/renderer/_data_row.py (render_data_row / render_data_table_rows, driven by a typed orthogonal RowCapabilities vector); the duplicate http/htmx_render.py::_render_table_row family is deleted. On an HTMX list refresh http/ builds the typed DataTable (build_data_table) and renders the <tbody> slice via the substrate — it no longer emits row HTML. Row model = unified compositional structure + named archetypes (data-table/list-region/embedded capability presets, not three emitters), legible to an agent as one f(columns,item,capabilities). Standing invariant: capabilities compose only via the stopPropagation protocol (the row owns the bare click; every interactive sub-element stops propagation) — a non-composing capability becomes a new archetype, never a branch in the core. Each phase is byte-stable (characterization-fixture-guarded). Completes ADR-0038 (which relocated other rendering into render/ but never addressed htmx_render.py); the prior "transport concern" framing for htmx_render.py is shown post-hoc by git archaeology (the split was a Jinja-removal migration artifact, #1042/#1064/#1361). Rejected: one flat DOM across all surfaces (erases the archetype signal); a shared leaf render_one_row() with divergent context-assembly above it (papers the seam). Follow-ons: Phase 3 (fold list-region/embedded), Phase 4 (#1494 peek/when_empty). Builds on ADR-0011/0023.
  • 0043Accepted (#1454). Closed-system AI cognition: every AI call has a governed subject. Two declared surfaces only (entity trigger + process step); AIJob.subject is a required poly_ref over a link-time-derived target set; POST /execute/{intent_name} removed; ProcessRun is the run referent for process-step AI. Fail-closed by schema (NOT NULL) + executor assertion + guard test. Builds on ADR-0042/0003/0017/0008. Realizes (does not reverse) ADR-0027's pre-committed escape hatch: the four-question interrogation failed against AegisMark's AIJob (an audit/gateway entity that references one of several domain entities and must be visible to the non-admin owner of the referenced row — ADR-0027's Q2 event-stream case, where the (type,id) pair is already sanctioned). Ships a typed poly_ref name [T1, T2] field (→ name_type text + name_id uuid, targets uuid-pk) + a name[Type].path scope selector that closes ADR-0027's load-bearing scope-composition objection: the branch selector names the target at authoring time so the predicate compiles to a normal typed subquery (name_type = 'T' AND name_id IN (SELECT id FROM t WHERE <sub>)) in app-layer + RLS-policy modes (degrades via #1447). Stays faithful to ADR-0027's two hard rejections (not ref union sugar; discriminator stays a visible queryable column). Ships with the dazzle db explain-scope traceability oracle (the price of the primitive per the failure-modes rubric). MVP: read/list/delete only (create/update raise E_POLY_VERB_UNSUPPORTED); targets uuid-pk; multi-branch via repeated rules. Rejected: the polymorphic_ref: block surface with explicit discriminator: (the terser poly_ref [..] field form keeps the discriminator visible without a block); ref X|Y|Z union sugar (ADR-0027); create/update poly probe + framework-AIJob adoption (follow-ons). Supersedes-in-part ADR-0027.
  • 0050Accepted (2026-07-01) — Option A chosen. First-party usage signal: framework inference input (not operator feature). Frames a currently-unmade decision behind #1517's "telemetry-gated" deferral of UX-maturity 1a (region-form inference) / 3a (action prominence): the framework never feeds end-user usage frequency into the DSL→render inference loop, so the data-driven-UI thesis (display:auto/semantic-enums/field-economy) hits a ceiling. The capture engine already exists but is orphanedhttp/runtime/analytics_collector.py records feature_use/action tenant-scoped, but mount_ops_platform() is never called (stranded in the ADR-0041 rename). Option A (recommended, narrowed): usage frequency is a first-class inference input — wire a narrowed collector into the app-factory lifespan (ADR-0005: no new singleton), expose ONE scope-safe (surface, field|action) count aggregate (chart-aggregate contract: pre-aggregated, tenant-fenced), one inferer reads it for 1a/3a with a static cold-start fallback (never worse than today) and an explain-style traceability hook. Out of scope of A: consent/GA4/PII (that's separate outbound product analytics). Option B: operator dashboard only — 1a/3a stay deferred, collector revived as opt-in ops surface or retired. Axis discipline (load-bearing): end-user usage ≠ Vitality (code-node health for MCP-graph trust, dev_docs/dazzle-vitality-*.md, planned-never-actioned) ≠ product analytics (analytics: DSL + compliance/analytics/, outbound). Rejected: greenfield (engine exists); GA4-stack-as-inference-signal (over-scoped — how it stalled before); fold Vitality in (different axis, sinks both); leave implicit (the forgotten-islet failure this ends). No code until the fork is picked. Landscape: dev_docs/2026-07-01-telemetry-observability-state-of-play.md.