Commit Graph
116 Commits
Author SHA1 Message Date
vh 707fae2b2c perf(leak_gate): one alternation pass for the split scan — lv-hemingway went from timing out at 5 min to 35 s
Per-surface scanning is O(surfaces x copies x corpus). lv-mccarthy (108 surfaces,
36 copies) finished in 8 s; lv-hemingway (881 surfaces, 10 copies) was still running
at 5 minutes and had to be killed. A gate too slow to run is not a gate. Same trick
scan() already uses: build one alternation, map the matched string back to its
surface by stripping separators.

Regression: identical verdict and identical per-surface hit counts on the pre-fix
lv-mccarthy tree (5 surfaces, 78 hits) and on the fixed one (0). Re-derived on the
two shipped corpora with the committed instrument rather than a scratch probe:

  lv-hemingway   GATE FAILED   Pasionaria, Primitivo, Chicote -- 6 hits each, all 6 copies
  lv-bronte      GATE PASSED   0
2026-09-17 11:43:25 -07:00
vh c55966433f fix(lv-mccarthy): the leak gate passed with five protagonist names still in every copy
`leak_gate.py` scans `\b(Surface)\b`. Any character inserted inside a name defeats
that pattern outright, so a mangled occurrence is unrenameable by rename.py AND
unreportable by the gate. lv-mccarthy's 2026-09-17 tree passed at "0 of 75
renameable and 0 of 37 sub-threshold" while carrying 13 occurrences of Bell,
Chigurh, Moss, Toadvine and Glanton in all six copies:

    B ell  C higurh  M oss  T oadvine    a small-caps drop cap kept as its own token
    Toad-vine  Glan-ton                  a print line-break hyphen kept by the extractor

Every visible occurrence HAD been renamed, which is what made the residue invisible
to a spot-read. Fixed at three levels, all three of which must stay:

  build_corpus_mccarthy.py rules 4 and 5 repair the source text — 32 split initials
  with a lowercase remainder, 5 hyphen-split names, each with an expected count so a
  master change fails the build. Rule 4's letter class is consonants only: `I` opens
  1,966 paragraphs, `A` 143 and `Y` 32 (Spanish `y`); folding any would corrupt 2,141
  lines to fix 32.

  leak_gate.py gains a separator-tolerant pass with its own positive and negative
  controls, and it FAILS the gate. Validated against the pre-fix tree: reports all
  five surfaces, exits 1. Its fragment filter is what makes it usable — a naive scan
  returns 18 false positives on Hemingway (`God damn`, `I run`) against 3 real ones;
  requiring one fragment to be a non-word of the corpus cleared all 18 and kept all 3.

  The whole D1→D3 chain is reproduced byte-identically before and after, so the fix
  is the only delta: 6 works, the entity map, the final map and all 36 copy files.

Cross-checked on the shipped corpora: lv-bronte is clean of this class, lv-hemingway
carries 3 (`Primi tivo`, `Pasionar ia`, `Chi cote`) and is live on fv-ml1.

Also in build_sft_pairs.py, both needed before lv-mccarthy's pairs:

  DEFECT 4, hard-wrap reflow. Measured on the SHIPPED lv-bronte adapter, which emits
  mid-sentence line breaks at 12.46 per 1k chars against 0.00 for its own base control
  and 0.00 for every Hemingway arm. McCarthy is the mixed case — The Road is wrapped,
  the other five works are not — so the corpus teaches the break as a coin flip. The
  obvious fix (join every interior newline) corrupts 46 two-speaker exchanges whose
  blank line was lost, and unmarked dialogue is the one thing this adapter exists to
  learn; the rule splits on sentence-final punctuation instead and takes the cheaper
  error. Self-targeting and off by default, so every shipped pair set is unchanged.

  A `mccarthy` register, which names the punctuation deliberately: the eval drives the
  base control arm with this same prompt, so tics left out of it are a surface trick
  only the adapter can perform, and delta_cb is a character-bigram measure.

  drop_leading_heading now also consumes Blood Meridian's dash-separated chapter
  arguments — 131 paragraphs, 0 in every other work of all three corpora.

And a RUNBOOK, because the D1→D3 session recorded nothing and the chain had to be
recovered by rebuilding candidates and matching sha256 against the artifacts on disk.
2026-09-17 11:37:33 -07:00
vh 5ddb0472e4 lv-mccarthy D3 on gx10: leak gate PASSED, and the val split is now bigger than Hemingway's
~/lv-mccarthy on pfi-gx10: corpus-clean, corpus-renamed (6 copies, 1,002 records), scripts.

  leak gate   0 of 75 renameable and 0 of 37 sub-threshold survive in any copy
              positive control 108/108 surfaces found in the unrenamed source
              negative control nonce absent from both trees

THREE McCARTHY-SPECIFIC DECISIONS, each forced by a measurement.

1. --scope corpus, NOT the default per-work map. The Border Trilogy shares characters
   across books -- 9 surfaces appear in more than one work, including Parham (The
   Crossing + Cities of the Plain), Grady and Cole (All the Pretty Horses + Cities of
   the Plain), Socorro and Héctor. A per-work map would give John Grady a different
   invented name in each novel, turning one character into two.

2. A NEW `mccarthy` rename preset rather than reusing `hemingway`. Both are
   Spanish-inflected, but Hemingway's romance pool carries it_IT and fr_FR for his
   Italian and French casts, and McCarthy writes neither language -- drawing from it
   would drop Italian and French surnames into a Texas-Mexico border novel. en_GB goes
   for the same reason. en_US + es_MX/es_ES at an even share.

3. --min-cap 5 to MATCH the entity map's floor. The first gate run FAILED with 45
   survivors, and the diagnosis is the Brontë lesson exactly: entities.py admits
   cap >= 5 while rename.py only renamed cap >= 8, so every entity between 5 and 7 sat
   in the map, was never renamed, and was counted as a leak. Hemingway never hit it
   because its map had sub_threshold_total 0.

⭐ --holdout-chapter NOW TAKES A LIST, and this is the change with the most downstream
effect. The val split is one chapter index per work, so its SIZE is set by how many
WORKS a corpus has, not how many words:

  Hemingway  10 works -> 9 val units -> 36,563 words/copy -> gate DECISIVE
  Brontë      4 works -> 4 val units -> 17,043 words/copy -> gate MARGINAL
  McCarthy    6 works -> 6 val units -> ~18,000 would have been Brontë's end of that

Holding out chapters 7 AND 17 gives 11 units and 40,653 words per copy -- larger than
Hemingway's, at a cost of 7% of the corpus -- on a corpus 40% smaller than his. No
amount of corpus size fixes a val split that scales with work count.

THE HUMAN GENDER PASS IS NOW AN AUDITABLE FILE, not a hand edit. The honorific/window
resolver scored 21 correct / 3 held / 1 WRONG against a 26-name control; the base-rate
proximity resolver built for Hemingway scored 18/6/1 and its own guard correctly
REFUSED to write. So the incumbent stands and four entries are fixed by hand in
gender_overrides_mccarthy.json, each carrying its evidence.

⚠ All four are female and all four look male-dominated in raw pronoun counts, because
this corpus runs 29,144 male pronouns to 5,036 female -- a base rate of 85.3% male.
Carla Jean Moss at 31m/21f would be 44m/8f at that base rate, so 21 female against an
expected 8 is decisive. Same arithmetic that recovered Pilar and Brett on Hemingway.
Alfonsa was in my control set and is correctly absent from the map at 4 occurrences,
below the min-count floor -- an error in the control, not the pipeline.

apply_gender_overrides.py refuses two ways: a name absent from the map is an error
rather than a silent no-op, and overruling a gender the detector already holds needs
an explicit "correcting": true so it cannot look like filling a held entity in a diff.
2026-09-17 08:59:00 -07:00
vh 5aa10bf138 lv-mccarthy D2: entity map + stoplist, both audits green — and audit_stoplist was scanning its own rationale
Entity map at ~/mccarthy-corpus/entities.json. 123 surfaces after a 107-surface stoplist.

  entities.py        27/27 controls -- 19 positive (Glanton, Toadvine, Rawlins, Blevins,
                     Alejandra, Chigurh, Moss, Bell, Boyd, Holden, Tobin, Magdalena,
                     Eduardo, Parham, Socorro, Webster, Redbo, Niño, Franklin) and 8 negative
  audit_stoplist     PASS -- no stoplisted surface is ever addressed as a person
  audit_entity_map   PASS -- positive `boy` 0.89, negative band tops out at Riddle 0.17,
                     all 5 remaining flags on the read-and-cleared list

⚠⚠ A DEFECT IN audit_stoplist.py ITSELF, latent for every corpus before this one. It built
its surface set from every list value in the stoplist JSON -- including `_why`, which by
convention is a LIST OF PROSE LINES. Every sentence of the rationale went into the matcher,
and the empty separator line matched the honorific pattern 139 times, printing a flag with no
surface name at the top of the report, above the one real catch. It now skips `_`-prefixed
metadata keys and empty strings.

THE ONE REAL CATCH WAS A CONTRADICTION INSIDE MY OWN FILE. `Franklin` sat in the geography
list because it is the old name for El Paso, while the same file's context note recorded
'I'm here to see Mr Franklin' -- a lawyer in All the Pretty Horses. The honorific audit found
the contradiction between the two halves of the file. Franklin is now renameable.

A SECOND SELF-INFLICTED ONE: the fragments list was a speculative A-Z, which stoplisted `I`
and `A` -- ordinary English words -- and `Sir I dont think I can do that` duly tripped the
honorific audit. It is now the four letters actually MEASURED as entities (E, H, T, K).
Stoplist what the entity map produced, not the alphabet.

Everything ambiguous was read in context before placement, and the reasoning is in the file:
  Socorro is the ranch COOK in Cities of the Plain, not the New Mexico town -- renameable
  Webster, Jackson, Harlan, Lamar are Glanton's men and lawmen, not places -- renameable
  Niño, Keno, Redbo are HORSES, the author's inventions -- renameable, the `Inglés` precedent
  Mangas, Travis, Venada, Moderno are genuinely dual-use -- renamed, the safe direction
  Santa, Varas, Griffin, Eagle, Avenue, Calle, Terrell are real geography -- stoplisted
  Yaqui and Gilenos are real peoples; Ford and Hashknives are a brand and a real outfit
  Ed (Ed Tom Bell) and JC are short but are names, read and kept renameable

Sensitivity floor, stated because it is part of the result: the top 170 of 199 surfaces were
classified. The bottom 29 were not individually read, so a rare real-world referent may be
renamed -- the safe direction, an accepted cost, not an oversight.
2026-09-17 08:43:46 -07:00
vh 0fa68cb465 lv-mccarthy D1 fix: three small-caps defects the entity map caught, and one I nearly added
D2's entity map returned `E`, `H`, `T` and `K` as renameable entities with 17-33 capitalised
occurrences each. A bare initial is never a name -- that is the `G` class from the Hemingway
build, where `G` was about to be renamed to a surname 248 times. Reading them in context
showed the McCarthy editions set section openings in small caps and the extractor mangled
them three different ways, none of which the D1 build repaired:

  1. SPLIT INITIAL   `T HE HOUSE was built`      -> `The house was built`      32 cases
     Hemingway's restore_smallcaps only fires on TWO or more split initials in a line, so it
     is structurally blind to these single ones.
  2. UNMARKED RUN    `THEY STOOD in the doorway` -> `They stood in the ...`    88 cases
     Concentrated in Cities of the Plain (49) and The Crossing (37).
  3. LOST INITIAL    `HE CANDLEFLAME`            -> `THE CANDLEFLAME`           1 case

