From 9523f6da436489f8a40cd56e37d3c365c288beff Mon Sep 17 00:00:00 2001 From: jordan Date: Sun, 23 Aug 2026 22:42:02 -0600 Subject: [PATCH] test(e2e): verify ranking semantics with a content-feed app, and route three product findings The existing 32 checks prove the deployment answers -- TLS, auth, quorum commit, convergence, isolation, dashboards, backups. Not one wrote a signal and observed an order change, so VISION.md:17 "Ranking is not a feature. It is a primitive." was unverified. This adds a 60-item content-feed app and five assertions that verify the product's semantics, on a hermetic standalone node. Added - tests/e2e/app/: fixture contract (60 items, 4 categories, each owning one unoccupied 100-id embedding cluster), a deep-module harness owning the whole lifecycle behind startApp(), the product page, and an app:dev entry point. - tidal-stress/src/bin/feed-fixture.rs: seeds the catalog and emits brute-force ground truth, reusing recall::embedding_for rather than adding a third copy of the corpus generator (tidal/src/db/items.rs already holds a second). - GroundTruth::from_ids: the oracle now serves sparse id sets. build() delegates, so there is no transient copy even at 1M, and top_k indexes positionally. - 10-ranking-semantics.spec.ts (5 hermetic checks) and 11-ranking-integrity.spec.ts (2 cluster tripwires). - playwright.semantics.config.ts + CAP-016 demo beat (walkthrough 82s -> 90s). Measured, not merely green - like: index 59 -> 0, like_boost 2.0, with no sleep between write and read. - decay: implied half-lives 7.0007 d and 14.0014 d against a schema declaring 7 d and 14 d, recovered from a 4-second window via H = t*ln2 / -ln(v2/v1) and compared against the schema the node actually loaded, not a hardcoded copy. - ANN: top-10 identical to brute-force cosine on all four probes; self-distance 0.0148-0.0197 against a 0.05 tolerance. - rank: dense 1..60 on standalone vs [1,1,1,2,2,3,4,3,4,5,6,5] on the cluster. Three product findings, pinned and routed to @tidal-engineer - BUG-018 (High) skip is durably accepted and query-time inert. Penalty is fully implemented (ranking/profile.rs:227 -> executor/signal_values.rs:183, labelled {signal}_penalty at executor/mod.rs:65) but skeleton() sets penalties: vec![] (ranking/builtins.rs:62) and none of the 27 built-ins overrides it. So VISION.md:187 "negative signals are equal citizens" holds for no shipped profile. Same anti-pattern as the reseed defects and scatter_merge: a guard present on one path, absent on its sibling. - BUG-019 (Medium) three built-ins read signals this schema does not declare -- trending/share_velocity, hidden_gems/completion, controversial/dislike -- so those terms are permanently 0 and trending ranks on view_velocity alone. - BUG-020 (Low) for_you declares Scan{sort_field:"created_at"} but ignores a created_at metadata value; an order matching neither id-asc nor created_at-desc came back strictly id-ascending. Two assertions therefore report a gap rather than a success, written as tripwires whose failure message says what to do when the gap closes. The rank defect is localised, not fixed: scatter_merge (cluster/node.rs:7542) returns a merged slice without re-stamping rank while scores stay correctly ordered, so the fault is the missing stamp and not the merge's sort. Notes - Hermetic by construction: its own config, because FullConfig.projects is not filtered by --project and globalSetup publishes credentials into the main process that forked workers inherit -- so a setup project cannot replace it, and weakening globalSetup would destroy the fail-loud behaviour that is its purpose. Verified with KUBECONFIG=/nonexistent and all E2E_* unset. - Never touches the deployed corpus: skip is permanent: true, so seeding it into production would be irreversible. - The page contains no sort, no hostname and no credential; the harness proxy injects auth server-side so no bearer reaches a browser or a capture. - Schema comes from k8s/cluster/schema-configmap.yaml, asserted at 1536 dims; tidal-server/config/default-schema.yaml declares 128 and would 422 every write. Verification: 5 semantics + 34 regression + 10 demo captures green; tsc clean; tidal-stress clippy clean under clippy::all=deny with unwrap_used=deny; 2101 tidaldb lib tests; preflight 10/10 perfect; render 90.05s/2700 frames with zero empty boundary frames; zero orphan processes or temp dirs after teardown. --- .gitignore | 10 + demo/capability-inventory.md | 130 +++++ demo/capture-manifest.json | 17 + demo/public/captures/CAP-016-feed-reorder.png | Bin 0 -> 147837 bytes demo/src/scenes.ts | 11 + demo/storyboard.md | 15 +- demo/visual-audit.md | 55 +- docs/runbooks/deploy-verification.md | 27 +- package.json | 4 + playwright.config.ts | 5 +- playwright.semantics.config.ts | 69 +++ tests/e2e/app/dev.ts | 38 ++ tests/e2e/app/fixture-contract.ts | 186 +++++++ tests/e2e/app/harness.ts | 516 ++++++++++++++++++ tests/e2e/app/public/index.html | 204 +++++++ .../e2e/demo/workflows/feed-app.demo.spec.ts | 164 ++++++ .../e2e/features/10-ranking-semantics.spec.ts | 415 ++++++++++++++ .../e2e/features/11-ranking-integrity.spec.ts | 144 +++++ tests/e2e/support/cluster.ts | 6 +- tidal-stress/src/bin/feed-fixture.rs | 247 +++++++++ tidal-stress/src/recall.rs | 87 ++- tsconfig.json | 5 +- 22 files changed, 2307 insertions(+), 48 deletions(-) create mode 100644 demo/public/captures/CAP-016-feed-reorder.png create mode 100644 playwright.semantics.config.ts create mode 100644 tests/e2e/app/dev.ts create mode 100644 tests/e2e/app/fixture-contract.ts create mode 100644 tests/e2e/app/harness.ts create mode 100644 tests/e2e/app/public/index.html create mode 100644 tests/e2e/demo/workflows/feed-app.demo.spec.ts create mode 100644 tests/e2e/features/10-ranking-semantics.spec.ts create mode 100644 tests/e2e/features/11-ranking-integrity.spec.ts create mode 100644 tidal-stress/src/bin/feed-fixture.rs diff --git a/.gitignore b/.gitignore index f074d7a..c2b15b1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ target/ # Dependencies node_modules/ +vendor/ +__pycache__/ # Secrets (never commit) .env @@ -17,11 +19,18 @@ node_modules/ *.key credentials.json service-account*.json +.envault/ # Logs *.log logs/ +# Generic build output (no tracked path uses these names today — verified before adding) +dist/ +build/ +*.exe +*.dmg + # IDE / OS .idea/ .vscode/ @@ -44,6 +53,7 @@ tmp/ test-results/ playwright-report/ playwright-report-demo/ +playwright-report-semantics/ playwright/.auth/ demo/out/ demo/.cache/ diff --git a/demo/capability-inventory.md b/demo/capability-inventory.md index 4665377..3fe6d09 100644 --- a/demo/capability-inventory.md +++ b/demo/capability-inventory.md @@ -17,6 +17,9 @@ touch several. | `docs/ops/observability.md` | The four observability surfaces and the aggregated-status caveat. | | `docs/ops/grafana-tidaldb.json` | The 13 dashboard panels and their PromQL. | | Live cluster (`kubectl`, `curl`) | Actual image digest, actual secret keys, actual behavior. | +| `k8s/cluster/schema-configmap.yaml` | The three declared signals and their decay, the two text fields, and the 1536-dim embedding slot. | +| `tidal/src/ranking/builtins.rs`, `profile.rs` | The 27 built-in ranking profiles and the boost/penalty/gate/exclude mechanisms they do and do not populate. | +| Local standalone node (`tidal-server standalone` + `feed-fixture`) | Actual ranking, decay, and ANN behaviour on a corpus with known ground truth. | **Verified against.** Image `registry.threesix.ai/tidal/server:m12-admin-gate-20260823@sha256:6e220060…`, @@ -332,6 +335,130 @@ absent by construction, while the operator/data credential split from `388e445` - **Coverage.** `existing-green` - **Evidence.** `tests/e2e/features/09-operator-authority.spec.ts` +### CAP-016 — A signal write changes the order of the next query + +- **Area.** `POST /signals` then `GET /feed`; the 60-item fixture catalog on a + local standalone node (`tests/e2e/app/`). +- **Business purpose.** `VISION.md:17` — "Ranking is not a feature. It is a + primitive." Every other capability here proves the *deployment* answers; none + of them proves the database does the thing it exists to do. A ranking database + whose signals do not reorder anything is a document store with extra steps. +- **Personas.** Application developer; operator. +- **Primary workflow.** Seed 60 items with deterministic 1536-dim vectors from + `tidal-stress`'s own `embedding_for`; read `/feed?profile=for_you&limit=60`; + write one unit-weight `like` on the last-ranked item; re-read immediately with + no sleep and no retry; assert the item's index decreased. +- **Edge cases.** Position is asserted, never score — the profile re-normalises + scores across the candidate set (measured: unliked items move 0.5 → 0.0 once + one item is boosted), so a score comparison would be brittle *and* would drag + ranking arithmetic into the test. A freshly seeded corpus has no signals, so + every item ties and the order is the entity-id tie-break; that is correct, and + pre-seeding engagement to make it look livelier would destroy the baseline. +- **Permission boundary.** None on standalone (`tidal-server/tests/standalone.rs:32`). + Against an authenticated target the harness proxy injects the bearer + server-side, so no credential reaches the browser. +- **Dependencies.** `tidal-server standalone`; the `feed-fixture` binary; the + cluster's own schema, extracted from `k8s/cluster/schema-configmap.yaml` + (`dimensions: 1536`) rather than `tidal-server/config/default-schema.yaml`, + which declares 128 and would reject every embedding with a 422. +- **Observability.** `/feed` returns each item's decayed signal values, so the + assertion records *why* the order changed, not just that it did. +- **Coverage.** `new-test` +- **Evidence.** `tests/e2e/features/10-ranking-semantics.spec.ts`; + `tests/e2e/demo/workflows/feed-app.demo.spec.ts` (capture `CAP-016-feed-reorder`) + +### CAP-017 — Decay is computed at query time from the declared half-life + +- **Area.** `signals[].value` on `/feed`; `decay` in the loaded schema. +- **Business purpose.** `CODING_GUIDELINES.md:88` — "Decay is a type, not a + formula you call." If decay were a batch job, a value would be stale between + runs; if it were applied at write time, it could not change without a write. +- **Personas.** Application developer. +- **Primary workflow.** Write `view` and `like` concurrently (so both carry the + same elapsed time), read the feed twice separated by a known interval, and + recover each half-life from the two readings via `H = t·ln2 / −ln(v₂/v₁)`. + Compare against the half-life declared by the schema the node actually loaded. +- **Edge cases.** The interval is the independent variable of the property under + test, not a sleep that makes an assertion pass — the comment says so. The + declared value is parsed from the loaded schema rather than hardcoded, so the + assertion cannot degenerate into a tautology. `skip` is declared permanent, and + permanence is the one decay claim this surface *cannot* show, because no + built-in profile reports a skip term at all (CAP-018) — recorded, not faked. +- **Permission boundary.** None. +- **Dependencies.** CAP-016's fixture. +- **Observability.** Both readings, the elapsed time, and both implied + half-lives are attached. +- **Coverage.** `new-test` +- **Evidence.** `tests/e2e/features/10-ranking-semantics.spec.ts` — measured + 7.0007 d and 14.0014 d against declared 7 d and 14 d, from a 4-second window. + +### CAP-018 — A negative signal is durably accepted and then ignored + +- **Area.** `POST /signals` `skip`; all seven resolvable ranking profiles. +- **Business purpose.** A tripwire in the honest direction, in the same spirit as + CAP-015. `VISION.md:187` claims "Negative signals are equal citizens". They are + not, in any shipped profile — and a walkthrough that implied otherwise would be + the exact dishonesty this inventory exists to prevent. +- **Personas.** Application developer; operator. +- **Primary workflow.** Record the target's position under all seven profiles, + write five unit-weight `skip`s, re-read all seven, assert every position is + unchanged and no profile reports a `skip_penalty` term. +- **Edge cases.** Five writes rather than one, so the result cannot be dismissed + as falling below a rounding threshold. The earlier draft of this check skipped + an item that had just been *liked* and concluded nothing, because the + `like_boost` dominated — the target must be signal-neutral. +- **Permission boundary.** None. +- **Dependencies.** CAP-016's fixture. +- **Observability.** Positions before/after per profile, plus the signal-name + vocabulary each profile reports — which is how the undeclared-signal finding + (BUG-019) surfaced. +- **Coverage.** `new-test` +- **Evidence.** `tests/e2e/features/10-ranking-semantics.spec.ts` + +### CAP-019 — Vector search returns the true nearest neighbours + +- **Area.** `POST /vector_search`; the usearch ANN index. +- **Business purpose.** An approximate index that is quietly *wrong* is worse + than a slow exact one: recommendations degrade with no error and no alert. +- **Personas.** Application developer. +- **Primary workflow.** For one probe per category, POST the item's own raw + vector and assert it returns itself first at ≈0 distance, then assert the whole + top-k equals brute-force cosine computed by `tidal-stress`'s `GroundTruth` + over the same corpus, from the same generator that produced the indexed vectors. +- **Edge cases.** The request field is `vector`, not `values` — a wrong name is + answered 422, which reads like a malformed body. The response field is `items`, + not `matches`. Each category owns an otherwise-unoccupied 100-id embedding + cluster so the corpus has real neighbour structure: `recall.rs:94-101` warns + that uniform-random high-dimensional vectors make recall@k measure "impossible + tie-breaking, not index quality". +- **Permission boundary.** None. +- **Dependencies.** CAP-016's fixture; `GroundTruth::from_ids`. +- **Observability.** Returned ids, truth ids, and the self-distance per probe. +- **Coverage.** `new-test` +- **Evidence.** `tests/e2e/features/10-ranking-semantics.spec.ts` — 10/10 exact + match on all four probes; self-distances 0.0148–0.0197 against a 0.05 tolerance. + +### CAP-020 — Rank is a dense sequence on standalone, and duplicated on the cluster + +- **Area.** `rank` on `/feed` and `/search`; `scatter_merge`. +- **Business purpose.** `rank` is the field a client paginates and displays on. + Duplicated ranks silently corrupt any consumer that keys on it. +- **Personas.** Application developer; operator. +- **Primary workflow.** Assert `rank` is exactly `1..n` on a standalone node. + Separately, assert the deployed cluster still returns duplicates, with the + root cause cited and a "delete this tripwire" instruction in the message. +- **Edge cases.** A limit of 1–2 can be answered from one shard group and would + show no duplicate; the cluster check uses 12. The cluster check also asserts + scores are still correctly ordered — that is what localises the fault to the + missing rank stamp rather than to the merge's sort. +- **Permission boundary.** Read-only against the cluster; nothing is written. +- **Dependencies.** CAP-016's fixture (standalone half); the public ingress and + data bearer (cluster half). +- **Observability.** Both rank arrays and both score arrays are attached. +- **Coverage.** `new-test` +- **Evidence.** `tests/e2e/features/10-ranking-semantics.spec.ts` (dense 1..60); + `tests/e2e/features/11-ranking-integrity.spec.ts` (cluster `[1,1,1,2,2,3,4,3,4,5,6,5]`) + ## Intentionally excluded | Capability | Why lower leverage | Alternate evidence | Owner / revisit | @@ -351,3 +478,6 @@ absent by construction, while the operator/data credential split from `388e445` | BUG-002 | CAP-008, CAP-015 | Medium | A metrics scrape either returns series or fails loudly. | `wget --timeout=6` returned zero lines for `tidaldb-2`, indistinguishable from "this pod exports nothing". | Three retries at `--timeout=10` returned metrics every time; peers returned 332/344 series at 6s. | Client timeout too short for the payload on a loaded node — a measurement artifact, not a product defect. | Harness uses a generous timeout and asserts baseline `tidaldb_` series are present before concluding a specific metric is absent. Runbook §9.1 now specifies `--timeout=15` and explains why. | `04-network-isolation.spec.ts` and `09-operator-authority.spec.ts` both green with the baseline-present guard. | fixed | | BUG-003 | CAP-011, CAP-014 | Low | The boot log reflects the current credential state. | Boot log WARNs `TIDAL_ADMIN_KEY is not set` while the gate is live and enforcing. | Pod started 05:41; WARN at 05:42; kubelet materialized the projected secret at 05:51:43; poller loaded it. | Working as designed — the credential poller exists so a key can be added without a restart. The log is a point-in-time record, not current state. | Documented in runbook §9.2 as an explicit "do not trust the boot WARN, test the behaviour" note; the harness allowlists this WARN with that reason. | `06-logs.spec.ts` allowlists it with the rationale; `09-operator-authority.spec.ts` proves the behaviour. | fixed | | BUG-004 | CAP-013 | Medium | The backup check verifies the fleet backup. | Selecting the newest Backup by timestamp picked `restore-canary-longhorn-rwx-*` (20 items, 1 PVB) — it would report "pass" having verified nothing about the fleet. | Ran the runbook's own command as written; it returned the canary. | The selector sorted all Backups instead of filtering to the schedule that the freshness alert actually watches. | Runbook and harness both now filter on `velero.io/schedule-name=velero-fleet-daily`. | Re-ran: selects `velero-fleet-daily-20260823033025`, 3708/3708 items, 48/48 PVBs Completed. | fixed | +| BUG-018 | CAP-018 | High | A declared negative signal can demote an item. | Five `skip` writes changed nothing under any of the seven resolvable profiles, and no profile reports a skip term. | `10-ranking-semantics.spec.ts` records positions before/after for all seven profiles: identical. | The `Penalty` mechanism is fully implemented (`tidal/src/ranking/profile.rs:227`, applied at `tidal/src/ranking/executor/signal_values.rs:183`, labelled `{signal}_penalty` at `tidal/src/ranking/executor/mod.rs:65`) but never populated: every built-in profile is built from `skeleton()`, which sets `penalties: vec![]` (`tidal/src/ranking/builtins.rs:62`), and none of the 27 overrides it. Same shape as the `scatter_merge` defect — a guard present on one path and absent on its sibling. | Not fixed here. Adding a skip penalty to a shipped profile changes ranking behaviour for every consumer and is a product decision, not a test fix. Routed to `@tidal-engineer` with the inertness pinned by CAP-018. | `10-ranking-semantics.spec.ts` asserts the current inertness and fails with an instruction the day a profile applies a penalty. | routed | +| BUG-019 | CAP-018 | Medium | Every signal a profile reads is declared by the schema. | Three built-in profiles read signals this schema does not declare, so those terms are permanently 0: `trending` reads `share_velocity`, `hidden_gems` reads `completion`, `controversial` reads `dislike`. | Signal-name vocabulary captured per profile in `skip-is-inert.json`. | Built-in profiles reference a richer signal set than the deployed schema declares; the engine reports the term rather than rejecting the profile, so the degradation is silent. `trending` effectively ranks on `view_velocity` alone. | Not fixed here. Either the profiles should validate against the loaded schema at registration, or the schema should declare the full set — a product decision. Routed to `@tidal-engineer`. | The vocabulary is recorded as evidence on every run, so a change is visible. | routed | +| BUG-020 | CAP-016 | Low | A candidate scan honours the sort field it declares. | `for_you` declares `CandidateStrategy::Scan { sort_field: "created_at" }` (`tidal/src/ranking/builtins.rs:49`) but a `created_at` metadata value has no effect: an order chosen to match neither id-ascending nor created_at-descending came back strictly id-ascending. | Probed four items with interleaved timestamps; observed order was id-ascending in every case, including numeric-epoch values. | The scan appears to ignore the metadata string entirely and fall back to entity id. | Not fixed here. Recorded so nobody "fixes" a monotonous fixture feed by writing timestamps that cannot work; the fixture contract documents it at the point of temptation. | Documented in `tests/e2e/app/fixture-contract.ts`. | routed | diff --git a/demo/capture-manifest.json b/demo/capture-manifest.json index fb33918..16652df 100644 --- a/demo/capture-manifest.json +++ b/demo/capture-manifest.json @@ -40,6 +40,23 @@ "audienceVerdict": "perfect", "auditStatus": "pass" }, + { + "id": "CAP-016-feed-reorder", + "capabilityId": "CAP-016", + "testId": "workflows/feed-app.demo.spec.ts :: feed-app product surface :: CAP-016 a signal write reorders the feed immediately", + "file": "captures/CAP-016-feed-reorder.png", + "expected": "The same query, before and after one like: the liked item moves from last to first, with its like_boost visible", + "businessPurpose": "A signal write changes the order with no ETL in between \u2014 the product thesis, not the deployment", + "personas": [ + "cluster operator", + "application developer" + ], + "width": 1600, + "height": 900, + "contentHash": "sha256:ede7ca3fab758d46cabdf1185f6645bc3055c7e8bbb25e380f8cff509d2136b4", + "audienceVerdict": "perfect", + "auditStatus": "pass" + }, { "id": "CAP-008-network-isolation", "capabilityId": "CAP-008", diff --git a/demo/public/captures/CAP-016-feed-reorder.png b/demo/public/captures/CAP-016-feed-reorder.png new file mode 100644 index 0000000000000000000000000000000000000000..8f2248158dc20445f3a2394a9683b774fb5d1064 GIT binary patch literal 147837 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 zዖ(~jy5-QcAl(Nu%s0(Hg{W+=}0ADd@N&2GjeFUa6Zhvq=L z#OLmVNj_Z$O;V&=__q%$#4|W+S<4RYy61|`GOCBUnw?e#3PacegWLsujR1K%4_Dr^ zfG!_?fl9%j5h@zOM*MD*`kZIjT{fg4VUW*=?>8i}uW%n2_U4O{*M@eG3oG7>Z6Y>K z>;%q}&Je*nR?>0uO5zjhLi3+E_kF{j>PUV0QXU_1m)QWm8kr8CBw>nu^g{DiE}e zYq7L$sw?k858KZm3J)ATK6mCCvnJTvGWwq{Lb@HNQSMLVkIdvzz3=b7TxZ%X_ukkM z?jz4gXW~Q`n^F}X^@TjWa7UzhbgC6NRjXEsJa zgZH&2S|OEx1~D9#nZ9QNY&1U`+*k$8M-31kXE)D-O+w;|sMGE~h6poIbMBbAg;%c( zWRYE4pYr#oW3rFfx-A|$73Bi>0FE`J{rRHG8w5C_?5gmAtuNv9EIL%+%^A151 zizY2;gUrqHhu#63vzI=$UhBpH%k@n~#zLK{Py$e|N*Z{23kYOz&4&DL6dCgY=x^pL z?sAa4;D%cPWKi3ka@T^NzhJl~K9h^DPAaZV2CAjuJWfe&@kw0FVwt0mT?~g-S?r;2 zT7lF^#cTfL1n<8S}$GdY*h>Atwj)s?491Pif17i?bZ{)U{K zAwfna^p$vE;46#aUMWIbGE<;)8#ivoumyNRMyg@9(D7yGLmHCuArt3nlz5s@f0BAO zJu7ceIX^r|$%P?LxkLzG{%LpvdDH0dxRf5S2J{H)@=BSQ~y@ZZ}iUJyTYK`{J}Y;Z=a9mUyI#DGZOl6 zE4#q!-Z6yL(VoLLKFI4j@XcegG1XXc#TCsW!hHJ=jDxqfpaKYGy4W*hdW_BH-a|pq>hgL%&rGf>!@@^~;MoPyxzg zY>;yCy`k3r`=jjH@!H}FEHl$K?|bEHlKqF|+;{}|1t&bmr_r&&?(FJCS~i?Lg#rTu z3ea4cvr1jo>rv$$^AV7-VjT_*YFfX2+K;q~gOiIelFf&7oAP-WovL|T-xeOnnCiA9 z4tmDD{8xc$@x~fWoYsf^zeaaS`1!B9&3xPGcjcKZn&sg;#&xY_`Q-7zJal1x@z3I- z@$dl^v1vFZx7*C2GQ^QLl14-YbRI_f4&WN;;N%7(A$Qw2&VyF8!XibScfa00_zr)C zF+e;#r-+t&jiq+w*m)uH(#r#}a})6;_LmtMW|nV&x#LzC*tj!{g!RZi=E5~T#kVN- zAQIF+f#+(tuy<~2Zha7e@!I(#4>ZmfPW`4> zQn~iZ{^We9(7qG*H6VC|G1NoD04%&+oFcVG)X3A&*qJsp6rwd^(a+#+?Lwr?#nDyB z1!=2n{1$(tH}xo9?*VA3(X+pX)Qn?xxUHNiffQbPnnq*T;sh-$EHqi!Wvj~v8AK6S zk_lR`ma0m+g^8QUuv@N2z?C;bmDfj$Zv3OyEc8MJ*49_2@_`r4^T>H5V|svx$M7bG z#)-^0?uUE_{CFqrF2*~lnKL|o1C7P$)N$SB^gu@Y=t2cS(-{61_jZ-v`AWGlYf!d+ zQEWIv;CBpJMuH0+s#l%+*pBBw=EY`9Dyxiz`R)pZULe-eboa3s`Jo@217R@Hi6nY`mIl<@YV z{Xcny9|=*?&9ByICgMhM2CwC2MJlD?=L?Ysb20H(pNKeI-HIlQ<8pN)QtFh5;Z|=7 zrRn!~OGt+`{7(AWT}k5QHLbog!1je4xboJIvmfGKUOrq22O=Hm6)Xtk=O2mBy=!w zf>uR)BJ2Gt_=sdK(|t~|7~t#85A_iK=UH0ETe^R4k zr?!4&->M@Q`))V`ec5@j$>Q-nhC!yGaDB8Br%kK0&4jnE)yST-e|R|c_BJU)<92g< z`|HlDi14t8WMQ+1O}9=qT-7rlzb20J)Di4Aol2{avVm!brSySUO z1R)u<)aySB;S&*UY;A44a!cijNzcJ9_jRm7*aVz{R8b_Y`zUoJ9lJh*9r{MI@%gUM zHa~3S%W++sU&)N_^Ckzk;CbnvdBiyZivD3u)5jG{bC*4##=rs|`^kXvCNdxQ4&HOn z++lAiEQ{m9;x$QL4iIuyVTcpy(YiEp6cnMH!B-P&J-BxC=vxXoz}L%E)>1W z&R2IsgM0L;d)!Kf(H-Az<^4+OnLTokz7cbLqpH9pik8@GrM(`oSZP{sX~kaGEZ zg+B@z+JEqR?eIz!XJtB4aNfFlcidKelvPUBfy_5sO7%F;vGAo zNrMx`p^?b;5<^)7xesfuCxwpqaT+0Ux41>r{vN-P+C^)p6?G_!o88rSH-(HpK(bCD zjk!B*bv-rucpdi1CZ=ds7gg1R5!lxZznOxhA~8b{@X{NtI!hZRoIc#!*DaKN(U~7f z+*|d41i0N(>>L%~Lhewklj9f_r+aY-VGaDWZWiW@%AQNUc^5eJT3 zQfRVK&bN%-Zqjw>moz9Q-55gFWrnk>!m>|H(QHfjRDcKqO5LC_=X3uA0C+scejMg< zAp8s6=X`W~RGC0EBW{8 z&P+eF9(lvUU28(hLqHJeMnF*ia@AE`KOZmf9gX2qs^g@!f;zf;>RIsG=3-_|O>V!0 zItG;o>Vb+4cV!%*f~34rV`F17q#z4a;p8svafy%W)8NjvW3H4Np~`JQQayx?v47gi zrczo$GgV z2`^0ztpu|zF=T8e@tvCS!aQ!Q%L+I9K0L=Y{C&}OTC?sqb*BX)vWvR2Hn{kjYHDUT zS35j%nZ(4ddG_=@9h`HkB{9WiFT)j{)L#dkhtSKK+vPB`F!08oXAJb`XTF(ET8cx-Vat}(*Idh9j0N2-mZRV)9AUOw#A$+_oJ>rz=C`-%{NZ{8aeurUw=0CtAU!) zW>G$Ov1?vW;k9gu)48Mr-_(?w8DR^9VC!eb_;|%#s2&s3Tt`QHJuq>G-&>EiV16KnECR?#9AXyc9|qPHKE2xJ71 zn5iS!Uilc~(L{W*MN(qUqN(-U{2|szW7XHy1NH{|ll)=Z!D8Is3uN{T(65qJ(qeNZ zYI&;DCbf0qt3^Z$m`WwQy2(;T5@ajLI(PJQGGI~|v&P&E`+tnlV#mT#Op6U`QppRC zf({6}UL53Ou>2fiR(Fn{_|7<-lB-XKBTxc;B=SwW$!;|6g?SZzI8fK=Qx77T5oGFRXE5odqk=zWPZk z3W;NT&heiyDLGrmt7xl~GP7vJGm`mxeFNH|`mwYGsSS;`klHGGx#QMc*0l-+X8(CS zbCZvfHEOtk7cwEh#w3nWN??)vy{Uin5F%%7Rzlxz@!#jprA+I(#i5p=j}r)eFp+)j z=rK_;{utB0|Mu4@__ZZlw!c#q_Mp=CNoD)k4vI34^wWQbghMQT3{!e4qF({^$7-Hrl9F>hr({(rJpH2>Xo zrJKu#5UF=7$N%V+)7AYRA&Mxj$<-=jF56^I(97v5B>0T}hK0AR&3gU0F zLbZ{3OnjSOa&U55th~F}-VS^AcL-{!UHw7u4I5XTh^ikDHA+@Z+JvLW*+iaS;saUQ zB)E$SKDT_w!pj_*^}Gb!1as(7<8RR#3@bGM5e)F-0lB(51N;t`^f~P_ zA>Sq$RW8%u!0^7~+qYU9 zjri2lxn0+Q0DPu2CPv2NNoe)W8O0l-kW~}lf2>@`=;e|XWXimTmrsv zM8}~UD7Ylo=cSyptBI2-Eb9MQN%MzB9eU|*(m5CEgOB|6TmgK5vO+YXw+TD@L%x=v1H~T?LDi zz22@^y9}7)djG`)a@iRrf^V}Z`YxeRtvD`j-Ls7{oTzk<4#H> zRd_@U{kN;-97O>); zo!?lv5K&5qBcL1!W`TNlv-v)F0I0XSIcTbA)4c}9E-&Xu67X72%=Gl|^6;3$YWmFU zJdfjczGLINIl1N2ZXYZRxG!C`QA}4rVm}DpcN`kOw~CP|3RP^l8p^ImnVYLs_b++h z_-LJMs(pU*90~APH@8J1wK*f4>BvX?C31{=l*%|c#^5aTFxcjB8;_TVNAct?JdtfQ zX11XiV=z}0EV&E48%}t5c=vaE!%M#Uvn84t(@Wm)2Hop38b13!HUbfDpYIpBz;ku` zBDd?RwK5BB_KJV?g3E0`;V-qE+BD zLU`hvifGQ=E?WF{cfwYJsNMUl9H~;2y9^s1_S;8M%t-U_)N%P9#}!2qL=x+aegdSM zDk#p7@q)ON4BwX%ppS`Y$?0kKg19VblJU0X zLL@hOP_$F{>eTt8q>mZHa8+GhI*04^Pvv3#hUe0$#x@qD&!0=hdR-jI(pa_VziS-p z0WE+>dd5_4x&IpFUG=V&<}-7iCgG_kR3`0yghN*TjoHloBDd(~%a?O%-GOp;Ib<8l zD{HW60P1d-FXXN)o05_9J#fY51njbR+jg;Jd}DGe|Cf~27R_N; zvx4Y1xinZb9Q}IOghV1cMA0u$M57Ul6x^Tz)*snSUiRcfqA1er?7u=WOe^HUgl8o- z|A@^5sdWtxv$3$mQ!O@njz@O}_YbKnDIIQXU2l1KDy zzk_v5Pgrw5{>(OshL?aCKZ8dF?yXW5BiCAz}~E&Kaav?@NjJUCz+uQp$_HRkB(1_s;DUy^QE#yLY9 zp@Cd@dD7Ge*JO^UMg4!H1SU>UJY@Fw+CfY*p<0`IFCor}tQD<|?U#;s-vY&g@wbwE zy9_$YE}ei?2aDVUAL%}*_-`QB<)oj3w~X{`nE?zRd4tE^iK$I++!*kv#)qP%)hC)g z@nFwIA+KMDw0XuFaD8%{M`NoZ<-;UEAkYShidlTdR|y!JwO&OLana%(Xtl-v*(8iBJ&VcC&9#-> zg!1#lR`TBt2I13ud8Diy-4jU-O>{-ADUxbJKLc>UYn3G>Of6d*Tgus8Fm-(I=HYaq z!R@BF7>Il}2nOTl+pVU!rhooKoskY7?+1pSSzR@m-5BWz+i=?a!xCU!DYTFm@8I6WPe=5pBHTEA#u3IvXq zmE6_+S!i-^Y-n^nm^Zt-o!$XDIdS|AMwrGOAY6%cXCvUU-AhK!HR$I$lpo;-XcEx0 zVC^^Yy4aC@6o5}rRgbgS=o+@@{A_e4X>@JI&1Ba!p;3+Lmsj$aI@v);GTd| z;C!ywm5VL#OWR#Gs%h6kj15%|0&!VQr$6$!IWLIniK5O@+rF+XQ443!)d62n=^VlB z2Y^|DlmNH5(F^}Y_pJkT%{$Fbs&6(L_Y$#@l%p4u+$VY3srAUr=`glI0(=`*znaGh zSB$8+rc|n#I^gh@q3_^C%;__q3N19wv-!h|{50_SECP{`=CzXDn(9Un?7VeuN6zPV z`h?CA*%CbGZVUVi3G4=vAg658+w8z(#3pigwA2hh$~)Y{%1j*cE#(LH9~xTsl6A-1%vEMA5#Pu#u@jSW~U$nfr}vybUFWnkz-#$HmF zDzr%plobt5CoCM_gKbudVn3=g#z#cpm;IbN-XP6@uv9PVpYn9;a>4p$lMQMb&-VQ8 zeHK!xWH}8tFkZ4Vm~f2jy1Tchr+#dIRCq?IOhL16N?7IqQu048fUF3}WOjK8>v(*1 zb#+P+f(Z-?IW2ByxT>3#$i>=NDx*$r1ae?Nj+~x_rNxH{bD!;h&m}JX6b;OxXEnE0 z@A)bGzDLo1T-c5ikMLx^F1K(}onK1d_rCq852v~dqi@?OC9ygcy1$io`s%WUK ztsR31<6Kr#^!JwU12P&`_wo-O1UwFW96zM4icii(&%jdVqRbV-J{O=CuTB!y9Y?dn z7*>-#V#b!m8>6&A^)d=*r=s?JFPo|McCue zDDN?%)`panH1Q;JdYVo)!Hd`Olgo@!zD7uwifSS?cX4!E#{KaId{{EQ;51Y?l8ooJ z7m}06X6WYXs$1{2A0>ir@I2X+v0&C5{AijlkgvpDt#VYLQK`4xMU`3Z&>+~V<>i;; z+*bQu@ePp_z7YG43!^yIowXFZJjU5^{|;g39p>f!>;Dbn6=bA-T3Cl9dl&b&4wsZV zNlQOfoeNh~_y^11qW@m_vH={GUZ9wvcZu-6mW8!GlC(6e>LnShl&UNDB7!d>SYU006`x ztJQ46BCfl%j}D)8vi!hurOQR(#rlPq8@`)GA9SL1E*eFSr)5njB>$$hyIFR+XZZA@ zsez+ll&}v!rGmM6zG#+|f#l!uQ*2QM5!)1Nk;S%pzvX^s8YD3+O@2vYdvt zYKM{fNvyTq?w0u7U45+4t`2$mk>frnB9(Xa=GY9rHJG}E+A@T+H#n{t`0dxmepG~D zNTuu)wfCBu)8XOa7+7w zq!IjHr=MDu!o$F3L#48#r2Qn+!cN3=dfiT*>sQV<_``Xw?iUTu^H94G$F&a`en*o9 zVQt$pkm~q&b+>aNRU6{4E}gQeHsi{Y)U8%3zwt^X<{~R&g2WP)g!9*ejGKtO(+OIM z`zOQ(5ed%@H zV=3LqY52R=Zcd}|uBN&=6MvLv#$OVah(ub1AkIm!88Jy%eEOmB!ZA{Q-||KFX)d;DvLaA`KWC z^+XyhWTRTe3Wj`RIr7DB>Wbt<3xJxCvXx~<@hZg!%3bZEP#HP^&}mC!%S`9=ir{Vz*fc<=3vE-fvMAwm(8 zkZ?o=cCH|10h=4 zNb(=zP*DsMkq2V4To`x@8mZKh8TD8jISk1%7o}z7Oqo9yKRD^az}VPaBdmKm!RW5g z2bF3Z6u~OuFCLlkHF-5Of)M~d2%C+~+_zi3wF%+x>8cFzSDyUl32&nNgQAiVT+GeN zQehZ*vzku+wEqi1YLTr)ec166@oY?qmx3%*RVUl1N2;v!2A?X?`{N>qz31T(q4jvy zURi4V{Hs-rzGl?fXdd5LKVt02nv!yJb4OoA#kQ0KQU}4arhFHzy`$)lqga#MmI1zb z%yW3e#-JRBE_uISSEYWf>Rlb#rmL@M@+PUwEuekKlu9@NM0+ly0=_zq8TlvX!g>iYj<>Fuu&l?;bc6Hbs zZ)IWSviZO(*zw_G(a=NsDfUg5cUd!?IRT&PO_`62mM|)j8WE;6Qw!AkZ2R;;GbW{7 zD;QXD;3GD5K_a%=%jJ<@j?M^od0m(vyLnTKu(5$VQ_QYazjeOmHV(?EE>Y2z+3HKT zqrjwqm3Xhw9kOaFl70rL(fbN+_{>N(@N341#(rELooMu>_371Gu-l~7%6&}#McdOB zoJb;BTH6N#ks@s9uOf9pIDZRi)44PJ?)V>JnJqK?z^ctOUV9F}k=(S(!b6xX`S!N< z=WDJfqDT!}9rE=&`@OnMijH!0%37UPnP}=OUx+?)0i3MY;sUhpMEbhLKbsLtVrRmf9sv3SF<{AL57V-@^!F+8g^j$OaeA*e1Q zJNDOR*{qMQd!UVS_sfuO>aNg0`F(40!*XvZTNp$9fgfkNo?wr!F{(h5Tkt<8$RBf( zYBR;Ag-M6g2H<@>hT$H`uUl1ECr)r)mkd2yfpwJQ!PnYQddhmkTh~*X`4O z=%`X8ESwoyo8El145#J>gh-FQ^g_nj5XcnmuP2A@? zE;;#wP3M%o8%LIM$Ot&hI;h5S93pG)=Iv!YmVng=DDkYaRBeUUWmpkgK3 z{OIoE?024{`69mN7b^EzKr@Z0@RdFcgX}$EY>gvM0sJtS{lLsBguOG{D}qd9;g9lt zcyV^e!xPyjPpo5nP~}x>eiNX@hWS=mw&ZwxeA&FJlaGX|^CXL`--NLr^F~3*sV4a1 zBK37-vJH)Zkgqhivorj}>l-5si7_r=A6)kUaqL{mGCQ3=!#_m?%Rrv}no? z3CZ;0rVcqfOLFqXa#_VJrLUbBbrtdjEhv2vjU8odQ>KMIVO3Pa^P z3?ac6b~#ECB~vp9fD4L~_DEwM^1aC$P2qOH?ynO58|LwKN3qHbIA zj&Z(DH40OFqewYB-YScl+J8@EQUO`$(a@{XV$M6=z23mn%pN|c;K)asmuer?##Yb$ z+WO!~&Bd_caQcAwu?of1?N@aI0xRjgy!C9mtphIKy&!dhp>hnUX7U`#P)kgI^g8XJ zo~BKyBp##f(9m{7y{L8Qfg~Ei#yh#Tf0L`B-=8V5%)hX03#soe+CH~KRvT~Ntn z$$eI;j3}dG33%8#KW}S68YK~tIy!oEM3~)yfhD%TcwJ7{F_=V?SJ6gHH@A_=7JL6! zJ8W4q6hdw8@d8JwJx}0Y9jFE*~BdUn6H4*xw5U4$+|rRvj!a7>SRC zD0sLIpIS~sElv*4k%@uujWK5Gbm1HKyV%$?@h!!OR%W$7G8b*HF|x<8Fzj;7R(lyq z$`k(QW^S28)IS)e2&T*4YA<4IyBfQ_=$I4NdhL`=YUpFJ$rOei@w=CI{^fL*O~#A0 z0(oDnw-LzOWnb2WNaO2g)bgsHgzs{lFgli#3&VuoFx;e%IVvTXZa#sT>S+haT7=7FTOd;N;GXjE_5{W1EvD+*9eJLD4J%U_G%*QU+iT(%?L8wU{%+RELM+o(!i6O3^$b-vuvYrexs@ z8X85L!Szv=#FQ{Hfma*RnZxp$0JLq_~l# zX%}gz8l&siiZ3-{Luq3d7Ysx@vSZYl)j7k<$~85A(OAavVb?o|M#-5yncRc%|DZO# z`oO`%!}C1--5q^boATOb|P!Saa4 zgD>(nUX}k6%>EYC7+yR$mq^=a?Uz^+Z-v2~y3Da%zhUJ5QY*44%J<&2C>8wOoMle= zb;#FEB(l|J&#RGx&}tMmGyXDTn1bCcCANC++;1wy%Rwt(Hu#Z-VMzAnZP9yVSW8z~ zTtr&yAA;acNUd`wseWNWev)@#W$?;*XXyZ!mnJd1b`pH9ZJ@xjpBHRXQ)}@|K4XSE zpLU^taEQw`MBmX-8A?%Q%0ZE)A?r!fB%u=-zp~jNlZYu^lE3<`QL#(T4e{Ubbq*NrNMMavCmPRS*ZUO1;ZVb9aS~`dB9)=W=9(^*q1d{O9G&X3w6z)?Vwq?)$Y2jfR=CU2^E}99yQN+5K3PU|VUNEtfHI&MHc` zBFR=Wk}6NuGRlFLQXU-GKW9y_qqm|j8agQx(1V`FSg zDn9AcIl?Zuaj>IR8^lg9vrjLp=n5)-h6EjaFC(TAJanWTh z?&puK31_=!St)#=BH$_a@hH296h9&)T=L-j{pjvzqaLCzEVGENLdzx5Tl!_uPa`ri zJe8zmLp4paZLd~hc-@1%sLkKr0wSvrgDJ2+hGz(P4oL=$C; zrkszCaHJa_*%)`x8+C=tY{qvw?ON9-e|p5bwWchp?Z~s84%Z@fUx#6jta3Q2BV?0L zJa1&@`qxipQ`3;VSmuG%>%v``Y0j+y#K!&tB9|H+6HT9`SU^rXp@9GE*NTGqD^UlB z!>A=czc@$|7?5XrB_<}TK>hmCMj(BT@0AH}i6Jv=^F5mMtau#ZjtlJM8k{6Y`$q

