Files
stack/docs/requirements/hierarchy-schema.md
T
fred ee8c593858
ci/woodpecker/pr/ci Pipeline was canceled
docs: hierarchy schema contract revision 2 (terra review findings 1-9)
- tenancy-FK exemption: canonical workspace_id is the one permitted
  business->class reference; all other forms forbidden with control test
- record class = 4 node tables + hierarchy_grants per A1 8.1.2
- grant uniqueness via UNIQUE NULLS NOT DISTINCT (or 6 partial indexes);
  NOT NULL on role/granted_by/name/slug
- grant FK delete actions split: target cascade, principal RESTRICT
- audit parity: REQ-AUD-001 guarantees, same-transaction outbox,
  deletion-safe snapshot linkage, cascade coverage, rollback witness
- roll-up never-a-write restored to full A1 strength (no canonical writes)
- ownership invariant completed via contract 2; both-sides transfer rule
  marked new policy; legacy premise corrected
- corrected cardinality witness (one-parent-per-child + sibling control)
- provenance fixes: REQ-MIG-001 attribution dropped, projects table
  described accurately, D8-rank-1 claim removed
2026-08-26 18:29:53 -05:00

15 KiB
Raw Blame History

Hierarchy Schema Contract (D2)

Status: DRAFT — awaiting ratification (webui-audit S2, contract 1 of 9). Authority: PRD D2/D9/D13 (Part I §4) and the native-kanban SOT Amendment A1 (docs/requirements/native-kanban-sot.md §8, ratified 2026-08-25). This document turns the ratified hierarchy into a concrete schema contract: tables, cardinalities, constraints, and ownership/transfer semantics. It is the prerequisite for the hierarchy command family and for the RBAC grant model (contract 2, docs/requirements/rbac-grant-model.md).

Revision 2 (independent review, GPT-5.6 terra): tenancy-FK exemption made explicit (§1.1); record class extended to include hierarchy_grants (§1.1); provenance corrections on legacy tables and the planning projects table (§1.3, §2 naming note); NOT NULL and NULLS NOT DISTINCT grant uniqueness (§2.6, §3.2); grant FK delete actions split cascade/restrict (§3.3); transfer transaction includes its audit write (§4.3); ownership invariant completed via contract 2 with the both-sides rule marked as new policy (§4.2, §4.4); hierarchy audit brought under REQ-AUD-001-equivalent guarantees with deletion-safe linkage (§5.2); roll-up never-a-write restored to full A1 strength (§5.4); §6 rebuilt with bounded observables for every MUST (allowlist, command surface, audit, corrected cardinality witness).

Scope: the tenancy/authorization structure record class — companies, estates, platform-projects, workspaces, hierarchy grants, their parentage, and constraints. Out of scope: the RBAC grant vocabulary and evaluation semantics (contract 2), roll-up projection semantics (contract 8), kanban planning entities inside workspaces (SOT §5), migration or retirement of legacy flat data (future work; see §1.3).

1. Record class and placement

  1. The tenancy/authorization structure record class defined by Amendment A1 §8.1.2 comprises five tables: the four node tables of §2 AND hierarchy_grants (§3) — A1 includes hierarchy-level access grants in the class. Every rule addressed to "the class" in this contract (payload prohibition, mutation path, audit) binds all five tables. Class rows carry parentage, naming, grant, and audit-linkage data only — never task, plan, or any business/orchestration payload. References from business/orchestration rows into the class are limited to exactly one form: the canonical workspace_id tenancy column that REQ-TEN-001 requires on every canonical row, referencing workspaces.id. No business/orchestration row may reference a company, estate, platform-project, or grant id in any position, and no business/orchestration row may reference a workspace id in any non-tenancy position (dependency, claim target, work subject).
  2. Hierarchy records are NOT workspace-scoped rows: REQ-TEN-001's workspace_id obligation binds business/orchestration rows and does not apply to this class (A1 §8.1.2). The workspaces table itself is the anchor the obligation points at.
  3. The legacy flat tables (teams, and the Brain planning projects table in packages/db/src/schema.ts) are not part of this class. What A1 §8.1.4 pins is narrower: the planning projects table and platform_projects stay distinct tables. This contract adds, as new policy ratified here: neither teams nor projects is repurposed as a hierarchy table. Their eventual migration or retirement is future work that no existing REQ assigns; it is out of scope here.

2. Tables and cardinalities