Rule 1 requires a FOLLOWING all-caps word, because `A TV was playing` and `A Mexican was
changing` are an article plus a capitalised word, not a drop cap. All four such probes
verified untouched. Rule 2's `[a-z]` lookahead is what makes it safe: lowercasing every
all-caps run at a block start would eat a genuine shout or a sign, and requiring the run to
be followed immediately by a lowercase word means it is a sentence continuing. All 23
distinct first words of the 88 were checked and are real words -- HE, WHEN, THE, THEY,
QUINQUAGESIMA -- except one, which was case 3.

⚠⚠ AND A SECOND LOST-INITIAL ENTRY WAS NEARLY SHIPPED THAT WOULD HAVE CORRUPTED THE TEXT.
`HEY RODE` -> `THEY RODE` looked right from a survey of the BUILT corpus. The raw master has
`THEY RODE` intact, twice: `HEY RODE` was matching as a SUBSTRING, and the unanchored replace
produced `TTHEY RODE`, which rule 2 then lowercased to `Tthey rode`. Two things caught it --
the count assertion (expected 1, replaced 2) and then reading the master. Rule 3 is now a
block-anchored regex rather than a string replace, so a substring cannot fire it.

⚠ My first corruption check also missed it, searching for `TTHEY` when the pipeline had
already lowercased it to `Tthey`. Check the shape the pipeline actually emits, not the shape
you imagined it would.

Totals move 584,756 -> 584,716 words, 167 units unchanged. Both guards still pass: quote
marks 0.0/10k, author's own name 26 -> 0. Entity map positive control is 14/14 on real
McCarthy characters (Glanton, Toadvine, Rawlins, Blevins, Alejandra, Chigurh, Moss, Bell,
Boyd, Holden, Tobin, Magdalena, Eduardo, Parham); `T` and `E` no longer appear as entities.
2026-09-17 08:40:31 -07:00
vh 82aa0b6d76 lv-krakauer: PARKED — research is not a voice (operator, henge id 82)
Operator ruling: "he's a great writer because of his research, not because he has a strong
identifiable voice."

That reason is about the AUTHOR rather than the data, and it is the better of the two on the
table -- the other being the unmeasurable fraction of quoted material. It also names a
selection criterion this line did not have: ask whether there IS a voice worth adapting
before investigating whether a clean corpus can be built. That question was never asked here.
I surveyed the holdings, built the corpus, measured all fifteen containment pairs, and fixed
three stripping defects the name guard caught -- all real work, none of it touching the thing
that decided it.

A voice adapter is worth its corpus-plus-training-plus-gate only when the target has a prose
signature a reader could pick out blind. McCarthy: 0.0 quote marks per 10k against Hemingway's
838. Hemingway: spare declaratives, heavy unattributed dialogue. Brontë: periodic sentences
built on semicolons and dashes. If that sentence is hard to write, the author is a park.

Nothing is deleted. The corpus (126 units, 422,880 words) and the builder stay committed and
re-runnable; the park entry records what exists, what was never started (D2), and what would
unpark it -- a re-extraction preserving indentation and italics, which would fix the quoted-
material problem but not the operator's objection. The builder's own docstring now carries a
stop notice so a future session finds the reason at the artifact rather than only in memory.
2026-09-17 08:34:47 -07:00
vh 4be063071a lv-krakauer D1: 126 units, 422,880 words — and an unmeasured fraction is not his prose
The first non-fiction corpus in this line. Builds clean and should not be trained on until
an operator scope call is made; the reason is in the module docstring and the manifest.

  into-the-wild               25u   67,606w  caps-title    [smallcaps 21][back -1,015][epi -52]
  missoula                    32u  115,841w  chapter-word  [smallcaps  8][front -858][back -2,874]
  under-the-banner-of-heaven  33u  118,171w  caps-title
  where-men-win-glory         36u  121,262w  chapter-word  [smallcaps  3][front -1,548][back -6,093]

⚠⚠ THE UNRESOLVED PROBLEM IS QUOTATION, AND IT IS NOT MEASURED BECAUSE IT CANNOT BE.
Krakauer quotes constantly and at length -- McCandless's journals and letters, Tillman's
diaries, court transcripts, depositions, Mormon historical documents, and whole paragraphs
of Jack London and Wallace Stegner at the chapter heads. In print those are indented or
italic; the extraction lost both, so inside the master they are ordinary paragraphs and no
signal this builder can read separates them from his own sentences.

Only 52 words were removable -- chapter-head epigraphs whose all-caps attribution line
survived. That is 0.01% and it is NOT the answer: the method would report 0.0% for a book
made entirely of undated block quotes. The stated floor rather than the number is what a
reader needs. This is the same error as excluding The Torrents of Spring from Hemingway --
another author's style under the target's name -- distributed rather than concentrated, and
the fraction is unknown. Scope is the operator's call, exactly as fiction-only was.

THREE DEFECTS THE NAME GUARD CAUGHT, none of which the build would have reported otherwise:

  1. Back matter searched only the LAST unit. Where Men Win Glory's ACKNOWLEDGMENTS sits at
     94.8% and the splitter made 41 units, so the apparatus landed in unit 37 with NOTES and
     BIBLIOGRAPHY after it -- all past a strip that only looked at unit 41. Into the Wild
     kept its acknowledgments AND a full-page advertisement for another of his books. Now
     windowed to the last 25% and cut before the split.
  2. Relying on the splitter to drop front matter did not work. Units begin at the first
     heading mark, and in two works the ebook's table of contents sits above the author's
     note -- giving the splitter a `Chapter Thirty-Two` to start on, so unit 1 swallowed the
     apparatus and its signed `Jon Krakauer , February 2015`. Now cut at that signature,
     windowed to the first 10%.
  3. Zero was the wrong bar. 21 survivors became 2, and both were read: `Lewis Krakauer
     loved his five children deeply` is Krakauer writing about his own father in the two
     autobiographical chapters of Into the Wild, and the other is a reader's letter he
     quotes calling him a kook. Hemingway's own name in his corpus was always publisher
     apparatus, so 0 was right there; this author writes about himself. The allowance is
     pinned at 2 and every survivor is printed with context, so a master change or a strip
     that stops working fails loudly instead of widening in silence.

Both strips are windowed in OPPOSITE directions from McCarthy's, which is the point worth
carrying: McCarthy's apparatus is at the end and the earliest marker wins; Krakauer's is at
both ends and the same marker words appear in his front matter at 0.0-0.6% of the file.
2026-09-17 08:24:59 -07:00
vh f3bf3ca89c lv-mccarthy D1: 167 units, 584,756 words, and a style that looks exactly like damage
Six complete novels from the licensed Kvasir masters. Same record schema as the Brontë,
Yarros and Hemingway builders, so entities.py, rename.py, leak_gate.py and the trainers run
unchanged. Splits via the new shared split_units module.

  all-the-pretty-horses  33u   99,242w  paragraph-blocks  [back -1,768w] [drop cap restored]
  blood-meridian         23u  116,651w  roman-numeral     [back   -354w]
  cities-of-the-plain    30u   90,166w  paragraph-blocks
  no-country-for-old-men 13u   69,841w  roman-numeral     [back   -463w]
  the-crossing           49u  149,985w  paragraph-blocks  [back    -30w]
  the-road               19u   58,871w  paragraph-blocks

THE THING THIS BUILDER PROTECTS IS A VOICE THAT READS AS A DEFECT. McCarthy uses no
quotation marks and drops the apostrophe from most contractions -- dont, aint, wont, didnt.
Measured over the built corpus: 0.0 quote marks per 10k words against Hemingway's 838, and
123 apostrophes against his 241. repair_typography.py normalises "toward what the text
does" and would put the quotes back, deleting the single most identifiable thing about the
author before training starts. This builder runs NO typography normalisation and then
ASSERTS the quote density, so a future well-meaning change fails the build instead of
quietly undoing it.

⚠ That same property will make the voice gate easy to pass for the wrong reason.
voice_distance.py is Burrows's Delta over character bigrams; an adapter that learns only
"emit no quotation marks" moves delta_cb a long way without having learned a sentence. A
punctuation-normalised secondary read needs pre-registering before this one is gated.

Exclusions, measured rather than assumed:
  - two truncated catalogue rows dropped for their complete mobi siblings (Blood Meridian
    epub 1,167w, The Crossing epub 222w -- both real prose, both `accepted`)
  - nothing else. All 15 cross-work 8-gram containment pairs measured on the Hemingway
    precedent; worst is 0.10%. Six independent works, no subsumption.

Back matter rides inside the last unit in four of six works and the marker differs every
time -- THE END, a dumped Table of Contents, a Reader's Guide, an About-the-Author, press
blurbs, a CIP page. It carried the author's own name 26 times across the raw masters. Both
guards report and gate: name 26 -> 0, quotes 0.0/10k.

⚠⚠ The back-matter strip runs BEFORE the split here, inverting the Hemingway order. Blood
Meridian and The Crossing end with a dumped table of contents made of bare roman numerals on
their own lines -- the exact shape of a chapter marker. Splitting first feeds the TOC to the
splitter as two dozen extra chapters; only the 150-word floor accidentally saves it today.

One lost drop cap is patched by name, not by heuristic: the All the Pretty Horses epub opens
`HE CANDLEFLAME` because the decorative T was an image the extractor dropped. A general
restore-the-missing-initial rule would have to guess the letter, so this is asserted against
the known string and fails loudly if the master ever changes.

The alphabet is re-derived, not inherited: 1,411 non-ASCII letters across 14 forms
(á é í ñ ó ú ü). The Border Trilogy is half set in Mexico, so the Yarros ASCII-only
conclusion does not transfer -- same finding as Hemingway, same reason.
2026-09-17 08:17:57 -07:00
vh 705fa3a65b split_units: choose a unit mode by SIZE, not by count, and fall back to paragraph blocks
McCarthy and Krakauer both need this before a corpus can be built, so it is a shared module
rather than a third copy of the Hemingway splitter.

THE INHERITED RULE IS "MOST UNITS ABOVE A FLOOR" AND IT BREAKS ON PART MARKERS. Measured:

  Cities of the Plain   4 roman marks -> 4 units, median 22,312w   <- the book's PARTS
  The Crossing          4 roman marks -> 4 units, median 37,310w   <- same

"Most units" scores 4 over the 1 that finding-nothing gives, so it wins, and the existing
guard only fires at exactly one unit. A 37,000-word "chapter" sails through and every
downstream tool accepts it. Size is now the eligibility test: a mode qualifies only if its
median unit is inside [600, 12000] AND no single unit holds half the work.

TWO THINGS A CONTROL RUN CAUGHT, BOTH NOW FIXED IN THE RULE. The first version scored
eligible modes by "median closest to target". Run over Hemingway, whose markers are known
good, it chose caps-title over the book's own chapters on True at First Light:

  bare-numeral  20 units  median 5,337w  max  11,155   <- the real chapters
  caps-title     6 units  median   777w  max 113,886   <- median looked BETTER

caps-title matched five stray all-caps lines, so five tiny units sat beside one holding 97%
of the book. A median cannot see that distribution; a max bound can. And caps-title is the
weakest of the four signals, which is why the tiebreak among eligible modes is now PRIORITY
(contents > chapter-word > roman > bare-numeral > caps-title), not size.

