BI-P002, BI-P003, BI-P004, BI-P005, BI-P009, BI-P010, BI-P011 and BI-P012 from SIAX Analytics & BI — Final Production Program Part I, all in 0pt, the
declared owner of every one of them.
Every file is new. Not one existing file is modified. git diff main -- src/analytics/clickhouse-analytics.ts is 0 lines: BI-P009 says preserve the
existing working ClickHouseAnalytics, and it is preserved literally, then
composed rather than replaced. .gitea/workflows/** is untouched.
Four purpose-built ClickHouse tables + rollup MV, benchmarked against the counterfactual
BI-P011
Four-layer tenant isolation
BI-P012
Every named limit + CL0UD quota + per-tenant admission control
Verification — what it looks like when the answer is NO
This estate has produced many false greens, so each claim below was made
falsifiable before it was made.
The BI-P009 non-regression line.tests/analytics/tenant-predicate-regression.test.ts
asserts the literal string tenant_id = {tenant:String} on the URL the adapter
builds. I deleted the forcing from both compilers and re-ran it: 2 of 6
tests go RED, then green again once restored. Transcript §1–§2.
BI-P011, hole shown open before it is shown closed. Against a disposable
ClickHouse seeded with 50 rows for t_alpha (value 1) and 50 for t_beta
(value 1000):
without the fix
with the fix
layer 2 (compiler forcing)
the same statement minus the predicate returns 50 050
Layer 5 (separate cluster tier) is supported by construction but not
operated — said plainly in the docs rather than ticked off.
BI-P010, measured not asserted. 5.4M SaaS events + 4M vehicle projections
(2M under test, 2M decoys across 50 other tenants), plus the "one giant table"
the prompt warns against:
Question
Purpose-built
One giant table
90-day metric total, one tenant
48.8 ms, 40 960 rows read
174.4 ms, 5 400 000 rows read
Vehicle point lookup by key
49.3 ms, 8 192 rows read
236.6 ms, 7 401 152 rows read
Granules read
5 / 660
660 / 1 149
Finding that does not flatter the design, stated in the docs: at this scale
the AggregatingMergeTree rollup (37.98 ms) was not faster in wall time
than scanning raw events (32.55 ms). It reads half the rows; its real value is
the long TTL. Do not quote it as a speed win.
BI-P005, not self-certified. A hand-rolled Arrow encoder is worthless
unless somebody else's parser agrees, so three independent implementations
were used: pyarrow 25.0.1 and DuckDB 1.5.5 decode both ClickHouse's
Arrow and ours to identical values and types (verdict PASS), and ClickHouse's own Arrow C++ ingests a 20 000-row 0pt-encoded batch and
returns exact aggregates — that last one runs in CI on every push.
BI-P012, both halves. Each named limit is fired individually and
identified by ClickHouse code (158/307/159/396/241). The half usually skipped
— "without degrading unrelated tenant" — is timed: an unrelated tenant's query
runs five times while a real CPU bomb burns, and must still return the correct answer.
Three defects the tests caught
A constraint that could never fail.CHECK (array_length(allowed_purposes, 1) >= 1)
evaluates to NULL for '{}', and a NULL CHECK passes in Postgres — so
the GDPR purpose-limitation guard admitted purposeless grants. Fixed with coalesce(...); the test that found it is pinned as a regression.
An estimator that refuses to estimate.EXPLAIN ESTIMATE run under the
caller's own max_rows_to_read fails with code 158 exactly when the
estimate is over budget — the only case that matters. Planner calls now use
a narrower envelope (planningSettings).
A partial result that looks complete.result_overflow_mode=break with max_result_rows=10 returns HTTP 200 with 65 409 of 100 000 rows and
nothing says it is short. Every SELECT ships throw.
The architecture boundary, enforced not asserted
BIApplicationBinding has no credential column — in TypeScript or in SQL.
Two tests assert its absence (migration text, and information_schema.columns
on a live Postgres), so adding one later is a test failure. A bound BI
application authenticates with a CL0UD identity and receives compiled,
tenant-forced results; it never learns a DSN. AnalyticsIdentity is a separate
parameter from the request, never a field on it — this estate has already
shipped a live cross-tenant hole by reading a tenant out of a request body.
⚠️ Verified, and it changes what can be claimed
Probed live on server7 (read-only, metadata only, nothing written):
There is no opt_analytics database anywhere in the estate. The
"existing working" ClickHouse provider has never run against a deployed
instance. It works — proven here against real ClickHouse — but it is not
in production.
server7's ClickHouse is the b0ts plane and is off limits to this module.
It holds opt_scraped: 51 tables, 407 793 471 rows including persons
and cz_owners. clickhouse-analytics.ts's own header already forbids
pointing this module at it (Ratsit/person-PII legal case, written DPIA
prohibition). The briefing note about "40M+ Swedish vehicle rows on server7"
is accurate about the server and describes the plane 0pt analytics may not
use.
It could not satisfy BI-P011 anyway: zero columns named tenant_id or project_id on the whole server, and zero row policies.
A separate ClickHouse (or at minimum its own database with restricted users)
is needed before any of this reaches production. Details: evidence/bi-p002-p012/server7-clickhouse-probe.md.
Gates
typecheck (blocking) EXIT=0
test (blocking) 463 passed | 80 skipped (543) EXIT=0
build (blocking) EXIT=0
lint (advisory) 12 pre-existing errors in tests/p0/* — ZERO in any file this branch adds
The five tests/integration/analytics-* suites run in CI's integration job
against the live ClickHouse service container, which already hard-fails on a
skipped suite.
One honest gap:tests/analytics/analytics-domain-postgres.realinfra.test.ts
(15 tests: restore drill, immutability triggers, tenant scoping) cannot run
in CI — there is no Postgres service container in the quality job, and
adding one would mean editing .gitea/workflows/**, a protected file pattern
that would block this PR from merging. It was run for real against a
disposable postgres:16-alpine; the transcript is committed at evidence/bi-p002-p012/test-transcript.txt §7. Wiring it into CI is a
separate, workflow-touching change.
Not done, and said so
executeQuery (analyst SQL) is declared and refused with CAPABILITY_UNAVAILABLE until BI-P017 (dialect contract) and BI-P033
(authorization) exist.
Fourteen of the nineteen entities have tables, constraints and types but no
repository methods yet; their consumers arrive with BI-P025…BI-P035.
BI-P011 layer 5 (per-customer cluster tier) is supported, not operated.
## What this is
BI-P002, BI-P003, BI-P004, BI-P005, BI-P009, BI-P010, BI-P011 and BI-P012 from
*SIAX Analytics & BI — Final Production Program Part I*, all in `0pt`, the
declared owner of every one of them.
**Every file is new.** Not one existing file is modified. `git diff main --
src/analytics/clickhouse-analytics.ts` is 0 lines: BI-P009 says *preserve the
existing working `ClickHouseAnalytics`*, and it is preserved literally, then
composed rather than replaced. `.gitea/workflows/**` is untouched.
| Prompt | Deliverable |
|---|---|
| BI-P002 | 19 canonical entities (verbatim names) + `sql/008` + tenant-scoped repository + restore drill |
| BI-P003 | RAW/NORMALIZED/CURATED/SERVING, with the "no dashboard reads raw" rule enforced |
| BI-P004 | `DatasetContract` + fail-closed compatibility gate |
| BI-P005 | Arrow IPC encode/decode + ClickHouse pass-through, no new npm dependency |
| BI-P009 | `SiaxAnalyticsEngine` (all 8 capabilities) + ClickHouse provider |
| BI-P010 | Four purpose-built ClickHouse tables + rollup MV, benchmarked against the counterfactual |
| BI-P011 | Four-layer tenant isolation |
| BI-P012 | Every named limit + CL0UD quota + per-tenant admission control |
## Verification — what it looks like when the answer is NO
This estate has produced many false greens, so each claim below was made
falsifiable before it was made.
**The BI-P009 non-regression line.** `tests/analytics/tenant-predicate-regression.test.ts`
asserts the literal string `tenant_id = {tenant:String}` on the URL the adapter
builds. I deleted the forcing from *both* compilers and re-ran it: **2 of 6
tests go RED**, then green again once restored. Transcript §1–§2.
**BI-P011, hole shown open before it is shown closed.** Against a disposable
ClickHouse seeded with 50 rows for `t_alpha` (value 1) and 50 for `t_beta`
(value 1000):
| | without the fix | with the fix |
|---|---|---|
| layer 2 (compiler forcing) | the same statement minus the predicate returns **50 050** | `t_alpha` gets **50**, `t_beta` gets **50 000** |
| layer 3 (row policy) | unrestricted user, predicate-free `uniqExact(tenant_id)` → **2** tenants | restricted user, *identical* statement → **1**; `sum(value)` = 50, not 50 050 |
Layer 5 (separate cluster tier) is supported by construction but **not
operated** — said plainly in the docs rather than ticked off.
**BI-P010, measured not asserted.** 5.4M SaaS events + 4M vehicle projections
(2M under test, 2M decoys across 50 other tenants), plus the "one giant table"
the prompt warns against:
| Question | Purpose-built | One giant table |
|---|---|---|
| 90-day metric total, one tenant | **48.8 ms**, 40 960 rows read | 174.4 ms, **5 400 000** rows read |
| Vehicle point lookup by key | **49.3 ms**, 8 192 rows read | 236.6 ms, **7 401 152** rows read |
| Granules read | **5 / 660** | 660 / 1 149 |
Finding that does *not* flatter the design, stated in the docs: at this scale
the `AggregatingMergeTree` rollup (37.98 ms) was **not faster in wall time**
than scanning raw events (32.55 ms). It reads half the rows; its real value is
the long TTL. Do not quote it as a speed win.
**BI-P005, not self-certified.** A hand-rolled Arrow encoder is worthless
unless somebody else's parser agrees, so three independent implementations
were used: **pyarrow 25.0.1** and **DuckDB 1.5.5** decode both ClickHouse's
Arrow and ours to identical values and types (verdict `PASS`), and
**ClickHouse's own Arrow C++** ingests a 20 000-row 0pt-encoded batch and
returns exact aggregates — that last one runs in CI on every push.
**BI-P012, both halves.** Each named limit is fired individually and
identified by ClickHouse code (158/307/159/396/241). The half usually skipped
— "without degrading unrelated tenant" — is timed: an unrelated tenant's query
runs five times *while* a real CPU bomb burns, and must still return the
**correct** answer.
## Three defects the tests caught
1. **A constraint that could never fail.** `CHECK (array_length(allowed_purposes, 1) >= 1)`
evaluates to NULL for `'{}'`, and a NULL CHECK **passes** in Postgres — so
the GDPR purpose-limitation guard admitted purposeless grants. Fixed with
`coalesce(...)`; the test that found it is pinned as a regression.
2. **An estimator that refuses to estimate.** `EXPLAIN ESTIMATE` run under the
caller's own `max_rows_to_read` fails with code 158 exactly when the
estimate is over budget — the only case that matters. Planner calls now use
a narrower envelope (`planningSettings`).
3. **A partial result that looks complete.** `result_overflow_mode=break` with
`max_result_rows=10` returns **HTTP 200 with 65 409 of 100 000 rows** and
nothing says it is short. Every SELECT ships `throw`.
## The architecture boundary, enforced not asserted
`BIApplicationBinding` has **no credential column** — in TypeScript or in SQL.
Two tests assert its absence (migration text, and `information_schema.columns`
on a live Postgres), so adding one later is a test failure. A bound BI
application authenticates with a CL0UD identity and receives compiled,
tenant-forced results; it never learns a DSN. `AnalyticsIdentity` is a separate
parameter from the request, never a field on it — this estate has already
shipped a live cross-tenant hole by reading a tenant out of a request body.
## ⚠️ Verified, and it changes what can be claimed
Probed live on server7 (read-only, metadata only, nothing written):
* **There is no `opt_analytics` database anywhere in the estate.** The
"existing working" ClickHouse provider has **never run against a deployed
instance**. It works — proven here against real ClickHouse — but it is not
in production.
* **server7's ClickHouse is the b0ts plane and is off limits to this module.**
It holds `opt_scraped`: 51 tables, **407 793 471** rows including `persons`
and `cz_owners`. `clickhouse-analytics.ts`'s own header already forbids
pointing this module at it (Ratsit/person-PII legal case, written DPIA
prohibition). The briefing note about "40M+ Swedish vehicle rows on server7"
is accurate about the server and describes the plane 0pt analytics may not
use.
* It could not satisfy BI-P011 anyway: **zero** columns named `tenant_id` or
`project_id` on the whole server, and **zero** row policies.
A separate ClickHouse (or at minimum its own database with restricted users)
is needed before any of this reaches production. Details:
`evidence/bi-p002-p012/server7-clickhouse-probe.md`.
## Gates
```
typecheck (blocking) EXIT=0
test (blocking) 463 passed | 80 skipped (543) EXIT=0
build (blocking) EXIT=0
lint (advisory) 12 pre-existing errors in tests/p0/* — ZERO in any file this branch adds
```
The five `tests/integration/analytics-*` suites run in CI's `integration` job
against the live ClickHouse service container, which already hard-fails on a
skipped suite.
**One honest gap:** `tests/analytics/analytics-domain-postgres.realinfra.test.ts`
(15 tests: restore drill, immutability triggers, tenant scoping) **cannot run
in CI** — there is no Postgres service container in the `quality` job, and
adding one would mean editing `.gitea/workflows/**`, a protected file pattern
that would block this PR from merging. It was run for real against a
disposable `postgres:16-alpine`; the transcript is committed at
`evidence/bi-p002-p012/test-transcript.txt` §7. Wiring it into CI is a
separate, workflow-touching change.
## Not done, and said so
* `executeQuery` (analyst SQL) is **declared and refused** with
`CAPABILITY_UNAVAILABLE` until BI-P017 (dialect contract) and BI-P033
(authorization) exist.
* Fourteen of the nineteen entities have tables, constraints and types but no
repository methods yet; their consumers arrive with BI-P025…BI-P035.
* BI-P011 layer 5 (per-customer cluster tier) is supported, not operated.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Implements the eight BI program prompts that make 0pt the canonical SIAX
analytics capability. Every file is NEW: not one existing file is modified,
and src/analytics/clickhouse-analytics.ts is byte-identical to main.
BI-P002 — the nineteen canonical entities, verbatim, with tenant_id +
project_id on every tenant-owned record. sql/008 gives each one a table.
The four version entities BI-P002 names as immutable are enforced by BEFORE
UPDATE/DELETE triggers, not by a TypeScript `readonly` — proven by direct SQL
against a live Postgres.
BI-P003 — RAW/NORMALIZED/CURATED/SERVING with the read rule enforced:
assertReadableByBiSurface refuses a RAW dataset to a dashboard unless a
recorded exception exists. All eight required declarations are NOT NULL.
BI-P004 — DatasetContract and a fail-closed compatibility gate. Fifteen
breaking-change classes are each tested twice: refused under a minor bump,
accepted under a major one.
BI-P005 — Arrow IPC alongside JSON, with a hand-written FlatBuffers writer so
no npm dependency (or CVE surface) is added. Conformance comes from three
independent implementations, not from self-agreement: pyarrow 25.0.1 and
DuckDB 1.5.5 read identical values from both ClickHouse's Arrow and ours, and
ClickHouse's own Arrow C++ ingests our 20,000-row batch with exact aggregates.
BI-P009 — SiaxAnalyticsEngine with all eight capabilities. The existing
ClickHouseAnalytics is COMPOSED, never replaced. executeQuery (analyst SQL) is
declared and deliberately refused with CAPABILITY_UNAVAILABLE until BI-P017 and
BI-P033 land, rather than opening the raw-SQL hole BI-P011 enumerates.
BI-P010 — four purpose-built tables, not one giant one, and the difference is
measured against the counterfactual: 48.8ms/40,960 rows vs 174.4ms/5,400,000
rows for the same question; 5/660 granules vs 660/1149.
BI-P011 — layered isolation, each layer proven with the layer above removed.
Without the forced predicate the same statement returns 50,050 (both tenants);
with it, t_alpha gets 50. Without a row policy an unrestricted user sees 2
tenants; the restricted user running the IDENTICAL statement sees 1.
BI-P012 — every named limit, fired individually and identified by ClickHouse
error code, plus the half usually skipped: an unrelated tenant is timed while
a real CPU bomb is in flight and must still return the CORRECT answer.
Three defects found by writing tests that can fail:
* `CHECK (array_length(purposes,1) >= 1)` can NEVER fail — array_length is
NULL for '{}' and a NULL CHECK passes. Fixed with coalesce(); the test
that caught it is pinned.
* EXPLAIN ESTIMATE run under the caller's own max_rows_to_read fails with
code 158 exactly when the estimate would be over budget. Planner calls now
use a narrower envelope.
* result_overflow_mode=break returns HTTP 200 with 65,409 of 100,000 rows
and no indication it is short. Every SELECT ships `throw`.
Verified rather than assumed: server7's ClickHouse holds `opt_scraped` (51
tables, 407,793,471 rows, `persons`/`cz_owners` included) — the b0ts plane this
module is prohibited from touching — has no `opt_analytics` database, zero
tenant_id columns and zero row policies. Nothing there was modified; every
proof ran against disposable containers.
Evidence: evidence/bi-p002-p012/{test-transcript.txt,bi-p010-benchmark.json,
bi-p005-arrow-roundtrip.json,server7-clickhouse-probe.md}
Docs: docs/analytics/BI_ANALYTICS_CORE.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CODEOWNERS rules
requested review from sax3l 2026-09-06 21:02:19 +00:00
admin
merged commit into main2026-09-06 21:04:04 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What this is
BI-P002, BI-P003, BI-P004, BI-P005, BI-P009, BI-P010, BI-P011 and BI-P012 from
SIAX Analytics & BI — Final Production Program Part I, all in
0pt, thedeclared owner of every one of them.
Every file is new. Not one existing file is modified.
git diff main -- src/analytics/clickhouse-analytics.tsis 0 lines: BI-P009 says preserve theexisting working
ClickHouseAnalytics, and it is preserved literally, thencomposed rather than replaced.
.gitea/workflows/**is untouched.sql/008+ tenant-scoped repository + restore drillDatasetContract+ fail-closed compatibility gateSiaxAnalyticsEngine(all 8 capabilities) + ClickHouse providerVerification — what it looks like when the answer is NO
This estate has produced many false greens, so each claim below was made
falsifiable before it was made.
The BI-P009 non-regression line.
tests/analytics/tenant-predicate-regression.test.tsasserts the literal string
tenant_id = {tenant:String}on the URL the adapterbuilds. I deleted the forcing from both compilers and re-ran it: 2 of 6
tests go RED, then green again once restored. Transcript §1–§2.
BI-P011, hole shown open before it is shown closed. Against a disposable
ClickHouse seeded with 50 rows for
t_alpha(value 1) and 50 fort_beta(value 1000):
t_alphagets 50,t_betagets 50 000uniqExact(tenant_id)→ 2 tenantssum(value)= 50, not 50 050Layer 5 (separate cluster tier) is supported by construction but not
operated — said plainly in the docs rather than ticked off.
BI-P010, measured not asserted. 5.4M SaaS events + 4M vehicle projections
(2M under test, 2M decoys across 50 other tenants), plus the "one giant table"
the prompt warns against:
Finding that does not flatter the design, stated in the docs: at this scale
the
AggregatingMergeTreerollup (37.98 ms) was not faster in wall timethan scanning raw events (32.55 ms). It reads half the rows; its real value is
the long TTL. Do not quote it as a speed win.
BI-P005, not self-certified. A hand-rolled Arrow encoder is worthless
unless somebody else's parser agrees, so three independent implementations
were used: pyarrow 25.0.1 and DuckDB 1.5.5 decode both ClickHouse's
Arrow and ours to identical values and types (verdict
PASS), andClickHouse's own Arrow C++ ingests a 20 000-row 0pt-encoded batch and
returns exact aggregates — that last one runs in CI on every push.
BI-P012, both halves. Each named limit is fired individually and
identified by ClickHouse code (158/307/159/396/241). The half usually skipped
— "without degrading unrelated tenant" — is timed: an unrelated tenant's query
runs five times while a real CPU bomb burns, and must still return the
correct answer.
Three defects the tests caught
CHECK (array_length(allowed_purposes, 1) >= 1)evaluates to NULL for
'{}', and a NULL CHECK passes in Postgres — sothe GDPR purpose-limitation guard admitted purposeless grants. Fixed with
coalesce(...); the test that found it is pinned as a regression.EXPLAIN ESTIMATErun under thecaller's own
max_rows_to_readfails with code 158 exactly when theestimate is over budget — the only case that matters. Planner calls now use
a narrower envelope (
planningSettings).result_overflow_mode=breakwithmax_result_rows=10returns HTTP 200 with 65 409 of 100 000 rows andnothing says it is short. Every SELECT ships
throw.The architecture boundary, enforced not asserted
BIApplicationBindinghas no credential column — in TypeScript or in SQL.Two tests assert its absence (migration text, and
information_schema.columnson a live Postgres), so adding one later is a test failure. A bound BI
application authenticates with a CL0UD identity and receives compiled,
tenant-forced results; it never learns a DSN.
AnalyticsIdentityis a separateparameter from the request, never a field on it — this estate has already
shipped a live cross-tenant hole by reading a tenant out of a request body.
⚠️ Verified, and it changes what can be claimed
Probed live on server7 (read-only, metadata only, nothing written):
opt_analyticsdatabase anywhere in the estate. The"existing working" ClickHouse provider has never run against a deployed
instance. It works — proven here against real ClickHouse — but it is not
in production.
It holds
opt_scraped: 51 tables, 407 793 471 rows includingpersonsand
cz_owners.clickhouse-analytics.ts's own header already forbidspointing this module at it (Ratsit/person-PII legal case, written DPIA
prohibition). The briefing note about "40M+ Swedish vehicle rows on server7"
is accurate about the server and describes the plane 0pt analytics may not
use.
tenant_idorproject_idon the whole server, and zero row policies.A separate ClickHouse (or at minimum its own database with restricted users)
is needed before any of this reaches production. Details:
evidence/bi-p002-p012/server7-clickhouse-probe.md.Gates
The five
tests/integration/analytics-*suites run in CI'sintegrationjobagainst the live ClickHouse service container, which already hard-fails on a
skipped suite.
One honest gap:
tests/analytics/analytics-domain-postgres.realinfra.test.ts(15 tests: restore drill, immutability triggers, tenant scoping) cannot run
in CI — there is no Postgres service container in the
qualityjob, andadding one would mean editing
.gitea/workflows/**, a protected file patternthat would block this PR from merging. It was run for real against a
disposable
postgres:16-alpine; the transcript is committed atevidence/bi-p002-p012/test-transcript.txt§7. Wiring it into CI is aseparate, workflow-touching change.
Not done, and said so
executeQuery(analyst SQL) is declared and refused withCAPABILITY_UNAVAILABLEuntil BI-P017 (dialect contract) and BI-P033(authorization) exist.
repository methods yet; their consumers arrive with BI-P025…BI-P035.
🤖 Generated with Claude Code
Implements the eight BI program prompts that make 0pt the canonical SIAX analytics capability. Every file is NEW: not one existing file is modified, and src/analytics/clickhouse-analytics.ts is byte-identical to main. BI-P002 — the nineteen canonical entities, verbatim, with tenant_id + project_id on every tenant-owned record. sql/008 gives each one a table. The four version entities BI-P002 names as immutable are enforced by BEFORE UPDATE/DELETE triggers, not by a TypeScript `readonly` — proven by direct SQL against a live Postgres. BI-P003 — RAW/NORMALIZED/CURATED/SERVING with the read rule enforced: assertReadableByBiSurface refuses a RAW dataset to a dashboard unless a recorded exception exists. All eight required declarations are NOT NULL. BI-P004 — DatasetContract and a fail-closed compatibility gate. Fifteen breaking-change classes are each tested twice: refused under a minor bump, accepted under a major one. BI-P005 — Arrow IPC alongside JSON, with a hand-written FlatBuffers writer so no npm dependency (or CVE surface) is added. Conformance comes from three independent implementations, not from self-agreement: pyarrow 25.0.1 and DuckDB 1.5.5 read identical values from both ClickHouse's Arrow and ours, and ClickHouse's own Arrow C++ ingests our 20,000-row batch with exact aggregates. BI-P009 — SiaxAnalyticsEngine with all eight capabilities. The existing ClickHouseAnalytics is COMPOSED, never replaced. executeQuery (analyst SQL) is declared and deliberately refused with CAPABILITY_UNAVAILABLE until BI-P017 and BI-P033 land, rather than opening the raw-SQL hole BI-P011 enumerates. BI-P010 — four purpose-built tables, not one giant one, and the difference is measured against the counterfactual: 48.8ms/40,960 rows vs 174.4ms/5,400,000 rows for the same question; 5/660 granules vs 660/1149. BI-P011 — layered isolation, each layer proven with the layer above removed. Without the forced predicate the same statement returns 50,050 (both tenants); with it, t_alpha gets 50. Without a row policy an unrestricted user sees 2 tenants; the restricted user running the IDENTICAL statement sees 1. BI-P012 — every named limit, fired individually and identified by ClickHouse error code, plus the half usually skipped: an unrelated tenant is timed while a real CPU bomb is in flight and must still return the CORRECT answer. Three defects found by writing tests that can fail: * `CHECK (array_length(purposes,1) >= 1)` can NEVER fail — array_length is NULL for '{}' and a NULL CHECK passes. Fixed with coalesce(); the test that caught it is pinned. * EXPLAIN ESTIMATE run under the caller's own max_rows_to_read fails with code 158 exactly when the estimate would be over budget. Planner calls now use a narrower envelope. * result_overflow_mode=break returns HTTP 200 with 65,409 of 100,000 rows and no indication it is short. Every SELECT ships `throw`. Verified rather than assumed: server7's ClickHouse holds `opt_scraped` (51 tables, 407,793,471 rows, `persons`/`cz_owners` included) — the b0ts plane this module is prohibited from touching — has no `opt_analytics` database, zero tenant_id columns and zero row policies. Nothing there was modified; every proof ran against disposable containers. Evidence: evidence/bi-p002-p012/{test-transcript.txt,bi-p010-benchmark.json, bi-p005-arrow-roundtrip.json,server7-clickhouse-probe.md} Docs: docs/analytics/BI_ANALYTICS_CORE.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>