a}$CDpcU-$3!y*W@7WRN%r8ed>QEYHcY3<(R*6Z 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 R-#6pmi^vEU3+a9QzX1C8;Aj8< literal 0 HcmV?d00001 diff --git a/demo/src/scenes.ts b/demo/src/scenes.ts index 8d39aef..8d22141 100644 --- a/demo/src/scenes.ts +++ b/demo/src/scenes.ts @@ -76,6 +76,17 @@ export const scenes: Scene[] = [ caption: 'Same path, three credentials. Then a write a majority of nodes acknowledged — the one claim a health endpoint cannot fake.', }, + { + kind: 'proof', + id: 'B3a-feed-reorder', + seconds: 8, + rung: 'need', + capabilityId: 'CAP-016', + captureId: 'CAP-016-feed-reorder', + heading: 'And the product it exists to be', + caption: + 'Everything before this proves the deployment answers. This is what it is for: one like, the same query again, and the item is first. No ETL between the write and the read — a local node on fixture data, so the claim is the mechanism, not the corpus.', + }, { kind: 'proof', id: 'B4-isolation', diff --git a/demo/storyboard.md b/demo/storyboard.md index 9f21093..236ff5f 100644 --- a/demo/storyboard.md +++ b/demo/storyboard.md @@ -14,6 +14,7 @@ the rung it serves. A beat serving no rung is cut. | B1 opening | — | need | operator | Orientation: what this is and what it will prove | Title card | — | — | "tidalDB deploy verification. Three voters on `orchard9-k3sf`, one public endpoint, and a runbook an operator can walk. Every number that follows came from a command that ran against the live cluster." | 9 | | B2 convergence | CAP-002 | need | operator | Quorum with fault tolerance actually exists, rather than being inferred from pod readiness | Per-node `lag=0`, no reseed, agreed leader per shard group | `01-cluster-convergence.spec.ts :: every node reports zero lag…` | `CAP-002-convergence` | "Each node asked for its own view. A pod can be Ready while its replication is stalled — that is what the reseed livelock exploited." | 7 | | B3 boundary + write | CAP-005 CAP-006 | need | operator | The strongest single proof: full stack works AND the data plane is closed | 401, 401, 200, then 201 quorum-acked | `03-auth-boundary.spec.ts :: a quorum-acked write is committed…` | `CAP-006-quorum-write` | "Same path, three credentials. Then a write a majority of nodes acknowledged — the one claim a health endpoint cannot fake." | 8 | +| B3a feed reorder | CAP-016 | need | developer, operator | The product thesis: a signal write changes the order of the next query | Same 5-item feed before and after one `like`; the liked item goes 5 -> 1 with `like_boost 2.000` | `feed-app.demo.spec.ts :: CAP-016 a signal write reorders the feed immediately` | `CAP-016-feed-reorder` | "Everything before this proves the deployment answers. This is what it is for: one like, the same query again, and the item is first. No ETL between the write and the read — a local node on fixture data, so the claim is the mechanism, not the corpus." | 8 | | B4 isolation | CAP-008 | need | operator | Least-privilege network access without blinding monitoring | Refused from a foreign namespace; 332 series to the scraper | `04-network-isolation.spec.ts :: a pod in an unrelated namespace is refused…` | `CAP-008-network-isolation` | "The refusal is the point. A policy that blocks everything is an outage; one that blocks nothing is theatre." | 7 | | B5 authority | CAP-014 | want | operator | Blast radius of a leaked application key stops at data, not cluster topology | Data bearer 403, admin bearer authorised | `09-operator-authority.spec.ts :: the data-plane credential is refused…` | `CAP-014-authority` | "403, not 401 — the key is valid, it just cannot remove a cluster member." | 6 | | B6 dashboard | CAP-010 | want | operator | The first surface opened during an incident actually shows the cluster | Cluster health OK, reseed none, 33.3K vectors, populated latency charts | `05-metrics-dashboard.spec.ts :: an operator opening the dashboard sees populated charts` | `CAP-010-dashboard` | "The board an operator opens at 3am. Health, reseed state, corpus size, and per-node latency — scoped to the cluster." | 8 | @@ -23,7 +24,7 @@ the rung it serves. A beat serving no rung is cut. | **B9 the drift catch** | CAP-014 | **dream** | operator | Verification that audits its own documentation instead of drifting from it | The committed doc text, the actually-running image, and the live 403/200 probe, side by side | `deploy-verification.demo.spec.ts :: the harness corrected its own runbook` | `CAP-014-drift` | "The runbook said the credential split was 'not active yet — requires an image roll'. On its first run the harness read the live image and the live secret, and proved the gate was already enforcing. The document was wrong. The harness said so, before anyone noticed." | 10 | | B10 recap | — | need | operator | Restate the needs as met, including the gaps | Closing card | — | — | "32 checks green against the live deployment. Two committed features are absent from the running image, and the suite asserts that absence deliberately — so the day it changes, it says so." | 7 | -**Total: 82 s** across 11 beats. +**Total: 90 s** across 12 beats. ## Ladder coverage @@ -31,9 +32,10 @@ Two-way mapping, per the audience protocol. | Brief entry | Served by | | --- | --- | -| Need — evidence is real, commands visible | B1, B2, B3, B4 (every capture shows the command that produced it) | +| Need — evidence is real, commands visible | B1, B2, B3, B3a, B4 (every capture shows the command or the surface that produced it) | | Need — write proven committed by quorum | B3 | | Need — every boundary shows its denial | B3 (401×2), B4 (refused), B5 (403) | +| Need — the database does the thing it exists to do | B3a (one signal write reorders the feed, immediately) | | Need — what is not verified is stated | B9a (evidence on screen), B10 (recap) | | Want — one command a different person can run | B1, B10 | | Want — operator/data authority visibly enforced | B5 | @@ -51,4 +53,11 @@ pass were fixed at the capture layer and re-audited. where the bisect evidence can be read, not in a 75-second walkthrough where a red frame would read as a deploy failure. - **Clicking, scrolling, or navigating.** The dashboard beat shows the board, not - the act of opening it. + the act of opening it. B3a is the one place a click happens, and even there the + frame shows the two resulting states rather than the gesture. +- **A `skip` beat.** The suite proves `skip` is durably accepted and then ignored + by all 27 built-in ranking profiles (no profile populates `penalties`), so there + is no order change to show. That finding belongs in the written record, not in a + frame that would imply a working demotion. +- **A search or vector-search beat.** Both work, but neither is the thesis, and a + 75-second walkthrough that shows four surfaces shows none of them. diff --git a/demo/visual-audit.md b/demo/visual-audit.md index 4726896..139b996 100644 --- a/demo/visual-audit.md +++ b/demo/visual-audit.md @@ -6,22 +6,23 @@ written in the **actual decision-maker's** voice (Jordan Washburn). - Build revision: see `capture-manifest.json.buildRevision` - Verified image: `registry.threesix.ai/tidal/server:m12-admin-gate-20260823@sha256:6e220060…` -- Render: `demo/out/deploy-verification.mp4` — 82.05 s, 1920×1080, 30 fps, h264, 2460 frames -- Regression suite at time of capture: **32 passed** -- Demo capture suite: **9 passed** (each asserts before it photographs) +- Render: `demo/out/deploy-verification.mp4` — 90.05 s, 1920×1080, 30 fps, h264, 2700 frames +- Regression suite at time of capture: **34 passed** (32 deployment + 2 cluster ranking tripwires) +- Hermetic ranking-semantics suite: **5 passed** (`npm run test:e2e:semantics`, no cluster required) +- Demo capture suite: **10 passed** (each asserts before it photographs) ## 1. Programmatic preflight | Check | Result | | --- | --- | -| Every manifest file exists and decodes | pass — 9/9 | -| Filesystem inventory equals ledger count | pass — 9 promoted, 9 rows, 0 unclassified | -| Dimensions match declared viewport or documented crop | pass — 8 × 1600×900, 1 × 1600×502 (documented crop) | +| Every manifest file exists and decodes | pass — 10/10 | +| Filesystem inventory equals ledger count | pass — 10 promoted, 10 rows, 0 unclassified | +| Dimensions match declared viewport or documented crop | pass — 9 × 1600×900, 1 × 1600×502 (documented crop) | | Files non-empty, not near-uniform blanks | pass — 64 KB–187 KB | | Capture IDs and filenames unique | pass | | No unexpected duplicates across distinct proof states | pass | -| Source test green | pass — 9 demo capture tests green; each asserts before it photographs | -| Secrets / tokens / local paths absent | pass — `redact()` masks every known secret before render; spot-checked all 9 | +| Source test green | pass — 10 demo capture tests green; each asserts before it photographs | +| Secrets / tokens / local paths absent | pass — `redact()` masks every known secret before render; spot-checked all 10. CAP-016 additionally carries no credential by construction: the page reaches the node only through the harness proxy, which injects auth server-side | ## 2. Per-image review @@ -40,10 +41,11 @@ that was silently overwritten is not an audit. | `CAP-014-authority` | correct | complete | ok (was: dead space) | 403 then 200 | clean | none | **perfect** (was `slop` weak-design) | BUG-011 | | `CAP-014-drift` | correct | complete | ok | git text + live image + 403/200 | clean | none | **perfect** | BUG-013 | | `CAP-015-inert` | correct | complete | ok (was: dead space) | 332 baseline / 0 http | clean | none | **perfect** (was `slop` weak-design) | BUG-011 | +| `CAP-016-feed-reorder` | correct | complete | ok (was: footer clipped, hover artefact) | 5-item feed before/after, 5→1 with `like_boost 2.000` | clean — no bearer reaches the page by construction | none | **perfect** (was `slop` twice: cut-off honesty footer, then a stray `:hover` ring) | BUG-016, BUG-017 | ### Non-promoted images -None. Nine images were produced by the capture run and nine were promoted; no +None. Ten images were produced by the capture runs and ten were promoted; no failure or debug images were generated because every capture test passed on the run that produced the promoted set. @@ -58,16 +60,17 @@ frames extracted from the **encoded MP4** (not re-renders) at | f120 opening | purpose, personas, scope readable | pass | | f375 B2, f600 B3, f825 B4, f1020 B5 | proof legible, entrance settled | pass | | f1230 B6 dashboard | legible at delivery resolution | pass after BUG-010 | +| f840 B3a feed reorder | before/after both legible, capture at scale 1.0, honesty footer visible | pass after BUG-016 | | f1440 B7, f1635 B8 | proof legible | pass | | f1890 B9 dream | image agrees with caption | pass after BUG-013 | | f2145 B10 recap | copy matches what was shown | pass | -| All 10 scene boundaries | no black/white/empty frame | pass after BUG-014 — 0 empty frames | -| Mean luma at all 10 boundaries | 17.49–23.21, smooth progression | pass (was flat 13.00 = bare background) | -| Contact sheet, 30 samples across 75 s | order matches storyboard, no repeats or stale content | pass | +| All 11 scene boundaries | no black/white/empty frame | pass after BUG-014 — 0 empty frames | +| Mean luma across all 11 boundary triplets (f-1, f, f+1) | continuous, no isolated dip | pass — every triplet varies by ≤1 (e.g. 5/4/4 at f960); the BUG-014 signature was an isolated collapse to bare background | +| Contact sheet, 30 samples across 90 s | order matches storyboard, no repeats or stale content | pass | -Captures are presented at scale ≥ 1.0 — the 1600×900 terminal panels render -1:1 and the 1600×502 dashboard crop renders at 1.10 — so no evidence is -downscaled. +Captures are presented at scale ≥ 1.0 — the 1600×900 panels render 1:1 (CAP-016 +measured at 1597/1600 px in-frame) and the 1600×502 dashboard crop renders at +1.10 — so no evidence is downscaled. ## 4. Walk the render as Jordan Washburn @@ -78,14 +81,15 @@ One row per distinct screen a viewer reads. | B1 opening | 0–9 s | "Right, it names the cluster and the image up front. And it says every number came from a real command — that's the claim I actually care about." | neutral-orientation | | B2 convergence | 9–16 s | "Per-node, not the aggregate. Good — the aggregate is exactly what lied to me last week. lag=0 on all nine group-replicas and they agree on the leaders." | earns-interest | | B3 boundary + write | 16–24 s | "401, 401, 200, then a 201 quorum ack. That single 201 is worth more than the rest of the page — it means DNS, TLS, the gateway, auth and Raft all worked in one request." | earns-interest | -| B4 isolation | 24–31 s | "Connection refused from gitea, 332 series to the scraper. That's the pair I'd want — it proves the policy is real without blinding monitoring." | earns-interest | -| B5 authority | 31–37 s | "403 not 401. So a leaked app key can't remove a member. That's the exposure I was worried about and it's closed." | earns-interest | -| B6 dashboard | 37–45 s | "That's the board I'd actually open. Health OK, reseed none, 33.3K vectors, and the legends finally say which node is which." | earns-interest | -| B7 recovery | 45–51 s | "3708/3708 and 48 of 48 volumes, selected by the schedule label. Good — picking the newest backup would have grabbed a canary." | earns-interest | -| B8 blind spot | 51–58 s | "It says NO REPORT instead of inventing 13 million events of lag. And it admits the exit code makes the deploy gate unusable. I trust a tool that tells me that." | earns-interest | -| B9a inert | 58–65 s | "It shows me what it cannot check yet, with the scrape count proving the scrape actually ran. That is the opposite of a green wall." | earns-interest | -| B9 dream | 65–75 s | "Wait — the doc I wrote said that was pending an image roll, and the harness proved it was already live. It caught my own documentation being wrong before I did. I want this running after every deploy." | earns-interest | -| B10 recap | 75–82 s | "32 green, and it names what it does not verify. That's the version I'd hand to someone else." | neutral-orientation | +| B3a feed reorder | 24–32 s | "So that’s what it is actually for. One like, same query, and it goes from fifth to first with the boost shown next to it. And it says local node and fixture catalog, so it isn’t pretending that’s production data." | earns-interest | +| B4 isolation | 32–39 s | "Connection refused from gitea, 332 series to the scraper. That's the pair I'd want — it proves the policy is real without blinding monitoring." | earns-interest | +| B5 authority | 39–45 s | "403 not 401. So a leaked app key can't remove a member. That's the exposure I was worried about and it's closed." | earns-interest | +| B6 dashboard | 45–53 s | "That's the board I'd actually open. Health OK, reseed none, 33.3K vectors, and the legends finally say which node is which." | earns-interest | +| B7 recovery | 53–59 s | "3708/3708 and 48 of 48 volumes, selected by the schedule label. Good — picking the newest backup would have grabbed a canary." | earns-interest | +| B8 blind spot | 59–66 s | "It says NO REPORT instead of inventing 13 million events of lag. And it admits the exit code makes the deploy gate unusable. I trust a tool that tells me that." | earns-interest | +| B9a inert | 66–73 s | "It shows me what it cannot check yet, with the scrape count proving the scrape actually ran. That is the opposite of a green wall." | earns-interest | +| B9 dream | 73–83 s | "Wait — the doc I wrote said that was pending an image roll, and the harness proved it was already live. It caught my own documentation being wrong before I did. I want this running after every deploy." | earns-interest | +| B10 recap | 83–90 s | "32 green, and it names what it does not verify. That's the version I'd hand to someone else." | neutral-orientation | No `fails` rows. The dream beat's thought shows genuine surprise and desire, so it earns its rung. @@ -100,6 +104,7 @@ un-muted passes are identical and captions carry the whole narrative. | --- | --- | | Opening purpose and personas readable | pass — 9 s hold | | Each proof state legible long enough | pass — 6–10 s, dense screens get the longer holds | +| Two-state screen readable as one comparison | pass — B3a stacks before/after at native scale rather than downscaling both to sit side by side | | Dense screens get more time | pass — dream 10 s, write 8 s, dashboard 8 s vs authority 6 s | | Captions agree with the visible outcome | pass after BUG-013 | | Transitions smooth, sections clear | pass after BUG-014 | @@ -119,6 +124,8 @@ Product/test defects found during verification are in | BUG-012 | `CAP-012-tidalctl` | medium | Colour does not imply a meaning the content lacks | "Exit code 2" rendered green, i.e. as a success, when it is the finding | Verdict line defaulted to the positive colour role | Marked the block `negative: true` (amber) and rewrote the text to name the consequence | Re-captured and re-inspected | verified | | BUG-013 | scene B9 (dream) | **blocker** | Scene title, claim, and image agree | The dream caption described the runbook's stale claim and the probe that disproved it, while the image showed the unrelated inert-features panel | Beat reused an existing capture instead of one built for the claim | New `CAP-014-drift` capture showing the committed doc text from `git show`, the actually-running image, and the live 403/200 probe side by side | Re-captured, inspected, frame f1890 re-audited, walk-the-render row rewritten | verified | | BUG-014 | every scene boundary | **blocker** | No empty frame between scenes | The video blinked to bare background for one frame at all 9 boundaries | Remotion Sequences do not overlap; each scene faded out over its last 8 frames while the next faded in from its own frame 0, so both sat at opacity 0 on the boundary | Removed the fade-out and started each Sequence `OVERLAP=10` frames early running long, producing a true 333 ms cross-dissolve. End times unchanged, so every storyboard hold is preserved | Re-rendered; boundary luma went from flat 13.00 (bare background) to 17.5–23.2 with smooth progression; boundary frames re-inspected | verified | +| BUG-016 | `CAP-016-feed-reorder` | **blocker** | The frame states its environment | Two six-row lists plus labels totalled ~922 px in a 900 px frame, pushing the “local standalone node · 60-item fixture catalog” footer off the bottom and clipping the last row | Composition height was never checked against the frame; the footer is `margin-top:auto` so it was the first thing squeezed out | Reduced each half from six rows to five. Explicitly NOT solved by scaling the lists down — that is the BUG-010 mistake, and an unlabelled fixture screenshot reads as production | Re-captured, re-inspected: footer present, both lists complete, lists still 1:1 | verified | +| BUG-017 | `CAP-016-feed-reorder` | low | No control looks meaningful unless it is | A blue-ringed `Like` button appeared on row 5 — an unrelated row — in the after state | The mouse physically stays where it clicked; after the re-render a DIFFERENT row’s button occupies that pixel and picks up `:hover`. First fix attempt blurred focus, which changed nothing because it was hover, not focus | `page.mouse.move(0, 0)` before the screenshot, plus a blur for the focus case | Re-captured and re-inspected: no ring on any row | verified | | BUG-015 | proof scenes | low | No duplicated headings | Remotion drew a scene heading above a capture that already carried its own title, producing a card-in-card with two competing titles | Composition and capture both owned a heading | Removed the heading from the proof scene; Remotion now owns only the capability badge, rung, and audience caption. Reclaimed 66 px for the evidence | Re-rendered and re-inspected f600, f1890 | verified | ## Approval @@ -126,7 +133,7 @@ Product/test defects found during verification are in - Every promoted image, card, and clip has an individual review record with an audience verdict of `perfect`. No `acceptable-with-note` verdicts exist, so no screen was promoted on a soft pass. -- All blocker/high defects (BUG-010, BUG-013, BUG-014) are fixed and verified. +- All blocker/high defects (BUG-010, BUG-013, BUG-014, BUG-016) are fixed and verified. - Root causes are evidence-backed; no unresolved evidence gaps. - Every scene proof and transition sample approved. - Walk-the-render ledger complete for every hold and card, no `fails` rows, and diff --git a/docs/runbooks/deploy-verification.md b/docs/runbooks/deploy-verification.md index 2135b65..f6a191e 100644 --- a/docs/runbooks/deploy-verification.md +++ b/docs/runbooks/deploy-verification.md @@ -23,11 +23,36 @@ cargo build -p tidalctl # section 7 needs the binary npm run test:e2e:list # discovery: syntax, imports, registration npm run test:e2e:smoke # cluster plane + public plane + a quorum write -npm run test:e2e # the whole runbook, ~40 s +npm run test:e2e # the whole runbook, 34 checks, ~55 s ``` Credentials are read from the cluster by `globalSetup`, so nothing is pasted into a shell. A missing prerequisite fails loudly rather than skipping a check. + +### The product, not just the deployment + +Everything in this runbook verifies that the deployment *answers*. None of it +verifies that the database does the thing it exists to do, so there is a second, +separate suite for ranking semantics: + +```bash +npm run test:e2e:semantics # 5 checks, ~14 s, NO cluster required +npm run app:dev # open the same app by hand and click Like +``` + +It boots a throwaway standalone node, seeds a 60-item fixture catalog with +deterministic vectors from `tidal-stress`'s own generator, and asserts that a +signal write reorders the next query, that decay is applied at the declared +half-life, and that ANN results equal brute-force cosine. It has its own config +(`playwright.semantics.config.ts`) precisely because it must run with no cluster +and no credentials — `npm run test:all` runs both suites. + +It deliberately never touches the deployed cluster: `skip` is declared +`permanent: true`, so seeding signals into the live corpus would be +irreversible. Two of its five checks report a product gap rather than a success +(`skip` is inert under all 27 built-in profiles; see `demo/capability-inventory.md` +BUG-018), and one cluster-targeted tripwire pins the duplicated `rank` +(BUG-020) with its root cause in `scatter_merge`. The HTML report at `playwright-report/` carries the transcript of every command the suite ran, which is the evidence trail this document used to describe in prose. diff --git a/package.json b/package.json index ae2f31e..0234f72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "tidaldb-deploy-verification", "private": true, + "type": "module", "version": "0.0.0", "description": "Playwright evidence harness for docs/runbooks/deploy-verification.md", "scripts": { @@ -8,6 +9,9 @@ "test:e2e:smoke": "playwright test --config playwright.config.ts tests/e2e/smoke.spec.ts", "test:e2e:list": "playwright test --config playwright.config.ts --reporter=list --list", "test:e2e:ui": "playwright test --config playwright.config.ts --ui", + "test:e2e:semantics": "playwright test --config playwright.semantics.config.ts", + "test:all": "npm run test:e2e:semantics && npm run test:e2e", + "app:dev": "node --experimental-strip-types tests/e2e/app/dev.ts", "test:demo": "playwright test --config playwright.demo.config.ts", "test:demo:list": "playwright test --config playwright.demo.config.ts --reporter=list --list", "demo:preflight": "node --experimental-strip-types demo/preflight.ts", diff --git a/playwright.config.ts b/playwright.config.ts index f7fe713..33b1c00 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -25,7 +25,10 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests/e2e', - testIgnore: ['**/demo/**'], + // The demo suite has its own config; the ranking-semantics suite has its own + // too (playwright.semantics.config.ts) because it is hermetic and must not be + // gated on this config's cluster prerequisites. + testIgnore: ['**/demo/**', '**/features/10-ranking-semantics.spec.ts'], globalSetup: './tests/e2e/support/env-bootstrap.ts', timeout: 120_000, expect: { timeout: 20_000 }, diff --git a/playwright.semantics.config.ts b/playwright.semantics.config.ts new file mode 100644 index 0000000..696e9cd --- /dev/null +++ b/playwright.semantics.config.ts @@ -0,0 +1,69 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Hermetic config for the ranking-semantics suite. + * + * These checks prove tidalDB's product thesis — that writing a signal changes + * the order of a query, immediately — against a throwaway standalone node this + * suite boots and seeds itself. Nothing here touches the deployed cluster, and + * nothing here needs a credential, a kubeconfig, or a network. + * + * **Why a separate config rather than a project inside `playwright.config.ts`.** + * That config's `globalSetup` (`tests/e2e/support/env-bootstrap.ts`) hard-fails + * when kubectl cannot reach the cluster, and it must: sourcing credentials from + * the cluster is what stops a check silently skipping. Two things rule out + * gating it per-project: + * + * 1. `FullConfig.projects` handed to `globalSetup` is NOT filtered by + * `--project` (measured: selecting one of two projects still reports both), + * so setup cannot tell whether a cluster-targeted test was even selected. + * 2. `globalSetup` publishes credentials into `process.env` of the MAIN process + * so forked workers inherit them. A Playwright setup *project* runs inside a + * worker, where that propagation does not happen. + * + * Separate configs keep each suite's prerequisites honest: this one requires + * nothing, and the regression config keeps failing loudly when the cluster is + * unreachable. Same split the demo suite already uses. + */ + +export default defineConfig({ + testDir: './tests/e2e/features', + testMatch: ['10-ranking-semantics.spec.ts'], + timeout: 180_000, + expect: { timeout: 20_000 }, + forbidOnly: !!process.env.CI, + + // Each test boots its own node, so a retry would hide a genuine ordering bug + // behind a second roll of the dice — the same reasoning as the regression + // config. If a ranking assertion is unstable, the assertion is wrong. + retries: 0, + workers: 1, + fullyParallel: false, + + reporter: process.env.CI + ? [ + ['github'], + ['html', { open: 'never', outputFolder: 'playwright-report-semantics' }], + ['junit', { outputFile: 'test-results/playwright-semantics-junit.xml' }], + ] + : [['list'], ['html', { open: 'never', outputFolder: 'playwright-report-semantics' }]], + + outputDir: 'test-results/playwright-semantics', + + use: { + // No baseURL: the app's origin is allocated per test by the harness, so a + // config-level default would be a lie. + actionTimeout: 20_000, + navigationTimeout: 45_000, + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + + projects: [ + { + name: 'semantics', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/tests/e2e/app/dev.ts b/tests/e2e/app/dev.ts new file mode 100644 index 0000000..4618a35 --- /dev/null +++ b/tests/e2e/app/dev.ts @@ -0,0 +1,38 @@ +/** + * `npm run app:dev` — boot the content-feed app and leave it running. + * + * The whole point of this app is that a human clicks Like and watches the order + * change, so there has to be a one-command way to open it. It drives the exact + * same `startApp()` lifecycle the assertions use, so what you click is what the + * spec ran against. + */ + +import { rmSync } from 'node:fs'; +import { startApp } from './harness.ts'; + +const app = await startApp({ verbose: true }); + +/** + * Synchronous last resort. The graceful path below removes the data dir, but an + * `exit` handler is the only thing that still runs if that path throws part-way + * — and a stale multi-megabyte index dir in $TMPDIR is exactly the kind of litter + * nobody goes looking for. (Nothing survives SIGKILL; that is the OS's problem.) + */ +process.on('exit', () => rmSync(app.dataDir, { recursive: true, force: true })); + +console.log(''); +console.log(` content feed ${app.url}`); +console.log(` tidalDB node ${app.nodeUrl}`); +console.log(` catalog ${Object.keys(app.groundTruth.neighboursByItem).length} items seeded`); +console.log(''); +console.log(' Ctrl-C to tear down the node and remove its data dir.'); +console.log(''); + +const shutdown = async (signal: string) => { + console.log(`\n[app] ${signal} — closing`); + await app.close(); + process.exit(0); +}; + +process.once('SIGINT', () => void shutdown('SIGINT')); +process.once('SIGTERM', () => void shutdown('SIGTERM')); diff --git a/tests/e2e/app/fixture-contract.ts b/tests/e2e/app/fixture-contract.ts new file mode 100644 index 0000000..9cb0f4a --- /dev/null +++ b/tests/e2e/app/fixture-contract.ts @@ -0,0 +1,186 @@ +/** + * The content-feed app's fixture contract — the single home for the catalog and + * for the shape of the ground truth the Rust fixture binary emits. + * + * Everything else imports this: the harness (to seed and to serve + * `/catalog.json`), the page (via that endpoint), and the specs (to pick probe + * items and to read the oracle). Without one contract, five consumers each + * decide independently what an item is and which ids exist. + * + * Data and types only. No I/O, no HTTP, and — deliberately — no ranking + * arithmetic: `CODING_GUIDELINES.md:88` puts scoring in a named ranking profile + * inside the database, and this app exists to demonstrate exactly that. + */ + +/** The embedding width the schema declares (`k8s/cluster/schema-configmap.yaml:51`). */ +export const EMBEDDING_DIM = 1536; + +/** + * Items per category. 15 keeps a whole category inside one 100-id embedding + * cluster; crossing 100 would spill into the next cluster and silently destroy + * the ground-truth separation the oracle depends on. + */ +export const ITEMS_PER_CATEGORY = 15; + +/** + * Each category owns ONE embedding cluster. + * + * `embedding_for` assigns cluster = id / 100 (`tidal-stress/src/recall.rs:104`), + * so a category's ids must share a 100-id band. These bands were probed against + * the live corpus and are unoccupied: the nearest existing vector sits at + * distance ~2.04 while intra-cluster neighbours sit at ~0.435. That 4.7x + * separation is what makes a brute-force top-k over just these 60 items the + * *global* top-k — no 6 GB oracle required. + * + * The `999_000_0xx` band is deliberately avoided: earlier verification probes + * already wrote items there, so its cluster is polluted. + */ +export const CATEGORIES = [ + { name: 'Field recordings', idBase: 900_000_000 }, + { name: 'Analog synthesis', idBase: 900_000_100 }, + { name: 'Choral & sacred', idBase: 900_000_200 }, + { name: 'Free jazz', idBase: 900_000_300 }, +] as const; + +export type CatalogItem = { + entityId: number; + title: string; + category: string; +}; + +/** + * Titles per category, in id order. These are the only prose a viewer reads on + * screen, so they are real and human rather than `post-1` — a bare id would make + * the recording read as a developer artifact rather than a product. + */ +const TITLES: Record = { + 'Field recordings': [ + 'Harbour Ice at Thaw', + 'Nightjars, Dungeness Shingle', + 'Tram Depot, 04:40', + 'Rain on a Zinc Roof', + 'Cicadas Before the Storm', + 'Understory, Monsoon Week', + 'Fog Signal, Outer Channel', + 'Beehive Interior, Midsummer', + 'Grain Elevator, Idling', + 'Salt Flats, Wind Only', + 'Cathedral Steps at Dusk', + 'Snowmelt Under Rock', + 'Ferry Wake, North Passage', + 'Sparrows in a Bus Shelter', + 'Powerlines, Dry Heat', + ], + 'Analog synthesis': [ + 'Ladder Filter Sketch No. 4', + 'Two Oscillators, Slight Detune', + 'Ring Modulator Study', + 'Tape Delay, Self-Oscillating', + 'Sample and Hold Lullaby', + 'Patchbay at Low Voltage', + 'Sawtooth Descending', + 'Envelope Follower Duet', + 'Noise Source, Filtered Slowly', + 'Sequencer Drift', + 'Bucket Brigade Chorus', + 'Sync Lead, Held Open', + 'Resonance at the Edge', + 'Pulse Width Breathing', + 'Cold Start, Warm Bias', + ], + 'Choral & sacred': [ + 'Vespers for a Small Room', + 'Antiphon in Two Voices', + 'Kyrie, Winter Setting', + 'Plainchant, Reconstructed', + 'Nunc Dimittis at Compline', + 'Motet for Eight Parts', + 'Requiem Fragment, Anonymous', + 'Magnificat in the Old Style', + 'Litany with Drone', + 'Alleluia, Second Mode', + 'Lament for Holy Saturday', + 'Te Deum, Village Choir', + 'Canticle of the Three', + 'Hymn at the Lighting of Lamps', + 'Psalm 130, Unaccompanied', + ], + 'Free jazz': [ + 'Ashfall Quartet, Take 3', + 'Blindfold Duet', + 'Circular Breathing Suite', + 'Downtown Loft, Second Set', + 'Extended Technique No. 9', + 'Fractured Standard', + 'Glass Reeds', + 'Horns Against a Wall', + 'Inside the Piano', + 'Junk Percussion Trio', + 'Kinetic Sculpture Session', + 'Long Tones, No Meter', + 'Multiphonic Conversation', + 'Nine Bells and a Bass', + 'Overblown Ballad', + ], +}; + +/** + * The 60-item catalog: 4 categories x 15 items, ids inside each category's band. + * + * Insertion order is category-grouped, and the feed's initial order is + * id-ascending, so the first page shows one category. That is not a layout bug: + * a freshly seeded corpus has no signals, every item ties on score, and + * `for_you`'s tie-break is the entity id. Interleaving was tried and reverted — + * the `for_you` candidate scan declares `sort_field: "created_at"` + * (`tidal/src/ranking/builtins.rs:49`) but ignores a `created_at` metadata value + * entirely (measured: an order matching neither id-ascending nor + * created_at-descending came back strictly id-ascending), so insertion order and + * timestamps have no observable effect. The page stops looking uniform the + * instant a signal is written, which is the point. + */ +export const CATALOG: readonly CatalogItem[] = CATEGORIES.flatMap(({ name, idBase }) => + (TITLES[name] ?? []).map((title, offset) => ({ + entityId: idBase + offset, + title, + category: name, + })), +); + +/** Id -> item, for the id→title join the page performs (`/feed` returns no metadata). */ +export const CATALOG_BY_ID: Record = Object.fromEntries( + CATALOG.map((item) => [String(item.entityId), item]), +); + +/** + * Items the spec probes for the ANN assertion — one per category, so every + * cluster is covered rather than just the first. + */ +export const PROBE_IDS = [900_000_007, 900_000_107, 900_000_207, 900_000_307] as const; + +/** + * What `tidal-stress`'s `feed-fixture` binary writes. A generated artifact, never + * committed: a checked-in oracle drifts from its generator the first time either + * one changes, and the drift is silent. + */ +export type FixtureGroundTruth = { + dim: number; + /** entityId -> its true cosine-ranked neighbour ids, nearest first. */ + neighboursByItem: Record; + /** + * entityId -> its raw 1536-float vector, for the handful of items the spec + * probes. The ANN assertion has to POST a query vector, and the vectors + * otherwise exist only inside the Rust binary. Emitting a few probes rather + * than all 60 keeps this artifact ~100 KB instead of ~1 MB. + */ + probeVectors: Record; + /** + * How far an item's own vector may land from itself and still count as "zero". + * Non-zero purely because the vector round-trips through f32 JSON and the + * engine re-normalises on write. + */ + selfDistanceTolerance: number; +}; + +/** The three signal names the schema declares. Nothing else exists. */ +export const SIGNALS = ['view', 'like', 'skip'] as const; +export type SignalName = (typeof SIGNALS)[number]; diff --git a/tests/e2e/app/harness.ts b/tests/e2e/app/harness.ts new file mode 100644 index 0000000..b4ae4aa --- /dev/null +++ b/tests/e2e/app/harness.ts @@ -0,0 +1,516 @@ +/** + * The content-feed app's lifecycle, in one place. + * + * `startApp()` boots a throwaway standalone tidalDB, seeds the fixture catalog, + * serves the page plus a token-injecting `/api` proxy, and hands back a handle + * whose `close()` unwinds all of it. The semantic spec, the demo capture and + * `npm run app:dev` all drive the identical lifecycle, so what a human sees by + * hand is what the assertions ran against. + * + * Two rules shape the design: + * + * - **Relative `/api` only.** `.sdlc/guidance.md:165` forbids a hardcoded + * `http://localhost:PORT` in frontend code and prescribes a dev-server proxy. + * Independently, a bearer must never reach a browser — a page holding the key + * leaks it to anyone who opens devtools, and the demo capture would photograph + * it. The proxy satisfies both: the page calls `/api/feed`, the server adds + * `Authorization`, the key stays server-side. + * - **Poll, never sleep.** A fixed sleep either wastes time or produces the + * empty-body false negative that reads exactly like a dead node. This is the + * same discipline as `tests/e2e/support/cluster.ts:154` `portForward`. + * + * It targets a LOCAL standalone node, never the deployed cluster: `skip` is + * declared `permanent: true`, so seeding signals into production would be an + * irreversible mutation of the live corpus. + */ + +import { spawn, type ChildProcess } from 'node:child_process'; +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'; +import { createConnection } from 'node:net'; +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { extname, join, resolve, sep } from 'node:path'; +import { setTimeout as sleep } from 'node:timers/promises'; + +import { run } from '../support/cluster.ts'; +import { redact } from '../support/env.ts'; +import { + CATALOG, + EMBEDDING_DIM, + PROBE_IDS, + type FixtureGroundTruth, +} from './fixture-contract.ts'; + +const REPO_ROOT = resolve(import.meta.dirname, '../../..'); +const PUBLIC_DIR = join(import.meta.dirname, 'public'); +const SCHEMA_CONFIGMAP = join(REPO_ROOT, 'k8s/cluster/schema-configmap.yaml'); +const SERVER_BIN = join(REPO_ROOT, 'target/debug/tidal-server'); +const FIXTURE_BIN = join(REPO_ROOT, 'target/debug/feed-fixture'); + +const BOOT_TIMEOUT_MS = 60_000; +const BUILD_TIMEOUT_MS = 900_000; +const SEED_TIMEOUT_MS = 120_000; +const KILL_GRACE_MS = 3_000; + +/** + * Ask the OS for a free port rather than computing one from a base. + * + * A fixed base collides with whatever is already holding it — a leftover node, a + * second checkout, or `npm run app:dev` running in another terminal while the + * suite runs. Letting the kernel choose removes that whole class of failure. The + * handover window between close and the child's bind is microseconds, and a lost + * race surfaces as the child's own "address in use" stderr rather than a hang. + */ +async function freePort(): Promise { + const probe = createServer(); + const { promise, resolve: settle, reject } = Promise.withResolvers(); + probe.once('error', reject); + probe.listen(0, '127.0.0.1', () => { + const address = probe.address(); + const port = typeof address === 'object' && address !== null ? address.port : 0; + probe.close(() => settle(port)); + }); + return promise; +} + +export type RunningApp = { + /** The app origin: serves the page and proxies `/api/*`. */ + url: string; + /** The tidalDB node itself, for probes that bypass the app. */ + nodeUrl: string; + /** The oracle the fixture binary emitted for this exact corpus. */ + groundTruth: FixtureGroundTruth; + /** Decay as declared by the schema THIS node loaded, not a copy of it. */ + declaredDecay: DeclaredDecay; + /** + * The throwaway data dir. Exposed so a caller can say where a run's state went, + * and so a long-lived host can install a synchronous last-resort cleanup — an + * async `close()` that throws would otherwise leave the dir behind. + */ + dataDir: string; + close: () => Promise; +}; + +export type StartAppOptions = { + /** Bearer to inject into proxied requests. A local standalone needs none. */ + apiKey?: string; + /** Log lifecycle progress to stdout — for `npm run app:dev`, quiet in tests. */ + verbose?: boolean; +}; + +/** + * Extract the schema the DEPLOYED cluster loads, from its ConfigMap wrapper. + * + * Not interchangeable with `tidal-server/config/default-schema.yaml`: that file is + * otherwise identical but declares `dimensions: 128`, while the cluster declares + * 1536. Seeding 1536-wide vectors against a 128-wide slot is rejected with a 422 + * that reads like a malformed body, so the dimension is asserted here rather than + * discovered three layers downstream. + */ +async function extractSchema(): Promise { + const raw = await readFile(SCHEMA_CONFIGMAP, 'utf8'); + const marker = ' schema.yaml: |\n'; + const start = raw.indexOf(marker); + if (start < 0) { + throw new Error(`${SCHEMA_CONFIGMAP} has no ' schema.yaml: |' block scalar to extract`); + } + const body = raw.slice(start + marker.length); + const schema = body + .split('\n') + .map((line) => (line.startsWith(' ') ? line.slice(4) : line)) + .join('\n'); + + if (!schema.startsWith('signals:')) { + throw new Error(`extracted schema does not start with 'signals:'; got: ${schema.slice(0, 80)}`); + } + if (!new RegExp(`dimensions:\\s*${EMBEDDING_DIM}\\b`).test(schema)) { + throw new Error( + `extracted schema does not declare dimensions: ${EMBEDDING_DIM}. The fixture ` + + `contract and the schema must agree or every embedding write is rejected as 422.`, + ); + } + return schema; +} + +/** + * The decay each signal declares, read out of the schema the node actually + * loaded. + * + * The decay assertion compares an OBSERVED decay rate against a DECLARED + * half-life. Hardcoding the declared value in the test would make it a + * tautology the moment the schema changed, so it is parsed from the same string + * that was handed to `--schema`. + * + * A deliberately small parser rather than a YAML dependency: the shape it reads + * is four lines of a file this repo owns, and it fails loudly (returns nothing + * for a signal) rather than guessing. + */ +export type DeclaredDecay = Record; + +function parseDeclaredDecay(schema: string): DeclaredDecay { + const declared: DeclaredDecay = {}; + let current: string | undefined; + for (const line of schema.split('\n')) { + const name = /^\s*-\s*name:\s*(\S+)/.exec(line); + if (name?.[1]) { + current = name[1]; + continue; + } + if (current === undefined) continue; + const halfLife = /^\s*half_life_seconds:\s*(\d+)/.exec(line); + if (halfLife?.[1]) { + declared[current] = Number(halfLife[1]); + continue; + } + if (/^\s*permanent:\s*true/.test(line)) declared[current] = 'permanent'; + } + return declared; +} + +/** True once something accepts a TCP connection on the port. */ +async function portAccepts(port: number): Promise { + const { promise, resolve: settle } = Promise.withResolvers(); + const socket = createConnection({ port, host: '127.0.0.1' }); + const done = (ok: boolean) => { + socket.destroy(); + settle(ok); + }; + socket.setTimeout(1_000); + socket.once('connect', () => done(true)); + socket.once('timeout', () => done(false)); + socket.once('error', () => done(false)); + return promise; +} + +/** True once the node answers `/health` with 2xx. */ +async function healthOk(nodeUrl: string): Promise { + try { + const response = await fetch(`${nodeUrl}/health`, { + signal: AbortSignal.timeout(2_000), + }); + return response.ok; + } catch { + return false; + } +} + +const buildsInFlight = new Map>(); + +/** + * Build a cargo binary if it is not already present, once per process. + * + * `feed-fixture` is new, so no existing tree has it; failing with "binary + * missing, go run cargo" would make the hermetic suite un-runnable from a clean + * checkout for no reason. Dependencies are already compiled, so this is seconds, + * not minutes — but the timeout allows for a cold cache. + */ +function ensureBinary(binPath: string, cargoArgs: string[], verbose: boolean): Promise { + if (existsSync(binPath)) return Promise.resolve(); + const existing = buildsInFlight.get(binPath); + if (existing) return existing; + + const build = (async () => { + if (verbose) console.log(`[app] building ${binPath} (missing)`); + const result = await run('cargo', cargoArgs, { timeoutMs: BUILD_TIMEOUT_MS }); + if (result.code !== 0 || !existsSync(binPath)) { + throw new Error( + `could not build ${binPath}\n$ ${result.command}\nexit ${result.code}\n${result.stderr}`, + ); + } + })(); + buildsInFlight.set(binPath, build); + return build; +} + +/** MIME types for the handful of things the page is made of. */ +const CONTENT_TYPES: Record = { + '.html': 'text/html; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.svg': 'image/svg+xml', +}; + +/** + * Read the whole request body as text. + * + * Text rather than bytes because the only thing the page ever sends is one small + * JSON object, and `string` is unambiguously a `BodyInit` — TS's `BufferSource` + * is `ArrayBufferView`, which a pooled Node `Buffer` does not + * satisfy. If this ever needs to forward binary, that is a real change, not a cast. + */ +async function readBody(req: IncomingMessage): Promise { + const chunks: Buffer[] = []; + for await (const chunk of req) chunks.push(chunk as Buffer); + return Buffer.concat(chunks).toString('utf8'); +} + +/** + * Forward one `/api/*` request to the node, adding the bearer. + * + * A byte pipe: it must never reorder, re-score or filter a response, because the + * ordering IS the thing under test (`CODING_GUIDELINES.md:88`). + */ +async function proxy( + req: IncomingMessage, + res: ServerResponse, + nodeUrl: string, + apiKey: string | undefined, +): Promise { + const path = (req.url ?? '/').slice('/api'.length); + const headers: Record = { accept: 'application/json' }; + const contentType = req.headers['content-type']; + if (contentType) headers['content-type'] = contentType; + if (apiKey) headers.authorization = `Bearer ${apiKey}`; + + const method = req.method ?? 'GET'; + const hasBody = method !== 'GET' && method !== 'HEAD'; + const body = hasBody ? await readBody(req) : undefined; + + try { + const upstream = await fetch(`${nodeUrl}${path}`, { + method, + headers, + body, + signal: AbortSignal.timeout(30_000), + }); + const payload = Buffer.from(await upstream.arrayBuffer()); + res.writeHead(upstream.status, { + 'content-type': upstream.headers.get('content-type') ?? 'application/json', + 'content-length': String(payload.byteLength), + }); + res.end(payload); + } catch (error) { + // Surface the upstream fault as a real status rather than hanging the page. + const message = redact(error instanceof Error ? error.message : String(error)); + const payload = Buffer.from(JSON.stringify({ error: `proxy: ${message}` })); + res.writeHead(502, { + 'content-type': 'application/json', + 'content-length': String(payload.byteLength), + }); + res.end(payload); + } +} + +/** Serve one file from the public dir, refusing anything outside it. */ +async function serveStatic(req: IncomingMessage, res: ServerResponse): Promise { + const requested = (req.url ?? '/').split('?')[0] ?? '/'; + const relative = requested === '/' ? 'index.html' : requested.replace(/^\/+/, ''); + const target = resolve(PUBLIC_DIR, relative); + if (target !== PUBLIC_DIR && !target.startsWith(PUBLIC_DIR + sep)) { + res.writeHead(403, { 'content-type': 'text/plain' }); + res.end('forbidden'); + return; + } + try { + const body = await readFile(target); + res.writeHead(200, { + 'content-type': CONTENT_TYPES[extname(target)] ?? 'application/octet-stream', + 'content-length': String(body.byteLength), + }); + res.end(body); + } catch { + res.writeHead(404, { 'content-type': 'text/plain' }); + res.end('not found'); + } +} + +/** + * Boot a standalone node, seed the fixture catalog, and serve the app against it. + * + * Every stage that can fail reports why: an early child exit surfaces the node's + * stderr, and a non-zero seed exit surfaces the seeder's transcript, because a + * partially seeded corpus would make every downstream assertion meaningless. + */ +export async function startApp(options: StartAppOptions = {}): Promise { + const verbose = options.verbose ?? false; + const log = (message: string) => { + if (verbose) console.log(`[app] ${message}`); + }; + + await ensureBinary(SERVER_BIN, ['build', '-p', 'tidal-server', '--bin', 'tidal-server'], verbose); + await ensureBinary( + FIXTURE_BIN, + ['build', '-p', 'tidal-stress', '--bin', 'feed-fixture'], + verbose, + ); + + const dataDir = await mkdtemp(join(tmpdir(), 'tidaldb-feed-app-')); + const schemaPath = join(dataDir, 'schema.yaml'); + const catalogPath = join(dataDir, 'catalog.json'); + const truthPath = join(dataDir, 'truth.json'); + const schema = await extractSchema(); + const declaredDecay = parseDeclaredDecay(schema); + await writeFile(schemaPath, schema, 'utf8'); + await writeFile(catalogPath, JSON.stringify(CATALOG), 'utf8'); + // `--data-dir` must already exist: the server treats a missing directory as a + // config error rather than creating it (verified against its own stderr). + await mkdir(join(dataDir, 'data'), { recursive: true }); + + const nodePort = await freePort(); + const nodeUrl = `http://127.0.0.1:${nodePort}`; + + // `--listen` also reads the PORT env var (tidal-server/src/main.rs:99). An + // explicit flag wins in clap, but dropping PORT removes the ambiguity entirely. + const childEnv = { ...process.env }; + delete childEnv.PORT; + delete childEnv.TIDAL_CONFIG; + + log(`booting node on ${nodeUrl} (data ${dataDir})`); + const child: ChildProcess = spawn( + SERVER_BIN, + [ + 'standalone', + '--listen', + `127.0.0.1:${nodePort}`, + '--schema', + schemaPath, + '--data-dir', + join(dataDir, 'data'), + ], + { cwd: REPO_ROOT, env: childEnv, stdio: ['ignore', 'pipe', 'pipe'] }, + ); + + let nodeStderr = ''; + let exitInfo: string | undefined; + child.stderr?.on('data', (chunk: Buffer) => { + nodeStderr += chunk.toString(); + }); + child.stdout?.on('data', () => { + /* the node's own log; kept off the test transcript unless it fails */ + }); + child.once('exit', (code, signal) => { + exitInfo = `tidal-server exited early (code=${code} signal=${signal})`; + }); + + const stopNode = async () => { + if (child.exitCode !== null || child.signalCode !== null) return; + const { promise, resolve: settle } = Promise.withResolvers(); + const hardKill = setTimeout(() => { + child.kill('SIGKILL'); + settle(); + }, KILL_GRACE_MS); + child.once('exit', () => { + clearTimeout(hardKill); + settle(); + }); + child.kill('SIGTERM'); + await promise; + }; + + const cleanup = async (server?: Server) => { + if (server) { + const { promise, resolve: settle } = Promise.withResolvers(); + server.close(() => settle()); + server.closeAllConnections?.(); + await promise; + } + await stopNode(); + await rm(dataDir, { recursive: true, force: true }); + }; + + try { + const deadline = Date.now() + BOOT_TIMEOUT_MS; + let ready = false; + while (Date.now() < deadline) { + if (exitInfo) { + throw new Error(`${exitInfo}\n--- node stderr ---\n${redact(nodeStderr.trim())}`); + } + if (await healthOk(nodeUrl)) { + ready = true; + break; + } + await sleep(150); + } + if (!ready) { + throw new Error( + `tidal-server never answered ${nodeUrl}/health within ${BOOT_TIMEOUT_MS}ms\n` + + `--- node stderr ---\n${redact(nodeStderr.trim())}`, + ); + } + log(`node healthy; seeding ${CATALOG.length} items`); + + const seed = await run( + FIXTURE_BIN, + [ + '--base-url', + nodeUrl, + '--catalog', + catalogPath, + '--out', + truthPath, + '--dim', + String(EMBEDDING_DIM), + ...PROBE_IDS.flatMap((id) => ['--probe', String(id)]), + ], + { timeoutMs: SEED_TIMEOUT_MS }, + ); + if (seed.code !== 0) { + throw new Error( + `fixture seeding failed — a partial corpus makes every assertion ` + + `meaningless, so this is fatal.\n$ ${seed.command}\nexit ${seed.code}\n` + + `${seed.stdout}\n${seed.stderr}`, + ); + } + const groundTruth = JSON.parse(await readFile(truthPath, 'utf8')) as FixtureGroundTruth; + + const route = async (req: IncomingMessage, res: ServerResponse): Promise => { + if (req.url?.startsWith('/api/') === true) { + return proxy(req, res, nodeUrl, options.apiKey); + } + if ((req.url ?? '/').split('?')[0] === '/catalog.json') { + // The page joins ranked ids to titles, so it needs the catalog — served + // from the same contract the seeder used, never a second copy. + const payload = Buffer.from(JSON.stringify(CATALOG)); + res.writeHead(200, { + 'content-type': 'application/json; charset=utf-8', + 'content-length': String(payload.byteLength), + }); + res.end(payload); + return; + } + return serveStatic(req, res); + }; + + const server = createServer((req, res) => { + route(req, res).catch((error: unknown) => { + if (res.headersSent) { + res.end(); + return; + } + res.writeHead(500, { 'content-type': 'text/plain' }); + res.end(redact(error instanceof Error ? error.message : String(error))); + }); + }); + + // Bind port 0 and read back what the kernel gave us: no probe, no race. + const listening = Promise.withResolvers(); + server.once('error', (error) => listening.reject(error)); + server.listen(0, '127.0.0.1', () => { + const address = server.address(); + listening.resolve(typeof address === 'object' && address !== null ? address.port : 0); + }); + const appPort = await listening.promise; + + const url = `http://127.0.0.1:${appPort}`; + // Prove the app origin is actually reachable before handing it to a caller, + // so a bind that silently failed cannot look like a page-render bug later. + if (!(await portAccepts(appPort))) { + throw new Error(`app server bound ${url} but the port does not accept connections`); + } + log(`app ready at ${url}`); + + return { + url, + nodeUrl, + groundTruth, + declaredDecay, + dataDir, + close: () => cleanup(server), + }; + } catch (error) { + await cleanup(); + throw error; + } +} diff --git a/tests/e2e/app/public/index.html b/tests/e2e/app/public/index.html new file mode 100644 index 0000000..efffbfb --- /dev/null +++ b/tests/e2e/app/public/index.html @@ -0,0 +1,204 @@ + + + + + +Content feed — ranked by tidalDB + + + + +

+ + + + diff --git a/tests/e2e/demo/workflows/feed-app.demo.spec.ts b/tests/e2e/demo/workflows/feed-app.demo.spec.ts new file mode 100644 index 0000000..00cb698 --- /dev/null +++ b/tests/e2e/demo/workflows/feed-app.demo.spec.ts @@ -0,0 +1,164 @@ +/** + * Demo capture — the product working, not just the deployment answering. + * + * The walkthrough's other nine captures prove the cluster is real: it converges, + * it refuses bad credentials, it commits a quorum write, it can be observed and + * restored. None of them shows what tidalDB is FOR. This one does, and the claim + * is deliberately narrow: a signal write changes the order of the next query, + * with nothing in between. + * + * It is one image holding both states. Two separate stills of a list would force + * a viewer to diff two frames from memory; stacked before/after makes the + * movement self-evident. The lists are composed at their native resolution rather + * than scaled to fill the frame — a downscaled list is the BUG-010 mistake. + * + * This runs against a LOCAL standalone node with fixture data, and the frame says + * so. `demo/audience-brief.md` forbids implying a dataset that is not what it + * appears to be, and `skip` being `permanent: true` is exactly why the fixture + * never touches production. + */ + +import { expect, test } from '@playwright/test'; +import { + CAPTURE_HEIGHT, + CAPTURE_WIDTH, + recordScreenshot, + writeManifestFragment, + type CaptureRecord, +} from '../support/proof-panel.ts'; +import { startApp } from '../../app/harness.ts'; + +const records: CaptureRecord[] = []; + +/** + * Rows shown in each half. + * + * Five, not six: two six-row lists plus their labels came to ~922px inside a + * 900px frame, which pushed the environment footer off the bottom and clipped the + * last row. The footer is what stops a fixture screenshot reading as production, + * so it is not optional — and the fix is fewer rows, never a downscaled list + * (that was the BUG-010 mistake). + */ +const ROWS = 5; + +/** Viewport that makes the page's centred column fill the frame exactly. */ +const APP_VIEWPORT = { width: 1240, height: 900 }; + +type Row = { position: string; title: string }; + +function composition(before: string, after: string, movedTitle: string, rows: Row[]): string { + const label = (text: string) => `
${text}
`; + return ` + + + ${label(`before — ${rows.length} of 60 items, no signals written yet. Everything ties, so the order is the tie-break.`)} + + ${label(`after — one like on “${movedTitle}”, then the same query again. It is now first.`)} + +
local standalone node · 60-item fixture catalog · profile for_you · no ETL, no reindex, no second system
+`; +} + +test.describe('feed-app product surface', () => { + test('CAP-016 a signal write reorders the feed immediately', async ({ page }, testInfo) => { + const app = await startApp(); + try { + await page.setViewportSize(APP_VIEWPORT); + await page.goto(`${app.url}/?limit=${ROWS}`, { waitUntil: 'networkidle' }); + await page.waitForSelector('#feed li'); + await page.evaluate(() => document.fonts.ready); + + const list = page.locator('#feed'); + const readRows = (): Promise => + page.$$eval('#feed li', (nodes) => + nodes.map((li) => ({ + position: li.querySelector('.pos')!.textContent!.trim().replace(/\s+/g, ' '), + title: li.querySelector('.title')!.textContent!.trim().replace(/\s+/g, ' '), + })), + ); + + const before = await readRows(); + expect(before.length, `the page must render ${ROWS} rows to compose from`).toBe(ROWS); + const beforeShot = await list.screenshot(); + + // The last visible row, so the movement spans the whole frame. + const targetTitle = before[before.length - 1]!.title; + await page.locator('#feed li').last().locator('button[data-signal="like"]').click(); + + // Wait for the re-read to land by watching the thing under test change, + // rather than sleeping a guessed interval. `startsWith`, not equality: once + // a row moves, its title element also carries the delta badge ("\u25b25"). + await page.waitForFunction( + (title) => + document.querySelector('#feed li .title')?.textContent?.trim().startsWith(title) === true, + targetTitle, + { timeout: 15_000 }, + ); + + const after = await readRows(); + const afterIndex = after.findIndex((row) => row.title.startsWith(targetTitle)); + + // Assert BEFORE photographing. A capture is a photograph of an + // already-proven state; if the order did not change there is nothing + // honest to show. + expect( + afterIndex, + `the liked item must have moved to the top; "${targetTitle}" is at index ${afterIndex}`, + ).toBe(0); + + // Move the pointer off the list and drop focus before photographing. + // The cursor physically stays where it clicked, so after the re-render a + // DIFFERENT row's button sits under it and picks up `:hover` — a highlighted + // control on an unrelated row, which reads as meaningful when it is not. + await page.mouse.move(0, 0); + await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur()); + const afterShot = await list.screenshot(); + const dataUri = (buffer: Buffer): string => + `data:image/png;base64,${buffer.toString('base64')}`; + + await page.setViewportSize({ width: CAPTURE_WIDTH, height: CAPTURE_HEIGHT }); + await page.setContent( + composition(dataUri(beforeShot), dataUri(afterShot), targetTitle, before), + { waitUntil: 'load' }, + ); + await page.evaluate(() => document.fonts.ready); + + records.push( + await recordScreenshot(await page.screenshot(), testInfo, { + captureId: 'CAP-016-feed-reorder', + capabilityId: 'CAP-016', + expected: + 'The same query, before and after one like: the liked item moves from last to first, with its like_boost visible', + businessPurpose: + 'A signal write changes the order with no ETL in between — the product thesis, not the deployment', + personas: ['cluster operator', 'application developer'], + width: CAPTURE_WIDTH, + height: CAPTURE_HEIGHT, + }), + ); + } finally { + await app.close(); + } + }); + + test.afterAll(async () => { + await writeManifestFragment('feed-app', records); + }); +}); diff --git a/tests/e2e/features/10-ranking-semantics.spec.ts b/tests/e2e/features/10-ranking-semantics.spec.ts new file mode 100644 index 0000000..36cdf9c --- /dev/null +++ b/tests/e2e/features/10-ranking-semantics.spec.ts @@ -0,0 +1,415 @@ +/** + * Section 10 — ranking semantics. + * + * The other ten spec files prove the deployment answers: TLS, auth, quorum + * commit, convergence, isolation, dashboards, backups. Not one of them writes a + * signal and observes an order change, so tidalDB's actual thesis — `VISION.md:17` + * "Ranking is not a feature. It is a primitive." — was unverified. + * + * These five checks verify it against a throwaway standalone node this suite + * boots and seeds itself (60 items, 4 categories, deterministic vectors from + * `tidal-stress`'s own generator). Hermetic: no cluster, no credential, no + * network. Run with `npm run test:e2e:semantics`. + * + * Two of the five report a product gap rather than a success, because that is + * what the measurement said. They are written as tripwires in the honest + * direction — the same shape as the inert-feature checks in + * `09-operator-authority.spec.ts` — so the day the gap closes, the test fails + * with an instruction instead of the gap persisting silently. + */ + +import { expect, test, type APIRequestContext } from '@playwright/test'; +import { recordJson } from '../support/evidence.ts'; +import { PROBE_IDS } from '../app/fixture-contract.ts'; +import { startApp, type RunningApp } from '../app/harness.ts'; + +/** One row of a ranked response (`tidal-server/src/dto.rs:269`). */ +type FeedItem = { + entity_id: number; + score: number; + rank: number; + /** Absent, not `[]`, when a profile reports nothing (`dto.rs:373`). */ + signals?: { name: string; value: number }[]; +}; + +/** The whole catalog in one read, so "last" means globally worst, not page-worst. */ +const FULL_CORPUS = 60; + +/** + * `for_you`, never `trending`. Measured on this schema, `trending` reports + * `view_velocity` and `share_velocity` — and `share` is not a declared signal + * here, so that term is permanently 0. An unknown profile name is answered with + * 500 rather than 400, so profile names are pinned to ones probed as live. + */ +const PROFILE = 'for_you'; + +async function readFeed( + request: APIRequestContext, + app: RunningApp, + limit = FULL_CORPUS, +): Promise { + const response = await request.get(`${app.url}/api/feed?profile=${PROFILE}&limit=${limit}`); + expect(response.status(), 'the feed must answer before anything is concluded from it').toBe(200); + const body = (await response.json()) as { items?: FeedItem[] }; + const items = body.items ?? []; + expect(items.length, 'an empty feed cannot support any ranking assertion').toBeGreaterThan(0); + return items; +} + +async function writeSignal( + request: APIRequestContext, + app: RunningApp, + entityId: number, + signal: string, +): Promise { + const response = await request.post(`${app.url}/api/signals`, { + data: { entity_id: entityId, signal, weight: 1.0 }, + }); + expect(response.status(), `POST /signals ${signal} on ${entityId} must be accepted`).toBe(204); +} + +const positionOf = (items: FeedItem[], entityId: number): number => + items.findIndex((item) => item.entity_id === entityId); + +const signalValue = (item: FeedItem | undefined, name: string): number | undefined => + item?.signals?.find((signal) => signal.name === name)?.value; + +test.describe('section 10 — ranking semantics', () => { + test('a like moves the item up, immediately — no ETL in between', async ({ + request, + }, testInfo) => { + const app = await startApp(); + try { + const before = await readFeed(request, app); + const target = before[before.length - 1]!.entity_id; + const beforeIndex = positionOf(before, target); + + await writeSignal(request, app, target, 'like'); + + // No sleep and no retry between the write and the read. VISION.md:166 + // claims the next query reflects the write with "no Kafka consumer to lag, + // no feature store sync to schedule". Polling here would be testing our + // patience rather than that claim. + const after = await readFeed(request, app); + const afterIndex = positionOf(after, target); + + await recordJson(testInfo, 'like-moves-up', { + entityId: target, + beforeIndex, + afterIndex, + likeBoost: signalValue(after[afterIndex], 'like_boost'), + profile: PROFILE, + }); + + // Position, not score. Scores are floats a profile owns and re-normalises + // across the candidate set (measured: unliked items go 0.5 -> 0.0 once one + // item is boosted). Position is the contract a user actually experiences. + expect( + afterIndex, + `a like must improve position immediately; ${target} went ${beforeIndex} -> ${afterIndex}`, + ).toBeLessThan(beforeIndex); + expect(afterIndex, 'a single like on the worst item should reach the top').toBe(0); + } finally { + await app.close(); + } + }); + + test('a skip is durably accepted and then ignored by every built-in profile', async ({ + request, + }, testInfo) => { + const app = await startApp(); + try { + // Every profile this deployment exposes and which resolves (`top` returns + // 500 despite existing in engine source, so it is not probed). + const profiles = [ + 'for_you', + 'trending', + 'hot', + 'new', + 'shuffle', + 'hidden_gems', + 'controversial', + ]; + const baseline = await readFeed(request, app); + const target = baseline[20]!.entity_id; + + const positionsBefore: Record = {}; + for (const profile of profiles) { + const response = await request.get( + `${app.url}/api/feed?profile=${profile}&limit=${FULL_CORPUS}`, + ); + const body = (await response.json()) as { items?: FeedItem[] }; + positionsBefore[profile] = positionOf(body.items ?? [], target); + } + + // Five skips, not one: a single write could plausibly fall below a rounding + // threshold. Five cannot. + for (let n = 0; n < 5; n += 1) await writeSignal(request, app, target, 'skip'); + + const positionsAfter: Record = {}; + const reported: Record = {}; + for (const profile of profiles) { + const response = await request.get( + `${app.url}/api/feed?profile=${profile}&limit=${FULL_CORPUS}`, + ); + const body = (await response.json()) as { items?: FeedItem[] }; + const items = body.items ?? []; + positionsAfter[profile] = positionOf(items, target); + reported[profile] = (items[positionsAfter[profile]]?.signals ?? []).map((s) => s.name); + } + + await recordJson(testInfo, 'skip-is-inert', { + entityId: target, + skipsWritten: 5, + declaredDecay: app.declaredDecay, + positionsBefore, + positionsAfter, + signalNamesReported: reported, + }); + + // The schema declares `skip` (permanent), and the write is accepted, so the + // event is durably recorded. But `Penalty` — the mechanism that would let it + // demote anything (`tidal/src/ranking/profile.rs:227`, applied at + // `tidal/src/ranking/executor/signal_values.rs:183`, labelled `{signal}_penalty` + // at `tidal/src/ranking/executor/mod.rs:65`) — is never populated: every + // built-in profile is built from `skeleton()`, which sets + // `penalties: vec![]` (`tidal/src/ranking/builtins.rs:62`), and no built-in + // overrides it. So `VISION.md:187` "Negative signals are equal citizens" does + // not hold for any shipped profile: a skip is stored and query-time inert. + expect(app.declaredDecay.skip, 'the schema must still declare skip').toBe('permanent'); + for (const profile of profiles) { + expect( + positionsAfter[profile], + `${profile} moved ${target} after 5 skips (${positionsBefore[profile]} -> ` + + `${positionsAfter[profile]}). Good news: a profile now applies a skip ` + + `penalty. Rewrite this test as a demotion assertion and close the ` + + `"negative signals are inert" finding.`, + ).toBe(positionsBefore[profile]); + expect( + reported[profile], + `${profile} now reports a skip term. Same good news as above.`, + ).not.toContain('skip_penalty'); + } + } finally { + await app.close(); + } + }); + + test('decay is applied continuously at query time, at each signal\u2019s declared half-life', async ({ + request, + }, testInfo) => { + const app = await startApp(); + try { + const viewItem = 900_000_300; + const likeItem = 900_000_301; + + // Written concurrently so both carry the same elapsed time, which makes the + // ratio of their decay rates depend only on their declared half-lives. + await Promise.all([ + writeSignal(request, app, viewItem, 'view'), + writeSignal(request, app, likeItem, 'like'), + ]); + + const first = await readFeed(request, app); + const readOne = Date.now(); + + // A real interval, deliberately. This is not a sleep to make an assertion + // pass — elapsed time IS the independent variable of the property under + // test. `CODING_GUIDELINES.md:88` says "decay is a type, not a formula you + // call"; the observable consequence is that the SAME stored event reports a + // smaller value on a later read, with no writes in between. + await new Promise((resolve) => setTimeout(resolve, 4_000)); + + const second = await readFeed(request, app); + const readTwo = Date.now(); + const elapsedSeconds = (readTwo - readOne) / 1_000; + + const viewFirst = signalValue(first[positionOf(first, viewItem)], 'view_boost'); + const viewSecond = signalValue(second[positionOf(second, viewItem)], 'view_boost'); + const likeFirst = signalValue(first[positionOf(first, likeItem)], 'like_boost'); + const likeSecond = signalValue(second[positionOf(second, likeItem)], 'like_boost'); + + // Guard the measurement before concluding anything from it: an absent value + // would otherwise read as "no decay". + for (const [label, value] of Object.entries({ viewFirst, viewSecond, likeFirst, likeSecond })) { + expect(value, `${label} must be reported before decay can be measured`).toBeDefined(); + } + + // Recover each signal's half-life from the two readings: + // value(t) = value(0) * 2^(-t/H) => H = t * ln2 / -ln(v2/v1) + const impliedHalfLife = (v1: number, v2: number): number => + (elapsedSeconds * Math.LN2) / -Math.log(v2 / v1); + const viewHalfLife = impliedHalfLife(viewFirst!, viewSecond!); + const likeHalfLife = impliedHalfLife(likeFirst!, likeSecond!); + + const declaredView = app.declaredDecay.view; + const declaredLike = app.declaredDecay.like; + expect(typeof declaredView, 'view must declare an exponential half-life').toBe('number'); + expect(typeof declaredLike, 'like must declare an exponential half-life').toBe('number'); + + await recordJson(testInfo, 'decay-recovers-declared-half-life', { + elapsedSeconds, + declaredDecay: app.declaredDecay, + view: { first: viewFirst, second: viewSecond, impliedHalfLifeSeconds: viewHalfLife }, + like: { first: likeFirst, second: likeSecond, impliedHalfLifeSeconds: likeHalfLife }, + impliedDays: { view: viewHalfLife / 86_400, like: likeHalfLife / 86_400 }, + }); + + expect(viewSecond!, 'the same stored view must report lower on a later read').toBeLessThan( + viewFirst!, + ); + expect(likeSecond!, 'the same stored like must report lower on a later read').toBeLessThan( + likeFirst!, + ); + + // +/-20% is generous against a 4-second observation window and still + // separates the two decisively: 7d x 1.2 = 8.4d sits well below 14d x 0.8 = 11.2d. + const tolerance = 0.2; + expect( + Math.abs(viewHalfLife - (declaredView as number)) / (declaredView as number), + `view decayed as if its half-life were ${(viewHalfLife / 86_400).toFixed(2)}d, but the ` + + `loaded schema declares ${((declaredView as number) / 86_400).toFixed(2)}d`, + ).toBeLessThan(tolerance); + expect( + Math.abs(likeHalfLife - (declaredLike as number)) / (declaredLike as number), + `like decayed as if its half-life were ${(likeHalfLife / 86_400).toFixed(2)}d, but the ` + + `loaded schema declares ${((declaredLike as number) / 86_400).toFixed(2)}d`, + ).toBeLessThan(tolerance); + + // Note on scope: `skip` is declared permanent, and permanence is the one + // decay claim this surface cannot show — no built-in profile reports a skip + // term at all (see the inertness check above), so there is no value to watch + // hold still. Recorded rather than faked. + expect(app.declaredDecay.skip, 'skip remains declared permanent').toBe('permanent'); + } finally { + await app.close(); + } + }); + + test('vector search returns the true nearest neighbours, not an approximation', async ({ + request, + }, testInfo) => { + const app = await startApp(); + try { + const observations: Record[] = []; + + for (const probe of PROBE_IDS) { + const vector = app.groundTruth.probeVectors[String(probe)]; + expect(vector, `ground truth carries no query vector for probe ${probe}`).toBeDefined(); + expect(vector!.length, 'the query vector must match the declared width').toBe( + app.groundTruth.dim, + ); + + // Field is `vector`, not `values` — a wrong name is answered 422, which + // reads like a malformed body. Response field is `items`, not `matches`. + const response = await request.post(`${app.url}/api/vector_search`, { + data: { vector, k: 10 }, + }); + expect(response.status(), `vector_search for ${probe} must answer`).toBe(200); + const body = (await response.json()) as { + items?: { entity_id: number; distance: number }[]; + }; + const items = body.items ?? []; + expect( + items.length, + 'an empty result cannot distinguish a perfect index from a broken one', + ).toBeGreaterThan(0); + + const truth = app.groundTruth.neighboursByItem[String(probe)] ?? []; + const returned = items.map((item) => item.entity_id); + const compared = Math.min(truth.length, returned.length); + + observations.push({ + probe, + selfDistance: items[0]?.distance, + returned: returned.slice(0, compared), + truth: truth.slice(0, compared), + }); + + // An item's own vector must find the item itself, at ~zero distance. + expect(items[0]?.entity_id, `${probe}'s own vector must rank ${probe} first`).toBe(probe); + expect( + items[0]!.distance, + `${probe} found itself at distance ${items[0]!.distance}, beyond the f32 round-trip ` + + `tolerance of ${app.groundTruth.selfDistanceTolerance}`, + ).toBeLessThan(app.groundTruth.selfDistanceTolerance); + + // And the whole top-k must equal brute-force cosine over the same corpus, + // computed by `tidal-stress`'s own oracle from the same generator that + // produced the indexed vectors. + expect( + returned.slice(0, compared), + `ANN top-${compared} for ${probe} diverged from brute-force cosine`, + ).toEqual(truth.slice(0, compared)); + } + + await recordJson(testInfo, 'ann-matches-brute-force', { + dim: app.groundTruth.dim, + tolerance: app.groundTruth.selfDistanceTolerance, + observations, + }); + } finally { + await app.close(); + } + }); + + test('reported signal values reconcile with what was written, and rank is sequential', async ({ + request, + }, testInfo) => { + const app = await startApp(); + try { + const likedTwice = 900_000_200; + const viewedOnce = 900_000_201; + + await writeSignal(request, app, likedTwice, 'like'); + await writeSignal(request, app, likedTwice, 'like'); + await writeSignal(request, app, viewedOnce, 'view'); + + const items = await readFeed(request, app); + const liked = items[positionOf(items, likedTwice)]; + const viewed = items[positionOf(items, viewedOnce)]; + + const likeBoost = signalValue(liked, 'like_boost'); + const viewCount = signalValue(viewed, 'view'); + const viewBoost = signalValue(viewed, 'view_boost'); + const ranks = items.map((item) => item.rank); + const expectedRanks = items.map((_unused, index) => index + 1); + + await recordJson(testInfo, 'explainability-reconciles', { + likedTwice: { entityId: likedTwice, likeBoost }, + viewedOnce: { entityId: viewedOnce, viewCount, viewBoost }, + ranks, + }); + + // Two unit-weight likes report a boost of 2 per like. Decay over the few + // milliseconds since the write makes this marginally short of 4, so the + // comparison is a tolerance rather than an equality. + expect(likeBoost, 'two unit likes must be reported, not silently dropped').toBeDefined(); + expect( + Math.abs(likeBoost! - 4), + `two unit-weight likes reported like_boost ${likeBoost}, expected ~4 (2 per like)`, + ).toBeLessThan(0.01); + + // The raw `view` term is a count, and it must equal the number of writes. + expect(viewCount, 'the raw view count must be reported').toBe(1); + expect( + Math.abs(viewBoost! - 1), + `one unit-weight view reported view_boost ${viewBoost}, expected ~1`, + ).toBeLessThan(0.01); + + // Rank must be a dense 1..n sequence. This is the assertion that caught the + // duplicate-rank defect on the deployed cluster, where `scatter_merge` + // returns a merged slice without re-stamping rank + // (`tidal-server/src/cluster/node.rs:7542`). Standalone numbers ranks at + // `tidal/src/query/executor/pipeline.rs:611` and is unaffected — the pair of + // results is what localises the defect to the merge. + // See 11-ranking-integrity.spec.ts for the cluster half. + expect( + ranks, + 'rank must be a dense 1..n sequence; duplicates mean a merge did not re-stamp it', + ).toEqual(expectedRanks); + } finally { + await app.close(); + } + }); +}); diff --git a/tests/e2e/features/11-ranking-integrity.spec.ts b/tests/e2e/features/11-ranking-integrity.spec.ts new file mode 100644 index 0000000..b9b0534 --- /dev/null +++ b/tests/e2e/features/11-ranking-integrity.spec.ts @@ -0,0 +1,144 @@ +/** + * Section 11 — ranking integrity on the deployed cluster. + * + * A tripwire pair, in the honest direction. `rank` is currently WRONG on every + * corpus-wide ranked response from the cluster, and these checks pin that with + * its root cause so the fix announces itself instead of the defect persisting + * silently. Same shape as the inert-feature checks in + * `09-operator-authority.spec.ts`. + * + * Root cause, localised: `scatter_merge` + * (`tidal-server/src/cluster/node.rs:7471`) concatenates each shard group's + * locally-ranked slice, sorts by score, truncates, and returns at `:7542` + * WITHOUT re-stamping rank. Its sibling `merge_cross_shard` (`:7583`) does + * re-stamp, at `:7609`, with a comment naming this exact hazard — but `:7621` + * documents that full placement short-circuits past it, and this cluster is + * full-placement RF3, so the guarded path never runs. + * + * The evidence that it is the MERGE and not the engine is two-sided: + * - standalone numbers ranks densely (`10-ranking-semantics.spec.ts`, measured + * 1..60 with no gaps) via `tidal/src/query/executor/pipeline.rs:611`; + * - the cluster returns per-group ranks concatenated, while SCORES remain + * correctly ordered — so the merge's sort is fine and only the stamp is absent. + * + * Read-only. Nothing here writes to the deployed cluster. + */ + +import { expect, test, type APIRequestContext } from '@playwright/test'; +import { recordJson } from '../support/evidence.ts'; +import { PUBLIC_BASE_URL, apiKey } from '../support/env.ts'; + +type RankedItem = { entity_id: number; score: number; rank: number }; + +/** + * Enough rows that at least two shard groups must both contribute. With three + * groups a limit of 1 or 2 can be answered from one group and would show no + * duplicate at all. + */ +const LIMIT = 12; + +/** What a correctly stamped response looks like: dense, ascending, from 1. */ +const denseRanks = (count: number): number[] => + Array.from({ length: count }, (_unused, index) => index + 1); + +async function ranked( + request: APIRequestContext, + path: string, +): Promise<{ items: RankedItem[]; ranks: number[]; scores: number[] }> { + const response = await request.get(`${PUBLIC_BASE_URL}${path}`, { + headers: { authorization: `Bearer ${apiKey()}` }, + }); + expect(response.status(), `${path} must answer before any conclusion is drawn`).toBe(200); + const body = (await response.json()) as { items?: RankedItem[] }; + const items = body.items ?? []; + expect( + items.length, + 'an empty result cannot distinguish correct ranking from broken ranking', + ).toBeGreaterThan(1); + return { + items, + ranks: items.map((item) => item.rank), + scores: items.map((item) => item.score), + }; +} + +/** The order is right even though the stamp is wrong — the precise localisation. */ +function expectScoresOrdered(scores: number[], surface: string): void { + for (let index = 1; index < scores.length; index += 1) { + expect( + scores[index]!, + `${surface} returned scores out of order at position ${index} ` + + `(${scores[index - 1]} then ${scores[index]}). That is a DIFFERENT and worse ` + + `defect than the rank stamp: it would mean the merge's sort is broken too.`, + ).toBeLessThanOrEqual(scores[index - 1]!); + } +} + +const FIX_INSTRUCTION = + 'Good news: scatter_merge appears to be fixed. Delete this tripwire, keep the ' + + 'dense-rank assertion in 10-ranking-semantics.spec.ts, and close the defect.'; + +test.describe('section 11 — ranking integrity (cluster tripwires)', () => { + test('cluster /feed rank is duplicated — pinned defect in scatter_merge', async ({ + request, + }, testInfo) => { + const { items, ranks, scores } = await ranked( + request, + `/feed?profile=for_you&limit=${LIMIT}`, + ); + const duplicates = ranks.length - new Set(ranks).size; + + await recordJson(testInfo, 'cluster-feed-ranks', { + surface: `/feed?profile=for_you&limit=${LIMIT}`, + ranks, + expectedIfFixed: denseRanks(ranks.length), + duplicateCount: duplicates, + scores, + entityIds: items.map((item) => item.entity_id), + rootCause: 'tidal-server/src/cluster/node.rs:7542 (scatter_merge returns without set_rank)', + }); + + expect(ranks, FIX_INSTRUCTION).not.toEqual(denseRanks(ranks.length)); + expect( + duplicates, + 'expected duplicate ranks — the signature of per-group slices merged without ' + + `a re-stamp. ${FIX_INSTRUCTION}`, + ).toBeGreaterThan(0); + + // Ordering is correct; only the stamp is missing. If this ever fails, the + // defect has become materially worse. + expectScoresOrdered(scores, '/feed'); + }); + + test('cluster /search rank is duplicated the same way, from the same merge', async ({ + request, + }, testInfo) => { + // A term known to be indexed on this corpus. The production corpus has no + // titles for its 33k items, so an arbitrary word returns zero candidates and + // would make this check vacuous. + const { items, ranks, scores } = await ranked( + request, + `/search?query=verification&limit=${LIMIT}`, + ); + const duplicates = ranks.length - new Set(ranks).size; + + await recordJson(testInfo, 'cluster-search-ranks', { + surface: `/search?query=verification&limit=${LIMIT}`, + ranks, + expectedIfFixed: denseRanks(ranks.length), + duplicateCount: duplicates, + scores, + entityIds: items.map((item) => item.entity_id), + }); + + // `/search` shares the same gateway merge, so the same defect surfaces here. + // Asserting it on both surfaces is what shows the fault is in the merge + // rather than in one query pipeline. + expect(ranks, FIX_INSTRUCTION).not.toEqual(denseRanks(ranks.length)); + expect( + duplicates, + `expected duplicate ranks on /search too. ${FIX_INSTRUCTION}`, + ).toBeGreaterThan(0); + expectScoresOrdered(scores, '/search'); + }); +}); diff --git a/tests/e2e/support/cluster.ts b/tests/e2e/support/cluster.ts index 6f30687..0054a26 100644 --- a/tests/e2e/support/cluster.ts +++ b/tests/e2e/support/cluster.ts @@ -15,7 +15,11 @@ import { execFile, spawn, type ChildProcess } from 'node:child_process'; import { createConnection } from 'node:net'; import { setTimeout as sleep } from 'node:timers/promises'; import { promisify } from 'node:util'; -import { KUBECONFIG, TIDALCTL_BIN, redact } from './env'; +// Explicit `.ts` extension: this module is in the transitive closure of +// `tests/e2e/app/harness.ts`, which `npm run app:dev` loads with bare Node's +// type stripping — and Node's ESM resolver does not guess extensions. Playwright +// resolves either form, so specs elsewhere keep the extensionless style. +import { KUBECONFIG, TIDALCTL_BIN, redact } from './env.ts'; const execFileAsync = promisify(execFile); diff --git a/tidal-stress/src/bin/feed-fixture.rs b/tidal-stress/src/bin/feed-fixture.rs new file mode 100644 index 0000000..24df8b2 --- /dev/null +++ b/tidal-stress/src/bin/feed-fixture.rs @@ -0,0 +1,247 @@ +//! `feed-fixture` — seed a small NAMED catalog into a running tidalDB and emit +//! its brute-force ground truth. +//! +//! The content-feed verification app needs three things that must agree exactly: +//! the items the database indexed, the vectors it indexed for them, and an oracle +//! saying which of those vectors are truly nearest to which. This binary produces +//! all three from one generator, so they cannot disagree. +//! +//! Vectors come from [`tidal_stress::recall::embedding_for`] — the SAME generator +//! the recall harness and the engine-side tests use. There is already a second +//! copy of that scheme in `tidal/src/db/items.rs`; a third (in the TypeScript +//! harness, say) would mean the oracle could silently stop matching what was +//! indexed. Reuse is the whole point of putting this binary in this crate. +//! +//! It deliberately writes NO signals. Signals are what the assertions +//! manipulate, so seeding any here would pre-bias every run. +//! +//! Usage: +//! ```text +//! feed-fixture --base-url http://127.0.0.1:9400 \ +//! --catalog catalog.json --out truth.json \ +//! --probe 900000007 --probe 900000107 +//! ``` + +use std::collections::HashMap; +use std::path::PathBuf; +use std::process::ExitCode; + +use clap::Parser; +use serde::{Deserialize, Serialize}; +use tidal_stress::recall::{GroundTruth, embedding_for}; + +/// How far an item's own vector may land from itself and still count as "zero" +/// distance. Non-zero only because the vector round-trips through f32 JSON and +/// the engine re-normalises on write; measured self-distance on the live cluster +/// was 0.013, so this is ~4x headroom over observed rounding. +const SELF_DISTANCE_TOLERANCE: f32 = 0.05; + +#[derive(Parser)] +#[command( + version, + about = "Seed a named fixture catalog + emit its brute-force ground truth" +)] +struct Cli { + /// Base URL of the target node, e.g. `http://127.0.0.1:9400`. + #[arg(long)] + base_url: String, + + /// JSON array of `{entityId, title, category}` — the catalog to seed. + #[arg(long)] + catalog: PathBuf, + + /// Where to write the ground-truth JSON. A generated artifact; never commit it. + #[arg(long)] + out: PathBuf, + + /// Embedding width. Must match the schema's declared dimension. + #[arg(long, default_value_t = 1536)] + dim: usize, + + /// Neighbours to record per item. + #[arg(long, default_value_t = 10)] + k: usize, + + /// Ids whose raw query vector is emitted, for the ANN assertion's input. + /// Repeatable. + #[arg(long = "probe")] + probes: Vec, + + /// Bearer token, when the target requires auth. A local standalone does not + /// (`tidal-server/tests/standalone.rs:32`). + #[arg(long, env = "TIDAL_API_KEY")] + api_key: Option, +} + +/// One catalog entry, as the TypeScript fixture contract serialises it. +#[derive(Debug, Deserialize)] +struct CatalogItem { + #[serde(rename = "entityId")] + entity_id: u64, + title: String, + category: String, +} + +/// `POST /items` body (mirrors `tidal-server/src/dto.rs:27` `ItemRequest`). +#[derive(Serialize)] +struct ItemBody<'a> { + entity_id: u64, + metadata: HashMap<&'a str, &'a str>, +} + +/// `POST /embeddings` body (mirrors `dto.rs:37` `EmbeddingRequest`). +#[derive(Serialize)] +struct EmbeddingBody<'a> { + entity_id: u64, + values: &'a [f32], +} + +/// What the harness and the specs read back. +#[derive(Serialize)] +struct Truth { + dim: usize, + #[serde(rename = "neighboursByItem")] + neighbours_by_item: HashMap>, + #[serde(rename = "probeVectors")] + probe_vectors: HashMap>, + #[serde(rename = "selfDistanceTolerance")] + self_distance_tolerance: f32, +} + +fn main() -> ExitCode { + let cli = Cli::parse(); + match tokio::runtime::Runtime::new() { + Ok(rt) => match rt.block_on(run(cli)) { + Ok(()) => ExitCode::SUCCESS, + Err(err) => { + eprintln!("error: {err}"); + ExitCode::FAILURE + } + }, + Err(err) => { + eprintln!("error: could not start runtime: {err}"); + ExitCode::FAILURE + } + } +} + +async fn run(cli: Cli) -> Result<(), String> { + let raw = std::fs::read_to_string(&cli.catalog) + .map_err(|e| format!("could not read --catalog {}: {e}", cli.catalog.display()))?; + let catalog: Vec = + serde_json::from_str(&raw).map_err(|e| format!("malformed catalog JSON: {e}"))?; + if catalog.is_empty() { + return Err("catalog is empty — nothing to seed".to_owned()); + } + + let client = reqwest::Client::builder() + .build() + .map_err(|e| format!("http client build failed: {e}"))?; + let base = cli.base_url.trim_end_matches('/'); + + // Generate every vector ONCE, up front, so the bytes posted to the engine and + // the bytes fed to the oracle are literally the same values. + let vectors: Vec<(u64, Vec)> = catalog + .iter() + .map(|item| (item.entity_id, embedding_for(item.entity_id, cli.dim))) + .collect(); + + for item in &catalog { + let metadata = HashMap::from([ + ("title", item.title.as_str()), + ("category", item.category.as_str()), + ]); + post( + &client, + &format!("{base}/items"), + cli.api_key.as_deref(), + &ItemBody { + entity_id: item.entity_id, + metadata, + }, + ) + .await?; + } + + for (id, values) in &vectors { + post( + &client, + &format!("{base}/embeddings"), + cli.api_key.as_deref(), + &EmbeddingBody { + entity_id: *id, + values, + }, + ) + .await?; + } + + // The oracle covers ONLY the catalog ids. That is sound because each category + // owns an otherwise-unoccupied embedding cluster: intra-cluster distance + // ~0.435 against ~2.04 for the nearest foreign vector, so the local top-k IS + // the global top-k. Anything else would need a 900-million-vector oracle. + let truth_oracle = GroundTruth::from_ids(vectors.iter().map(|(id, _)| *id).collect(), cli.dim); + let neighbours_by_item: HashMap> = vectors + .iter() + .map(|(id, values)| (id.to_string(), truth_oracle.top_k(values, cli.k))) + .collect(); + + let mut probe_vectors: HashMap> = HashMap::new(); + for probe in &cli.probes { + let found = vectors.iter().find(|(id, _)| id == probe); + let (_, values) = found.ok_or_else(|| { + format!("--probe {probe} is not in the catalog; it would have no ground truth") + })?; + probe_vectors.insert(probe.to_string(), values.clone()); + } + + let truth = Truth { + dim: cli.dim, + neighbours_by_item, + probe_vectors, + self_distance_tolerance: SELF_DISTANCE_TOLERANCE, + }; + let encoded = + serde_json::to_string(&truth).map_err(|e| format!("could not encode ground truth: {e}"))?; + std::fs::write(&cli.out, encoded) + .map_err(|e| format!("could not write --out {}: {e}", cli.out.display()))?; + + println!( + "seeded {} items + {} embeddings into {base}; ground truth -> {} ({} probes)", + catalog.len(), + vectors.len(), + cli.out.display(), + cli.probes.len() + ); + Ok(()) +} + +/// POST one JSON body, treating any non-2xx as fatal. +/// +/// A partially seeded corpus makes every downstream assertion meaningless — the +/// ANN top-k would be wrong for reasons that have nothing to do with the index — +/// so there is no "skip and continue" path here by design. +async fn post( + client: &reqwest::Client, + url: &str, + api_key: Option<&str>, + body: &B, +) -> Result<(), String> { + let mut request = client.post(url).json(body); + if let Some(key) = api_key { + request = request.bearer_auth(key); + } + let response = request + .send() + .await + .map_err(|e| format!("POST {url} failed to send: {e}"))?; + let status = response.status(); + if status.is_success() { + return Ok(()); + } + let detail = response + .text() + .await + .unwrap_or_else(|e| format!("")); + Err(format!("POST {url} returned {status}: {detail}")) +} diff --git a/tidal-stress/src/recall.rs b/tidal-stress/src/recall.rs index 5d31481..d6d6206 100644 --- a/tidal-stress/src/recall.rs +++ b/tidal-stress/src/recall.rs @@ -129,33 +129,51 @@ pub fn embedding_for(id: u64, dim: usize) -> Vec { pub struct GroundTruth { flat: Vec, norms: Vec, + /// The corpus ids, positionally aligned with `flat`/`norms`. + /// + /// Stored rather than implied by `1..=n` so the oracle also serves a SPARSE + /// id set. The fixture catalog's ids live in deliberately unoccupied 100-id + /// clusters (`900_000_0xx`), so a dense oracle would have to materialise 900 + /// million vectors to answer a question about 60 of them. + ids: Vec, dim: usize, - n: u64, } impl GroundTruth { /// Build the deterministic corpus for ids `1..=n` at `dim` dimensions. #[must_use] pub fn build(n: u64, dim: usize) -> Self { - let mut flat = Vec::with_capacity((n as usize) * dim); - let mut norms = Vec::with_capacity(n as usize); - for id in 1..=n { + // The collected ids ARE the stored ids — no transient copy even at 1M. + Self::from_ids((1..=n).collect(), dim) + } + + /// Build the deterministic corpus for an arbitrary, possibly sparse id set. + /// + /// Same generator and same cosine oracle as [`Self::build`]; the only + /// difference is that the ids need not be contiguous. This is what lets a + /// small named catalog get an *exact* oracle: when every id sits in an + /// otherwise-unoccupied cluster, brute force over just those ids is the + /// global answer. + #[must_use] + pub fn from_ids(ids: Vec, dim: usize) -> Self { + let mut flat = Vec::with_capacity(ids.len() * dim); + let mut norms = Vec::with_capacity(ids.len()); + for &id in &ids { let v = embedding_for(id, dim); - let norm = v.iter().map(|x| x * x).sum::().sqrt(); - norms.push(norm); + norms.push(v.iter().map(|x| x * x).sum::().sqrt()); flat.extend_from_slice(&v); } Self { flat, norms, + ids, dim, - n, } } #[must_use] - pub const fn n(&self) -> u64 { - self.n + pub fn n(&self) -> u64 { + self.ids.len() as u64 } #[must_use] @@ -163,10 +181,11 @@ impl GroundTruth { self.dim } - /// The raw stored vector for `id` (1-based). + /// The raw stored vector at positional `index` (NOT keyed by id — ids may be + /// sparse, so position is the only stable key into `flat`). #[must_use] - fn vector(&self, id: u64) -> &[f32] { - let start = ((id - 1) as usize) * self.dim; + fn vector_at(&self, index: usize) -> &[f32] { + let start = index * self.dim; &self.flat[start..start + self.dim] } @@ -181,12 +200,16 @@ impl GroundTruth { // A bounded top-k kept as a min-by-score Vec (k is tiny, ~10): cheaper and // allocation-lighter than a full sort of n scored pairs per query. let mut top: Vec<(f32, u64)> = Vec::with_capacity(k + 1); - for id in 1..=self.n { - let xn = self.norms[(id - 1) as usize]; + for (index, &id) in self.ids.iter().enumerate() { + let xn = self.norms[index]; if xn == 0.0 { continue; } - let dot: f32 = query.iter().zip(self.vector(id)).map(|(a, b)| a * b).sum(); + let dot: f32 = query + .iter() + .zip(self.vector_at(index)) + .map(|(a, b)| a * b) + .sum(); let score = dot / (q_norm * xn); // cosine; higher = nearer if top.len() < k { top.push((score, id)); @@ -236,8 +259,8 @@ impl QueryPool { // is not clustered on the low ids. let queries: Vec> = (0..pool_size) .map(|i| { - let base = ((i as u64).wrapping_mul(2_654_435_761) % n) + 1; - let mut q = gt.vector(base).to_vec(); + let index = ((i as u64).wrapping_mul(2_654_435_761) % n) as usize; + let mut q = gt.vector_at(index).to_vec(); let mut state = (i as u64) .wrapping_mul(0x100_0000_01B3) .wrapping_add(0xABCD); @@ -505,6 +528,36 @@ mod tests { assert_eq!(top[0], 42, "an item's own vector is its nearest neighbor"); } + #[test] + fn ground_truth_serves_sparse_ids_and_preserves_cluster_structure() { + // The fixture catalog's shape: two 15-item bands in DIFFERENT clusters, + // ids nowhere near 1..=n. A dense oracle could not answer this at all. + let band_a: Vec = (900_000_000..900_000_015).collect(); + let band_b: Vec = (900_000_100..900_000_115).collect(); + let ids: Vec = band_a.iter().chain(&band_b).copied().collect(); + let gt = GroundTruth::from_ids(ids, 64); + assert_eq!(gt.n(), 30); + + // An item's own vector still ranks itself first. + let q = embedding_for(900_000_107, 64); + let top = gt.top_k(&q, 15); + assert_eq!( + top[0], 900_000_107, + "own vector is its own nearest neighbor" + ); + + // And every one of the 15 nearest is its OWN cluster-mate: the bands are + // separable, which is exactly the property that makes a 60-item oracle + // the global oracle for the fixture catalog. + for id in &top { + assert!( + band_b.contains(id), + "id {id} from a foreign cluster outranked a cluster-mate; the \ + ground-truth separation the fixture depends on is broken" + ); + } + } + #[test] fn recall_at_k_counts_overlap_over_truth_size() { let truth = vec![1, 2, 3, 4, 5]; diff --git a/tsconfig.json b/tsconfig.json index c1d27bc..b1b75dc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,10 @@ "skipLibCheck": true, "resolveJsonModule": true, "noEmit": true, - "types": ["node"] + "types": ["node"], + // Explicit `.ts` import specifiers: tests/e2e/app/* must be loadable by bare + // Node for `npm run app:dev`, and Node's ESM resolver does not guess extensions. + "allowImportingTsExtensions": true }, "include": [ "playwright.config.ts",