CONTROLS, both green after the fix:
  positive  Hemingway's ten works, markers known good -> 8/10 reproduce the shipped mode and
            unit count exactly. The two differences are explained, neither is a mode error:
            short-stories used `contents`, which the harness does not supply, and The Old Man
            and the Sea was deliberately kept whole as CONTINUOUS.
  negative  40,000 words with no blank lines -> 1 unit. It refuses to fabricate divisions
            out of unstructured text rather than returning a plausible section count.

Result on the two new authors: McCarthy 167 units / 587,233 words, Krakauer 135 / 431,938,
both median ~3,200-3,500w against Hemingway's 3,128.

⚠ CORRECTION TO AN EARLIER SURVEY. I reported that all four Krakauer works carry zero
chapter markers. That was wrong and it was my regex, not the books: the survey pattern
required "Chapter" followed by a numeral, and Krakauer writes "CHAPTER ONE". Missoula and
Where Men Win Glory split on chapter-word (33 and 41 units); Into the Wild and Under the
Banner of Heaven on caps-title (28 and 33). Only McCarthy's All the Pretty Horses, Cities of
the Plain, The Crossing and The Road actually need the fallback.

The Hemingway builder is deliberately NOT repointed at this module. Its corpus is shipped and
its provenance sha is pinned by a live adapter; the one behavioural difference (The Old Man
and the Sea would section into 9 rather than stay whole) is an improvement nobody asked for
on a corpus nobody should churn.
2026-09-17 08:12:09 -07:00
vh 9f35c8d659 booth: four arms, one beat, one author-neutral prompt
Six beats through voices-base, lv-bronte, lv-yarros and lv-hemingway, all served from the
same process on fv-ml1 :8027 so only the adapter varies. Operator-requested side-by-side.
http://10.100.10.50:8090/b/lv-voices-four-arms/ (24h TTL; also on the link board)

THE PROMPT NAMES NO AUTHOR, deliberately. Each adapter trained under a prompt naming its
own, so driving all four with any one of those hands that arm a hint the others do not get
and the page would be measuring the prompt rather than the voice. The shared task skeleton
is kept and the author clause removed. One asymmetry is disclosed on the page: Brontë and
Hemingway trained on "a SHORT PASSAGE ... may run to several paragraphs" while Yarros
trained on "ONE paragraph", so the neutral prompt sits slightly off-distribution for all
three rather than for one.

THE CONTROL GETS A 4x LARGER TOKEN BUDGET, and publishing it any other way would have been
dishonest. Measured at the gate's 320-token budget:

  voices-base   median 26 prose words, 181-257 words of <think> planning first,
                and 5 of 12 cells never reach the prose at all
  the adapters  0 of 12 failures each, empty think block in 12 of 12, median 97-105 words

The adapters learned to skip the reasoning phase; the carrier has not. Showing the starved
control would conflate voice with budget discipline, so the control runs at 1200 tokens and
finishes every time, median 121 words. Both numbers are on the page.

Two seeds per cell behind a toggle, because one sample of a sampled process is an anecdote,
and a blind-mode toggle that hides which column is which. Sampler matches the gate harness
(temperature 0.9, top_p 0.95, "BEAT: " prefix).

Checked before publishing rather than after: all 36 adapter generations scored for verbatim
8-gram reuse, each arm against ITS OWN corpus. Brontë 0, Yarros 0, Hemingway 2 of 12 with a
longest run of 8 words, that run being "i don t know i don t know".

Layout verified by rendering it, not by reading the CSS: four equal 374px columns at 1600px
wide, no horizontal overflow, 24 cards, 48 panes. ⚠ nh3-dev's shared /opt/ms-playwright
tops out at chromium-1234, so playwright must be pinned to 1.61.0; a bare `npm i playwright`
pulls 1.63 and asks for a browser build that is not there.
2026-09-17 07:53:08 -07:00
vh 300ecc1276 voices-seat: ship lv-hemingway (ckpt850), and replace the memorisation control that passed it
Live on vllm-voices (fv-ml1 GPU0 :8027) beside voices-base, lv-yarros and lv-bronte.
Healthy 190 s after recreate, four models served, GPU0 96,092 -> 96,090 MiB. The adapter
was verified byte-identical to checkpoint-850 by sha256 across both transfer hops, and the
seat was verified by generating, not by reading its config: base emits 170 words of <think>
planning and never writes the passage, lv-hemingway writes the scene.

Gate design was pre-registered before any generation existed (0bb4938). Three arms, 60
held-out beats, 4 seeds, 240 generations per arm.

  A. VOICE   PASS 6.4x   +0.413 delta_cb, pairwise floor 0.064 -- and it clears the OLD
                         all-arms floor (0.113) too, so this verdict does not lean on the
                         rule change. Closes 73.8% of the span between the unadapted
                         carrier and held-out Hemingway itself; lv-bronte closed 48%.
  B. NOT COPIED  see below
  C. NO DAMAGE   PASS    ran-on +0.08, on-beat -0.14, both inside a 0.217 floor

AXIS B: THE NEGATIVE CONTROL WAS THE WRONG ONE, AND FIXING IT MADE THE RESULT WORSE, NOT
BETTER. memorization_check.py uses the base-unadapted arm as its control. Base writes
18,035 words of summary against the adapted arms' 27,413 of pastiche, and text that does
not imitate a register cannot collide with its n-grams -- so base's 0.00 measures "different
register", not "did not memorise". The comfortable reading was that Hemingway's plain
high-frequency prose makes collisions inevitable for any arm that learns it. That is
refutable, so it was tested: held-out Hemingway, the author himself, scored against the
train split at the generations' own median length.

  HELD-OUT HEMINGWAY (never trained)   370 chunks   0.01 hit-rate   mean-longest 0.1   max 10
  base-unadapted                       240 gens     0.00                        0.0        0
  ckpt850 (shipped)                    240 gens     0.07                        0.6        9
  positive control (train vs train)                                             160

