# 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.
