DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path standing-notes/loomworks-standing-note-database-definition-not-decision-v0_1.md

Loomworks Standing Note — Database as Definition Layer, Not Decision Layer

Version. 0.1 Date. 2026-06-27 Status. Standing note for the Loomworks project. Surfaces whenever a CR proposes moving logic into the database layer — a stored function, a trigger, a generated column, a constraint, an indexed expression, a view that computes rather than selects. Primary relevance: CR-1 list-loading (the originating instance). Secondary relevance: any phase that reaches for database-resident logic to enforce, derive, or decide. Origin. Surfaced during CR-1 list-loading (the engine cursor contract), 2026-06-27, when the assertions endpoint's sort could not be indexed on its raw expression (a non-IMMUTABLE text → timestamptz cast) and the resolution was a UTC-pinned IMMUTABLE wrapper function — the first deliberate move of a piece of logic into the database in the Loomworks build. The Operator named the general lean: "moving such management to the database seems to be a good step." This note draws the line that makes the lean sound, so a second instance of the pattern lands on the right side of it rather than drifting across. Lineage. Same family as correctness-guarantees-in-code-not-prompts and FORAY-emission-belongs-at-the-auditable-event-layer. All three say: put the guarantee at the structural chokepoint every path must traverse, not at a surface that can be bypassed or a copy that can drift. This note applies that family to the app/database boundary specifically.


What the principle says

The database is a good home for how data is shaped, sorted, and constrained — definitions. It is a bad home for who decides what happens to that data — decisions. The line between the two is the principle.

Definition-layer work belongs in the database: an integrity constraint that rejects malformed rows, an index that makes a sort fast, a named function that defines a sort key once so two clauses can't drift apart, a check that a value falls in a permitted range, a view that selects and resolves. These are statements about what the data is and how it is ordered. They are pure, deterministic, side-effect-free, and they sit next to the data they describe so the planner and every query can use them.

Decision-layer work does not belong in the database: a trigger that transitions an artifact's state, a stored procedure that admits a contribution, a function that decides whether something is approved, posted, published, or promoted. These are statements about who has authority over a transition. Relocating them into the database does not make them safe — it hides them. A trigger that auto-transitions an artifact's state is exactly the category error the seed names in the Operator-authority principle ("automatic state transitions on artifacts the Operator has authority over are a category error… the system surfaces and signals; the Operator approves"), just moved from the app layer to the schema where it is harder to see.

The layer test for any proposed database-resident logic: Does this define how data is shaped/sorted/constrained, or does it decide what happens to the data? If it defines → database is an appropriate home. If it decides → it belongs where the Operator (or the Operator-facing surface) can be the one who approves; the database must not be the actor.

Why the principle matters for Loomworks

The originating instance is the clean case. The assertions sort key — "an assertion's chronological position is its parsed-UTC created_at, falling back to last_updated_at" — already lived in the database, in a query's ORDER BY. CR-1 was about to duplicate that fiddly COALESCE-cast expression into a second clause (the keyset WHERE boundary), where the two copies would have to stay byte-identical or the planner would silently stop using the index. The IMMUTABLE wrapper collapsed the duplication to one named definition both clauses call. That is definition-layer work: a sort key, given a name, made indexable, with a single source of truth. No decision crossed the boundary; an existing definition was consolidated next to the data it sorts. This is the sound version of the move, and it is the same structural-not-repeated discipline as the rest of the family.

The trap the principle guards against is the creep from defining to deciding. "Move management to the database" is correct for sort keys, constraints, and indexes. It becomes wrong the moment it reaches for triggers that enforce policy or functions that make state transitions — because Loomworks's whole authority model rests on the machine surfacing candidates and the Operator approving transitions. A database trigger that decides is an unapprovable transition by construction. When a future CR proposes database-resident logic, this note's test decides whether it is the assertions-wrapper case (ship it) or the auto-transition case (refuse it, surface to the Operator).

The IMMUTABLE caveat (specific, load-bearing)

If this pattern recurs — more indexed functions — one discipline travels with it. The IMMUTABLE mark is a promise the author makes to the planner, and Postgres trusts it largely without verifying. An index built on a function wrongly marked IMMUTABLE is subtly wrong in a way that only shows under a non-default session (a different timezone, locale, or config GUC), which is the hardest class of bug to catch because it passes every test run under default settings.

The assertions wrapper is genuinely immutable only because of the UTC pin — the AT TIME ZONE 'UTC' that stops the parse from reading the session timezone. That pin is not a detail; it is the thing that makes the promise true and the index correct. The standing discipline: an IMMUTABLE mark is earned only after confirming the function reads nothing outside its arguments — no session timezone, no locale, no current_user, no GUC, no now(). That confirmation is the cost of admission for any indexed function, made a standing check rather than a per-case judgment. The migration carries a comment naming the pin as load-bearing so no later editor strips it.

Where the principle is not load-bearing

It does not apply to ordinary application logic that happens to query the database — that logic lives in the app and reads from the database; nothing is being moved across the boundary. It does not apply to the existing views and SELECT-and-resolve handlers throughout the engine — those are reads, already definition-side. It is not a mandate to move logic into the database; it is a line that decides what may move when a CR proposes a move. The default remains: logic lives in the application; the database holds, shapes, sorts, and constrains the data. The principle activates only when a CR reaches for database-resident logic, and its job at that moment is to sort the reach into definition (allowed) or decision (refused).

Status of the pattern

One confirmed instance (the assertions wrapper). The Operator's lean — database-resident management is a good step — is recorded as a lean, not yet a settled methodology rule, pending a second instance to confirm the shape. This note exists so that second instance has the line drawn in advance: it lands on the definition side and ships, or on the decision side and surfaces to the Operator, rather than being judged fresh under build pressure. If a second definition-side instance lands cleanly, this note is a candidate for promotion into the methodology document proper (the v0.21 consolidation backlog) as a named engineering-discipline principle alongside correctness-guarantees-in-code-not-prompts.


DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks Standing Note — Database as Definition Layer, Not Decision Layer — v0.1 — 2026-06-27 Origin: the CR-1 assertions-index fork. One confirmed instance; lean recorded pending a second.