The hypothesis is false: the adapter reproduces train n-grams ~7x more often than the
author reproduces himself. That is real and is on the record. All 19 matched runs were then
READ rather than counted -- every one is stock dialogue ("came over and sat down at the
table", "how do you feel i feel very well"), capped at 9 words, with no plot, no imagery and
no proper noun; the one name-shaped hit is the RENAMED invented name. Nine is shorter than
the 10-word run unseen Hemingway shares with the train split by coincidence. Elevated rate,
zero protectable content. Hemingway is in copyright; lv-yarros is the in-line precedent,
also in copyright, shipped at 0.10 against a 0.07 control. Unload is 0.003 s.

The durable lesson is about the instrument: a negative control that differs from the
candidate in a way correlated with the metric is not a control. memorization_selfsim.py and
memorization_dump_matches.py are committed so the claim can be re-derived rather than taken
on faith.

SHIPPED ckpt850, NOT the loss minimum at step 1750. The two are indistinguishable on voice
-- 0.072 apart against a 0.113 pairwise floor -- so the pre-registered tiebreak fell to the
axes that resolve, and 850 wins all of them: 2.3x tighter seed spread (0.050 vs 0.113),
lower memorisation, less ran-on, half an epoch less overfit. ckpt1750's spread is one seed
(0.491, 0.449, 0.468, then 0.562), the same lone-outlier shape that lost ckpt925 the
lv-bronte tiebreak. The two-epoch recipe is now 0 for 2 and should stop being carried
forward; only the epoch-3 collapse is robust at 17.4x jitter.

servers/fv-ml1/ssh-target was a bare IP, so deploy-stack.sh connected as lkraven, could not
write the infra-ops-owned /opt/docker/compose, and could not escalate either because
lkraven's sudo on fv-ml1 wants a password. Now infra-ops@10.251.50.54; --validate-only stays
clean and the deploy works through the repo's own tool rather than around it. Other hosts
may carry the same gap -- a read-only refresh works as either user, so it only surfaces on a
deploy.
2026-09-17 03:36:29 -07:00
vh 5e6611466c audit_pairs_sourcenames: --filter-out, so the detector is also the fix
An already-built pair set cannot be repaired by build_sft_pairs.py --source-entities;
that flag only works at generation time. Hemingway's and Yarros's sets both predate it.

The contamination is in the BEAT, so dropping the row removes it outright. Measured on
the Hemingway train pairs: 7,094 -> 7,024, 70 dropped, 0.99% of the training data. That
is cheaper and cleaner than regenerating 70 beats against a second generator session,
which would leave the set mixed-provenance for the sake of 1% more data.

Verified by read-back rather than by the write succeeding: re-auditing the filtered file
reports 0 of 7,024 on both columns, controls green, GATE PASS.

Two refusals rather than a best-effort write:
  - a contaminated RESPONSE column aborts. That is a different fault -- pairs built
    against an unrenamed corpus -- and dropping rows would hide it instead of fixing it.
  - more than one --pairs input aborts, because the output is a single file and would
    silently merge train and val into one.

Also cross-validated the detector against the lv-bronte pair sets on real data, where the
answer is already on the record:
  pairs-full + pairs-val (post-fix)   0 of 3,858   matches the recorded "0 leaks across
                                                   3,858 pairs" exactly
  pairs-full.CONTAMINATED             15 of 792 = 1.89%, Rochester x6, Jane, Brocklehurst
                                      x2, Beck, Fairfax, Burns, Helen, Eyre -- against a
                                      record of "13 of the first 714 beats (1.8%)" with
                                      the same names
An independently written instrument reproducing a documented finding at the right
magnitude, on the right names, is the control that says its zeroes mean absent and not
blind.
2026-09-17 02:37:35 -07:00
vh 051b99e063 audit_entity_map: the rename can damage the prose and no gate will ever say so
audit_stoplist.py finds surfaces wrongly held OUT of the entity map -- a stoplisted
character is an undetectable leak. This is the mirror: surfaces wrongly held IN it.
leak_gate.py only ever asks whether the author's names are GONE, never whether
non-names were spared, so renaming `the Chinese` into an invented surname passes it
perfectly.

Found sideways on Hemingway. The pairs audit reported beats naming African, Chinese,
X-ray, Republican and Cezanne as leaks -- correctly, those surfaces really were removed
from the corpus. Reading why turned up the larger defect: they should never have been
renameable in the first place.

Measured on the Hemingway map, both controls green:
  positive  `other` 764/1356 article-preceded = 0.56
  negative  100 honorific-confirmed people, highest Inglés at 0.26, bulk 0.00-0.06
  FLAGGED   130 of 946 surfaces, 1,616 instances = 0.162% of corpus words

The signal is an article in front of the surface: you write `the Frenchman` and `a
Martini`, never `the Rinaldi`. It is a heuristic and every hit is reported FOR READING,
never auto-removed -- `the Widow` and `the Informer` are genuine Hemingway epithet-names
that SHOULD be renamed, and the band's own top entry makes the point, since Inglés at
0.26 is an in-world nickname deliberately kept renameable and sits just under the bar.

Initials are excluded from the negative-control band rather than admitted to it. `Mr. P.`
is an initial, not a person, so letting it in lets a map defect poison the control that
validates the detector -- on Hemingway `P` (0.32, every occurrence `the P. O. U. M.`) was
the one surface failing a band whose next highest was 0.26. Initials take no article and
are invisible to the scan anyway, so every surface of two characters or fewer is now
listed unconditionally. Sixteen of them are in this map, C at 274 occurrences; the same
class as the `G` that was caught by hand about to be renamed to a surname 248 times.

The unresolved count that drives the exit code is computed over every flagged surface,
not the --show slice. Tying a gate's verdict to a display flag is the same defect as a
log filter that turns a real event into a clean zero.

Also corrects a wrong claim in audit_pairs_sourcenames.py's docstring: the Hemingway
rename did not HOLD 591 surfaces. Paris, Madrid and Spain survive because the stoplist
keeps them out of the entity map before it is built, so the map is exactly the removed
set -- 941 surfaces, 941 removed, 0 kept. Measured per run rather than assumed, because
a pipeline that carried kept surfaces into the map would report every `Paris` as a leak.
2026-09-17 02:01:01 -07:00
vh 0bb4938518 lv-hemingway: pre-register the v2 gate, and fix the floor rule that decided lv-bronte
The gate design is written before any generation exists, because lv-bronte's
verdict turned on a choice that was only visible after the numbers printed.

THE FLOOR RULE IS NOW PAIRWISE. lv-bronte computed the noise floor as the largest
within-arm seed spread across ALL arms present. Its ckpt475 shipped at +0.193
against a 0.251 floor set entirely by ckpt925 -- a third arm nobody was shipping,
on one outlier seed. Scored against the arm it was actually compared to, the floor
is 0.092 and the same gap clears at 2.1x. A candidate's verdict must not depend on
which other arms happened to be generated. voice_distance.py now prints both floors
and flags any disagreement, so the lv-bronte record stays comparable.

audit_pairs_sourcenames.py closes the blind spot leak_gate.py has by construction:
it reads the corpus and the renamed copies, never the generated beats, so it cannot
see a beat-writing model restoring the author's real character names. Run over the
Hemingway pairs, which predate build_sft_pairs.py --source-entities:

  val    0 of 200     -- the eval fixture is clean, the gate is unconfounded
  train  70 of 7,094 (0.96%)  -- Santiago x16, Catherine x7, Rinaldi x3, Brett,
                                 Harry, Jake, Pablo, Nick, Maria ...
  responses 0 of 7,294        -- the lv-bronte beat-only signature exactly

A matched surface is only counted when the rename actually removed it, verified
against the renamed copies, so a beat naming a held real-world place is not a leak.
Controls run every time: 941/941 surfaces found in the unrenamed source, nonce
absent from both trees, and 6 planted canonical names detected 6/6.

voice_distance.py --author is now REQUIRED. It was hardcoded "Yarros" and printed
"reference: held-out Yarros" over Brontë's numbers into a committed artifact. A
default would have moved the silent-wrong-label failure rather than removed it. The
stale "one seed-pair per arm / corroborates Base < Instruct" footer is replaced with
what the run actually carries.

Gate design: three arms (base-unadapted, ckpt1750, ckpt850), 60 beats, 4 seeds.
ckpt850 is present because the loss curve cannot separate it from ckpt1750 -- +0.0040
against a 0.0044 median neighbour jitter, with three checkpoints inside one jitter of
the minimum. adapter/ is excluded: +0.0762 is 17.4x the jitter and is resolved without
a gate.
2026-09-17 01:45:02 -07:00
vh c445ce9e93 memory: snapshot — lv-bronte shipped with a failed voice axis, next goal is landing lv-hemingway
In-flight rewritten for the next goal. lv-hemingway is TRAINED and nothing else
has been done to it: ship candidate is checkpoint-1750 (ep 1.97, eval 2.2783),
the end-of-run adapter is 0.0763 worse, and the v2 gate has not been run. Every
instrument it needs was parameterised during the lv-bronte run tonight and the
in-flight section names all four with their traps.

New detail files:
  2026-09-17-lv-bronte-gate.md            shipped, voice axis failed, why anyway
  2026-09-17-beat-contamination-leak.md   the leak the corpus gate cannot see
  2026-09-17-esh-fiber-outages.md         two Cityside failures, rotation fragility

Also commits the memorization_check.py parameterisation, which was left
uncommitted: its hardcoded Yarros defaults would have compared a Hemingway arm
against the Yarros corpus and reported a meaningless clean zero.

Auto-archival: index was 415 lines pre-run, over the 300 cap. Only five entries
cleared the 14-day age guard, and three of those carry open deferred pointers
(fused MoE park 47, nconnect=8, AI-tab belayed) and are referenced by in-flight.
A fourth — every CI job on pfi-fleet runs as root on ana-docker — is a live
security property rather than settled history, so it is held back deliberately.
One entry archived. The file stays over cap, which is the guard working: an
over-cap file that keeps live decisions beats a scannable one that lost them.
2026-09-17 01:29:09 -07:00
vh 8bb7686a16 audit_stoplist: a stoplist entry is an assertion the leak gate cannot check
Stoplisting a surface removes it from the entity map, so rename never touches it
and the gate never scans for it. That is exactly what a stoplist is FOR when the
surface is a real-world referent — and exactly how a wrongly stoplisted CHARACTER
becomes an undetectable leak. The gate reports 0 of N surviving and is telling the
truth about the set it was given.

Found by luck on lv-bronte: a generated beat said "Mrs. Leaven", and Leaven had
been filed under scripture as the bread noun. Reading it back: "Robert Leaven,
the coachman" — Bessie's married surname in Jane Eyre.

Running the audit instead of trusting that luck caught two more:
  Pierrot  "Madame Pierrot: she comes from Lisle, in France" — a teacher in
           The Professor, filed as the commedia dell'arte figure
  Samuel   "Mr. Samuel Wynne" — filed as scripture
and correctly CLEARED two:
  Wellington  "that Baal of a Lord Wellington" — the real Duke
  Moses       "the Rev. Moses Barraclough" — the documented dual-use

Signal is an honorific in front of the surface: real-world referents are not
addressed as Mr/Mrs/Miss/Madame/Lord. It is a heuristic and not a proof, which is
why every hit is REPORTED FOR READING and never auto-removed — Wellington and
Moses both trip it and both are correct. Exit 1 on anything not on --allow, so it
can gate a pipeline.

Blast radius of the three errors was 16 of 3781 train pairs and 3 of 80 val —
small, but they are the author's characters in training data, which is the one
thing this pipeline exists to prevent. Corpus rebuilt rather than dropping the
affected pairs: a corpus on disk that disagrees with its committed config is how
superseded claims get made. Gate re-passes at 0 of 368 (three more surfaces than
before, exactly the restored characters), both controls green.
2026-09-16 21:40:34 -07:00
vh e9e8c40b83 eval harness: sample the beat fixture from held-out val, and bind the eval prompt to the trained one
Two harness defects that would each make a voice number uninterpretable.

build_beat_fixture.py — the fixture is now SAMPLED from the val split rather than
hand-written. The original BabyYarros fixture was five hand-written beats about a
stray dog and a kitten: wrong genre, so 'He licked her clean' came back as
explicit sex from a romantasy adapter, and n=5 had a noise floor of 0.800 that
manufactured a +0.45 result which collapsed to +0.08 at n=120. Sampling from val
makes it in-genre and held out by construction, spread across works so a naive
head(30) is not one novel. Refuses outright if the pairs carry any split but val,
because a fixture drawn from training data makes every downstream number a
memorisation measurement wearing a voice label.

gen_beats_chat_yarros.py --system-from — the SYS constant in this harness is
Yarros's. Driving a Bronte or Hemingway adapter with it measures the arm under a
system prompt it was never trained on and confounds the carrier change with a
prompt change. Rather than duplicate the register table and rely on whoever runs
it to pick the matching one, read the prompt out of the pair build's own
provenance, which is the artefact that records what the adapter actually saw.
2026-09-16 21:18:44 -07:00
vh 7964d077de bronte-corpus: runbook — the five deviations and what the controls caught
Records the reproducible chain and, more usefully, why it diverges from the
Yarros/Hemingway pipeline in five places, each forced by a measurement rather
than a preference.

Includes the control post-mortem, which is worth keeping because in three of
four cases the CONTROL was wrong and the detector was right — the opposite of
the reflex. Adele vs Adele-with-a-grave, Hollow at a 0.235 lowercase ratio, and
Grace at 0.224 were all correct refusals. Blanche, at 0.0526 against a 0.05 bar,
was the one real detector miss.
2026-09-16 21:03:55 -07:00
vh 533cc0ce81 build_sft_pairs: reject beats that name characters the rename removed
A leak the corpus gate structurally cannot see, found on lv-bronte.

The rename strips the author's names from the prose and leak_gate.py proves
they are gone — 0 of 365 surviving on Brontë, both controls green. But the beat
is written by an LLM that READ THE PASSAGE, and if it recognises the book it
supplies the canonical names out of its own training. The beat is the
INSTRUCTION half of the pair, so training on it re-teaches exactly the
inventions the rename pipeline exists to remove, and the gate never looks at it:
the gate reads the corpus and the renamed copies, never the generated beats.

MEASURED on the first 714 Brontë pairs, before the filter existed:
  13 beats (1.8%) named source characters — Rochester x6, Jane x3,
  Brocklehurst x2, Beck, Fairfax, Helen, Burns, Eyre, Reed, Rivers
  0 of 714 RESPONSES did. The rename was perfect; the instruction side was not.
One beat read "Saoirse confirms Rochester's flaws, then agrees in English to
marry him" — a renamed name and a canonical one in the same sentence, which is
the mechanism in miniature.

Exposure scales with how well the generator knows the book, so it is WORST for
public-domain classics and mildest for recent work. That is exactly why the
Yarros and Hemingway runs came up clean and Brontë did not — their clean runs
are NOT evidence this cannot happen to them, and both should be rebuilt with
--source-entities if they are ever regenerated.

Adds a `sourcename` reject to vet() plus --source-entities, which takes the
UNRENAMED entity map and refuses any beat naming a surface from it. Firing at
roughly 3% of attempts on Brontë.

Also adds a `bronte` register. Brontë is the far end of the same axis from
Hemingway and the register has to say so, or the beat-writer produces modern
summary prose the passages never match.
2026-09-16 21:02:49 -07:00
vh fc834a8a23 bronte-corpus: gate lv-bronte for real — 0 of 365 with both controls green
The Brontë corpus's "0 of 203" was a HAND COUNT made before leak_gate.py
existed. On Yarros the automated instrument read 212 surviving where a hand
count said 86, so the hand count was never evidence. This runs the real gate,
and getting it to pass required fixing four defects the hand count could not
have seen.

CORPUS DEFECTS (repair_corpus_bronte.py, both measured):
  - 1,922 words of publisher back matter inside Shirley's last unit — a
    T. Nelson & Sons catalogue advertising Ainsworth, Marryat, Verne, Kingsley
    and Dickens, plus a Gutenberg transcriber's punctuation list. Not Brontë,
    and the source of the entity CHARLES. Same structural cause as the
    Hemingway run: a splitter cuts on headings, nothing follows the final one.
  - 1,368 Gutenberg italic spans. Two harms: they teach the adapter to emit
    underscores, and the underscore is a word character, so the gate's
    word-boundary scan cannot match inside an italicised name. An entity in
    italics is invisible to the gate — the same never-renamed-AND-never-
    reported shape as Yarros's possessive-only Afendra.

DETECTOR GAPS (phrase_map_bronte.json):
  - Blanche is 19 capitalised against ONE lowercase — ratio 0.0526, over the
    0.05 bar by a single token, so a named character with 19 mentions is
    dropped by a hair.
  - Grace (0.224) and Hollow (0.235) are refused correctly — both are common
    nouns — but Grace Poole and Hollow's Mill are Brontë's. Sampling all 21
    bare capitalised Grace found 20 are the character in direct address and
    exactly one is the theological noun.
  - Five compounds whose every component is non-renameable survive verbatim:
    Moor House, Marsh End, Vale Hall, Bigben Close, Royd Lane. The other 77
    audited phrases do not, because each has a renameable component.

GENDER (pin_known_gender.py): the inherited resolver put Jane MALE across 336
occurrences. Hemingway's base-rate resolver is strictly better here (1 wrong vs
4) but still fails on Jane, and the failure is structural, not tuning — Brontë's
three narrators are first-person, so their names appear almost only in dialogue
surrounded by other characters' pronouns. Ground truth is pinned separately from
the resolver's evaluation so the two are never conflated.

