71d3b7b78692c31ba0596ec88874986ff1b58116
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
71d3b7b786 |
E10 Report issue template + E12 /report archive, to spec
PHASE 0 — the spec, read not recalled. E10: "Hybrid: dark billboard header that survives every client, light paper body Gmail can't wreck. 600px, stacked, no webfont dependence." Content law: "One email per slate day. Top read, what changed, the record. Nothing else." E12: "EVERY ISSUE SHOWS ITS OWN DAY RECORD -- THE ARCHIVE IS A LEDGER TOO." COMPOSED, NOT FORKED. The audit had E10 as PARTIAL, not absent: newsletterService already builds the daily report's CONTENT and lints its voice. What was missing is the designed hybrid SHELL, so reportTemplate.js is a template over that builder rather than a second report -- the same call made for the movement strip, and for the same reason. PHASE 1 — the hybrid shell is an ENGINEERING constraint, not a look, and the tests say so: Gmail strips style blocks, Outlook ignores flexbox, and a dark body renders as a black rectangle in several clients. Hence tables, inline styles, 600px fixed, system fonts, no image required to read, and the green SHIFTS from #00D4A0 to #00A57D on paper because the dark-mode green is unreadable there. FACT-CONTRACTED: a section whose data is absent is OMITTED and NAMED in `omitted`, never filled. There is no code path producing a placeholder figure. The honesty block carries the real numbers -- graded count, cleared-ceiling count, the realized rate against baseline, and that we do not issue A grades. E1'S LAW TRAVELS EVEN THOUGH ITS RENDERING CANNOT. An SVG strip is not reliable in email, so movementText carries the RULE: green only when the move favours the read, and a flat market says FLAT · [N]D rather than showing nothing. NO DESIGNER SAMPLE DATA. Nabers 1,120.5, No 128, DAY RECORD 9-4 are a spec for what a live issue renders; pasting them in would be fabrication carrying a designer's authority and would look entirely correct. Tested. PHASE 2 — /report is now the real archive, REPLACING the S41 redirect to /blog. That redirect existed because the surface did not; E12 built it, so the placeholder is correctly gone and the S41 test is updated rather than worked around. Every row carries its own day record, and an unknown record says UNSETTLED -- never a dash that reads as zero. Empty archive is an honest state. Backend: public read-only /api/report over Redis issues, plus the Next proxy. Both surfaces registered under the reachability guard. A test bug I made twice now: my check for forbidden sample values matched the template's own doc block, which NAMES those values as things never to paste. Documentation worth keeping, so both suites strip comments before matching -- a guard that reads its own warning is not reading the code. WAVE-2 STATUS: E1, F9-F11, E10, E12 done. Still gated -- F5 article media and E16/F8 on the card-system reconciliation; the in-season hub IA on the social chat's formula; E9/E15 on model; E2/E6 on licensing. Read-only throughout; serving fingerprint unchanged including newsletterService; accrual clock unchanged at 0 eligible dates. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9 |
||
|
|
49e76068da |
Doctrine + E1 movement strip + F9-F11 offseason hub shell, to spec
PHASE 0 — specs/ARCHETYPE-TAXONOMY-DOCTRINE.md records the ruling as
shared law: 83 designed glyphs are the full four-sport taxonomy; a glyph
renders ONLY where its archetype is modeled and proven. 39 of 83 map to a
real archetype and are wired; the 44 unmapped are DORMANT SLOTS for
WNBA/NBA/Soccer, not a wiring gap. Wiring them would mean inventing 44
archetypes to consume artwork -- decoration presented as classification,
which is forbidden. DUAL THREAT and PAINT BOSS are modeled archetypes with
no mark: the mirror gap, flagged to the design side. When a sport's
archetypes ship, activation is a MANIFEST lookup, not new art.
PHASE 1 — E1 movement strip. The spec's own line is "the movement strip is
defined once here and reused everywhere a line has a past", so it is a
primitive, not a fourth chart.
RECONCILED RATHER THAN FORKED: lib/gradeShift.js ALREADY implements E1's
colour law -- toward/against/flat, including the direction flip that makes
an UNDER's favourable move the opposite sign of an OVER's. MovementStrip
CONSUMES buildGradeTimeline instead of reimplementing it, and a test
asserts it never redefines isUnder. GradeShift stays the grade-history
view; this is the reusable strip. That is the card-fork lesson applied
before it could happen again.
Spec laws honoured: STEPS NOT CURVES (H then V, no smoothing -- a curve
invents prices that never traded, and a test rejects any C/S/Q/T command);
green only when the move FAVOURS the read; FLAT renders as a hairline plus
FLAT · [N]D because a flat market is a finding; and too little history
says NO MOVEMENT HISTORY rather than rendering blank.
PHASE 2 — F9-F11 offseason hub shell, built from Vyndr Offseason.dc.html.
The spec's load-bearing words are used verbatim: "OUTLOOKS REPRICE ON NEWS
· NOT GAME ODDS" (an offseason number is not a game line), the QUIET WIRE
empty state ("No outlook-moving news since X. We don't manufacture
movement."), WHAT CHANGED TODAY as the hero with the countdown ambient and
top-right, the tag-colour-is-meaning row anatomy, the open -> NOW -> FAIR
triplet with the movement strip embedded, and the OUTLOOK ONLY block where
every row carries NOT GRADED.
THE DESIGN FILE'S SAMPLE DATA IS NOT IN THE COMPONENT. Wembanyama +420 ->
+330, Nabers cleared 11:42 AM, the Summer League names -- all of it is a
SPEC for what a live feed renders, and copying it in would be fabrication
carrying a designer's authority. A test asserts none of those strings
appear.
The IN-SEASON information architecture is NOT invented here. The spec
covers an offseason hub; nothing specifies how content, articles, wire and
the live slate share year-round navigation. That remains the open design
gap, and the route notes it.
Two test bugs caught and fixed: my first assertions matched my own doc
comments -- the ordering check found "WHAT CHANGED TODAY" in the header
block and the no-curves check caught the word "curve" in the sentence
explaining why curves are wrong. A guard that reads its own explanation is
not reading the render; both now strip comments first.
PHASE 3 — both surfaces registered under the reachability guard. Read-only
throughout, serving fingerprint unchanged, accrual clock unchanged at 0
eligible dates.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
|
||
|
|
c575a708c7 |
Content studio API + preview page; widen the reachability guard; correct
two inventory errors INVENTORY CORRECTION, and it was mine. Phase 2's two "orphans" are NOT orphans -- my board grepped only web/src/app and missed component-level mounting. The transitive check says both are already mounted: BookComparisonPanel -> GradeResultCard -> app/scan/page.tsx NewsWire -> ExploreHub -> app/explore/page.tsx So book comparison is DONE (wired to /api/books, rendering on the grade card) and THE WIRE is DONE-BY-DESIGN, mounted in ExploreHub. Its header names an "Offseason Hub" as its home, and that hub genuinely does not exist -- but that is board item #8, not a mounting bug, and inventing a surface to satisfy a comment would be the wrong fix. The lesson is the same one this session keeps teaching: I checked one directory and reported a conclusion the check could not support. ALSO CAUGHT: I overwrote src/routes/content.js, which was the Session-29 content-templates route, by picking a filename without looking. Restored from git with no work lost; the new surface lives at /api/content-studio and both now coexist. PHASE 0/1 — /api/content-studio serves finished posts (copy, branded card, card_svg, the fact_contract each was REQUIRED to have, and the facts that actually backed it) plus a POST for editorial status in Redis. Private via internal key; the Next proxy holds the key server-side so the browser never does. /studio renders it as a thin client -- copy and card side by side with the fact contract visible, because reviewing copy by reading it is exactly how a wrong number ships. Never-blank: a night with nothing generated says so. API-FIRST is the point: the endpoint an autonomous poster will call is the one the page already renders, so the agent handoff is a pointer change, not a rebuild. Contract documented at docs/CONTENT-STUDIO-API.md. EXPRESS 5 BROKE 23 SUITES at first: `router.get('/:date?')` throws at mount time in Express 5, taking down everything that imports app.js. Two explicit routes instead. PHASE 3 — the reachability guard is widened from grade-fields-only to a general built-but-unread check. Book comparison, THE WIRE and the content studio are now registered surfaces; a page counts as its own entry point (Next mounts it by convention) while everything else must trace to one. 22 checks green; a registered-but-unimported surface still goes red. FULLY ISOLATED: read-only on model/slate/ledger, serving fingerprint verified unchanged, accrual clock unchanged at 0 eligible dates. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9 |
||
|
|
09186ea609 |
Wire the composed surface: the honest fields now reach a user
PHASE 0 re-check caught the same failure a THIRD time, mine again. Last
turn I created GradeScaleLegend.tsx and never mounted it, and I reported
that the separation flag "renders per band" -- it did not. grep: zero
frontend references to separates_from_base_rate, served_grade or
factor_adjustment. The honest grade was reaching the API payload and dying
at the adapter boundary.
That is three occurrences in three orders of the same shape: built,
correct, unread. gradeBands, then served_grade, then the legend.
PHASE 2 — the composed surface is wired end to end:
analyzeViaEngine1 -> served_grade + factor_adjustment on the payload
scan/page.tsx -> ScanResponse types them and forwards them
gradeAdapter -> gradeMeaning, separatesFromBaseRate,
bandRealizedRate, factorsApplied, refusalReason
GradeResultCard -> renders "WHAT THIS GRADE MEANS"
What a user now sees that they could not before: what the band has
actually realized, an amber note when the read CANNOT be separated from
the baseline, and -- only where a factor actually fired, with its proven
sign -- what moved the read, in plain language rather than feature names
("where he hits it vs who is standing there").
No narrative on props where nothing fired: factorsApplied is empty and the
block self-hides. Only the three PROVEN hits factors have labels, so an
unproven factor cannot acquire prose by being added to the map.
PHASE 1 — GradeScaleLegend is now MOUNTED on the grade card (compact). The
ceiling is a stated position where the grade is, not a page a user would
have to find.
PHASE 3 hand-verified through the real adapter across twelve states: B+
with 3/0 factors, B with 1, C+/C/C- all flagged not-separable, D, F, a
switch-hitter case where 2 of 3 factors fire, two refusals and a
no-forecast. never-blank PASS, no-manufactured-A PASS, no-narrative-when-
nothing-fired PASS, separation-flag-reaches-card PASS.
No A-threshold loosening. No calibrated number leaks. p_win never mutated.
engine_grade still read by zero serving code. No Bonferroni slot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
|
||
|
|
5930f18d81 |
Free proof surface: /record — tier-record-forward, honest CLV building panel
Presentation over existing endpoints. src/ untouched (git diff empty): no grade, model or ledger change. Pricing/migration are Builds 2/3. TIER-RECORD-FORWARD. /record reads the canonical public aggregates (/api/accuracy + /api/ledger/model) and prints them as-is. B 60% n512 and C 57% n413 ship with C honestly BELOW B; A (n2), D (n5) and F (n5) render HOLLOW with their real sample instead of a rate. Sport slicing (all/mlb/wnba) is client-side because the endpoints ignore ?sport= — mlb 526 @62%, wnba 411 @54% come from the sports map. THE LOAD-BEARING RULE, enforced in lib/proofRecord.js and locked by tests: where the source withholds a percentage it stays null. A is 1/2 and therefore 50% is derivable — a test asserts we do NOT derive it, because the API withheld it on purpose (n < 20). CLV IS AN HONEST ABSENCE, NOT A NUMBER. beat_close_pct is null because clvCaptureReliable() has not passed. The panel says "NOT PUBLISHED YET" and explains that any percentage printed today would be measuring our collection gaps as much as our edge; it surfaces the accruing sample (937) but no rate. Tests assert the panel never falls back to clv_beat/clv_sample (34/937 = 3.6%) and that the serialized panel contains no "3.6" — that number is computable and would be wrong, which is the exact fabrication this surface exists to refuse. The panel is built to receive a real number later without a redesign. HELD, and named on the page rather than faked: calibration and accuracy-over-time are absent because there is no honest source (no claimed-vs-actual endpoint; window_days fixed at 30 with no series). The page says so, and says it is not because they are unflattering. A page-level test asserts no hard-coded percentage exists in the markup, so no figure can drift from the aggregate, and that the page never touches /api/snapshot or itemized rows — the Build-1 gate holds and the exploit stays dead. Floor: 320 suites / 3986 tests green (16 new), web build exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc |
||
|
|
bf7c0a3c08 |
Wave 3: /compare built (real head-to-head); resolution tail scoped, not shipped
No grade, ledger or scoring change. Push scoring untouched.
REVIEW ZERO 0.3/0.4 — THE RESOLUTION TAIL DOES NOT FIRE. The resolver is
POST /api/grading/resolve (routes/grading.js:208), and its fanout at :356-371
covers webPush, telegram and discord — but:
- share-card generation: SPEC'D-NOT-BUILT. Not in the fanout at all (grep
shareCard in grading.js = 0). shareCards/renderer.js exists with ZERO
callers, so the component is built but no step would ever invoke it.
- push notifications: BUILT-NOT-FIRING. In the fanout but gated on
webPush.configured() (VAPID). push_subscriptions = 0 rows and
user_notifications = 0 rows — nothing ever subscribed or delivered.
- Telegram result posts: BUILT-NOT-FIRING (gated on BOT_TOKEN + CHANNEL_ID).
- Discord result posts: BUILT-NOT-FIRING (gated on webhookFor('results')).
- recap (all-Final trigger): SPEC'D-NOT-BUILT. No recap file exists in src/.
AND THE WHOLE TAIL IS UNREACHABLE: nothing calls /api/grading/resolve — there is
no ESPN poller in the repo. The live settlement path is the scheduler's
settleAllOutcomes + settleAllLedgers, which fans out to opsNotify only (ops
alerts), with no user-facing output. So even the built channels have no trigger.
Per the order's own rule, ShareCard, /notifications, result posts and recap are
therefore ALL SCOPED, none shipped — no dead shells over a silent pipeline.
BUILT — /compare. Semantics (0.2): a same-market head-to-head, two players with
every row a measure BOTH sides are scored on, aligned via alignRows so the
numbers are comparable — deliberately not two disconnected graded props. Reads
the live /api/stats/player/:name?sport= aggregate. Honest-absent three ways: an
unresolved side reads NO DATA while the other still renders; a measure only one
side has renders a dash, never 0; if neither resolves the page refuses to
compare. NO VERDICT — it shows measures and says the reader draws the call.
Two pre-existing tests (vyndrPhaseE, vyndrParityQA) asserted the in-development
placeholder; both superseded rather than deleted — they now assert the stronger
properties against the real page (live fetch, no sample players, NO VERDICT,
NO DATA, "not a zero").
Floor: 316 suites / 3930 tests green (10 new), web build exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
|
||
|
|
72a14dc4cd |
Build /api/props/top-graded server selector: rank with p_win, serve without it
New READ endpoint. No grade, ledger row, lock_line, or scoring write. Push
scoring untouched.
REVIEW ZERO CORRECTED THE PREMISE: the handler NEVER EXISTED in any commit
(searched git rev-list --all for a /top-graded definition in src/ — zero hits).
Not "removed" — the three axios callers (cheatsheetGenerator, gradeOfTheDay,
widget) and the Next proxy were written against a phantom endpoint, so those
three content generators have silently received [] for their entire life.
Contract recovered from the four consumers, not guessed: {props:[...]},
?sport=UPPERCASE (absent = all sports, which gradeOfTheDay relies on) + ?limit,
rows carrying player/stat/line/direction/sport/grade/confidence? plus the
player_name/stat_type aliases and game_id.
POPULATED-PATH RISK FOUND: the board's populated branch had never run in prod,
and dashboard/page.tsx:463 calls g.stat.replace(/_/g,' ') UNGUARDED (g.player
also feeds the row key, /scan URL and heading; sport must be UPPERCASE for
SportPill). toRow requires non-empty string player+stat and a finite line,
uppercases sport, and DROPS unrenderable rows — a shorter board beats a broken
one.
THE LEAK BOUNDARY (why this is server-side): the browser cannot rank on p_win
for all tiers because stripModelPrice deliberately withholds it from unentitled
tiers. Order of operations is
read cache -> RANK with p_win (every tier) -> map rows incl. model fields
-> stripModelPrice(rows, tier) -> serialize
so a free caller receives the paid RANKING without the paid VALUES. Tier comes
from resolveTierFromRequest, which FAILS CLOSED to 'free'. Cache-Control is
private under a bearer token, public otherwise (the /api/snapshot precedent).
ONE SHARED DEFINITION, no drift: new src/utils/gradeRanking.js
(takeablePWin/descNullsLast/rankGrades). heroPropService now imports
takeablePWin instead of its inline copy (behaviour unchanged — it was that
logic verbatim); the selector imports rankGrades; web/src/lib/slateAdapter
keeps its mirror (the browser cannot import src/, S25) and a test cross-checks
the two on identical fixtures (playerName.js precedent). Board is grade-first
("top GRADES"), hero is p_win-first ("top read") — they differ BY DESIGN and
agree within the leading tier.
HONEST LIMIT: the Next proxy (cachedBackendJson) sends no Authorization header
and caches under a shared key, so via the dashboard every viewer gets the
free-tier payload — correct order, no paid values. That is the SAFE behaviour;
forwarding auth into a shared cache is exactly how a paid payload leaks to
anonymous viewers. Per-tier delivery through the proxy needs a tier-keyed cache
and is not done here.
Verified on real prod snapshot data (anonymous path): MLB 8 props, WNBA 10,
0 paid-field leaks, render-contract safe on every row, sport uppercase.
Floor: 311 suites / 3882 tests green (18 new — leak test uses POPULATED p_win,
not today's nulls: entitled gets p_win and it drove the order, unentitled gets
a byte-identical order with all five MODEL_FIELDS absent and no trace in
JSON.stringify, while book/fair market facts survive). Web build exit 0.
Dashboard visual is auth-gated -> tagged for the Chrome audit, not faked.
Held: edge_pct rescale/retirement (Order B); board columns/contract unchanged;
tier-keyed proxy caching.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
|