docs: roll-up projection contract (S2 contract 8)
ci/woodpecker/pr/ci Pipeline is pending

This commit is contained in:
fred
2026-08-26 19:41:47 -05:00
parent 2a30c68b84
commit 61c495dd63
+191
View File
@@ -0,0 +1,191 @@
# 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
13.
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. **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
`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 (§6.7 witness): adding any
field beyond identifiers, names of visible nodes, and status
counts 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.
## 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
contributes nothing to any total — not a count, not a row, not a
presence marker.
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.
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
(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, and it is never
authoritative (§1.4).
## 5. Dependencies and phase timing
1. The roll-up depends on A5 ranks 13: 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, not its wire shape.
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 13 exist.
## 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.
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.
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
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
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)
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 nonexistent-node response equivalence 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.
## 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
honestly) — or, as the alternative, be indistinguishable from a
nonexistent node until it contains a readable workspace?