docs: hierarchy schema contract revision 2 (terra review findings 1-9)
ci/woodpecker/pr/ci Pipeline was canceled

- 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
This commit is contained in:
fred
2026-08-26 18:29:53 -05:00
parent 7ef620cda7
commit ee8c593858
+175 -86
View File
@@ -5,82 +5,122 @@ 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 (`docs/requirements/native-kanban-sot.md` §8, ratified 2026-08-25). This
document turns the ratified hierarchy into a concrete schema contract: document turns the ratified hierarchy into a concrete schema contract:
tables, cardinalities, constraints, and ownership/transfer semantics. It is tables, cardinalities, constraints, and ownership/transfer semantics. It is
the prerequisite for the D8 rank-1 tool (hierarchy command family) and for the prerequisite for the hierarchy command family and for the RBAC grant
the RBAC grant model (contract 2). 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, Scope: the tenancy/authorization structure record class — companies,
estates, platform-projects, workspaces, their parentage, and the grant estates, platform-projects, workspaces, hierarchy grants, their parentage,
attachment points. Out of scope: the RBAC grant vocabulary and evaluation and constraints. Out of scope: the RBAC grant vocabulary and evaluation
semantics (contract 2), roll-up projection semantics (contract 8), kanban semantics (contract 2), roll-up projection semantics (contract 8), kanban
planning entities inside workspaces (SOT §5), migration of legacy flat data planning entities inside workspaces (SOT §5), migration or retirement of
(SOT REQ-MIG-001). legacy flat data (future work; see §1.3).
## 1. Record class and placement ## 1. Record class and placement
1. The four tables below form the **tenancy/authorization structure record 1. The **tenancy/authorization structure record class** defined by
class** defined by Amendment A1 §8.1.2. They carry parentage, naming, and Amendment A1 §8.1.2 comprises five tables: the four node tables of §2
grant data only — never task, plan, or any business/orchestration AND `hierarchy_grants` (§3) — A1 includes hierarchy-level access grants
payload. No business/orchestration row may reference a hierarchy record in the class. Every rule addressed to "the class" in this contract
as a dependency, claim target, or work subject. (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 2. Hierarchy records are NOT workspace-scoped rows: REQ-TEN-001's
`workspace_id` obligation binds business/orchestration rows and does not `workspace_id` obligation binds business/orchestration rows and does not
apply to this class (A1 §8.1.2). The `workspaces` table itself is the apply to this class (A1 §8.1.2). The `workspaces` table itself is the
anchor the obligation points at. anchor the obligation points at.
3. The legacy flat tables (`teams`, `projects` in `packages/db/src/schema.ts` 3. The legacy flat tables (`teams`, and the Brain planning `projects` table
— the Brain planning table, not the hierarchy level) are not part of this in `packages/db/src/schema.ts`) are not part of this class. What A1
class and are not repurposed. The SOT forbids reusing the flat model for §8.1.4 pins is narrower: the planning `projects` table and
the hierarchy; their eventual migration or retirement is REQ-MIG-001 `platform_projects` stay distinct tables. This contract adds, as new
work, out of scope here. 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 ## 2. Tables and cardinalities
Naming: the level above workspaces is `platform_projects`, per A1 §8.1.4 Naming: the level above workspaces is `platform_projects`, per A1 §8.1.4.
the existing `projects` table already means workspace-internal planning The existing `projects` table is Brain planning data (so labeled in
entities, and the schema MUST NOT merge the two. (A rename of either `packages/db/src/schema.ts`; it carries no `workspace_id`), and the schema
remains an implementation-PR decision under A1; this contract pins only that MUST NOT merge the two. (A rename of either remains an implementation-PR
they stay distinct tables.) decision under A1; this contract pins only that they stay distinct tables.)
1. `companies` — id (uuid pk), name, slug (unique per deployment), 1. `companies` — id (uuid pk), name, slug (unique per deployment),
created_at, updated_at. N per deployment (D2). created_at, updated_at. N per deployment (D2).
2. `estates` — id, name, slug, `company_id` NOT NULL → 2. `estates` — id, name, slug, `company_id` NOT NULL →
`companies.id` ON DELETE RESTRICT. Exactly one company per estate. `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 → 3. `platform_projects` — id, name, slug, `estate_id` NOT NULL →
`estates.id` ON DELETE RESTRICT. Exactly one estate per `estates.id` ON DELETE RESTRICT. Exactly one estate per
platform-project. platform-project; an estate holds any number of platform-projects.
4. `workspaces` — id, name, slug, `platform_project_id` NOT NULL → 4. `workspaces` — id, name, slug, `platform_project_id` NOT NULL →
`platform_projects.id` ON DELETE RESTRICT. Exactly one platform-project `platform_projects.id` ON DELETE RESTRICT. Exactly one platform-project
per workspace. This table is the referent of every `workspace_id` column per workspace. This table is the referent of every `workspace_id` column
the SOT requires on canonical rows. the SOT requires on canonical rows.
5. **Chain resolution is by construction.** Because every parent FK is NOT 5. **Chain resolution is by construction.** Because every parent FK is NOT
NULL and single-valued, each workspace resolves to exactly one NULL and single-valued (one FK column, no parentage edge tables, no
platform-project → estate → company chain (A1 §8.3 acceptance 1). There multi-parent forms, no nullable "detached" states), each workspace
are no parentage edge tables, no multi-parent forms, and no nullable resolves to exactly one platform-project → estate → company chain (A1
"detached" states. §8.3 acceptance 1). One-parent-per-child is the constrained direction;
6. **Slug scoping.** `estates.slug` is unique within its company, many children per parent is valid data.
`platform_projects.slug` within its estate, `workspaces.slug` within its 6. **Slug scoping.** All `name` and `slug` columns are NOT NULL.
platform-project (composite unique constraints). Display names are `estates.slug` is unique within its company, `platform_projects.slug`
unconstrained. 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 7. No hierarchy table carries a `metadata` jsonb column or any
free-form payload field. Parentage, naming, timestamps, and the audit free-form payload field. The columns declared in this section and §3
linkage of §5 — nothing else (A1 §8.1.2). 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 ## 3. Grant attachment points
The grant vocabulary (which roles exist, what each permits, how inheritance The grant vocabulary (which roles exist, what each permits, how evaluation
and revocation evaluate) is contract 2. This contract pins only the schema and revocation work) is contract 2. This contract pins only the schema
shape contract 2 attaches to: shape contract 2 attaches to:
1. `hierarchy_grants` — id, subject (exactly one of `user_id``users.id`, 1. `hierarchy_grants` — id, subject (exactly one of `user_id``users.id`,
`team_id``teams.id`; CHECK-enforced exactly-one-of), target (exactly `team_id``teams.id`; CHECK-enforced exactly-one-of), target (exactly
one of `company_id`, `estate_id`, `platform_project_id`; one of `company_id`, `estate_id`, `platform_project_id`;
CHECK-enforced exactly-one-of), `role` (text; vocabulary owned by CHECK-enforced exactly-one-of), `role` (text NOT NULL; vocabulary and
contract 2), granted_by`users.id`, created_at. its CHECK constraint owned by contract 2 §2), `granted_by` NOT NULL →
2. Uniqueness: at most one grant row per (subject, target, role). `users.id`, created_at.
3. Deleting a hierarchy record deletes its grants (the one permitted 2. Uniqueness: at most one grant row per (subject, target, role). Because
cascade in this class — a grant on a deleted node is meaningless and the subject and target columns are nullable by design, ordinary
fail-open if retained). Deleting a user or team follows the platform PostgreSQL composite uniqueness treats NULLs as distinct and would not
rule for principal deletion (identity contract §7; deletion is currently enforce this. The implementation MUST use a single
gated). `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 4. Workspace-level access is evaluated, not stored here: a grant at any of
the three levels evaluates down the chain to workspace-scoped the three levels evaluates down the chain to workspace-scoped
authorization (A1 §8.1.3). No `workspace_id` column exists on authorization (A1 §8.1.3). No `workspace_id` column exists on
@@ -92,69 +132,118 @@ shape contract 2 attaches to:
"Assets are transferable subject to the structure" (PRD Part I §4): "Assets are transferable subject to the structure" (PRD Part I §4):
1. A transfer is an audited UPDATE of exactly one parent FK on exactly one 1. A transfer changes exactly one parent FK on exactly one hierarchy row:
hierarchy row: workspace → new platform-project, platform-project → new workspace → new platform-project, platform-project → new estate, estate
estate, estate → new company. Nothing else changes: business and → new company. Nothing else in the class or the SOT changes: business
orchestration rows inside affected workspaces are untouched, keep their and orchestration rows inside affected workspaces are untouched, keep
`workspace_id`, and never cross a workspace boundary (A1 §8.1.3 "chain their `workspace_id`, and never cross a workspace boundary (A1 §8.1.3
maintenance"). "chain maintenance").
2. Transfer authorization requires authority over BOTH the source and the 2. Transfer authorization requires authority over BOTH the source and the
destination parent (the vocabulary for "authority" is contract 2; the destination parent. This both-sides predicate is **new policy
both-sides requirement is structural and binds here). introduced by this contract pair** (D2/A1 do not state it); its
3. A transfer is a single-row, single-statement mutation; there are no evaluation semantics are contract 2 §5. The structural half — that the
multi-row transfer batches at the schema level. Bulk moves are N audited 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. transfers.
4. Hierarchy records have no `owner_id`. Ownership in the hierarchy IS the 4. Hierarchy records have no `owner_id`. Ownership in the hierarchy IS the
grant structure (§3) — a "company owner" is a subject with the top role grant structure: a "company owner" is a subject with an `owner` grant
on that company, not a column. This avoids the dual-write failure mode on that company or an ancestor (contract 2 §2), not a column. The
of the legacy `teams.owner_id`/`teams.manager_id` columns, which encode ownership invariant across the contract pair: a node may hold zero
authority outside any evaluable grant model. 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 ## 5. Mutation path, audit, and deletion
1. All hierarchy mutations flow through the same sole-writable-SOT, 1. All hierarchy mutations flow through the same sole-writable-SOT,
fail-closed, audited Gateway command path as everything else (A1 §8.2.3, fail-closed, audited Gateway command path as everything else (A1 §8.2.3,
REQ-API-001). No direct-DB writers, no raw CRUD endpoints. REQ-API-001). No direct-DB writers, no raw CRUD endpoints.
2. Every create, rename, transfer, grant change, and delete emits a 2. **Audit parity.** A1 §8.2 leaves every pre-existing REQ binding, so
semantic audit event carrying actor, verb, target record, and (for hierarchy mutations get REQ-AUD-001's guarantees, not a weakened
transfers) source and destination parents. Hierarchy audit events substitute. Concretely:
reference hierarchy records; they are not workspace-scoped rows and do - Every create, rename, transfer, grant create/change/revoke, and
not ride the workspace outbox (REQ-AUD-001 binds business rows; the delete — including every grant deletion cascaded by a node delete —
implementing PR defines the hierarchy audit store under the same emits a semantic audit event carrying actor, verb, target, and (for
append-only rules). 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 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 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 operation subject to the kanban SOT's own rules and is not granted any
new semantics by this contract. new semantics by this contract.
4. Roll-up reads (A1 §8.1.3) touch none of these tables' write paths and 4. **Roll-up is never a write** (A1 §8.2.2, preserved at full strength). A
are specified by contract 8; this contract only guarantees the chain roll-up read mutates nothing — not hierarchy state, and not business or
they aggregate over is unique and non-null. 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 ## 6. Verification requirements
Binding on the implementing PRs (extends A1 §8.3): Binding on the implementing PRs (extends A1 §8.3):
1. Schema tests: chain uniqueness by construction (insert attempts with 1. Schema witnesses (real PostgreSQL, §6.8): chain construction insert
null/duplicate parents fail); composite slug uniqueness per parent; with a null parent FK refused; insert with one valid parent accepted;
CHECK-enforced exactly-one-of on grant subject and target; grant two siblings under one parent accepted (the control proving the
uniqueness per (subject, target, role). constraint rejects only what §2.5 forbids); catalog assertion that each
2. Transfer tests: parent-FK update moves the subtree resolution and 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 modifies zero business/orchestration rows (row-count and content
assertions on workspace contents before/after); transfer without assertions on workspace contents before/after); transfer without
authority on either side is refused. authority on the source or on the destination side is refused (with
3. Deletion tests: delete with children refused at the database level; contract 2 §7.8).
delete of a leaf cascades its grants and nothing else. 6. Deletion tests: delete with children refused at the database level;
4. Authorization tests (with contract 2): a grant at each level yields delete of a leaf cascades its grants and nothing else; deleting a user
exactly the workspace permissions the chain implies; revocation up the or team that is a grant subject (or `granted_by` referent) is refused
chain propagates (A1 §8.3 acceptance 2). (RESTRICT witnesses for §3.3).
5. Negative tests: no business/orchestration table accepts a hierarchy 7. Negative tests: no business/orchestration table accepts a company,
record id in any dependency/reference position; roll-up endpoints cannot estate, platform-project, or grant id in any reference position, and
mutate hierarchy state; readers see aggregates only over workspaces they none accepts a workspace id in any non-tenancy position; the canonical
are authorized on, with no cross-tenant existence oracles (A1 §8.3 tenancy FK control — a business row inserted with a valid
acceptance 3). `workspace_id` succeeds, with an invalid one is refused; roll-up
6. Real-PostgreSQL coverage for the constraint-witness tests (unique/CHECK/ endpoints mutate no canonical state anywhere (assert zero writes across
RESTRICT behavior), using the `ci-postgres` service in the `test` CI hierarchy AND workspace tables, not hierarchy only); readers see
step; mocked specs cannot witness database constraints. 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 ## Ruling request