Also: min-cap lowered 8 to 3, which pulled Bertha, Ferndean, Rochesters and
Creemsvort in from below the old floor; corpus-scope rename so a name below
threshold in one novel is not printed verbatim there while renamed in another.

Gate: 0 of 365 surviving, positive control 365/365, negative control clean,
phrase audit 0 of 82. Floor stated: 3 capitals per work, 5 recurrences.
2026-09-16 20:51:11 -07:00
Vuong Hoang ebc4dac6d8 grok-token-broker: hold a refreshable session credential behind a rotation-safety gate 2026-09-16 15:12:38 -07:00
Vuong Hoang 6282833669 pairs: fix the three construction defects and the abbreviation-truncation bug 2026-09-16 08:12:01 -07:00
Vuong Hoang 03b4a3f62c BabyHemingway D2+D3: entities, base-rate gender resolver, rename preset, leak gate passes 2026-09-16 07:55:34 -07:00
Vuong Hoang 9598d0b4a7 BabyHemingway D1: fiction-only corpus builder with measured exclusions 2026-09-16 07:40:44 -07:00
Vuong Hoang 75051243ae BabyYarros: amend the decision rule to gate on voice and non-memorization (operator-authorised) 2026-09-15 14:04:42 -07:00
Vuong Hoang efb734586b BabyYarros: raw-surface scoring and a memorization check with both controls 2026-09-15 13:23:27 -07:00
Vuong Hoang 713e83dd5e BabyYarros: pre-register the pair-vs-rawtext decision rule before the arms are read 2026-09-15 11:19:11 -07:00
Vuong Hoang 90ed506db3 fix(pairs): apply_chat_template returns a BatchEncoding, and assert the mask boundary 2026-09-15 10:51:49 -07:00
Vuong Hoang 9b3d3c80cb BabyYarros Option C: instruction-pair builder and the assistant-masked pair trainer 2026-09-15 10:45:51 -07:00
vh 687c6999f3 fix(esphome): actually disable remote-build — two switches, only one closes the port
ha-dev found the WS API and tested the read half; this runs the write. But the
command they identified is the wrong half, which is worth recording because the
naming actively misleads.

  remote_build/set_offloader_settings {remote_builds_enabled: false}
      the OUTBOUND half — this dashboard sending builds to peers.
      Persists, reads back false, and leaves the receiver listening.

  remote_build/set_settings {enabled: false}
      the receiver-side master switch, per ReceiverController.set_settings's
      own docstring. Tears the listener down live, no restart needed.

Set both. Verified across a restart: 6055 absent, zero peer-link bind lines,
zero mDNS advertisements, both switches read back false. Persisted at
_remote_build.enabled in /config/.device-builder.json — which did not exist
until the flag was first changed, so 'no on-disk representation' was true only
of the default state.

ESPHOME_REMOTE_BUILD_HOST=127.0.0.1 is KEPT as a backstop rather than removed.
The off state now lives in one JSON file whose in-code default is enabled:True
(controllers/remote_build/_state.py) and whose module's stores soft-recover to
an empty model on a malformed blob rather than erroring — so a lost or corrupt
settings file silently re-enables remote-build. With the env var set, that
regression binds loopback instead of 0.0.0.0.

Also finishes deploy-stack.sh properly. This was patched three times in one
session because -a is -rlptgoD and a non-root identity cannot apply owner,
group, permissions OR times to a root-owned directory; each patch fixed one
letter and the next deploy failed on the next one, every time exiting 23 AFTER
a successful transfer. The rule is now written into the script: the deploy
syncs content, the conventions own metadata. --no-o --no-g --no-perms
--omit-dir-times. Verified: clean run, destination keeps 2775 root:docker with
setgid intact.
2026-09-14 19:05:11 -07:00
vh 8073a6aed9 fix(esphome): bind the remote-build peer-link to loopback; finish the rsync fix
ha-dev asked for the Device Builder 1.0.0 remote-build receiver to be turned
off: one instance, builds run locally, so the feature has no role, and it was
binding 0.0.0.0:6055 with mDNS advertisement on a privileged host-network
container that writes firmware to devices.

Reading the source first changed the framing. controllers/remote_build/
_state.py declares 'remote_builds_enabled: bool = True', so nobody enabled it
— it arrived on by default with the rewrite. And the flag has no on-disk
representation until it is changed: neither .device-builder.json nor
.device-builder-preferences.json carries it, and the only writer is the app's
own command API behind the UI. Setting it from a playbook would mean inventing
a schema for a model I have not read.

So this binds ESPHOME_REMOTE_BUILD_HOST=127.0.0.1 — a documented env var, no
entrypoint override — which removes the LAN reachability now and is verifiable
(ss reports 127.0.0.1:6055, was 0.0.0.0:6055). It is explicitly NOT the off
switch ha-dev asked for and the compose comment says so; the Settings toggle
is one UI click and the line can go once someone flips it.

Also completes yesterday's deploy-stack.sh fix, which was half a fix. --no-o
--no-g stopped rsync chgrp-ing a root:docker destination as a non-root
identity, but the very next deploy failed the same way one layer along —
'failed to set times on ...' — because a non-root identity cannot utime() a
root-owned directory either. Same exit 23 after a successful transfer. Added
--omit-dir-times. Fixing only the group half looked fixed until the next run,
which is the whole reason this is worth a line in the script's comment.
2026-09-14 18:55:54 -07:00
vh d1769ed114 feat(esphome): pin 2026.8.2, relocate config into backup coverage, rotate creds
ha-dev requested all three on esh-docker-vm (operator-authorized); the stack
had no canonical copy, so it is added to stacks/ rather than edited in place.

Pinned ghcr.io/esphome/esphome:2026.8.2 — it was bare, which is exactly how it
sat on 2025.8.2 for a year: docker pulled latest once at container creation
(2026-04-20, from a layer cached 2025-08-29) and never re-pulled. Every current
Everything Presence sensor failed config validation on that build. Verified
after: esphome version reports 2026.8.2 and the vendor's own Pro package now
validates clean (exit 0, 'Configuration is valid!'), which is the item that
unblocks the six waiting sensors.

Relocated /path/to/esphome/config (the upstream template placeholder, taken
literally by docker) to /opt/docker/conf/esphome, matching the mosquitto
pattern. Copied and checksum-verified all 5763 files before removing the
original, with a tarball kept at /root/pre-change-archive/.

Credentials moved off test/ChangeMe to the vaulted 32-char secret
(esh-docker-vm/esphome-dashboard), passed via a host-only .env so nothing
plaintext enters git.

Three things the job surfaced that were not in the request:

  The directory is 538 MB, not the 3 KB reported — .esphome/platformio is
  508 MB of PlatformIO toolchain and .esphome/build another 31 MB, both
  regenerable. Relocating as-asked would have inflated restic's /opt/docker
  source ~45x against its own ~12 MB budget, so both subtrees are excluded in
  /etc/restic/profiles.yaml. The 3 KB of actual config is now covered, which
  was the point.

  2026.8.2 logs a DEPRECATION for the bare USERNAME/PASSWORD env names and says
  they will stop working in a future release — a silent auth loss on some later
  bump, on a privileged host-network container that can flash any ESP device on
  the LAN. Switched to ESPHOME_USERNAME/ESPHOME_PASSWORD; the warning is gone.

  Device Builder 1.0.0 opens a NEW listener on 0.0.0.0:6055 (remote-build
  peer-link) that 2025.8.2 did not have.

Also fixes deploy-stack.sh: plain 'rsync -a' makes rsync chgrp the destination
as the deploy identity, which since the 2026-09-14 root:docker normalisation is
not root. It failed with 'Operation not permitted' and exit 23 AFTER
transferring content — a loud error on a deploy that had succeeded. --no-o
--no-g lets the setgid bit assign the group instead.
2026-09-14 18:47:54 -07:00
vh 68fa80f44d feat(scripts): add kb — direct search over the personal Worldtree KB
The Worldtree HTTP API cannot answer a question about the operator's notes.
/search there searches conversation MESSAGES, so a note that plainly exists
comes back as a clean empty result with no error attached. On 2026-09-14 a
search for 'shrimp' returned 0 hits; searching for 'the' and 'a' also returned
0, which is the only reason the empty result was read as an empty ACCOUNT
rather than an empty KB. kb reads the markdown tree directly instead:
deterministic, ~0.9s for 7,634 files, no tokens.

Two measurements shaped the design rather than being assumed:

  7,492 of 7,634 notes are INGESTED library material (4,155 fiction chapters,
  3,287 book sections, 50 academic papers) and only ~142 are hand-written.
  A flat relevance list buries the wanted note under a hundred chapters of
  Austen, so NOTES and LIBRARY are ranked and reported separately.

  Only 137 notes carry a frontmatter summary: key. Ingested notes use a
  '## Summary' body heading instead and some have neither, so the description
  falls back through all three shapes.

Two bugs caught by controls before shipping, both of which produced confident
wrong output rather than an error:

  Deriving the word list from argv meant a quoted