Naming: the level above workspaces is platform_projects, per A1 §8.1.4. The existing projects table is Brain planning data (so labeled in packages/db/src/schema.ts; it carries no workspace_id), and the schema MUST NOT merge the two. (A rename of either remains an implementation-PR decision under A1; this contract pins only that they stay distinct tables.)

  1. companies — id (uuid pk), name, slug (unique per deployment), created_at, updated_at. N per deployment (D2).
  2. estates — id, name, slug, company_id NOT NULL → companies.id ON DELETE RESTRICT. Exactly one company per estate; a company holds any number of estates.
  3. platform_projects — id, name, slug, estate_id NOT NULL → estates.id ON DELETE RESTRICT. Exactly one estate per platform-project; an estate holds any number of platform-projects.
  4. workspaces — id, name, slug, platform_project_id NOT NULL → platform_projects.id ON DELETE RESTRICT. Exactly one platform-project per workspace. This table is the referent of every workspace_id column the SOT requires on canonical rows.
  5. Chain resolution is by construction. Because every parent FK is NOT NULL and single-valued (one FK column, no parentage edge tables, no multi-parent forms, no nullable "detached" states), each workspace resolves to exactly one platform-project → estate → company chain (A1 §8.3 acceptance 1). One-parent-per-child is the constrained direction; many children per parent is valid data.
  6. Slug scoping. All name and slug columns are NOT NULL. estates.slug is unique within its company, platform_projects.slug within its estate, workspaces.slug within its platform-project (composite unique constraints). Display names are unconstrained beyond NOT NULL.
  7. No hierarchy table carries a metadata jsonb column or any free-form payload field. The columns declared in this section and §3 are exhaustive: a class table's column set is exactly its declared set (verified per §6.2) — nothing else (A1 §8.1.2).

3. Grant attachment points

The grant vocabulary (which roles exist, what each permits, how evaluation and revocation work) is contract 2. This contract pins only the schema shape contract 2 attaches to:

  1. hierarchy_grants — id, subject (exactly one of user_idusers.id, team_idteams.id; CHECK-enforced exactly-one-of), target (exactly one of company_id, estate_id, platform_project_id; CHECK-enforced exactly-one-of), role (text NOT NULL; vocabulary and its CHECK constraint owned by contract 2 §2), granted_by NOT NULL → users.id, created_at.
  2. Uniqueness: at most one grant row per (subject, target, role). Because the subject and target columns are nullable by design, ordinary PostgreSQL composite uniqueness treats NULLs as distinct and would not enforce this. The implementation MUST use a single UNIQUE NULLS NOT DISTINCT constraint across (user_id, team_id, company_id, estate_id, platform_project_id, role) or six equivalent partial unique indexes (one per subject×target form). The pinned Drizzle ORM supports nullsNotDistinct().
  3. Delete actions are split by column class:
    • Target FKs (company_id, estate_id, platform_project_id): ON DELETE CASCADE — the one permitted cascade in this class. A grant on a deleted node is meaningless and fail-open if retained. Cascaded grant deletions are audited per §5.2.
    • Principal FKs (user_id, team_id, granted_by): ON DELETE RESTRICT. The identity contract (§7.3) gates user deletion today and defines no team-deletion rule; this contract does not invent one. These FKs stay RESTRICT until an explicit deletion-and-retention contract ratifies otherwise.
  4. Workspace-level access is evaluated, not stored here: a grant at any of the three levels evaluates down the chain to workspace-scoped authorization (A1 §8.1.3). No workspace_id column exists on hierarchy_grants — workspace membership (REQ-ID-001) remains its own mechanism inside the SOT schema, and the chain adds where grants can be declared, never a bypass.

4. Ownership and transfer

"Assets are transferable subject to the structure" (PRD Part I §4):

  1. A transfer changes exactly one parent FK on exactly one hierarchy row: workspace → new platform-project, platform-project → new estate, estate → new company. Nothing else in the class or the SOT changes: business and orchestration rows inside affected workspaces are untouched, keep their workspace_id, and never cross a workspace boundary (A1 §8.1.3 "chain maintenance").
  2. Transfer authorization requires authority over BOTH the source and the destination parent. This both-sides predicate is new policy introduced by this contract pair (D2/A1 do not state it); its evaluation semantics are contract 2 §5. The structural half — that the transfer command evaluates it before mutating — binds here.
  3. A transfer transaction contains exactly two writes: the single-row parent-FK update and its audit event (§5.2). There are no multi-row transfer batches at the schema level; bulk moves are N audited transfers.
  4. Hierarchy records have no owner_id. Ownership in the hierarchy IS the grant structure: a "company owner" is a subject with an owner grant on that company or an ancestor (contract 2 §2), not a column. The ownership invariant across the contract pair: a node may hold zero direct owner grants (authority can derive from an ancestor grant); node creation names the initial owner grant in the same audited operation and the wizard seeds the first company's owner the same way (contract 2 §4.3); transfer and revocation semantics are contract 2 §§56. This avoids column-encoded authority of the kind the legacy schema carries (teams.owner_id and teams.manager_id are required user FKs, and team_members.role is a further authority field — none of them evaluable under a grant model).

