AI-review fixes: correctness, statistical honesty, and format currency - #82
Draft
LlamaAdam wants to merge 24 commits into
Draft
AI-review fixes: correctness, statistical honesty, and format currency#82LlamaAdam wants to merge 24 commits into
LlamaAdam wants to merge 24 commits into
Conversation
Two knowledge-log poisoners from the 2026-08-13 AI review (roadmap #1): - propose_then_iterate() proposed against the --new file and then simmed two pre-existing files, so the recorded manifest and the simmed diff were unrelated. It now proposes against the OLD deck, materializes --new via apply_proposal_to_deck (same legality invariants as auto-curate), records applied adds/cuts as the manifest with the LLM intent under requested_*, and fails fast if --new already exists (before any LLM spend) or --old is missing. - web save_iteration stored the payload's margin verbatim, which carries ComparisonReport.margin = abs(new-old); the knowledge-log column is documented signed. Web-saved regressions read as improvements in any pooled analysis. Margin is now always recomputed as new_wins-old_wins. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
- edhrec_client: commander/tag/salt/average-deck fetchers now try json.edhrec.com first and fall back to the __NEXT_DATA__ HTML scrape; shared payload walkers, same cache-poisoning guards on both paths. De-brittles ingestion and gives the bot-challenge-blocked salt backfill a JSON route. - staples/deck_builder_manabase: add the 10 triomes (3+ color identities, tiered below untapped duals) and the 10 MKM surveil duals (top budget-tier two-color duals); resolvable offline via _TIERED_LANDS. - deck_health now imports the builder's LAND_COUNT_BAND (33-40) instead of a hand-kept (33,38) — the grader no longer docks ~24 points from the app's own 39-40-land builds. - target_land_count learns optional MDFC spell-front discounting (0.5/land, matching deck_health's weighting); callers unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
…staleness Roadmap #3 from the 2026-08-13 AI review — the estimator encoded pre-Oct-2025 Commander bracket beta rules: - Two-card combo floor defers to combo_detection's speed rule (late- assembling pairs are B3-legal since the 2025-10-21 update); unknown speed still floors B4 (conservative on missing data). - Tutor 4+ auto-bump demoted from 'official rules transcription' to a clearly-labeled power heuristic — the official tutor restriction was repealed 2025-10-21 (the GC list carries efficient tutors now). - Extra-turn floor re-keyed on chainability: B4 at 3+ extra-turn cards, or 2 plus a curated recursion/copy enabler; 1-2 non-chaining cards are a weighted nudge (B3-legal in low quantities). - _EXTRA_TURN_CARDS +7 and _MLD_CARDS +8 well-known misses; both lists now regenerable offline from oracle snapshots via scripts/refresh_card_lists.py --only extra-turns|mld (tutors stay curated — oracle text can't separate tutors from fetches/ramp). - Legality freshness: LegalityReport carries data_age_days + data_warning when backing snapshots exceed 45 days (7 B&R windows/yr now); doctor gains an oracle-snapshot staleness check. No auto refetch storms — informational only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
Roadmap #2 from the 2026-08-13 AI review: - heuristic_verdict counts decisive as old_wins + new_wins (head-to- head), matching the repo's own knowledge-log convention — it previously counted filler-won pod games, so the min_decisive gate effectively always passed. - Fixed absolute-margin thresholds replaced with an exact two-sided binomial test vs p=0.5 (stdlib math.comb) in both verdict sites (analyst.heuristic_verdict, _proposer_sim._verdict_from_ab). Under the old rule, P(|new-old| >= 4) at 20 decisive games is ~0.50 — half of neutral swaps earned a confident verdict. Strong verdicts now require p < 0.05 (>= 15-5 at n=20). margin_* knobs deprecated but accepted; --sim-margin retained as a pre-filter. - LLM-facing sim summaries (claude/ollama verdict paths) now carry signed_margin, winner, draws, and h2h_decisive instead of the absolute margin — a small model could previously read margin:6 on a regression as an improvement. Verdict prompt rewritten to match. - ComparisonReport.to_dict gains signed_margin + h2h_decisive (add-only; margin/winner keys untouched). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
Roadmap #4 from the 2026-08-13 AI review: - consistency.py claimed 'Wizards' Commander rules don't grant a free mulligan' — they do (CR 103.5c), and deck_builder_manabase's Karsten table already depends on it. The Monte Carlo now gives the first mulligan a fresh 7 with no bottoming; paid mulligans follow normal London count-down. Every keep/on-curve stat was systematically pessimistic (measured: avg kept hand 6.12 -> 6.47 on a mulligan-prone deck). - Dashboard now reports sim coverage: deck names are preflight-checked against the vendored Forge card-script corpus and unsupported cards get a warning pill + detail modal — previously a deck full of cards Forge can't simulate simmed as silently-partial data. Additive sim_coverage payload key; fail-quiet when no corpus is vendored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
- architecture.md: advisor 'routes to 7 sources' -> 3 backends + filters (the other four _advisor_* modules are models/filters); layering paragraph now states web sits on top and names the three deliberate reporting-surface exceptions to 'lower never imports higher' (status/doctor -> knowledge_log, deck_dashboard -> archetype/staples). - pyproject.toml: script-naming comment cited PROJECT.md and audit_workflow.md, consolidated away 2026-05-13. - STATUS.md: un-pin the hand-maintained '3002 passed' suite counts (three copies, all stale) in favor of 'run pytest for the live number'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
…ty split Two directional errors in the freshly-rebased bracket floors (round-2 cross-examination findings): - combo_detection priced two-card combos by summed printed MV, so reanimator pairs (Worldgorger Dragon 6 + Animate Dead 2 = 8 >= 7) read 'late-game B3-legal' despite a ~4-mana effective assembly. Creature pieces partnered with a reanimation spell are now priced at min(printed MV, cheapest reanimation MV) — pattern-matched against real oracle wording with a hard-tagged offline fallback set. Missing data stays conservative (B4). - _EXTRA_TURN_CHAIN_ENABLERS mixed one-shot rebuys with repeatable engines; 2 extra turns + Eternal Witness hard-floored B4. Split by repeatability per the official 'chained or looped' language: repeatable engines (Reiterate, Mirari, Mystic Sanctuary, Timetwister, Underworld Breach, Past in Flames) keep the hard floor; one-shot rebuys (Eternal Witness, Regrowth, Snapcaster, Twincast, ...) become a capped 0.25 weight. Calibration pins added for both directions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
Verified-by-execution gaps (round-2 finding): Negate, Swan Song, Light Up the Stage, Prey Upon, Diabolic Edict, and Earthquake all classified 'other'. Added patterns with false-positive guards + real-oracle fixtures: restricted counterspells, impulse draw, fight/bite removal, edicts, X-damage wipes, ward (protection), Treasure plurals (ramp). Zero expectation drift in role_target_report/deck_health consumers; interaction.py inherits the new wipe shapes by design. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
Round-2 statistical-honesty batch: - meta_test no longer prints 'references BEAT your deck' on records the binomial test can't support (CLI default is 2 games/ref; a 1-2 split now reads 'not statistically distinguishable' with the split needed for significance). - Verdict floors aligned: AnalystConfig.min_decisive_games now defaults to the canonical MIN_DECISIVE_GAMES_FOR_VERDICT (20), defined beside the binomial primitive and imported by _proposer_sim (an 8-0 sweep no longer earns 'kept' @ 0.97 through the analyst while the proposer path calls identical data inconclusive). - _proposer_sim docstring example corrected: 26-14 does NOT clear alpha=0.05 at n=40 (p=0.081); the true boundary is 27-13. - Web sim tooltips restated on the decisive-game basis (±0.22/±0.11/±0.07); '40 (confident)' relabeled '40 (verdict floor)'. - save_iteration stores margin=NULL (not a fabricated 0) when no head-to-head decisive games exist. - New scripts/backfill_web_margins.py: dry-run-by-default recompute of legacy web-saved absolute margins from persisted sim reports, hard fenced to id >= 314 (pre-fix rows are seat-attribution-suspect; recomputing them would launder artifacts). - test_meta_test's live-EDHREC fetch stubbed (was the suite's one environment-dependent failure). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
Negative-mode P03 (critical) + P16: - improve.py advanced the base deck on `reward >= --sim-margin` with a default margin of 1 — a raw one-win margin, no significance test, no decisive gate. A 23-22 split over 45 games permanently advanced the deck and logged it as validated. The accept decision now routes through _proposer_sim._verdict_from_ab (exact binomial at VERDICT_ALPHA over the standard decisive floor); --sim-margin survives as a back-compat pre-filter. - Sim/apply failures returned reward 0.0, entering arm statistics as measured ties; they are now skipped pulls that leave arm stats untouched, with the reason logged. - Rewards normalized to the signed decisive margin in [-1, +1]: raw O(±20) margins dwarfed UCB1's c*sqrt(ln N / n) bonus and Thompson's unit variance, collapsing both policies to greedy-on-one-noisy-pull. decisive == 0 yields no reward rather than a fabricated tie. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
Round-2 finding: the name-keyword content scan needed 3 hits against tiny lists, so ~70-85% of real decks fell to the 'midrange' default — which made pool archetype-diversity a de facto no-op (every arrangement 'violated', WARN-and-ship fired routinely) and meant the bracket estimator's combo/stax weights never fired. - classify() now derives labels from oracle-backed signals the pipeline already computes: game-ending combos + tutor density -> combo; interaction report (stack/wipe/instant share) -> control; a new ~10-pattern stax oracle table -> stax; tribal + curve/creature share -> aggro; midrange stays the honest default. Public signature unchanged; lookups are disk-cache-only (pool curation classifies ~60 candidates per run); a cold cache degrades to the name scan rather than reporting a false 'no lock pieces' zero. - The NotImplementedError LLM stubs are gone — an offline v2 makes them moot. - pool_curator ranks survivors on the Wilson lower bound and finally acts on suspected_inflated (demoted at equal bound) instead of merely tagging it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
Negative-mode P09: _discover_profiles re-enumerated vendor/forge* per call and the free-queue was a per-call local, so the module's own 'two chunks never share a profile' guarantee held only within one invocation. With the web UI launching background sim jobs, a second web job or a concurrent CLI run could double-book a profile and collide on its deck dir, cache, and forge.log. Each profile now carries an advisory .commander-builder.lock acquired via os.open(O_CREAT|O_EXCL) — atomic and, unlike fcntl, present on Windows (the primary desktop target). Locked profiles are skipped during discovery; all-locked fails fast with an actionable message instead of hanging; locks older than a generous max-sim duration are reclaimed by mtime (a bricked profile is worse than a rare double-book after a SIGKILLed JVM); release happens in a finally on every exit path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
…ath, price staleness Polish batch from rounds 1-2 and negative mode: - propose_then_iterate aborts before the sim when the applier drops every proposed pair (it used to sim a deck against itself for 10+ games and record a noise row). - _materialize_proposed_deck copies padded_count/padded_breakdown/ dropped_* into the manifest, so the manifest-matches-diff invariant holds on short imports too — the exact mismatch class the fix was written to eliminate. - EDHREC JSON-first no longer double-sleeps on fallback, and skips the JSON probe for pasted URLs that have no JSON twin. - The JS error log moves out of vendor/ into the user config home (P21); the old file is left alone with a pointer note. - Pricing payloads carry price_data_age_days + a stale flag past the same 45-day threshold legality already uses (P19) — prices came from undated snapshots that can be months stale after a reprint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
P22: 16 COMMANDER_BUILDER_*/MTG_CARDS_DIR flags existed in source with no reference anywhere (several documented only in CHANGELOG archaeology). Adds a single table under Persistence locations, and updates the js-error-log row to its new ~/.commander-builder home. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
…xt saves Round-2 web-integrity batch: - _sim_coverage built a fresh CardsLoader per request, and any MDFC front-face or unsupported card fell through to a full ~32k-file corpus scan whose result was then discarded — so every dashboard load of exactly the decks the feature exists to flag paid a 1-2s rescan. The supported-card index is now memoized behind a lock and keyed on corpus mtime, so a Forge upgrade still invalidates it. - PUT /api/deck_text was the one deck writer that skipped the Name=/filename restamp dck_meta exists to protect: pasting deck A's text into deck B's editor stored Name=A and made every later sim misattribute B's wins. Saves are now restamped, written via temp file + os.replace, and rejected with a 400 when the body has no [Main] section. - The three log_error tests move with the sink to the config home (the polish batch relocated it out of vendor/). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
…ixture roles
The full-suite run caught what per-batch runs could not:
- The round-2 ward pattern matched the bare keyword, so any creature
that merely HAS ward (Miirym, Phyrexian Fleshgorger) classified as
'protection'. That fills the ROLE_TARGETS protection quota — which
exists to guarantee a deck can protect its commander — with bodies
that protect nothing but themselves, suppressing the advisor's real
protection recommendations. The pattern now requires a GRANT
('has/have/gains ward'), so equipment, auras and instants still
count and resilient threats no longer do.
- Two concurrent batches added 30 oracle fixtures without declaring
expected roles; the repo's own tripwire test caught it. All 30 are
now declared, each annotated with the pattern it exercises.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
…uild opt-in, era stamps, sim caveat) Five decisions from the 2026-08-17 owner review: - The Flask before_request hook now rejects any request whose Host isn't loopback (127.0.0.1/localhost/::1, optional port), closing the DNS-rebinding path to deck reads and PUT /api/config. Note: serving with --host 0.0.0.0 will now 403 LAN Host headers, which is the intended trade. - [REF] decks (Moxfield top-likes) leave filler seats, matching the [PREMADE] popularity rule; they remain pool candidates, since a ranked seat and an unranked filler seat are different jobs. - The 30+30 rebuild tier is opt-in (COMMANDER_BUILDER_REBUILD_TIER, matching the card_score convention): auto-mode caps at overhaul and says why, because the health score gating that 6x cost multiplier has never been validated. --mode rebuild is unaffected. - knowledge_log schema v3 stamps every row with its measurement era (1 pre-seat-attribution, 2 mixed denominators, 3 head-to-head margin verdicts, 4 significance verdicts) so the three incompatible eras stop contaminating pooled analysis. Date decides and id only breaks ties — an id-first draft retro-labelled fresh low-id rows. Unknowable rows stay NULL rather than guessed. - README states plainly that a kept verdict certifies 'better against Forge's AI', not 'better at your table'. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
…--min-id) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
…ore advancing
Owner decisions A3 and A2 (2026-08-17):
- --sim-games now defaults to min_sim_games_for_verdict() (40 pod
games) instead of 5. At 5 games --run-sim could only ever record
'inconclusive', so the flagship close-the-loop flag structurally
could not close the loop. A cost line prints before the spend
('running 40 pod games (~67 min) -- the minimum for a statistically
meaningful verdict'), and --smoke restores the 5-game sanity check
while saying plainly that it will record inconclusive.
- Unattended improve runs now require a SECOND independent A/B in the
same direction before advancing the base deck. Greedy round-chaining
defaults ON (a false 'kept' becomes the base every later round is
measured against); the interactive bandit explorer defaults OFF
since UCB1 already re-pulls arms. Explicit --replicate/--no-replicate
always wins.
A failed replication invents no new verdict label: the row takes the
second run's verdict with a 'replication_failed: run 1 kept, run 2
neutral ... base NOT advanced' note, run 1's sim report intact. The
confirm run is a bare A/B, never a round or a pull, so it cannot
recurse. Confirmed swaps cost 2x sim time, stated up front.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
Owner decisions C2 and C3 (2026-08-17): POLITICS GUARD. Forge's AI does not negotiate, pick an archenemy, or model an opponent's incentive to pay a tax, so goad / monarch / vote / tempting-offer / Rhystic-tax / pillow-fort cards read to the sim as no-ops. A loop that cuts what doesn't move the margin will therefore 'empirically' cut exactly the cards that define multiplayer Commander: a near-zero margin on one of these is evidence the instrument is blind, not evidence against the card. staples gains an oracle-pattern politics tagger (six tags, with guards so 'unless you pay' upkeep costs and 'unless its controller pays' soft counters don't match). Tagged cards are exempt from margin-driven cuts in all three cut paths — the heuristic loop, card_score's ranking, and now the orchestrator, which is the only path Claude's and bracket-peers' cuts traverse. Skips are disclosed via AdviceReport.skipped_for_politics rather than silently shortening the list. Per-deck opt-out: [metadata] PoliticsGuard=off, carried across re-imports, failing safe to ON on any unrecognized value. ARCHIDEKT LANE. Acquisition rode Moxfield's undocumented private API alone; one ToS or CDN change stranded imports, harvest, peers and meta-test refs at once. Archidekt (public, documented) is now a real single-deck import lane via import_deck(source=)/--archidekt, with fallback on 5xx/403/URLError/parse failures but NOT on 404 (a working API saying 'no such deck' must not silently import a different one). No id translation is attempted. Moxfield-only capabilities (bulk bracket harvest, top-likes search) now name the working alternative when they fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
The gate checked the training triple's shape -- manifest, decided verdict, >=40-game sim -- but not whether the verdict meant anything. A pre-e8777b6 row can satisfy all three and still be worthless: its wins were credited to the wrong deck. The fine-tune learns the VERDICT, so the era that matters is the one that produced the LABEL, not the one that produced the raw counts. Rows must now come from measurement era >= 4, the first era whose kept/reverted comes from the significance test. Era 3 is reported separately as 'relabelable' rather than dropped: its measurement is sound (same decisive denominator), only its verdicts came from the game-count-invariant |margin| >= 4, so re-scoring the stored sim report promotes it. Eras 1-2 are unrecoverable and excluded, as is an unstamped row -- unknown provenance is not evidence of good provenance. --health discloses both buckets so the headline number never reads as 'that's all the history there is'. Worth noting alongside the same day's sim-default change: at the old default of 5 games this gate was mathematically unreachable, since every row the flagship flag produced fell under its own 40-game bar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the findings of three AI review rounds plus a negative-mode critique of the whole program. Analysis documents live in mtga-advisor#3 (
docs/ollama-analysis/). One commit per theme.Full suite: 3,520 passed, 164 skipped, 0 failed (baseline was 3,230 passed / 1 failed — the one environment-dependent live-network test is now properly stubbed).
Round 1 — the joint top-5 roadmap
aad2d0e--auto-proposenever applied the LLM's proposal to disk: it proposed against the new file and simmed two pre-existing decks, so every knowledge-log row paired a manifest with an unrelated diff. Now proposes against the old deck, materializes--newviaapply_proposal_to_deck, records applied adds/cuts, and fails fast before any LLM spend. The web writer also stored the absolute margin in a column documented as signed, making regressions read as improvements.af2960027bc2d5commander-doctorwarn on stale card data.0407c4bconsistency.pyclaimed the rule doesn't exist whiledeck_builder_manabasedepended on it, so every keep/on-curve stat was pessimistic. The dashboard also preflight-checks decks against the vendored Forge corpus instead of silently producing partial sim data.b30cff0json.edhrec.combefore the brittle HTML scrape; manabase essentials gain triomes and surveil duals;deck_healthshares the builder's land band so the grader stops docking the app's own builds.Round 2 + negative mode
a12380freward >= --sim-margin(default 1) permanently advanced a deck on a 23-22 split, and crashed sims entered arm statistics as measured ties. Now routes through the binomial verdict, records failures as skipped pulls, and normalizes rewards so UCB1/Thompson actually explore.7cae564suspected_inflated.926be8fO_EXCL, notfcntl), with mtime-based stale reclaim.c6714cePUT /api/deck_textwas the one deck writer skipping theName=restamp — pasting deck A's text into deck B made later sims misattribute wins.f15d31fmeta_testno longer claims "the references BEAT your deck" on a 1-2 record, verdict floors are aligned, tooltips state decisive-basis noise, and a fenced backfill script can repair legacy margins.e7fadf8vendor/, price-staleness warnings.67eba3813bdafb,0c734c0Notes
docs/ollama-analysis/DECISIONS_FOR_REVIEW.mdin the analysis PR; the follow-on work (replication for unattended runs, sim-default raise, local-model router, umbrella CLI, politics guard) is queued from there.🤖 Generated with Claude Code
https://claude.ai/code/session_01Mdg9HA1ZkdKQ3wpZvra3ou