Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3231b91691 | ||
|
|
be32b693d0 |
@@ -1,380 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
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, and audit-linkage data only — never
|
|
||||||
task, plan, or any business/orchestration payload.
|
|
||||||
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),
|
|
||||||
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).
|
|
||||||
|
|
||||||
## 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, 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).
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
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,
|
|
||||||
grant create/change/revoke, delete) — the event exists after commit
|
|
||||||
with actor/verb/target and same-transaction atomicity; a rolled-back
|
|
||||||
mutation leaves no event (rollback witness); 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).
|
|
||||||
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.
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
# Deployment Mode and Conversion Contract (D3)
|
||||||
|
|
||||||
|
Status: DRAFT — awaiting ratification (webui-audit S2, contract 6 of 9).
|
||||||
|
Authority: PRD D3 (Part I §3) — two modes chosen at install time,
|
||||||
|
Standalone and Enterprise, with the mode table (brains, user-data
|
||||||
|
isolation, secrets, conversion); Standalone → Enterprise conversion is
|
||||||
|
**one-way** and Enterprise is a **terminal state**. PRD D14 (Part I §7)
|
||||||
|
— the per-user brain split is optional in Standalone and keeping it is
|
||||||
|
the recommended default because it preserves forward-compatibility with
|
||||||
|
the one-way conversion. PRD D11 (Part I §9) — v1 ships the Standalone
|
||||||
|
flow only; Enterprise conversion is explicitly deferred. PRD D3
|
||||||
|
federation clause — federation is intentionally not fully designed,
|
||||||
|
deferred, and nothing in v1 may foreclose it.
|
||||||
|
|
||||||
|
Revision 2 (luna review F1–F7): the identity precondition restated in
|
||||||
|
identity-contract terms with a conversion-local acknowledgment record
|
||||||
|
this contract owns (F1); a durable, keyed preparation state with a
|
||||||
|
Standalone-safe representation rule, an in-transaction re-check fence,
|
||||||
|
and an exact flip boundary (F2); the §5.4 unknown-value rule stated
|
||||||
|
directly without the contradictory non-exhaustiveness clause (F3); the
|
||||||
|
D14 boundary bound here with a stable column-allowlist witness instead
|
||||||
|
of delegated to an unratified layout (F4); the conversion witness
|
||||||
|
matrix extended to every §4.2/§4.4 condition (F5); the mode-record
|
||||||
|
writer coverage imported concretely from contract 1 §6.3 with a named
|
||||||
|
schema, closed writer set, crafted-write probe, and mode-resolution
|
||||||
|
assertion (F6); the mode read command flagged as a §12.1 drafting
|
||||||
|
addition rather than a D8 mandate (F7). Ownership language aligned
|
||||||
|
with contract 3 revision 2: mode is recorded at bootstrap and read by
|
||||||
|
the wizard as input.
|
||||||
|
|
||||||
|
This contract binds the mode as a canonical platform property (§2), the
|
||||||
|
per-mode obligations and which contract owns each (§3), the conversion
|
||||||
|
transition (§4), the v1 non-foreclosure obligations (§5), and their
|
||||||
|
witnesses (§6). Domain semantics stay with their owning contracts:
|
||||||
|
wizard branching (contract 3 §2), identity/SSO
|
||||||
|
(`identity-lifecycle.md`), custody and per-user brain mechanics
|
||||||
|
(contract 7, `custody-schema.md`), tool mapping
|
||||||
|
(`tool-gateway-mapping.md`).
|
||||||
|
|
||||||
|
## 1. Definitions
|
||||||
|
|
||||||
|
1. **Mode**: the platform-wide deployment mode, exactly one of
|
||||||
|
`standalone` or `enterprise`. The vocabulary is closed in v1;
|
||||||
|
extension (e.g. a federation mode) is by amendment to this contract,
|
||||||
|
never ad hoc.
|
||||||
|
2. **Conversion**: the one-way transition `standalone → enterprise`.
|
||||||
|
No other mode transition exists.
|
||||||
|
3. **Conversion preconditions**: the verifiable conditions of §4.2 that
|
||||||
|
must all hold before the mode record may change.
|
||||||
|
4. **Preparation unit**: one re-runnable piece of pre-conversion work —
|
||||||
|
the migration of one secret to the Vault backend, or the partition
|
||||||
|
of one user's brain content (§4.3).
|
||||||
|
|
||||||
|
## 2. Mode is a canonical recorded property
|
||||||
|
|
||||||
|
1. Mode is recorded canonically in the platform database at bootstrap
|
||||||
|
as the operator's install-time choice (D3: modes are "chosen at
|
||||||
|
install time"). The record is a single-row keyed record
|
||||||
|
(`platform_mode`: mode value, recorded-at timestamp, bootstrap epoch
|
||||||
|
reference); this contract owns it, the bootstrap writer performs the
|
||||||
|
one v1 write (§6.2), and the wizard reads it as input (contract 3
|
||||||
|
§2.3). Mode is never derived from feature state (presence of Vault,
|
||||||
|
count of brains, count of users), and no component may infer a
|
||||||
|
different mode than the record states.
|
||||||
|
2. The record is readable by any authenticated user through a Gateway
|
||||||
|
command with CLI exposure. This read command is a **drafting
|
||||||
|
addition** ratified with this contract (PRD §12.1), not a D8
|
||||||
|
mandate: D8 binds only that any surface exposing the value goes
|
||||||
|
through official tooling. When a webUI surface consumes the read, a
|
||||||
|
mapping row is added to `tool-gateway-mapping.md` by amendment —
|
||||||
|
the same route §4.4 already binds for the conversion command.
|
||||||
|
Components branch on the read value only.
|
||||||
|
3. The record is immutable except by the §4 conversion transition.
|
||||||
|
Editing it by direct database access, config file, environment
|
||||||
|
variable, or wizard re-run is non-conformant (contract 3 §2.3:
|
||||||
|
changing mode later is conversion, not a wizard re-run).
|
||||||
|
|
||||||
|
## 3. Per-mode obligations (owner map)
|
||||||
|
|
||||||
|
The PRD mode table binds four rows; this contract assigns each an
|
||||||
|
owning contract so no obligation is unowned and none is bound twice:
|
||||||
|
|
||||||
|
| Obligation | Standalone | Enterprise | Owner |
|
||||||
|
| ------------------- | -------------------------------------- | -------------------------------------------------- | --------------------------------------------- |
|
||||||
|
| Brains | one mosaic-brain (system + user files) | system brain for config + one brain per user | contract 7 (custody/brain mechanics) |
|
||||||
|
| User-data isolation | single user | no user-data leakage between users; sharing opt-in | contract 7 (enforced by architecture, D14) |
|
||||||
|
| Secrets | OpenBao/Vault or flat files | OpenBao/Vault REQUIRED | this contract (§4.2 gate; steady-state check) |
|
||||||
|
| Conversion | may convert to Enterprise, one-way | terminal state | this contract (§4) |
|
||||||
|
|
||||||
|
The Standalone brains row states the default layout, not the only
|
||||||
|
valid one: the D14 per-user split is a MAY in Standalone with keeping
|
||||||
|
it the recommended default (PRD §7, contract 7 §6), and Vault-backed
|
||||||
|
secrets are equally valid Standalone configuration. Both prepared
|
||||||
|
states are therefore themselves valid Standalone states — the fact
|
||||||
|
§4.3 relies on.
|
||||||
|
|
||||||
|
In Enterprise steady state, a flat-file secrets backend is
|
||||||
|
non-conformant; the platform refuses to start Enterprise-mode
|
||||||
|
components against a flat-file secrets configuration (fail-closed, not
|
||||||
|
warn-and-run).
|
||||||
|
|
||||||
|
## 4. Conversion transition
|
||||||
|
|
||||||
|
1. **Direction and terminality.** The only transition is
|
||||||
|
`standalone → enterprise`. `enterprise → standalone` does not exist:
|
||||||
|
there is no command, no admin override, and no support path. An
|
||||||
|
attempt is refused with the precondition/state error class of the
|
||||||
|
command envelope (`tool-gateway-mapping.md` §4.2).
|
||||||
|
2. **Preconditions (all verified before the record changes):**
|
||||||
|
- Secrets: OpenBao/Vault is configured and reachable, and every
|
||||||
|
required secret is served from the Vault backend — none from a
|
||||||
|
flat-file backend. Secret migration completes before conversion;
|
||||||
|
this contract does not define the migration tooling, only the
|
||||||
|
gate.
|
||||||
|
- Brains: the per-user brain split required by the Enterprise row of
|
||||||
|
§3 is established for **every** existing user (or the deployment
|
||||||
|
already kept the split, the D14 recommended default). Brain
|
||||||
|
partitioning mechanics are contract 7; this contract binds only
|
||||||
|
that the split is complete before the mode flips.
|
||||||
|
- Identity: at least one platform administrator account exists that
|
||||||
|
is active in identity-contract terms — authenticated capability,
|
||||||
|
not banned, not deactivated (identity §2, §5). And the conversion
|
||||||
|
request carries a **configuration acknowledgment**: the current
|
||||||
|
canonical values of registration mode and per-provider JIT
|
||||||
|
enablement (identity §2.2, §4.1), echoed back in the request. A
|
||||||
|
mismatch between the echoed values and the canonical values at
|
||||||
|
verification refuses the conversion. This acknowledgment record
|
||||||
|
is conversion-local, owned by this contract, and stored with the
|
||||||
|
§4.4 audit event as the precondition evidence; it adds no
|
||||||
|
identity-contract obligation and no mode-specific identity
|
||||||
|
default — identity's own defaults remain valid states.
|
||||||
|
3. **Preparation state and the flip boundary.** Preparatory work is
|
||||||
|
tracked durably: each preparation unit (§1.4) records its
|
||||||
|
completion in a preparation table keyed by (bootstrap epoch, unit
|
||||||
|
identity — the secret's path, the user's id), written in the same
|
||||||
|
transaction as the unit's own effect where the unit's backend
|
||||||
|
allows it, and reconciled from the backend's actual state where it
|
||||||
|
does not (a secret already served by Vault, a brain already split,
|
||||||
|
is complete regardless of the table). Units are at-most-once per
|
||||||
|
key and re-runnable across attempts. **Standalone-safe
|
||||||
|
representation:** every preparation unit moves the deployment into
|
||||||
|
a state that is itself valid Standalone configuration (§3 note), so
|
||||||
|
an interrupted preparation leaves a fully operational Standalone
|
||||||
|
deployment reading its state through the ordinary contracts — no
|
||||||
|
rollback, fencing, or special Standalone read path is needed, and
|
||||||
|
no component behavior may key on "preparation in progress".
|
||||||
|
**The flip:** one transaction that (a) locks the mode record, (b)
|
||||||
|
re-verifies every §4.2 precondition after acquiring the lock, and
|
||||||
|
(c) writes the mode record and the §4.4 audit event. Any re-check
|
||||||
|
failure aborts with no write. External state that changes after the
|
||||||
|
re-check but before commit is bounded by the transaction window;
|
||||||
|
an external backend (Vault) failing after conversion is an
|
||||||
|
Enterprise runtime fault handled by §3's fail-closed steady-state
|
||||||
|
rule, not a conversion defect. An interrupted or failed conversion
|
||||||
|
leaves the record `standalone` and the platform fully operational;
|
||||||
|
there is no intermediate mode and no half-converted state
|
||||||
|
observable through the record.
|
||||||
|
4. **Authority and audit.** Conversion is a platform-administrator
|
||||||
|
command carrying an explicit irreversibility acknowledgment in its
|
||||||
|
request (distinct from the §4.2 configuration acknowledgment). It
|
||||||
|
is an official Gateway/CLI command (D8): when built, it is added to
|
||||||
|
the tool↔Gateway mapping by amendment (`tool-gateway-mapping.md`
|
||||||
|
§3.3). The transition emits an audit event (actor, prior mode, new
|
||||||
|
mode, precondition evidence reference including the configuration
|
||||||
|
acknowledgment) in the same transaction as the record change; the
|
||||||
|
event survives indefinitely. A refused attempt emits a refusal
|
||||||
|
event naming the failed precondition class and actor, with no
|
||||||
|
mode-change event.
|
||||||
|
|
||||||
|
## 5. v1 obligations (non-foreclosure)
|
||||||
|
|
||||||
|
v1 ships Standalone only (D11); the conversion command is deferred
|
||||||
|
work. v1 still MUST:
|
||||||
|
|
||||||
|
1. Record the mode per §2 at bootstrap, with `enterprise` a reserved,
|
||||||
|
refused value for bootstrap — v1 bootstrap accepts `standalone`
|
||||||
|
only. The wizard reads the record (contract 3 §2.3); nothing in v1
|
||||||
|
writes it after bootstrap.
|
||||||
|
2. Keep the §2.3 immutability rule: no v1 surface mutates the mode
|
||||||
|
record.
|
||||||
|
3. Not foreclose conversion: the v1 platform database holds no
|
||||||
|
sensitive user content — sensitive categories live in the owning
|
||||||
|
user's brain, and postgres holds structure, consent records, and
|
||||||
|
pointers only (the D14 boundary, PRD §7). Custody mechanics are
|
||||||
|
contract 7's; this contract binds the boundary itself here so v1
|
||||||
|
cannot ship a layout that makes the §4.2 brain precondition
|
||||||
|
unsatisfiable, and §6.3 gives it a stable witness that does not
|
||||||
|
depend on contract 7's internals. Conversion implementation
|
||||||
|
additionally requires contract 7 ratified.
|
||||||
|
4. Not foreclose federation: v1 components accept exactly the two §1.1
|
||||||
|
values wherever a mode value is parsed and refuse any other value
|
||||||
|
**before side effects** — a refused configuration, not undefined
|
||||||
|
behavior and not a crash mid-operation. Forward compatibility lives
|
||||||
|
in storage and architecture, not in parser speculation: the mode
|
||||||
|
record's storage is not structurally locked to two values (no
|
||||||
|
database-level two-value enum), and any future value (e.g. a
|
||||||
|
federation mode) is defined by a versioned amendment to this
|
||||||
|
contract before any component accepts it. The PRD defers
|
||||||
|
federation's shape entirely; this contract does not presume it
|
||||||
|
arrives as a third mode value.
|
||||||
|
|
||||||
|
## 6. Verification requirements
|
||||||
|
|
||||||
|
Binding on the implementing PRs:
|
||||||
|
|
||||||
|
1. **Mode-record witness (v1):** after bootstrap the mode is readable
|
||||||
|
via the Gateway command and CLI and equals the bootstrap-recorded
|
||||||
|
choice; bootstrap with mode `enterprise` is refused; bootstrap with
|
||||||
|
any unknown mode value is refused before side effects (§5.4).
|
||||||
|
2. **Writer-coverage witness (v1):** the mode record's writer set is
|
||||||
|
closed by the same three-prong static assertion contract 1 §6.3(b)
|
||||||
|
defines — symbol, class-table literal, and raw-execution prongs
|
||||||
|
with its allowlist composition rules — scoped to the
|
||||||
|
`platform_mode` table, with a writer allowlist containing exactly
|
||||||
|
the bootstrap writer in v1 (and exactly plus the conversion command
|
||||||
|
at the conversion milestone). Companions: a crafted direct write
|
||||||
|
attempted in a test fails and leaves the record unchanged; a
|
||||||
|
mode-resolution assertion that no shipped component derives mode
|
||||||
|
from feature state (mode reads occur only through the §2.2 read
|
||||||
|
surface — static assertion over Gateway, CLI, bootstrap, and
|
||||||
|
repository sources).
|
||||||
|
3. **D14-boundary witness (v1):** a column-allowlist assertion in the
|
||||||
|
style of contract 1 §6.2 that the platform database schema contains
|
||||||
|
no sensitive-content column — the §5.3 boundary — stable regardless
|
||||||
|
of contract 7's internals (contract 7 §7 carries the full custody
|
||||||
|
witnesses).
|
||||||
|
4. **No-downgrade witness (conversion milestone):** with mode
|
||||||
|
`enterprise`, a conversion request to `standalone` (and any crafted
|
||||||
|
mode-write) is refused with the precondition/state error class and
|
||||||
|
no record change.
|
||||||
|
5. **Precondition witnesses (conversion milestone),** each refused
|
||||||
|
with no record change and no partial mode effect, parameterized
|
||||||
|
over both OpenBao and Vault where secrets are involved:
|
||||||
|
(a) secrets backend unreachable; (b) one required secret still
|
||||||
|
flat-file backed (migration incomplete); (c) one unpartitioned user
|
||||||
|
brain in a **multi-user** deployment where every other user is
|
||||||
|
partitioned; (d) no active platform administrator (the only admin
|
||||||
|
banned or deactivated); (e) configuration acknowledgment missing or
|
||||||
|
mismatching the canonical registration/JIT values; (f) actor not a
|
||||||
|
platform administrator (authorization refusal); (g) irreversibility
|
||||||
|
acknowledgment absent. And the steady-state rule: an
|
||||||
|
Enterprise-mode component started against a flat-file secrets
|
||||||
|
configuration refuses to start (§3).
|
||||||
|
6. **Interruption and fence witnesses (conversion milestone):** fault
|
||||||
|
injection aborting conversion after each preparation unit and
|
||||||
|
between preparation and flip leaves the record `standalone` and the
|
||||||
|
platform operational in Standalone semantics (§4.3
|
||||||
|
Standalone-safety), and a re-attempt completes without duplicating
|
||||||
|
prepared state (at-most-once keys); a precondition invalidated
|
||||||
|
after preparation but before the flip (a secret reverted to
|
||||||
|
flat-file) is caught by the in-transaction re-check and refused.
|
||||||
|
7. **Audit witnesses (conversion milestone):** a completed conversion
|
||||||
|
has exactly one mode-change audit event, same-transaction with the
|
||||||
|
record change (transaction linkage asserted), carrying actor, prior
|
||||||
|
mode, new mode, and the precondition evidence reference including
|
||||||
|
the configuration acknowledgment; a failed attempt has a refusal
|
||||||
|
event naming the failed precondition class and no mode-change
|
||||||
|
event; the mode-change event remains queryable after subsequent
|
||||||
|
unrelated audit activity (retention probe).
|
||||||
|
8. **Mapping witness (conversion milestone):** the conversion command
|
||||||
|
and the mode read command each have their
|
||||||
|
`tool-gateway-mapping.md` row (added by amendment per §2.2/§4.4)
|
||||||
|
before the commands ship.
|
||||||
|
|
||||||
|
## Ruling request
|
||||||
|
|
||||||
|
Ratify sections 1–6 as written, with one decision embedded:
|
||||||
|
|
||||||
|
- Decision (§5): v1 implements the **mode record and its immutability
|
||||||
|
only** — bootstrap records `standalone`, the `enterprise` value is
|
||||||
|
reserved and refused, and the conversion command itself is deferred
|
||||||
|
to the Enterprise milestone, consistent with D11's deferred list.
|
||||||
|
v1 carries three obligations beyond the record: the closed writer
|
||||||
|
assertion, the D14 column boundary, and the unknown-value refusal
|
||||||
|
(§6.1–§6.3) — these are the non-foreclosure floor, not hidden
|
||||||
|
conversion work. Alternative if rejected: build the conversion
|
||||||
|
command inside v1 — rejected because D11 scopes v1 to the Standalone
|
||||||
|
slice and conversion depends on contract 7 custody mechanics that
|
||||||
|
are themselves not in the v1 slice.
|
||||||
Reference in New Issue
Block a user