5. Mutation path, audit, and deletion

  1. All hierarchy mutations flow through the same sole-writable-SOT, fail-closed, audited Gateway command path as everything else (A1 §8.2.3, REQ-API-001). No direct-DB writers, no raw CRUD endpoints.
  2. Audit parity. A1 §8.2 leaves every pre-existing REQ binding, so hierarchy mutations get REQ-AUD-001's guarantees, not a weakened substitute. Concretely:
    • Every create, rename, transfer, grant create/change/revoke, and delete — including every grant deletion cascaded by a node delete — emits a semantic audit event carrying actor, verb, target, and (for transfers) source and destination parents, with the correlation, causation, idempotency, and per-target ordering guarantees REQ-AUD-001 defines.
    • The state change and its audit event(s) commit in the same transaction, delivered through a transactional outbox. Hierarchy events are not workspace-scoped rows and do not ride the workspace outbox; they get an equivalent hierarchy outbox under the same append-only, same-transaction rules.
    • Deletion-safe linkage: audit events reference their target by an immutable snapshot (id, slug, and parent chain at event time), never by a foreign key into the class tables, so append-only events survive the deletion of their target.
  3. Deletion is fail-closed bottom-up: a hierarchy record with children cannot be deleted (RESTRICT FKs, §2). Deleting a workspace is a SOT-side operation subject to the kanban SOT's own rules and is not granted any new semantics by this contract.
  4. Roll-up is never a write (A1 §8.2.2, preserved at full strength). A roll-up read mutates nothing — not hierarchy state, and not business or orchestration state: it must not mutate, claim, order, or gate workspace work. Contract 8 owns projection details but cannot narrow this rule. This contract additionally guarantees the chain roll-ups aggregate over is unique and non-null (§2.5).

6. Verification requirements

Binding on the implementing PRs (extends A1 §8.3):

  1. Schema witnesses (real PostgreSQL, §6.8): chain construction — insert with a null parent FK refused; insert with one valid parent accepted; two siblings under one parent accepted (the control proving the constraint rejects only what §2.5 forbids); catalog assertion that each child table has exactly one parent-FK column and no parentage edge table exists. Composite slug uniqueness per parent (duplicate slug under same parent refused; same slug under different parents accepted). Grant CHECKs: exactly-one-of subject and exactly-one-of target each witnessed (zero and two set → refused). Grant uniqueness: a duplicate (subject, target, role) row refused for each of the six subject×target forms, proving NULLS-NOT-DISTINCT semantics; NOT NULL on role, granted_by, and all name/slug columns witnessed.
  2. Column allowlist: an information_schema assertion that each class table's column set is exactly the set declared in §2/§3 — the bounded observable for no-payload (§2.7) and no-owner_id (§4.4).
  3. Command surface: an inventory assertion over the Gateway's registered hierarchy routes/commands proving the mutation surface is exactly the declared hierarchy command family — no generic CRUD endpoint and no non-command write path touches class tables (§5.1).
  4. Audit witnesses: for each mutation class (create, rename, transfer, grant create/change/revoke, delete) — the event exists after commit with actor/verb/target and same-transaction atomicity; a rolled-back mutation leaves no event (rollback witness); a node delete's cascaded grant deletions are each covered by events; events survive deletion of their target (query the events of a deleted node).
  5. Transfer tests: parent-FK update moves the subtree resolution and modifies zero business/orchestration rows (row-count and content assertions on workspace contents before/after); transfer without authority on the source or on the destination side is refused (with contract 2 §7.8).
  6. Deletion tests: delete with children refused at the database level; delete of a leaf cascades its grants and nothing else; deleting a user or team that is a grant subject (or granted_by referent) is refused (RESTRICT witnesses for §3.3).
  7. Negative tests: no business/orchestration table accepts a company, estate, platform-project, or grant id in any reference position, and none accepts a workspace id in any non-tenancy position; the canonical tenancy FK control — a business row inserted with a valid workspace_id succeeds, with an invalid one is refused; roll-up endpoints mutate no canonical state anywhere (assert zero writes across hierarchy AND workspace tables, not hierarchy only); readers see aggregates only over workspaces they are authorized on, with no cross-tenant existence oracles (A1 §8.3 acceptance 3).
  8. Real-PostgreSQL coverage for every constraint witness (unique/CHECK/ RESTRICT/NULLS NOT DISTINCT behavior), using the ci-postgres service in the test CI step; mocked specs cannot witness database constraints.

Ruling request

Ratify sections 16 as written, with one decision embedded: hierarchy records carry no owner column — ownership is expressed solely through grants (§4.4) — say "agreed" or name the ownership model you want.