From 4e643e0d0f1b56964075fd80f6d9a958c4e8e8aa Mon Sep 17 00:00:00 2001 From: fred Date: Fri, 28 Aug 2026 14:16:17 -0500 Subject: [PATCH] docs: company visibility classes (Ruling 4b amendment, contracts 1+3) --- docs/requirements/hierarchy-schema.md | 83 ++++++++++++++++++++++++-- docs/requirements/onboarding-wizard.md | 17 +++++- 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/docs/requirements/hierarchy-schema.md b/docs/requirements/hierarchy-schema.md index 5287133d..f66240c4 100644 --- a/docs/requirements/hierarchy-schema.md +++ b/docs/requirements/hierarchy-schema.md @@ -72,6 +72,20 @@ 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 see-and-ask-to-join invariants; §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. + 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 @@ -116,7 +130,9 @@ 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). + `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. @@ -142,6 +158,28 @@ decision under A1; this contract pins only that they stay distinct tables.) 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. Ruling 4 §5.2 open creation 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. The directory supports a + see-and-ask-to-join flow (Ruling 4b decision 5): an authenticated + user may request to join a directory-listed company. Two invariants + bind any implementation of that flow now, ahead of its own contract: + 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. The flow's transport, storage, + and request lifecycle are a follow-up contract. ## 3. Grant attachment points @@ -223,7 +261,8 @@ shape contract 2 attaches to: 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, grant create/change/revoke, and + - 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, @@ -248,6 +287,25 @@ shape contract 2 attaches to: 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 @@ -264,7 +322,10 @@ Binding on the implementing PRs (extends A1 §8.3): 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. + `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). @@ -368,10 +429,24 @@ Binding on the implementing PRs (extends A1 §8.3): 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). + cross-tenant existence oracles (A1 §8.3 acceptance 3) 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. ## Ruling request diff --git a/docs/requirements/onboarding-wizard.md b/docs/requirements/onboarding-wizard.md index 67153aa2..2ec0fd39 100644 --- a/docs/requirements/onboarding-wizard.md +++ b/docs/requirements/onboarding-wizard.md @@ -397,6 +397,14 @@ seed-workspace-scoped mutant, correctly refusing outside the seed set, passes branch (c), so the two branches detect distinct mutants. No other change. +Amendment 1 (Ruling 4b, 2026-08-28): §5.2's embedded decision was RULED +AGREED (Jason, 2026-08-27), and Ruling 4b adds company visibility +classes (hierarchy schema §2.8): open top-level creation always yields +a **private** company; publishing a company into the deployment-wide +directory is a separate, gated visibility mutation (hierarchy schema +§5.5) that is never part of the creation command. §5.2 is amended to +state both. + Scope: the Gateway-backed product onboarding wizard. Out of scope: the host-local install wizard (`mosaic wizard`, which drives host install and gateway bootstrap and is not this artifact — audit REPORT.md layer 3); @@ -1169,15 +1177,18 @@ collects no sensitive category, so v1 ships no custody surface. party, service actor, or wizard-privileged writer exists in this flow. 2. **Post-bootstrap top-level company creation** — the "N companies" flow - — is decided by the ruling below: any **eligible platform user** MAY + — RULED AGREED (Jason, 2026-08-27): any **eligible platform user** MAY create a top-level company and MUST name an initial `owner` grant in the same audited operation (contract 2 §4.3); the creator naming themselves is the default. Eligible means, in identity-contract terms: an authenticated account (identity §2) that is not banned (identity §7.1 — deactivation on this platform IS the better-auth ban; no separate deactivated state exists). No further role or grant - is required. Until that ruling, deny-by-default holds (contract 2 - §3.1): no implicit creation authority exists. + is required. Creation always yields a **private** company + (`visibility = 'private'`, hierarchy schema §2.8, Ruling 4b): the + creation command accepts no visibility argument, and publishing into + the deployment-wide directory is a separate, gated mutation + (hierarchy schema §5.5) that standard users cannot perform. 3. Child-node creation inside the wizard (estate, project, workspace under the seeded company) follows contract 2 §4.3: parent `owner` authority, no automatic grant needed — for canonical seed