diff --git a/docs/requirements/rollup-projection.md b/docs/requirements/rollup-projection.md index e35761d4..351c0047 100644 --- a/docs/requirements/rollup-projection.md +++ b/docs/requirements/rollup-projection.md @@ -16,6 +16,23 @@ read-only roll-up query over only readable workspaces, at every hierarchy level, as its own non-mutating query tool, dependent on ranks 1–3. +Revision 2 (terra review F1–F7): membership-only readability is now +workspace-local — it contributes at the workspace node only and never +promotes ancestor visibility; upward aggregation requires an effective +chain role, and §1 defines direct vs effective grants in contract 2's +terms (F1). The no-oracle rule gains a defined equivalence predicate +(normalized byte equality with an enumerated volatile-field set) and a +partial-scope hidden-sibling witness (F2). §2.5 enumerates the closed +semantic result and denial schemas field-by-field, including the +explicit-zero representation (F3). Cache invalidation, when a cache +exists, is witnessed per invalidator class (F4). Non-authoritative and +never-gate rules gain an import-graph/data-flow witness, and the +mutation check is aligned to contract 1 §6.7's both-table zero-write +assertion (F5). The fixture gains a second estate with distinct counts +and explicit company-, estate-, project-grant, and membership cases +(F6). The §5.3 legacy-row exclusion and pre-rank no-obligation rules +are disclosed as drafting additions (F7). + This contract binds the projection semantics (§2), reader authorization semantics (§3), read-only enforcement (§4), dependencies and phase timing (§5), witnesses (§6), and disclosed drafting additions (§7). It @@ -30,16 +47,26 @@ row with contract 5 (`tool-gateway-mapping.md`). 1. **Roll-up**: the read-only projection of per-workspace task counts by status, aggregated up the contract 1 parent chain (workspace → platform-project → estate → company). -2. **Readable workspace** (for a given reader): a workspace where the - reader's authorization permits reading task state — either - workspace membership under the SOT's own membership rules - (REQ-ID-001), or an effective chain role at that workspace per - contract 2 §3 evaluation. The role vocabulary is contract 2 §2's; - this contract adds no role and no new authority source. -3. **Aggregation scope** (of a hierarchy node, for a given reader): - the set of readable workspaces in that node's descendant subtree - (for a workspace node, the workspace itself if readable). -4. **Projection**: a generated, non-authoritative view in the sense of +2. **Effective chain role** (at a node, for a reader): the role + contract 2 §3 evaluation yields at that node — from a grant on the + node itself (a **direct grant**) or from a grant on an ancestor + whose domain covers it (an **inherited grant**, contract 2 §3.2). + The role vocabulary is contract 2 §2's; this contract adds no role + and no new authority source. +3. **Chain-readable workspace** (for a reader): a workspace where the + reader's effective chain role permits reading task state. +4. **Member-readable workspace** (for a reader): a workspace readable + only through workspace membership under the SOT's own membership + rules (REQ-ID-001), with no effective chain role. Membership + confers workspace-local semantics only (contract 2 §3.1, §7.4): it + never contributes authority, visibility, or aggregation upward. +5. **Aggregation scope** (of a hierarchy node, for a reader): the set + of chain-readable workspaces in that node's descendant subtree; + plus, when the node is itself a workspace, that workspace if it is + chain-readable or member-readable. A member-readable workspace + therefore contributes to exactly one node's aggregation scope: its + own. +6. **Projection**: a generated, non-authoritative view in the sense of `native-kanban-sot.md` §3 invariant 5 — derived from SOT rows, never an import source, never authoritative. @@ -60,35 +87,68 @@ row with contract 5 (`tool-gateway-mapping.md`). aggregation — metrics, trends, forecasting, scoring, velocity, cross-workspace derived analytics, dashboards computed across workspaces — remains a `native-kanban-sot.md` §6 non-goal - (§8.2.4). The response schema is closed (§6.7 witness): adding any - field beyond identifiers, names of visible nodes, and status - counts is an amendment to this contract. + (§8.2.4). The response schema is closed (§2.5; §6.7 witness): + adding any field is an amendment to this contract. 4. **Non-authoritative.** No consumer may treat roll-up output as a source of record; it is recomputable at any time from SOT rows and is never imported, persisted as authoritative state, or used to - gate work. + gate or deny work (witness §6.8 — both the write-path and the + decision-path prohibitions are witnessed). +5. **Closed semantic schema.** The successful result consists of + exactly these semantic fields, and no others: + - `node`: the queried node — exactly `id`, `type` (one of the four + contract 1 levels), `name`. + - `totals`: one entry per status value of the typed lifecycle — + every status key present, a count of zero represented explicitly + as `0`, never by key absence. + - `children`: the reader-visible (§3.2) child nodes of `node`, + each carrying exactly the same `id`/`type`/`name`/`totals` + shape, recursively down to the workspaces in the reader's + aggregation scope. A node outside §3.2 visibility never appears + at any depth. + There are no optional result fields. The denial/nonexistent + response is the contract 5 §4.2 not-found-class error envelope + with no fields beyond that envelope. The wire DTO is expressed + under contract 5 §4.1, and MUST be a faithful serialization of + exactly this semantic field set: a wire field with no + corresponding semantic field above is a conformance defect. ## 3. Reader authorization semantics 1. **Scope rule.** A reader's roll-up over any node aggregates ONLY - the reader's aggregation scope (§1.3). An unreadable workspace + the reader's aggregation scope (§1.5). An unreadable workspace contributes nothing to any total — not a count, not a row, not a - presence marker. + presence marker. A member-readable workspace contributes only at + the workspace node itself (§1.4–§1.5): querying it directly + succeeds; it never appears in, and never adds to, any ancestor's + response for that reader. 2. **Node visibility.** A node appears in a roll-up response iff the reader's aggregation scope at that node is non-empty, or the - reader holds a grant on the node itself (contract 2 §3.2 makes the - node and its subtree the grant's domain). Per the ruling below, a - granted node whose aggregation scope is empty appears with zero - counts; a node the reader has no authority on does not appear at - all. + reader holds an effective chain role at the node (§1.2 — direct or + inherited; contract 2 §3.2 makes a grant's domain the node and its + subtree, so an ancestor grant makes empty descendants visible per + the ruling). Per the ruling below, a node with an effective chain + role but an empty aggregation scope appears with zero counts. + Workspace membership alone never makes any non-workspace node + visible. A node where the reader has neither an effective chain + role nor a non-empty aggregation scope does not appear at all. 3. **No existence oracle.** The response MUST NOT disclose the existence, count, name, or any property of unreadable workspaces or of nodes outside §3.2 visibility — no "N workspaces hidden" fields, no total-vs-visible discrepancy fields. A query naming a - node outside §3.2 visibility returns the same response as a query - naming a nonexistent node (fail closed, `rbac-grant-model.md` §3.5 - pattern: a decision path that cannot read grant state denies). -4. **Live evaluation.** Readability is evaluated per contract 2 §3.5 + node outside §3.2 visibility MUST satisfy the §3.4 response + equivalence with a query naming a nonexistent node (fail closed, + `rbac-grant-model.md` §3.5 pattern: a decision path that cannot + read grant state denies). +4. **Response equivalence predicate.** Two responses are equivalent + when they carry the identical HTTP status, the identical contract + 5 §4.2 error code, and byte-identical bodies after normalizing + exactly the declared volatile envelope fields — correlation id and + response timestamp, and nothing else. The implementing PR declares + that volatile-field list in the witness; any additional + normalization is a conformance defect. This is contract 5 §4.2's + same code/status/shape rule made executable. +5. **Live evaluation.** Readability is evaluated per contract 2 §3.5 (live rows or transactionally-invalidated cache). Revocation propagates per contract 2 §6: the next roll-up query decided after the revoking transaction commits excludes the revoked scope. @@ -106,8 +166,8 @@ row with contract 5 (`tool-gateway-mapping.md`). 3. **Freshness.** v1 computes the roll-up live from SOT rows at query time. A cache is an implementation option only if it is invalidated in the same transaction as any task, hierarchy, grant, - or membership mutation that affects it, and it is never - authoritative (§1.4). + or membership mutation that affects it (each invalidator class + witnessed, §6.5), and it is never authoritative (§1.6). ## 5. Dependencies and phase timing @@ -117,55 +177,100 @@ row with contract 5 (`tool-gateway-mapping.md`). after them and reads their surfaces; it defines none of them. 2. The roll-up query is one tool with one Gateway mapping row under contract 5's regime (request/result/error/audit contracts there); - this contract binds its semantics, not its wire shape. + this contract binds its semantics (§2.5 defines the semantic + fields the contract 5 §4.1 DTO serializes), not its wire encoding. 3. Legacy task rows outside the typed lifecycle are not aggregated; the roll-up begins counting a workspace's tasks when they exist in the typed surface. No roll-up obligation attaches to v1 before - ranks 1–3 exist. + ranks 1–3 exist. Both rules are drafting additions disclosed in §7 + (they trace to no §8 sentence). ## 6. Verification requirements Binding on the implementing PRs. Every witness names, in its implementation, the exact endpoints/tools, tables, and fixtures it -exercises. The base fixture seeds two companies; under company A an -estate with two platform-projects and three workspaces with known task -counts across at least three statuses; under company B one workspace. +exercises. The base fixture seeds two companies; under company A **two +estates with distinct, non-identical count profiles**: estate A1 with +two platform-projects and three workspaces, estate A2 with one +platform-project and one workspace, all with known task counts across +at least three statuses; under company B one workspace. -1. **Correctness witness:** for a reader authorized on all of company - A, roll-up totals at workspace, platform-project, estate, and - company level equal the seeded counts, keyed by the typed status - values, with no double count across the chain. -2. **Scope witnesses:** a reader with a grant on one platform-project - sees exactly its subtree's counts at every queryable level; a - workspace-member-only reader sees exactly that workspace; for a - reader authorized on two of three sibling workspaces, the parent - totals equal the two readable workspaces' sum. +1. **Correctness witnesses:** for a reader holding a direct company-A + grant, roll-up totals at every level equal the seeded sums — each + workspace, each platform-project, estate A1 and estate A2 + separately (their distinct profiles asserted distinct), and the + company total equal to A1+A2 — keyed by the typed status values, + with no double count across the chain. For a reader holding a + direct estate-A1 grant, the estate-A1 result equals the A1 sum and + a company-A query returns company A with exactly A1's contribution + (estate A2 invisible). Each of the four grant levels — company, + estate, platform-project (below), workspace — has an explicit + case; none is simulated by unioning workspace access. +2. **Scope witnesses:** a reader with a direct grant on one + platform-project sees exactly its subtree's counts at every + queryable level; for a reader authorized on two of three sibling + workspaces, the parent totals equal the two readable workspaces' + sum. **Membership locality (§1.4):** a member-only reader queries + the workspace directly and receives its counts; the same reader + querying the workspace's parent (or any ancestor) receives the + §3.4-equivalent nonexistent-node response, and no ancestor + response for any other reader changes because of that membership. 3. **No-oracle witnesses:** the sibling-workspace response above contains no field disclosing the third workspace's existence (closed-schema comparison against a reader authorized on all three: identical field set, differing only in counts and visible - nodes); a query by an unauthorized reader naming company B returns - a response byte-equivalent in shape and status to the same query - naming a random nonexistent id. -4. **Empty-vs-hidden witness (ruling):** a reader granted on an empty - platform-project receives it with zero counts; with the grant - deleted, the same query returns the nonexistent-node response. -5. **Mutation witnesses:** the roll-up surface rejects every mutating + nodes). **Partial-scope hidden node:** the reader who sees the + parent and two siblings queries the hidden third workspace by its + real id — the response satisfies the §3.4 equivalence predicate + against the same query naming a nonexistent id, under one fixed + request context with the declared volatile-field normalization. + **Cross-tenant:** an unauthorized reader naming company B receives + a response §3.4-equivalent to naming a nonexistent id. Each + equivalence check is executable byte comparison after the declared + normalization, not a shape judgment. +4. **Empty-vs-hidden witness (ruling):** a reader granted (direct + chain grant) on an empty platform-project receives it with zero + counts — every status key present at `0` (§2.5); with the grant + deleted, the same query returns the §3.4-equivalent + nonexistent-node response. An inherited-grant case: a company + grant makes an empty descendant platform-project visible with zero + counts. +5. **Cache-invalidation witnesses (conditional):** bound only if the + implementation caches — for EACH invalidator class, prime the + cache, commit one mutation of that class, and assert the next + query reflects it: a task status change, a task creation, a + membership removal (the member-readable workspace disappears from + its own node's next query), a workspace reparenting (both old and + new parent totals correct), and a grant revocation. A live + (cacheless) v1 implementation records that fact and the witnesses + bind at the PR that introduces a cache. +6. **Mutation witnesses:** the roll-up surface rejects every mutating verb/command; a crafted attempt to issue a write through the roll-up's database path fails at the read-only boundary (§4.2); - after any roll-up query, an SOT row diff is empty. -6. **Revocation witness:** after revoking the grant that made a + after any roll-up query, the row diff is empty across BOTH the + workspace tables and the hierarchy tables (contract 1 §6.7's + both-table zero-write assertion). +7. **Closed-schema witness:** the response is asserted field-exact + against the §2.5 enumeration — `node` (id/type/name), `totals` + (every typed status present, explicit zeros), `children` + (recursive, visible only) — and a response carrying any field + outside the enumeration fails the assertion (carve-out boundary, + §2.3). The denial envelope is asserted field-exact against + contract 5 §4.2's envelope. +8. **Non-authoritative and never-gate witnesses:** (a) a static + production import-graph inventory (hierarchy contract §6.3 style, + production code over `apps/` and `packages/`, tests excluded) + shows no production module imports the roll-up query module or its + result DTO into any SOT write path, any authorization/gating + decision path, or any persistence beyond the response lifetime — + asserted in both directions (the roll-up module's consumers are + enumerated and each is a presentation surface); (b) a behavioral + probe: with roll-up output artificially perturbed (test double), + no authorization outcome and no work-gating decision anywhere in + the fixture suite changes — proving no gate consumes it. +9. **Revocation witness:** after revoking the grant that made a subtree readable, the next roll-up query excludes it (contract 2 §6.2 bound). -7. **Closed-schema witness:** the response schema is asserted - field-exact against §2.1/§3.2 (identifiers, visible-node names, - status counts) — a field outside the closed set fails the - assertion (carve-out boundary, §2.3). -8. **Non-authoritative witness:** no production module imports roll-up - output into SOT writes; a static route inventory (hierarchy - contract §6.3 style, production code over `apps/` and `packages/`, - tests excluded) shows the roll-up query modules perform no SOT - writes and no module feeds roll-up responses into a write path. ## 7. Drafting additions (PRD §12.1 disclosure) @@ -176,16 +281,25 @@ read-only nature, and the no-oracle acceptance are traced to 1. The §3.2 node-visibility rule and the granted-but-empty behavior (the ruling below). -2. The §3.3 nonexistent-node response equivalence as the concrete - no-oracle mechanism. +2. The §3.3–§3.4 nonexistent-node response equivalence, with its + normalized-byte-equality predicate, as the concrete no-oracle + mechanism. 3. The §4.2 read-only-transaction mechanical enforcement. -4. The §4.3 cache option with transactional invalidation. -5. The §2.3 closed response schema as an amendment boundary. +4. The §4.3 cache option with transactional invalidation and the + §6.5 per-invalidator witnesses. +5. The §2.5 closed response schema as an amendment boundary. +6. The §1.4 membership-locality rule — membership-only readability + contributes at the workspace node only (this contract's + reconciliation of `native-kanban-sot.md` §8.1.3 "authorized on" + with contract 2 §3.1/§7.4's workspace-local membership). +7. The §5.3 legacy-row exclusion and the §5.3 pre-rank no-obligation + rule. ## Ruling request -Ruling requested (one decision): shall a node the reader is granted on -but whose aggregation scope is empty appear in the roll-up with zero -counts (recommended — it lets the UI show a granted-but-empty subtree +Ruling requested (one decision): shall a node the reader holds an +effective chain role on (direct or inherited, §1.2) but whose +aggregation scope is empty appear in the roll-up with zero counts +(recommended — it lets the UI show a granted-but-empty subtree honestly) — or, as the alternative, be indistinguishable from a nonexistent node until it contains a readable workspace?