diff --git a/EVOLUTION_LOG.md b/EVOLUTION_LOG.md index 7ede4188..74ecea39 100644 --- a/EVOLUTION_LOG.md +++ b/EVOLUTION_LOG.md @@ -319,3 +319,10 @@ Graph nodes updated: task-log rows for #55 rounds 1–2 and this entry's close-o **Lesson (reusable rule)** | A verification step that bundles two receipts into one output can misread its own evidence — one receipt per command, and grep for the EXPECTED string, not the absence of failure. **Graph nodes updated** | v0.0.41 release row (#171/#173 shipped). +## 2026-09-26 — Importing the discipline, not just the doctrine (#177) +**What I tried** | The comments kept citing `common/src/testing/freebuff-offer-invariants.ts` as the thing "that exists to catch the offer-without-gate shape" — so fetch the cited file and port the checks, not the prose. +**What failed + evidence** | Two of my test expectations were wrong before the checker was right. First, the double-drift case: I expected the lock AND root violations, but #173 had given every row its own root — only the lock fires now, which is the corrected doctrine working (my assertion was a fossil of the old map). Second, CodeRabbit caught the checker's blind spot: my missing-label check accepted ANOTHER model's nonempty label — the exact GLM 5.2 Desktop incident upstream documented (earned row rendered under the fallback's label). Wrong ≠ missing, and only an independent expected lookup sees the difference. +**What worked** | `freebuffOfferViolations` in contracts with six checks wired to OUR gates, applied to three surfaces in their own tests, seeded violations for every check. One surprise from upstream's shape: an empty offer set is a violation — the test that wires itself to the wrong list is the failure a green run hides. +**Lesson (reusable rule)** | (1) An invariant cited in a comment is executable somewhere — find the file, port the checks, and wire them to the surfaces that must hold. (2) A checker that has never failed is a mirror: seed its violations or it tests nothing but the happy list. (3) Wrong and missing are different failure shapes — "not undefined" is not "correct"; verify labels against an independent expectation. +**Graph nodes updated** | offer-invariants row (#177). + diff --git a/docs/KNOWLEDGE_GRAPH.md b/docs/KNOWLEDGE_GRAPH.md index 8bba6ece..7af6afe5 100644 --- a/docs/KNOWLEDGE_GRAPH.md +++ b/docs/KNOWLEDGE_GRAPH.md @@ -235,3 +235,4 @@ Model opportunities seen in `free-agents.ts:125-132`: `deepseek`, `mimo`, `minim | 2026-09-26 | PROBE ETIQUETTE AS CODE + BAN CHECK + LIVE RECEIPT (#171): the #141 laws became enforceable code — `probeEtiquette.ts` (pure, time injected): 1 admission / 60 min (the "1 hour" is the SPACING between admissions, not a session length — a session is a seat held for a turn and released; admission CHURN was the ban trigger), never 02:00-05:00 UTC for active probes, PASSIVE mode exempt so the checker works exactly when needed; `banCheckVerdict.ts` (pure): status+body → verdict, a bare 403 is NOT a ban (the wire keys on the body status); `ban-check.ts`: default ONE authenticated session-surface GET (no seat, no turn), `--active` requires `--i-understand-the-etiquette` and is VETOED by the guard (exit 3, runbook pointer, retry window). LIVE RECEIPT (passive, zero seats): still banned — `403 {"status":"banned"}`, first confirmation since 09-24; zero admissions run per the cheapest-touch law (the GET already proved what an admission would). Plus `docs/operations/ban-appeal-141.md` (drafted, UNSENT — owner decision) and the picker row-matrix audit test (servable/locked/drained classes, badge×lock×consent law in one table). Effect `globalDate` law caught two raw-Date constructions — pure logic takes epoch-ms in, scripts opt out via header. LAW: runbook rules that matter get compiled — a rule a 2am operator can override is a suggestion, not a law. | scripts/probeEtiquette.ts (+test), banCheckVerdict.ts (+test), ban-check.ts, docs/operations/ban-appeal-141.md, modelPickerRowMatrix.test.ts, PR #171 | | 2026-09-26 | SERVER VERDICT GOVERNS THE LOCK (#173): research into the #166 doctrine's wire truth found its missing half — upstream `FreebuffFreebucksInfo.planRequiredModelIds` is the SERVER's per-viewer verdict ("a client deciding this for itself would be reading its own belief"), present = authoritative (the gate turns on the resolved access tier, and once the COUNTRY), absent = static-list fallback (old-client law). The port corrected a #162 doctrine error: tier-LOCKED ≠ INADMISSIBLE — upstream keeps gpt-6-luna, mimo-v2.6-pro AND gemini-3.8-flash in its pairing map (base3-free-luna-6, base3-free-mimo-2-6-pro, base3-free-gemini-3-8-flash), so entitled sticky picks run the REAL model through their own roots instead of silently coercing; a limited viewer is refused by the server with plan-required prose, which our gate renders client-side first. Contracts: field on FreebuffProviderFreebucks, pairing map = upstream's exact admission set (10 rows), offer-without-gate rewritten to upstream's TRUE shape (forbidden pair = locked ∧ FRESHLY SELECTABLE, not locked ∧ servable). Session: field declared on the freebucks projection (pass-through body already carried it — the census error was invisible because the wire was never read). Web: ONE precedence authority — every lock gate (picker join, disabled reason, switch, all three send legs) reads the server verdict through resolveLockedModelBlock; CodeRabbit Major #2 caught the stale-verdict hazard (helper declared after both useCallback'd plan handlers, deps missing it → snapshot-only refresh left a stale verdict blocking entitled viewers / admitting newly locked ones) — hoisted above both handlers, deps wired. Contracts 24/24, adapter 10/10, chat 294/294, typecheck clean, gates + Tests verified on final head. LAW: a client-side gate built from a census is a BELIEF — read the wire for the verdict; and the forbidden pair in any offer-without-gate law is the exact pair upstream's own comment names, not the one your first draft found convenient. | packages/contracts/src/server.ts, model.ts (+test), FreebuffSession.ts, FreebuffAdapter.modelcoherence.test.ts, modelLockedRow.logic(+test), ModelPickerContent.tsx, ChatView.tsx, modelPickerRowMatrix.test.ts, PR #173 | | 2026-09-26 | RELEASE v0.0.41 (#171/#173→#175): published through the full runbook — bump PR #175 (gates + Tests verified on head) → build + stage → Publish gate GREEN on 7494a9636 (FIFTH consecutive) → publish → triad: E409, packument `latest=0.0.41` in 3 polls (~5 min), fresh-dir install 0.0.41 + audit 0 + `--version` match + boot 200/200 (teardown shape from #168 worked FIRST try). Carries #171 (etiquette guard + ban check + live still-banned receipt + appeal draft) and #173 (server verdict governs the lock; pairing map restored to upstream's exact admission set of 10 rows; locked ≠ inadmissible). | .publish-stage @ 0.0.41, PR #175, #171/#173 shipped | +| 2026-09-26 | OFFER-INVARIANTS CHECKER PORTED (#177): fetched the file upstream's own comments cite — `common/src/testing/freebuff-offer-invariants.ts` — and ported the discipline with our gates in the checks: model ids fan out across separate hand-kept lists ON PURPOSE, so only `offered ⊆ accepted` is checked (drift the other way is how a staged retirement works); an empty offer set is a VIOLATION ("the one failure mode a green run hides"); six checks (map membership → fresh surfaces never offer locked rows, listed surfaces skip it → resolver never coerces (#152) → roots answer their own pairing (the default root answering for a non-default model IS a coercion) → display names render the row as itself). Applied to THREE surfaces in their own tests (driver fresh picker, driver listed picker, adapter admission set) — adding a gate to the checker applies it everywhere at once. CodeRabbit Major caught the wrong-label shape: `nameFor` returning ANOTHER model's nonempty label passed every check — the exact GLM 5.2 Desktop incident upstream documented (earned row rendered under the fallback's label; wrong, not missing) — fixed with an independent `expectedNameFor` lookup + seeded teeth. Seeded-violation tests prove every check can fail; the double-drift case correctly yields exactly the lock violation since #173 gave every row its own root. Contracts 302/302, driver 8/8, adapter 8/8, chat 294/294, typecheck clean, gates + Tests verified on final head c4403e3c6. LAW: a checker that has never failed is a mirror, not a gate — seed its violations; and an invariant worth citing in a comment is worth importing as code. | packages/contracts/src/freebuffOfferInvariants.ts (+test), index.ts, FreebuffDriver.snapshot.test.ts, FreebuffAdapter.subagents.test.ts, PR #177 |