ci/woodpecker/pr/ci Pipeline was canceled
Turns PRD D2/D13 and kanban-SOT Amendment A1 into a concrete schema contract: companies/estates/platform_projects/workspaces with NOT NULL RESTRICT parent chains (chain uniqueness by construction), per-parent slug scoping, hierarchy_grants attachment shape (vocabulary deferred to contract 2), transfer as single audited parent-FK update requiring both-sides authority, no owner columns (ownership = grants), fail-closed bottom-up deletion, Gateway-only mutation path, and a verification matrix with real-Postgres constraint witnesses.
164 lines
8.7 KiB
Markdown
164 lines
8.7 KiB
Markdown
# 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 D8 rank-1 tool (hierarchy command family) and for
|
||
the RBAC grant model (contract 2).
|
||
|
||
Scope: the tenancy/authorization structure record class — companies,
|
||
estates, platform-projects, workspaces, their parentage, and the grant
|
||
attachment points. 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 of legacy flat data
|
||
(SOT REQ-MIG-001).
|
||
|
||
## 1. Record class and placement
|
||
|
||
1. The four tables below form the **tenancy/authorization structure record
|
||
class** defined by Amendment A1 §8.1.2. They carry parentage, naming, and
|
||
grant data only — never task, plan, or any business/orchestration
|
||
payload. No business/orchestration row may reference a hierarchy record
|
||
as a dependency, claim target, or 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`, `projects` in `packages/db/src/schema.ts`
|
||
— the Brain planning table, not the hierarchy level) are not part of this
|
||
class and are not repurposed. The SOT forbids reusing the flat model for
|
||
the hierarchy; their eventual migration or retirement is REQ-MIG-001
|
||
work, 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 already means workspace-internal planning
|
||
entities, 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.
|
||
3. `platform_projects` — id, name, slug, `estate_id` NOT NULL →
|
||
`estates.id` ON DELETE RESTRICT. Exactly one estate per
|
||
platform-project.
|
||
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, each workspace resolves to exactly one
|
||
platform-project → estate → company chain (A1 §8.3 acceptance 1). There
|
||
are no parentage edge tables, no multi-parent forms, and no nullable
|
||
"detached" states.
|
||
6. **Slug scoping.** `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.
|
||
7. No hierarchy table carries a `metadata` jsonb column or any
|
||
free-form payload field. Parentage, naming, timestamps, and the audit
|
||
linkage of §5 — nothing else (A1 §8.1.2).
|
||
|
||
## 3. Grant attachment points
|
||
|
||
The grant vocabulary (which roles exist, what each permits, how inheritance
|
||
and revocation evaluate) is contract 2. This contract pins only the schema
|
||
shape contract 2 attaches to:
|
||
|
||
1. `hierarchy_grants` — id, subject (exactly one of `user_id` → `users.id`,
|
||
`team_id` → `teams.id`; CHECK-enforced exactly-one-of), target (exactly
|
||
one of `company_id`, `estate_id`, `platform_project_id`;
|
||
CHECK-enforced exactly-one-of), `role` (text; vocabulary owned by
|
||
contract 2), granted_by → `users.id`, created_at.
|
||
2. Uniqueness: at most one grant row per (subject, target, role).
|
||
3. Deleting a hierarchy record deletes its grants (the one permitted
|
||
cascade in this class — a grant on a deleted node is meaningless and
|
||
fail-open if retained). Deleting a user or team follows the platform
|
||
rule for principal deletion (identity contract §7; deletion is currently
|
||
gated).
|
||
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 is an audited UPDATE of exactly one parent FK on exactly one
|
||
hierarchy row: workspace → new platform-project, platform-project → new
|
||
estate, estate → new company. Nothing else 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 (the vocabulary for "authority" is contract 2; the
|
||
both-sides requirement is structural and binds here).
|
||
3. A transfer is a single-row, single-statement mutation; 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 (§3) — a "company owner" is a subject with the top role
|
||
on that company, not a column. This avoids the dual-write failure mode
|
||
of the legacy `teams.owner_id`/`teams.manager_id` columns, which encode
|
||
authority outside any evaluable 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. Every create, rename, transfer, grant change, and delete emits a
|
||
semantic audit event carrying actor, verb, target record, and (for
|
||
transfers) source and destination parents. Hierarchy audit events
|
||
reference hierarchy records; they are not workspace-scoped rows and do
|
||
not ride the workspace outbox (REQ-AUD-001 binds business rows; the
|
||
implementing PR defines the hierarchy audit store under the same
|
||
append-only rules).
|
||
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 reads (A1 §8.1.3) touch none of these tables' write paths and
|
||
are specified by contract 8; this contract only guarantees the chain
|
||
they aggregate over is unique and non-null.
|
||
|
||
## 6. Verification requirements
|
||
|
||
Binding on the implementing PRs (extends A1 §8.3):
|
||
|
||
1. Schema tests: chain uniqueness by construction (insert attempts with
|
||
null/duplicate parents fail); composite slug uniqueness per parent;
|
||
CHECK-enforced exactly-one-of on grant subject and target; grant
|
||
uniqueness per (subject, target, role).
|
||
2. 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 either side is refused.
|
||
3. Deletion tests: delete with children refused at the database level;
|
||
delete of a leaf cascades its grants and nothing else.
|
||
4. Authorization tests (with contract 2): a grant at each level yields
|
||
exactly the workspace permissions the chain implies; revocation up the
|
||
chain propagates (A1 §8.3 acceptance 2).
|
||
5. Negative tests: no business/orchestration table accepts a hierarchy
|
||
record id in any dependency/reference position; roll-up endpoints cannot
|
||
mutate hierarchy state; readers see aggregates only over workspaces they
|
||
are authorized on, with no cross-tenant existence oracles (A1 §8.3
|
||
acceptance 3).
|
||
6. Real-PostgreSQL coverage for the constraint-witness tests (unique/CHECK/
|
||
RESTRICT behavior), using the `ci-postgres` service in the `test` CI
|
||
step; mocked specs cannot witness database constraints.
|
||
|
||
## Ruling request
|
||
|
||
Ratify sections 1–6 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.
|