NOTES — 40 matches, showing 12

  Sous Vide Shrimp
    ATLAS/Cooking/Sous Vide/Sous Vide Shrimp.md
    Thawed shrimp should be sous vide at 135°F (57°C) for 30-40 minutes.

  Beef Stew
    ATLAS/Cooking/Sous Vide/Beef Stew.md
    This note outlines sous vide cooking temperatures and times for stew meat

  Pulled Pork
    ATLAS/Cooking/Sous Vide/Pulled Pork.md
    This note explains how to cook pulled pork sous vide: set the precision

  Brisket Sous Vide
    ATLAS/Cooking/Sous Vide/Brisket Sous Vide.md
    Here''s a concise summary:

  Ribs Sous Vide
    ATLAS/Cooking/Sous Vide/Ribs Sous Vide.md
    Here''s a concise summary:

  Derusting Solution
    ATLAS/Chemistry/Derusting Solution.md
    This note details how to create an enhanced rust removal soak by adding specific

  CNC with Raspberry Pi, USBIP & Camera
    clippings/CNC with Raspberry Pi, USBIP & Camera.md
    Here''s a concise summary of the note:

  SF - Victor
    ATLAS/Buy List/SF - Victor.md
    This order confirmation details 7 separate shipments totaling $2,533.45,

  Espresso Martini
    ATLAS/Cooking/Espresso Martini.md
    This note provides a recipe for a cocktail combining vodka, coffee liqueur,

  Brazilian Cheese Bread - Pão de Queijo
    ATLAS/Cooking/Brazilian Cheese Bread - Pão de Queijo.md
    This note provides a recipe for Brazilian cheese bread (#brazilian #food

  Congee Chao
    ATLAS/Cooking/Congee Chao.md
    This note provides the basic ratio (1 part rice to 7 parts water) for making

  White Bread
    ATLAS/Cooking/Baking/White Bread.md
    Here''s a concise summary:

LIBRARY (ingested books, fiction, papers) — 635 matches, showing 12

  Pride and Prejudice — CHAPTER XXI.
    fiction/rex390-pnp/ch23.md
    Following Mr. Collins’s proposal, Elizabeth encounters Wickham and learns that Jane has received a letter from Caroline Bingley announcing the party's immediate departure for London. While Jane interprets this move as definitive proof of Bingley’s indifference and permanent absence, Elizabeth remain

  Pride and Prejudice — CHAPTER XXIV.
    fiction/rex390-pnp/ch26.md
    Following Bingley’s letter confirming his settlement in London and growing intimacy with Miss Darcy, Elizabeth doubts the sincerity of his attachment to Jane, while Jane remains optimistic that external influences rather than design are responsible for their separation. The sisters debate these diff

  Pride and Prejudice — “On the Stairs.” CHAPTERXXVII.
    fiction/rex390-pnp/ch29.md
    Elizabeth reunites with Jane in London, where Mrs. Gardiner reveals that Jane suffers from periodic dejection despite her cheerful exterior, and the women debate whether Mr. Wickham’s pursuit of Miss King is motivated by mercenary or prudent reasons. Elizabeth then accepts an invitation from her aun

  Pride and Prejudice — CHAPTER XXXII.
    fiction/rex390-pnp/ch34.md
    Mr. Darcy’s frequent visits to Hunsford Parsonage spark speculation among the locals, particularly Mrs. Collins, who suspects he is in love with Elizabeth despite her own dismissal of the idea. Their initial interactions reveal a clash of perspectives on social convenience and local attachment, whil

  Pride and Prejudice — Chapter XLVI.
    fiction/rex390-pnp/ch48.md
    Following Lydia’s elopement with Wickham, Elizabeth Bennet informs Mr. Darcy of the scandal, reflecting that her earlier failure to reveal Wickham’s true character may have prevented the crisis and doubting their intent to marry due to their lack of funds. While Darcy offers sympathetic silence befo

  Pride and Prejudice — CHAPTER XIII
    fiction/rex390-pnp/ch15.md
    Mr. Bennet announces that Mr. Collins, the heir to Longbourn, will visit on November 18th, prompting mixed reactions from his family regarding the entail and Collins’s pompous letter. Upon arrival, the tall and stately visitor formally compliments Mrs. Bennet’s daughters and praises the estate, thou

  Pride and Prejudice — Covering a screen. CHAPTER VIII.
    fiction/rex390-pnp/ch10.md
    In Chapter VIII, Elizabeth endures the superficial sympathy and class-based mockery of the Bingley sisters while they criticize her muddy appearance and "low connections," even as Darcy defends her eyes and acknowledges her sisterly affection. The chapter highlights a clash of values when Darcy argu

  Pride and Prejudice — “Conjecturing as to the date.” CHAPTER XLIII.
    fiction/rex390-pnp/ch45.md
    Elizabeth’s visit to Pemberley fundamentally shifts her perception of Mr. Darcy, as the estate’s elegance and Mrs. Reynolds’ glowing testimony reveal his true character as a kind master and brother. This admiration deepens into gratitude upon seeing his portrait, softening her view of his past pride

  Pride and Prejudice — CHAPTER LVI.
    fiction/rex390-pnp/ch58.md
    Lady Catherine de Bourgh arrives at Longbourn to confront Elizabeth Bennet, demanding she promise never to accept Mr. Darcy’s hand based on claims of superior lineage and the scandal surrounding the Bennet family. She argues that Elizabeth’s inferior birth and lack of fortune constitute a disgracefu

  Pride and Prejudice — PRIDE. and PREJUDICE
    fiction/rex390-pnp/ch02.md
    Jane Austen’s *Pride and Prejudice* is presented as her most perfect work, distinguished by its structural regularity where every incident drives the plot toward a denouement strictly connected to earlier events. The novel’s supreme merit lies in its masterpieces of humor and character creation, whi

  Pride and Prejudice — A note for Miss Bennet. CHAPTER VII.
    fiction/rex390-pnp/ch09.md
    Mr. Bennet’s estate entailed on a distant relation leaves his daughters with limited financial security, yet the family’s attention is dominated by the arrival of the militia in Meryton rather than Mr. Bingley’s fortune. Mrs. Bennet successfully engineers Jane’s stay at Netherfield by sending her ou

  Pride and Prejudice — CHAPTER XVI.
    fiction/rex390-pnp/ch18.md
    In Chapter XVI, Mr. Collins and the Bennet cousins visit Meryton, where Mr. Wickham captivates the room and initiates a conversation with Elizabeth regarding Mr. Darcy’s character. Wickham claims that Darcy unjustly withheld a valuable living promised by his father, attributing this act to jealousy
  arrived as ONE element and became a single three-word pattern. The phrase
  never appears in a note titled 'Sous Vide Shrimp', so the tool reported
  'no match' for a note it had just found for the bare word 'shrimp'. The
  needle is now split on whitespace.

  Resolving the payload from dirname $0 broke the moment it was symlinked onto
  PATH. Now readlink -f.

cat refuses any path resolving outside the KB root — the remote half runs as
root because the volume is root-owned.
2026-09-14 18:17:21 -07:00
vh dfa91a8eaf docs(fv-ml1): add curated LLM seat catalog (lineage/provenance/cards/speed) + bench script
Adds docs/pfi/llm-seat-catalog.md, the durable hand-curated record of what each
seat IS -- lineage, provenance, model-card facts, quantization, speculative
decoding, licenses, and measured warm tok/s + deep-prefill depth results with
their harness and date. It complements the auto-generated
fv-ml1-gpu-seat-inventory.md (live placement/KV/concurrency): the two split by
volatility, and the catalog defers to the inventory for any live number.

Adds scripts/seat-bench.py so the catalog's speed/depth numbers are reproducible
(warm decode tok/s, n=3, conc=1, median; deep prefill at ~0.97x max-model-len
with an allocator-log OOM scan). Serial by design -- concurrent deep prefills
would confound both OOM and tok/s.

Captures the 2026-09-14 measurements: all six generative seats prefill to ~255K
(coder ~16K) with zero OOM/CUBLAS/illegal-memory; warm decode 62.7-337.3 tok/s;
per-seat VRAM. seat-inventory.py now cross-links the catalog in its footer.
2026-09-14 10:45:31 -07:00
vh c2a70c13db feat(fv-ml1): daily drift alarm for the seat inventory
Wires scripts/seat-inventory.py --check to a user systemd timer on nh3-dev
(09:15 daily, Persistent=true so a missed run fires on next boot) that posts to
althing when the committed document stops matching the live box.

Alarms rather than auto-committing. A drift means something changed on the HOST,
which deserves a human look -- silently regenerating the doc would erase the
evidence of when the change happened and why, which is how the char-rp
substitution went unnoticed for three weeks.

The alarm includes the changed table rows, not just the fact of divergence, so
it does not send the reader hunting.

⚠ The post goes --to infra-ops, which is the fleet ops handle the reading
session also runs as. That is the documented exception -- a memo from cron to a
future session, the same pattern as the Beszel alerts -- so the message says so
in its first line, to stop a future session triaging its own alarm as peer mail
and trying to reply to it.

SuccessExitStatus=0 1 because a detected drift is a deliberate non-zero exit,
not a unit failure.
2026-09-13 23:11:46 -07:00
vh a91b841d86 feat(fv-ml1): generate the seat inventory from the live box instead of maintaining it by hand
The seat documentation must stay current, and a hand-written document cannot.
The LiteLLM config described char-rp as a 31B model on a host and GPU it had not
been on since 2026-08-24 -- three weeks of silent drift in a file that read as
authoritative, and the reason a seat spent that period serving a model nobody
intended. Anything typed here drifts the same way; anything read off the running
containers cannot.

scripts/seat-inventory.py derives the whole document from the host:

- placement and VRAM from nvidia-smi compute-apps, mapped to containers through
  /proc/<pid>/cgroup -- nvidia-smi reports the vLLM engine child while docker
  reports the container pid, so matching them directly silently yields nothing
- weights and KV tokens parsed from each engine's own startup log, not derived
  arithmetically, with concurrency computed as KV tokens over context
- architecture, layer and expert counts, and the exact quantization group scheme
  (W4A4 vs W4A16 distinguished) from each model's config.json
- speculative-decoding method and k from the container argv, which is how the
  three incompatible methods on this box became visible
- lineage from the .PROVENANCE.txt SIBLING files -- they sit beside the model
  directory, not inside it, which is why an earlier pass wrongly reported two
  fully-documented seats as having no provenance
- gateway aliases resolved from the LiteLLM config on ana-docker

--check compares the committed document against the live box and exits non-zero
when they diverge, ignoring only the generation timestamp. Suitable for CI or a
scheduled drift alarm; read-only throughout, safe against production.

Also commits the KV_CACHE_BYTES override added to the MTP campaign runner, which
asserts the flag exists in the derived argv and aborts rather than running a
campaign that silently ignored it.
2026-09-13 23:01:44 -07:00
vh 3132a16ca0 fv-ml1: finish the renumber the cutover missed -- 16 dead dashboard links
Every fv-ml1 link on the Homepage dashboard was broken. Measured against the
live dashboard API before the fix: 16 entries pointing at the dead 10.250.50.54
and zero at the live 10.251.50.54, covering gen, M.O.G.-SEC, Scriberr, Embed,
Rerank, Reward, Coder, Dockge and six dormant seats.

The miss was structural, not careless. fv-ml1-rename-sweep.sh works from an
allowlist assembled from files that mention the HOST, and a homepage.href label
mentions only an IP -- so every stack whose sole stale reference was a label
fell outside it. The allowlist now covers those 24 files, and records how to
derive the list next time (grep the old address, subtract history) rather than
enumerating from memory.

History is still untouched, and the exclusions are now written down with the
reason each one keeps the old address: recorded benchmark results, whose
base_url is part of a measurement's provenance; the one LiteLLM comment
preserving a retired hand-test endpoint; and the cutover runbooks, where the old
address is the subject matter.

Two bugs found while applying it, both fixed here:

  - deploy-stack.sh rejected any stack name containing a dot, so qwen3.5-122b,
    qwopus3.5-122b and mistral-medium-3.5 could not be deployed by the script at
    all. The check exists to stop path traversal, which means rejecting ".." and
    "/" -- not every dot. Traversal is now rejected explicitly and tested.
  - stacks/scriberr/.env.example allowed CORS only from the dead IP and from
    scriberr.ana.internal, which no longer resolves; the box is at the fv site
    and DNS already carries scriberr.fv.internal. The live .env had both stale
    origins, i.e. an allowlist with nothing reachable in it.

Host side, applied separately: canonical pushed for the 16 stacks whose only
difference from the host was this renumber, and an in-place address-only fix for
the nine whose host copy has genuinely drifted or has no canonical copy, so that
drift survives for a deliberate reconciliation instead of being clobbered. Every
compose.yaml on fv-ml1 now reads 10.251.50.54. The labels themselves only take
effect at container creation, so the running containers still need recreating.
2026-09-12 23:05:29 -07:00
vh 8400f3aef5 scripts: pre-script the ana-ml2->fv-ml1 rename sweep (scoped, dry-run default, history-safe) 2026-09-11 19:13:53 -07:00
vh 5558d9c7d3 R49/BabyYarros: voice + beat evaluation tooling and the seat-free delta_cb
Adds the Yarros-side evaluation the training exists to justify: does the adapter
move arbitrary prose toward Yarros, and can the instruct arm still expand a beat
to a paragraph on direction. Yarros-flavoured voice prompts (modern/neutral/
romantasy tiers so any Yarros voice in the modern tier is adapter-attributable,
not prompt-supplied) and a Yarros-register beat SYS on the chat generator.

voice_distance.py is the honest slice of adjudication that needs no seat: Burrows's
Delta over character bigrams against held-out Yarros. Its first cut mis-framed the
noise floor — it used the same-author distance (held-out vs itself) as the
between-arm significance threshold, which is the target, not the threshold. Fixed
to the measured floor: the within-arm seed spread, which is this metric's sampling
variance at this sample size, computed from the two seeds already generated rather
than assumed.

Result on the built corpus, ordering base-125-tuned < instruct-tuned <
base-unadapted, both adapters clearing the 0.046 measured floor (base +0.157,
instruct +0.076), and the ordering corroborating the independent held-out loss
ordering (Base below Instruct). One seed-pair per arm, so it corroborates rather
than settles; the full frozen adjudication still needs a romantasy control panel,
a second seed, and the gen seat for the beat-incumbent leg.
2026-09-11 15:59:41 -07:00
vh 38eb40ace4 BabyYarros: stage the 4B-Base comparison arm, both concurrent and chained
One arm is not a comparison. On the Brontë corpus Base won held-out loss (2.814
against the instruct arm's 2.908) while instruct won instruction-following, and
that trade is the open question for Skaldsong. It has to be re-measured on this
corpus rather than carried over: BabyYarros is 12% larger and contemporary.

Two entry points because the right one depends on a measurement rather than an
argument. The box has 98 GiB free against an 18.4 GiB training footprint, so VRAM
is not the constraint -- but VRAM was never the binding constraint on a GB10 that
onboarding measured at 6x slower than ana-ml2 where compute predicts 2.7x, and
where batching was not a throughput lever. If the box is already at its bandwidth
roofline, a second job splits the same bandwidth and buys nothing.

So: launch-yarros-4b-base.sh takes an explicit --allow-shared-gpu that bypasses
the GPU-clear guard, and chain-yarros-4b-base.sh waits on the instruct run and
refuses if it produced no adapter. The bypass is an argument and never a default,
because the guard's normal job -- stopping a chain from firing into a live run --
is what keeps one lost run from becoming two. A shared-GPU launch stamps its own
log with a warning that its s/it is not comparable to a solo run, since the
harness is part of the number.

Both carry the same gate guard as the instruct arm: refuse to start unless the
leak gate report on disk says PASSED.
2026-09-11 10:22:21 -07:00
vh 7b0580dcbe BabyYarros: the leak gate passes, and it found three defects nobody was looking for
The gate is new. There was no committed instrument for "does any of the author's
own proper nouns survive the rename" -- the Brontë number was produced by hand
-- so leak_gate.py is now that instrument, and it runs both directions every
time: the same scan over the unrenamed source as a positive control, and a nonce
string as a negative one. A detector that only ever sees renamed text cannot
distinguish absent from blind.

Run against BabyYarros as built it reported 212 surviving entities, not the 86
recorded earlier, because it scans the whole corpus rather than each work
separately and it counts the sub-threshold entities rename never looked at.
Three findings came out of closing that.

The corpus had a typography defect of its own. The D1 notes correctly say no
unwrap was needed; a different defect was there instead. The Empyrean books set
their chapter epigraphs in small caps and the extractor rendered the run as
uppercase while leaving the large initial as a separate token, so the corpus
carried "M AJOR A FENDRA'S G UIDE TO THE R IDERS Q UADRANT" -- 106 lines, ~700
splits -- plus 52 drop caps like "T he flight field". That is where the entities
called IDERS, UADRANT, NAUTHORIZED and seventeen bare single letters came from.
A split initial next to an uppercased run is enough to recover the original
mixed case, so the restore is exact rather than approximate: a word with a split
initial was capitalised, an all-caps word without one was lowercase.

Back matter was inside the prose. The builder splits on chapter headings and
nothing follows the last one, so every work carried its acknowledgments,
newsletter pitches and cover-artist credits -- 4,555 words naming the author's
agent, editors and children, in a corpus whose entire purpose is that no
identifiable name survives.

And the gate passed at 0 of 314 while Afendra was still in every copy. The name
never appears unpossessed, so it keyed as an apostrophe form, and rename and the
gate both skip those as contractions -- unrenamed and unreported at once, which
is the worst failure shape available. Baxter escaped a different way: wilder
renders an in-book news article entirely in lowercase, putting the cap/lowercase
ratio at 0.13 against a 0.05 bar.

Then a second class the unigram scan structurally cannot see. Riders Quadrant,
Flame Section, War Games and Fourth Wing -- the book's own title -- are built
from ordinary words the detector correctly refuses to call names. The gate now
audits recurring capitalised 2-3grams against an explicit allow list, and
rename applies a phrase map after the entity pass.

Every new detector flag is opt-in and off by default, and the Brontë entity map
was re-derived after each change and confirmed identical in keys, surfaces and
every field. The stoplist was built by reading each surface in context, which is
why it is short: Violence is Xaden's nickname for Violet, and Continent,
Presentation, Barrens, Originals, Montserrat, Athena, Aura, Curator and Sage are
all in-world. A plausible-looking guess would have excluded most of them.

Final: 0 of 325 entities and 0 of 91 audited phrases survive in any of 30 copy
files, both controls passing. The sensitivity floor is stated in the gate's own
output -- 3 occurrences for a name, 5 for a phrase -- because a negative without
one is unfalsifiable.
2026-09-11 10:06:04 -07:00
vh 6dba912324 BabyYarros: corpus built, gender resolution fixed, rename blocked on leak gate
Located the source: five Rebecca Yarros works in the Kvasir licensed library, with
rights recorded as gated. Built D1 at 208 chapters and 780,744 words, which is 15%
larger than the Brontë corpus. No unwrap step was needed because Kvasir's cleaner
already emits flowing paragraphs, so the hard-wrap defect that cost a re-cut on
Brontë does not exist here. The alphabet was re-derived rather than inherited: 23
non-ASCII letters across three forms, against F02's 4 on a smaller sample. Same
ASCII-fold conclusion from a different measurement, which is the reason to re-derive
per corpus.

The interesting finding is a new pathology. In a rotating first-person POV corpus,
every book's narrator gets the wrong gender. Measured against six names verified in
the text, the pronoun resolver called Violet male, Leah male and Landon female --
three of eighteen wrong, and all three are the narrator of the book where they were
misgendered. A narrator is "I" in her own book, so her name appears mostly inside
the other lead's dialogue surrounded by his pronouns. This is Brontë's "Jane called
male" amplified by rotating POV. Title-first resolution, which fixed it for Brontë,
is nearly blind here because contemporary romance uses given names rather than
honorifics. What works is the POV header: resolve each name from the chapters it
does not narrate. Validated at 9 correct, 9 held, 0 wrong against the previous 7, 8
and 3 wrong, and the instrument refuses to write unless it beats what it replaces.

Re-pointing rename.py surfaced three bugs, two of which would have silently
corrupted the corpus. Gender came only from honorifics and the entities file's
gender field was ignored, so the POV fix had no effect until wired through; that
took wilder from 1 gendered entity to 13. The pool labels were hardcoded in a print
statement, so any non-Brontë preset crashed. And the collision-filter log claimed
it dropped names colliding with Brontë entities regardless of which corpus it
filtered against -- the logic was right but the message named the wrong corpus,
which is how a reader later concludes the filter ran on the wrong thing.

D3 is blocked and nothing has been trained. The leak gate shows 86 of 232
renameable source entities surviving where the Brontë run reached 0 of 203. It
decomposes into detector false positives that need a stopword filter rather than
renaming, genuine misses among worldbuilding proper nouns, and a third class whose
cause is not yet established. Training before the gate passes means fitting
in-copyright text with 86 identifiable source entities intact, in a corpus F02
already flagged as small enough for leak to be a real concern.
2026-09-11 08:46:45 -07:00
vh e15c5ee5ea Instruct probe: voice and instruction-following coexist
Trained the same corpus onto Qwen3-4B instruct rather than -Base, with seed, steps
and token count held so the carrier is the only variable. The chain picked
checkpoint-150 by loss automatically, applying the lesson that cost a re-cut on
4B-Base.

The central risk did not materialise. The assistant prior did not block the voice:
curly quotes land at 16 of 18, identical to the 4B-Base tuned arm, against 1 of 18
on the unadapted control, and task-leak is 0 of 18 where the base carrier leaked 4.
Instruction-following also survived raw-text training -- 10 of 10 on-beat through
the chat template, the same as the untuned control.

The cost is length discipline rather than comprehension. In-band dropped from 10 of
10 to 6 of 10 and the median went from 124 to 140 words. Training on Victorian
prose made it wordier, which is a soft degradation and not a break.

Held-out sits at 2.908 against 4B-Base's 2.814, and it plateaus without turning
where the base carrier overfit at step 75. The assistant prior competes for
capacity, so the instruct carrier absorbs less rather than overfitting more.

What raw-continuation training does not fix is the plot furniture. The tuned
instruct arm renders the beat and then drags the referent -- "He licked her clean...
my master thus, my husband thus", turning the dog into a man, because the corpus is
about masters and husbands. Another beat ran to 247 words and gave the narrator a
list of duties. That is precisely what instruction-pair training addresses, since
pairs teach render-this-and-stop where continuation teaches keep-writing. The probe
de-risks the instruction-pair path without substituting for it.

One metric note against future misreading: ran_on reports 10 of 10 on both arms and
is uninformative on this job, because a single paragraph contains no blank line for
it to find.
2026-09-11 08:32:30 -07:00
vh d9cf486004 Settle the Skaldsong architecture question with a ten-format negative result
Skaldsong's requirement is beat-sentence in, paragraph out, stitchable. That is
narrower than free-form continuation and it fails differently, so it was tested
directly rather than assumed from the earlier prompt work.

The adapted 4B carrier cannot do it. Ten prompt formats across three seeds each --
bare, paragraph-break, labelled, epigraph, one-shot, one-shot unlabelled,
three-shot, "it happened in this way", "I remember the whole of it", and the
letter prompt's label-and-begin move applied to a beat -- produced thirty samples
and not one that reliably renders the beat. They drift, they frame, or they
truncate. The cause is structural rather than a shortfall of the adapter: "write a
paragraph about this sentence" is an instruction, and a completion model has no
mechanism for "about". It continues the text it is handed.

Two formats failed worse than drifting and leaked pretraining task data. The
paragraph-break format emitted an NLI multiple-choice item, and label-begin emitted
a grammar-correction exercise. A standalone sentence followed by a blank line looks
exactly like a dataset entry, and style adaptation does not remove base-model task
artifacts.

The same five beats through the gen seat with a style instruction and no adapter
landed all ten samples inside the requested 90 to 140 word band, every one on its
beat, with no drift into a following scene. The voice, however, is generic literary
pastiche -- abstract-noun-heavy, fond of aphoristic openers, and over-written --
rather than Brontë.

Voice without direction against direction without voice. The product needs both,
which makes rebuilding the corpus as instruction-response pairs on an instruct
carrier the evidence-backed option rather than more prompt cleverness. This applies
to Yarros identically, because the carrier question is orthogonal to the author, so
the next corpus should not re-run this experiment.

Stitching also has its own failure mode, shown in the booth's third panel: the
paragraphs were generated independently and the point of view slides between them,
so by the fourth beat the narrator is both watching the girl carry the animals and
carrying them herself. A real stitcher has to feed prior paragraphs back as
context, which means the instruction-pair corpus wants multi-paragraph continuity
examples and not only isolated beat-to-paragraph pairs.
2026-09-11 06:24:51 -07:00
vh 8787daf04f Find the prompt recipe that works on a completion carrier
Two operator prompts settled the question the handoff prompt opened.

The winner labels the artifact and then begins it: "This is the letter I wrote
verbatim, my two short paragraphs, detailing the time I saw the mangy gray dog
meet and then lovingly and tenderly lick a calico kitten: Auntie, You'll never
believe what I saw--". Two of three seeds delivered the actual event in first
person, and one is the strongest output of the sweep -- the dog following her to
the mill, the kitten caught in a hedge, the dog carrying it out and laying it in
her lap before licking it. Coherent arc, correct subject, no gloom-override, no
meta-frame. It also reproduced the Gutenberg underscore-italics convention.

It works where the handoff failed for a specific reason. The handoff could be
satisfied by narrating compliance, because the letter did not yet exist. Naming the
artifact and starting it leaves nothing to narrate around, so the only continuation
is the artifact.

The chapter-heading prompt refuted a hypothesis of mine. I predicted that rendering
the title in the corpus's own conventions would make it land harder than the
operator's inline form. It did the opposite: both corpus-form seeds ignored the
title and opened unrelated scenes, while the inline form at least finished the
heading and wrote a chapter about the story. Corpus chapter titles are short and
decorative, so a long descriptive one in that slot reads as decoration to skip. A
label only instructs if the model treats that slot as load-bearing.

That prompt also surfaced an unnoticed consequence of the D2/D3 rename pipeline:
given "Alice", the adapter produced "Alexander the Alexander, as he was known in
Little London". The corpus was entity-renamed from a French/English pool, so the
adapter learned that character names come from that pool and rewrites outside names
into it. Callers cannot reliably name their own characters at prompt time. That is
not a defect of the rename, which exists to prevent memorising Brontë's cast, but
it is a usability constraint worth stating.
2026-09-11 06:04:41 -07:00
vh e47295d2ce Re-cut the 4B arms from checkpoint-75 and test an in-fiction instruction
The 4B tuned arms now come from checkpoint-75, the actual loss minimum at 2.813826
confirmed from loss-series.json rather than from reading the training log. Only the
tuned arms needed re-cutting; the base arm never touches the adapter.

One surprise in the comparison: step-75 and end-of-run differ on typography rather
than voice. Curly quotes read 16 of 18 against 17, collapse is 0 either way, but
the hard-wrap ratio is 0.33 at step-75 against 0.12 at the end. Further training
washes the residual line-break habit out while held-out loss gets worse, so best
loss and best typography are different checkpoints. Neither is near the original
0.85 defect and the corpus's own residual from preserved verse is 0.25.

The operator's second prompt embeds the instruction inside the fiction: Abernathy
tells the tale badly and then asks the narrator to retell it in a few short
paragraphs. Across six seeds the model acknowledged the handoff every time and
never once performed it. It wrote "I told it, briefly, to his satisfaction", and
"So I wrote it out, and kept it in my pocket-book", and one seed negotiated the
brief in character, objecting that it could not condense the story into a few short
paragraphs because there were too many points to touch.

The reason is structural rather than a failure of the adapter. In a novel, "she
retold the story" is an ordinary sentence, so the likeliest continuation of a
request is narration of compliance rather than compliance.

A trailing paragraph break did shift the behaviour -- one seed opened in the
narrator's own quoted speech -- so typography is a real lever, just not a
sufficient one. That is direct evidence on the instruct question raised earlier: if
the product is asking for a scene and getting the scene, no amount of in-fiction
framing substitutes for a post-trained instruction-follower, which favours
rebuilding the corpus as instruction pairs over more prompt cleverness.
2026-09-11 05:55:15 -07:00
vh 25eb65e23b Complete the R49 carrier sweep: 4B closes the continuity gap and overfits
The three rungs now sit on the same unwrapped corpus, seed, step count and token
count, so carrier size is the only variable. Held-out loss reads 3.329 at 0.6B,
3.018 at 1.7B and 2.814 at 4B -- deltas of 0.311 and then 0.204, diminishing but
still real.

4B answers the question the rung existed for. Scene-level continuity holds: on the
office-kitchen prompt it produces a named character with motivated dialogue, a
spatial layout the narrator navigates, and a physical description, all in one
passage, where 1.7B wrote pretty but eventless prose about opening doors and
looking at stars. On the letter prompt it opens the letter, promises to quote it,
and then quotes it across a paragraph break. Voice saturation is also the best of
any rung: curly quotes 17 of 18 against its own base arm's 1 of 18, and collapse 0
of 18 against 4 of 18.

Two findings that change earlier conclusions.

4B is the first rung to overfit inside one epoch. Its series runs 2.832, 2.816,
2.814, 2.820, 2.824, 2.825, 2.825 -- a minimum around step 75 and then a turn. Both
smaller rungs plateaued without turning, so the optimal epoch count shrinks as the
carrier grows and my earlier "one epoch is right for this corpus" holds only for
the small end. The consequence is operational: the adapter directory holds the
end-of-run weights at 2.825 rather than the step-75 best at 2.814, and it exists as
a recoverable checkpoint only because save_steps was set. The voice arms were cut
from the end-of-run adapter, so the booth understates 4B slightly.

The tone-override also appears to close. On the operator's frame prompt asking for
a wonderful story, 1.7B held the frame on every seed but killed the animals on two
of four; 4B kept them alive on both seeds, and one of them had the narrator doubt
the story he was told and supply a parallel childhood memory to explain the doubt.
That is a narrator with an interior position on the tale. Two samples per arm, so
directionally right rather than established.
2026-09-11 05:36:11 -07:00
vh 4cee4b179d Launch the 4B rung and record that voice transfer carries subject matter
Rung 3 of the sweep is training: Qwen3-4B-Base, one epoch, seed 4919, the same
unwrapped corpus, 159 steps at roughly 37.8 s/it for about 100 minutes. Carrier
size remains the only variable across the three rungs. A two-arm voice test is
chained behind it, gated on the adapter existing, so the rung is readable when it
lands rather than needing a second round trip to become informative.

The finding worth keeping came from an operator prompt rather than from my test
set. Asked for a wonderful story about a stray dog licking a calico kitten, framed
as reported speech, the 1.7B adapter held the frame across all four seeds and was
unmistakably Victorian throughout -- and two of the four turned the premise into
animal death, with the cat licking the puppy to death and the narrator's
interlocutor answering that he wished they were all dead.

That is not a coherence failure. It is Charlotte Brontë's preoccupations arriving
alongside her sentences: Jane Eyre opens on a beaten child, Helen Burns dies, and
Villette is saturated with grief. Train 680k words of that into a small carrier
and a request for something wholesome reaches for mortality.

My nine test prompts were all emotionally neutral and structurally could not have
surfaced this. The operator's prompt found it on the first try, which is worth
recording as a property of the test set rather than of the adapter.

The implication for the regime is that "voice transfer" includes tone and subject,
so wanting the voice without the gloom is a corpus-selection or prompt-framing
problem rather than a training-length one.

Also observed: one seed closed its anecdote and began "CHAPTER XIX. THE CHILD'S
CLOSET", having learned book structure unprompted. The base control on the same
prompt went modern and essayistic, so the shift is the adapter rather than the
carrier.
2026-09-10 22:58:10 -07:00
vh 9e18fd58ec Complete R49 rung 2 and booth the three-way voice comparison
Both rungs now sit on the same unwrapped corpus with seed, steps and token count
held, so carrier size is the only difference and the effect is attributable:
held-out 3.329 at 0.6B against 3.018 at 1.7B, a gap of 0.311 nats. The chained
0.6B rerun closed the confound the unwrap opened.

Two things in those numbers need stating or they will be misread.

First, the original wrapped-corpus 0.6B reached 3.172, which looks better than the
unwrapped 0.6B's 3.329 and is not. Different corpus means a different held-out
set, and the wrapped version's 5.7% newline tokens are near-deterministic after a
70-character line, so they deflate the loss with cheap wins. Removing them removed
the easy tokens. It is a measurement artifact, not a regression.

Second, a correction to my own earlier claim: I twice described the 0.6B run as
still descending and undertrained at 3.172. Its series reads 3.176, 3.173, 3.172,
3.172 -- it flattened. All three runs plateau, so one epoch is about right for
this corpus rather than short.

The three-way booth puts 1.7B base, 1.7B tuned and 0.6B tuned side by side on the
same nine prompts and seeds. The base arm is the control that matters: curly
quotes go 0 of 18 on 1.7B base to 15 of 18 on 1.7B tuned, and worksheet-or-
explainer collapse goes 3 of 18 to 0 of 18, so the shift is the adapter rather
than the larger carrier. Hard-wrapping fell from 0.85 to 0.18, confirming the
corpus unwrap carried through into the adapter.

Sense partially returned. The 1.7B arm produces locally coherent sequential
Victorian prose where the 0.6B produced word salad, but scene-level continuity
still breaks mid-passage.

One observation held loosely: curly quotes are slightly lower at 1.7B than 0.6B,
which would fit a bigger model's stronger priors resisting the adapter at the same
rank. That is untested and is not offered as established.
2026-09-10 22:38:11 -07:00
vh 935b35ac2e Unwrap the Brontë corpus and launch the 1.7B rung
Operator: "start the 1.7b training."

The 0.6B adapter learned the Gutenberg transcription's ~70-character line breaks
along with the prose -- its output wrapped at a mid-length-line ratio of 0.85
against the base model's 0.00. That is typography rather than style, and every
further rung would have inherited it, so the corpus is reflowed before rung 2
rather than after the sweep.

The reflow joins 57,430 of 85,380 paragraph blocks and keeps 27,950. Verse is the
hazard a blind join would destroy, so the decision is per block by median line
length: blocks whose lines cluster near the wrap width are flowed prose, blocks of
consistently short lines keep their breaks. Every kept multi-line block in the
sample was genuinely verse with its lineation intact. No line ended in a lone
hyphen, so the space-join could not split a word across lines. The acceptance
check is content identity -- " ".join(text.split()) byte-identical before and
after -- and it passed on all 852 records, proving only whitespace changed.

Concrete cost of the old defect: 5.7% of the training budget was newline tokens.
The same words pack to 5,210,112 tokens unwrapped against 5,525,504 wrapped.

The 1.7B run is live at 159 steps and roughly 18.7 s/it. Everything but the
carrier and the corpus is held from the 0.6B run: seed 4919, rank 32, lr 1e-4, seq
4096, batch 1 by accum 8, one epoch, eval and save every 25 steps so the minimum
is located rather than assumed.

That corpus change is a second variable and it is named as one. A 0.6B-vs-1.7B
comparison is descriptive, not attributable, until the chained 0.6B rerun on the
same unwrapped corpus lands behind it -- gated on the 1.7B actually producing an
adapter, because a chain that fires on failure turns one lost run into two.
"Did sense come back at 1.7B" is a within-arm reading and survives the confound;
any between-rung delta does not.

The original wrapped corpus is untouched, so the 0.6B run's pinned corpus sha
3959036cf851bf62 stays reproducible.
2026-09-10 15:37:51 -07:00