485 lines
29 KiB
Markdown
485 lines
29 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 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).
|
||
|
||
Revision 3 (terra re-review residuals): §4.3 transfer write inventory
|
||
reconciled with §5.2 — the transaction's writes are the single class-row
|
||
mutation plus that mutation's §5.2 audit writes (event + outbox record),
|
||
not "exactly two writes"; §6.3 extended with a closed writer-coverage
|
||
witness so an unregistered internal writer cannot pass a registered-route
|
||
inventory. (Terra's finding-8 residual — a stale contract 2 §7.8 backlink
|
||
to contract 1 §6.2 — was already fixed in contract 2 revision 2, which
|
||
cites §6.5; measured against `origin/contract/rbac-grants` head
|
||
`501112d2`.)
|
||
|
||
Revision 4 (terra r3 residual F7): the §6.3(b) writer-coverage assertion
|
||
extended to raw SQL — it now also fails on class-table name literals
|
||
inside SQL strings or tagged SQL templates outside the allowlist, so a
|
||
raw-SQL writer that touches no schema symbol is still caught.
|
||
|
||
Revision 5 (terra r4 residual F7): §6.3(b) gains a third prong — any
|
||
raw-SQL execution primitive outside the allowlist fails the assertion
|
||
regardless of its SQL content, closing the evasion where a
|
||
dynamically constructed table name carries neither a schema symbol nor
|
||
a class-table literal. The detection claim is now coextensive with
|
||
what the three prongs statically see.
|
||
|
||
Revision 6 (terra r5 residual F7 + new F8): the "two prongs" wording
|
||
corrected to three (F8); §6.3(b) gains the allowlist composition rules
|
||
(no generic raw-SQL helper is allowlisted; an allowlisted module may
|
||
not export caller-supplied-SQL execution) and fails outright on
|
||
runtime code-construction primitives; the detection claim is scoped
|
||
honestly to the stated syntactic forms, with evasions beyond static
|
||
reach assigned to §5.1 review/audit rather than claimed for CI.
|
||
|
||
Revision 7 (terra r6 new F9): the false-positive remedy no longer
|
||
contradicts the composition rules — legitimate non-hierarchy raw
|
||
execution (e.g. the db package's migration runner) is dispositioned
|
||
onto a second closed enumerated list, the infrastructure register,
|
||
exempt from prong (iii) only, still bound by prongs (i)/(ii), barred
|
||
from the writer allowlist, and importable only by registered modules
|
||
or the operational entry points.
|
||
|
||
Revision 8 (terra r7 residual F9): the register's import rule made
|
||
satisfiable by the live tree — imports are checked re-export-aware
|
||
(package barrels followed), and each registered module carries its own
|
||
closed importer enumeration, which may name operational entry points
|
||
such as the Gateway's startup migration hook; named importers stay
|
||
subject to prongs (i)/(ii) and gain no writer standing.
|
||
|
||
Revision 9 (terra r8 F10): revision 8 called the Gateway database
|
||
module the runner's "one live importer today". That was false — the
|
||
measured production importer set has four members. The enumeration
|
||
example now lists the complete measured set, and the import analysis
|
||
is extended to resolve literal dynamic `import()` routes, which two of
|
||
the four members use.
|
||
|
||
Amendment 1 (Ruling 4b, 2026-08-28): company visibility classes. The
|
||
directory exists so one shared company can serve many users instead of
|
||
each user creating a duplicate private company (Ruling 4b, webui-audit
|
||
lane, ruled 2026-08-27). §2.1 gains a `visibility` column; §2.8 defines
|
||
the two classes (`private`/`directory`), the directory's existence-only
|
||
disclosure, and the pre-binding invariants for the deferred
|
||
see-and-ask-to-join flow (no join-request surface is authorized here —
|
||
its flow is a follow-up contract); §5.2's mutation
|
||
enumeration gains the visibility change; §5.5 defines who may change
|
||
visibility (platform admins, plus a company-CRUD capability whose
|
||
definition is a follow-up amendment to contract 2 — until it ratifies,
|
||
admin-only); §6.1 and §6.9 add the witnesses; §6.7's existence-oracle
|
||
rule is scoped around the ratified directory carve-out. Top-level
|
||
creation (contract 3 §5.2) is unchanged and always yields a private
|
||
company. Upstream, SOT Amendment A2 (native-kanban-sot.md §9, this PR)
|
||
expressly extends A1 §8.1.2 to admit the visibility column and A1
|
||
§8.1.3 to admit the directory function — this contract relies on that
|
||
amendment, not on a reinterpretation of A1.
|
||
|
||
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, audit-linkage, and visibility-class
|
||
data only — never task, plan, or any business/orchestration payload.
|
||
Visibility (`companies.visibility`, §2.8) is admitted into that
|
||
enumeration by SOT Amendment A2 §9.1.1, which expressly extends A1
|
||
§8.1.2 for exactly this one column: it is disclosure data about the
|
||
class's own nodes — not a payload field, carries no business content,
|
||
and widens the payload prohibition for nothing else.
|
||
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),
|
||
`visibility` (text NOT NULL, DEFAULT `private`, CHECK constrained to
|
||
exactly `private` | `directory`; semantics §2.8), 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).
|
||
8. **Company visibility classes (Ruling 4b).** Every company is exactly
|
||
one of two classes, carried by `visibility`:
|
||
- `private` (the default): the company is disclosed only to subjects
|
||
holding a grant on it or on a descendant — the resting state every
|
||
company is created in. Open creation under contract 3 §5.2
|
||
(Ruling 4) survives unchanged: it creates private companies.
|
||
- `directory`: the company is listed in the deployment-wide company
|
||
directory. Directory listing discloses **existence, name, and slug
|
||
to every authenticated user — nothing else**: no subtree structure,
|
||
no roll-up aggregates, no workspace content, no grant or membership
|
||
information.
|
||
Visibility is disclosure, not authority. Content and structure access
|
||
to a directory-listed company still require explicit grants —
|
||
contract 2 §3.1 deny-by-default is unchanged, and the ownership model
|
||
(§4.4, contract 2 §4.3) is unchanged. Ruling 4b decision 5 wants a
|
||
see-and-ask-to-join flow for directory-listed companies. **This
|
||
contract authorizes no join-request runtime surface**: the flow in
|
||
its entirety — the ability to submit a request, its transport,
|
||
storage, and request lifecycle — is a follow-up contract, and until
|
||
that contract ratifies, the directory's only function is the
|
||
read-only listing above (A2 §9.1.2 admits nothing more). Two
|
||
invariants pre-bind that future contract now:
|
||
a join request confers no authority of any kind, and approval is
|
||
ordinary grant creation by an effective `owner` under contract 2 §4.1
|
||
— there is no other acceptance path.
|
||
|
||
## 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_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 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 mutates exactly one class-table row — the
|
||
single-row parent-FK update — and contains, beyond that, only the
|
||
§5.2 audit writes for that mutation (the audit event and its
|
||
hierarchy-outbox record, committing in the same transaction). No other
|
||
class, business, or orchestration row changes. 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 §§5–6. 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, visibility change (§5.5), 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).
|
||
5. **Visibility administration (Ruling 4b decisions 2–3).** Changing
|
||
`companies.visibility` is a hierarchy mutation through the §5.1
|
||
command path, audited per §5.2 (the event carries the old and new
|
||
visibility values as its semantic content). It is authorized for
|
||
exactly two actor classes: platform admins (`users.role = 'admin'`)
|
||
and subjects holding the company-CRUD capability that a follow-up
|
||
amendment to contract 2 will define — until that amendment ratifies,
|
||
the capability class is empty and the command is admin-only.
|
||
A company `owner` as such may NOT change visibility: standard users
|
||
cannot publish a company into the directory. This is the one
|
||
hierarchy mutation a platform admin performs without holding a
|
||
hierarchy grant, and it is ratified here as instance administration
|
||
(directory curation) in contract 2 §1.1's sense, not tenant access:
|
||
the command mutates the single `visibility` column, reads no tenant
|
||
content, and confers no grant — contract 2 §1.1's
|
||
no-implicit-tenant-access rule is otherwise untouched. Top-level
|
||
company creation (contract 3 §5.2) always creates
|
||
`visibility = 'private'`; the creation command cannot set or change
|
||
visibility.
|
||
|
||
## 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. Company
|
||
visibility (§2.8): a value outside `private`/`directory` refused with
|
||
both valid values accepted as the control; an insert omitting the
|
||
column defaults to `private`.
|
||
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: two witnesses, both required (§5.1). (a) Route
|
||
inventory: an assertion over the Gateway's registered hierarchy
|
||
routes/commands proving the registered mutation surface is exactly the
|
||
declared hierarchy command family — no generic CRUD endpoint. (b)
|
||
Writer coverage — the closed allowlist a route inventory cannot
|
||
provide: a static CI assertion over the Gateway and package sources
|
||
with three prongs, each bound to one explicitly enumerated allowlist
|
||
of hierarchy command/repository modules. (i) Symbol prong: write
|
||
references to the class-table schema symbols (insert, update, delete)
|
||
occur only in allowlisted modules. (ii) Literal prong: a class-table
|
||
name appearing inside a SQL string or tagged SQL template outside the
|
||
allowlist fails the assertion — this is what catches a raw-SQL writer
|
||
that references no schema symbol. (iii) Raw-execution prong: any call
|
||
to a raw-SQL execution primitive (the ORM's raw/unsafe constructors,
|
||
driver-level query/execute) outside the allowlist fails the
|
||
assertion, regardless of what the SQL string contains or how it is
|
||
constructed — the call site is statically detectable even when a
|
||
dynamically assembled table name is not, so a raw writer with a
|
||
runtime-built identifier is caught by its primitive, not its
|
||
payload. Two composition rules keep prong (iii) meaningful: the
|
||
allowlist names hierarchy command/repository modules only — a
|
||
generic raw-SQL helper or database-utility module is never
|
||
allowlisted; and an allowlisted module MUST NOT export a function
|
||
that executes caller-supplied SQL (such an export is itself a
|
||
raw-execution primitive, and the exporting module is treated as
|
||
unallowlisted for prong (iii) if it does). Legitimate raw execution
|
||
that is not a hierarchy writer — e.g. the migration runner in the
|
||
db package — lives on a second, separately enumerated
|
||
**infrastructure register**, distinct from the writer allowlist and
|
||
equally closed. A registered module is exempt from prong (iii) only:
|
||
prongs (i) and (ii) apply to it with no exemption, so it can hold no
|
||
class-table schema symbol or class-table SQL literal, and it can
|
||
never appear on the writer allowlist. To close the laundering path,
|
||
the same assertion checks imports, and the import analysis is
|
||
**re-export-aware**: it follows package barrels and re-exports, so a
|
||
route hidden behind an index module is still a route — and it
|
||
resolves literal dynamic imports the same way: an
|
||
`await import('<literal specifier>')` is an import edge like any
|
||
static import, not an evasion of the analysis (a dynamic import of
|
||
the db package whose specifier is not a literal fails the assertion
|
||
outright, because it makes the import graph unanalyzable). A
|
||
registered module may be imported only by other registered modules
|
||
or by importers named on that module's own closed importer
|
||
enumeration in the register — operational entry points such as the
|
||
migration/bootstrap CLI or the Gateway's startup migration hook.
|
||
The enumeration names the complete permitted production consumer
|
||
set, and completeness is measured, not asserted: the migration
|
||
runner's measured production importer set today has four members —
|
||
the Gateway database module (reached through the db package
|
||
barrel), the storage package's Postgres adapter, and two mosaic CLI
|
||
commands, the fleet-backlog command and the gateway verify command,
|
||
both routed through literal dynamic imports of the db package — so
|
||
its enumeration names those four. A module that only receives the
|
||
runner's functions by parameter injection (the gateway schema-check
|
||
module takes them as arguments from the verify command) has no
|
||
import edge of its own and is not enumerated. Any import route
|
||
outside the enumeration fails the assertion. Being a
|
||
named importer confers nothing else: the importer stays fully
|
||
subject to prongs (i) and (ii), gains no writer-allowlist standing,
|
||
and whether it uses the registered module beyond its operational
|
||
purpose is a §5.1 review question, not a static claim. Runtime code-construction
|
||
primitives (`eval`, `new Function`) anywhere in the scanned sources
|
||
fail the assertion outright, allowlist or not. Schema definitions
|
||
and generated migrations are excluded from the literal prong; a
|
||
false positive is resolved in the same PR by adding the module to
|
||
the one enumerated list its role permits — the writer allowlist for
|
||
a hierarchy command/repository module, the infrastructure register
|
||
for non-hierarchy raw execution — never by weakening the assertion,
|
||
and neither list may take a module the composition rules bar from
|
||
it. Both lists are closed, and the assertion's detection
|
||
claim is exactly its prongs: it statically surfaces every writer
|
||
expressed as a schema-symbol reference, a class-table SQL literal, a
|
||
raw-execution call site, or runtime code construction. An evasion
|
||
engineered outside those syntactic forms is a §5.1 violation that
|
||
review and audit own — the witness does not claim to catch what
|
||
static analysis cannot see, and any such evasion found later is
|
||
corrected as a conformance defect, not grandfathered.
|
||
4. Audit witnesses: for each mutation class (create, rename, transfer,
|
||
visibility change, grant create/change/revoke, delete) — the event
|
||
exists after commit
|
||
with actor/verb/target and same-transaction atomicity, and the
|
||
event's outbox record exists after the same commit — state row,
|
||
audit event, and outbox record are witnessed as one transaction
|
||
(REQ-AUD-001); a rolled-back
|
||
mutation leaves no event, no outbox record, AND no state effect —
|
||
a rolled-back create leaves no row, a rolled-back rename, transfer,
|
||
or visibility change leaves the prior values in place, and a
|
||
rolled-back delete or grant revoke leaves the row present
|
||
(rollback witness on all three legs, per REQ-AUD-001's
|
||
commit-or-roll-back-together acceptance); 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, as narrowed by
|
||
A2 §9.1.2) beyond the one
|
||
ratified carve-out — the §2.8 company directory, witnessed in §6.9.
|
||
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.
|
||
9. Visibility witnesses (§2.8, §5.5): the directory read returns exactly
|
||
the `visibility = 'directory'` companies to any authenticated user,
|
||
disclosing existence, name, and slug only (closed-field assertion on
|
||
the response shape); a private company never appears in the directory
|
||
for a reader without a grant on it (with the control: it appears in
|
||
that reader's granted-structure reads); a directory-listed company's
|
||
subtree, aggregates, and content remain refused for a non-granted
|
||
reader (disclosure ≠ authority); the visibility command is refused
|
||
for a non-admin actor — including an effective `owner` of the target
|
||
company — with the platform-admin accept control; top-level creation
|
||
yields `visibility = 'private'` and accepts no visibility argument;
|
||
each visibility change emits its §5.2 audit event carrying old and
|
||
new values — the full audit pattern for the mutation class
|
||
(same-transaction atomicity of state row, audit event, and outbox
|
||
record; rollback leaving no state effect, no event, and no outbox
|
||
record; actor/verb/target) is §6.4's, which enumerates
|
||
visibility change; this item adds only the old/new-value payload
|
||
assertion.
|
||
|
||
## 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.
|