diff --git a/docs/requirements/rollup-projection.md b/docs/requirements/rollup-projection.md new file mode 100644 index 00000000..4f0c0157 --- /dev/null +++ b/docs/requirements/rollup-projection.md @@ -0,0 +1,351 @@ +# Roll-up Projection Contract (S2 contract 8) + +Status: DRAFT — awaiting ratification (webui-audit S2, contract 8 of 9). +Authority: `native-kanban-sot.md` §8 (A1 amendment) — "task and status +visualization bubbles up the hierarchy as aggregation over workspaces +the reader is authorized on" (§8.1.3); roll-up is never a write and +bubble-up views are generated projections, non-authoritative and never +import sources (§8.2.2); the express, narrow carve-out from the +portfolio-analytics non-goal covers per-workspace task counts and +statuses aggregated up the parent chain over readable workspaces, and +nothing beyond that boundary (§8.2.4); acceptance requires that roll-up +endpoints cannot mutate state and that a reader sees aggregates only +over workspaces they are authorized on, with no cross-tenant existence +oracles (§8.3). A5 rank 5 names the deliverable: an authorized +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). + +Revision 3 (terra re-review residuals): the partial-scope witnesses are +reconstructed at levels where chain grants can actually differ — +platform-project siblings under one estate and estate siblings under +one company — because contract 1 §3.1/§3.4 defines no workspace-level +grant target, so no reader can hold a chain grant on two of three +sibling workspaces (F2). §2.5 now defines one field-exact recursive +record — every node, including the queried node and every leaf, is the +same five-field shape with a required, deterministically ordered +`children` array that is empty at workspaces — and the whole-result +rules (no optional fields, denial envelope, wire faithfulness) are +their own §2.6 at section scope (F3). The fixture assigns workspaces +to named platform-projects, and §6.1's grant-level cases are the three +levels contract 1 defines, with workspace-level access covered by the +membership case and stated as having no direct chain grant (F6). + +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 +defines the roll-up only: hierarchy shape stays with contract 1 +(`hierarchy-schema.md`), grant vocabulary and evaluation with contract 2 +(`rbac-grant-model.md`), the task lifecycle and status taxonomy with +`native-kanban-sot.md`'s typed surface, and the tool↔Gateway mapping +row with contract 5 (`tool-gateway-mapping.md`). + +## 1. Definitions + +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. **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. + +## 2. Projection semantics + +1. **Aggregate content.** The roll-up for a node reports, per + workspace in the reader's aggregation scope and as subtree totals: + task counts keyed by the typed lifecycle's status values (owned by + `native-kanban-sot.md`; this contract introduces no status), and + nothing else. Direct count/status aggregation is the entire + surface. +2. **Every level.** The roll-up is queryable at workspace, + platform-project, estate, and company level. A node's totals equal + the sum over its aggregation scope; chain resolution is contract 1 + §2.5's (every workspace resolves to exactly one chain), so no + workspace is counted twice and none is orphaned. +3. **Carve-out boundary.** Everything beyond direct count/status + 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 (§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 or deny work (witness §6.8 — both the write-path and the + decision-path prohibitions are witnessed). +5. **Closed semantic schema.** The successful result is exactly one + **roll-up node record**, a single recursive shape used at every + depth. A roll-up node record consists of exactly these five + fields, and no others: + - `id`: the node's identifier. + - `type`: one of the four contract 1 levels. + - `name`: the node's 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. At a workspace node, `totals` is + that workspace's own counts; at any other node, `totals` is the + sum over the node's aggregation scope (§2.2). This is how §2.1's + "per workspace and as subtree totals" content is carried: + per-workspace counts are the leaf records' `totals`, subtree + totals are the interior records' `totals`. + - `children`: a required array, present on EVERY node record. Its + elements are the reader-visible (§3.2) child nodes of this node, + each itself a complete roll-up node record, recursing down to + the workspaces in the reader's aggregation scope. At a workspace + node the array is exactly `[]` — a workspace record never has + children. The array is ordered deterministically, ascending by + `id`; the implementing PR asserts that ordering. A node outside + §3.2 visibility never appears at any depth. + + The queried node's record IS the whole result — there is no + wrapper field around it. + +6. **Whole-result rules.** There are no optional result fields at any + depth. 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 the §2.5 recursive + record: a wire field with no corresponding semantic field 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.5). An unreadable workspace + contributes nothing to any total — not a count, not a row, not a + 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 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 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. + +## 4. Read-only enforcement + +1. **Never a write.** No roll-up path may mutate, claim, order, or + gate work in any workspace (§8.2.2). The roll-up ships as a + non-mutating query tool (A5 rank 5) — a query surface with no + command counterpart. +2. **Mechanical enforcement.** The implementing PR executes roll-up + database work inside read-only transactions (or an equivalently + privilege-restricted path), so a mutation attempt fails at the + database boundary, not only by convention. +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 (each invalidator class + witnessed, §6.5), and it is never authoritative (§1.6). + +## 5. Dependencies and phase timing + +1. The roll-up depends on A5 ranks 1–3: contract 1's hierarchy tables + (the parent chain), contract 2's evaluator (readability), and the + typed Kanban lifecycle (the task state being counted). It ships + 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 (§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. 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 **two +estates with distinct, non-identical count profiles**: estate A1 with +two platform-projects — P1 holding workspaces W1 and W2, P2 holding +workspace W3 — and estate A2 with one platform-project P3 holding one +workspace W4, all with known task counts across at least three +statuses; under company B one workspace. + +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 three chain grant levels + contract 1 §3.1 defines — company, estate, platform-project + (below, §6.2) — has an explicit direct-grant case, none simulated + by unioning lower access. Workspace-level access has NO direct + chain grant (contract 1 §3.1/§3.4 define no workspace grant + target) and is covered by the §6.2 membership case. +2. **Scope witnesses:** a reader with a direct grant on + platform-project P1 only sees exactly P1's subtree counts + (W1+W2): a P1 query returns W1+W2; an estate-A1 query returns the + estate node with exactly P1's contribution, sibling project P2 and + its workspace W3 absent at every depth; a company-A query likewise + carries only P1's contribution. An estate-sibling case: a reader + with a direct grant on estate A1 only queries company A and + receives exactly A1's contribution, estate A2 absent. (Chain + grants exist only at company, estate, and platform-project — + contract 1 §3.1 — so partial scope among SIBLING WORKSPACES of + one project is not constructible by grants and is not witnessed; + the constructible partial-scope cases are the project- and + estate-sibling ones above.) **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 P1-only reader's estate-A1 response + above contains no field disclosing P2's or W3's existence + (closed-schema comparison against an estate-A1-granted reader's + response: identical field set, differing only in counts and + visible nodes). **Partial-scope hidden node:** the P1-only reader + — who sees estate A1 and the P1 subtree — queries hidden sibling + project P2 by its real id, and separately hidden workspace W3 by + its real id; each 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, 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 recursive record at every depth — exactly + `id`/`type`/`name`/`totals`/`children` on every node, every typed + status present with explicit zeros, `children: []` at every + workspace record, the declared ascending-`id` ordering, no + wrapper field — and a response carrying any field outside the + record at any depth fails the assertion (carve-out boundary, + §2.3). The denial envelope is asserted field-exact against + contract 5 §4.2's envelope (§2.6). +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. Drafting additions (PRD §12.1 disclosure) + +Proposed drafting additions, visible here for ratification, each +severable; the aggregation itself, its authorization scope, its +read-only nature, and the no-oracle acceptance are traced to +`native-kanban-sot.md` §8 and are not additions: + +1. The §3.2 node-visibility rule and the granted-but-empty behavior + (the ruling below). +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 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 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?