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.
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user