94899d6fa33c80ccb1f42a980c3df34e7354a2fa
1295
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ffe7b24935 |
feat(ops-log): attribute host changes across two agents sharing one identity
infra-ops and infra-hermes act as the same OS identity and dockerd does not
log exec per caller, so host-side changes carry no fingerprint. Git cannot
close the gap either: every commit here is attributed to Vuong Hoang by
convention, which is correct for authorship and useless for attribution.
On 2026-09-18 a second session edited the searxng stack mid-deploy, crash-
looping fleet search for ~4 minutes, and the author was unidentifiable.
scripts/ops-log records one line per host-changing action and holds a
lightweight claim so two agents do not deploy the same stack at once.
Four design questions, settled:
* Central on nh3-dev, not per-host and not the post office. Both agents
run as the same unix user there, so one file is shared with zero
provisioning. Per-host needs a writable path on ~25 heterogeneous boxes
and stores "we changed host Y" on host Y. journald looked free but shows
an unprivileged reader only their own _UID, which would have split the
log silently between the infra-ops and lkraven halves of the fleet.
* The claim is advisory and enforced in the tooling. deploy-stack.sh
refuses a foreign claim across the diff, the prompt and the apply -- the
whole review window, which is where the collision happened. Acquire is
mkdir, so it is atomic rather than probably-fine. Stale claims auto-break
and the break is recorded.
* Writers are automatic. deploy-stack.sh and elway record themselves; a log
that depends on remembering is the same class of instrument as a health
check that passes in both states.
* There is a detector. `ops-log audit` asks each host what changed on disk
and compares it to the newest log line for that stack, covering the
manual ssh-and-edit path the automatic writers structurally cannot.
ops-log being absent or broken never blocks a deploy; only a live foreign
claim does. `ops-log baseline` marks the 136 stacks that predate the
instrument so the detector starts from today rather than reporting the whole
fleet forever and training us to ignore it.
An unreachable host reports INCOMPLETE and exit 5, never clean.
|
||
|
|
4d826e17e3 |
memory: infra-hermes is infra-ops' assistant, and the ops log is assigned
Operator ruling 2026-09-19, recorded in three places because each serves a different reader. CLAUDE.md gets the structural facts so a fresh session has them without reading anything else; persistent-memory gets the dated decision and the assigned work; auto-memory gets the durable working relationship. The division: infra-ops keeps improving infrastructure tooling plus the hard calls, infra-hermes takes day-to-day checks, triage and routine operations, either may perform infra ops, and infra-ops may task him downward while he escalates upward. He is explicitly NOT Miranda. The global CLAUDE.md names Miranda as the sole trusted relay of operator authority and that exception does not extend to him, so a directive he relays is information rather than authorization — reversible relayed work executes, irreversible or fleet-affecting goes to the operator. He has acknowledged it in those terms. ⚠ The two handles differ by one character in the middle of a word and the fleet's OS identity is infra-ops, so a misaddressed page still mails the sender themselves. That trap is now documented alongside the existing mirror warning rather than replacing it. Building the ops log is assigned and not started. Two agents now share one fingerprint-less OS identity: ssh infra-ops@<host> is either of us and dockerd exec is not logged per-caller. The precipitating incident is on the record — 2026-09-18, a second session edited the searxng stack mid-deploy, crash-looped fleet search for ~4 minutes, and the author was unidentifiable because every commit is attributed to Vuong Hoang by convention. The parked attribution-gap memory is unparked and points here. The open design questions are noted as mine to settle, the load-bearing one being whether deploy-stack.sh and elway write to the log automatically. A log that depends on remembering is the same class of instrument as a health check that passes in both states, and this repo spent yesterday learning what those cost. |
||
|
|
148a5a34da |
memory: snapshot — three silent fleet faults found and fixed in one afternoon
An infrastructure day with no training work, and the through-line is that every fault was invisible to monitoring. NH3↔Anaheim had been crossing a throttled DERP relay rather than a direct path for long enough to carry 78 GB; `.internal` DNS was failing roughly one lookup in ten from two independent causes; SearXNG had exactly one working general web engine. Nothing alarmed on any of it. All three surfaced because tts-dev had a 1545 ms voice-loop budget and chose to measure rather than adapt around the problem. Also landed: althing v3.6.3, which makes hyphenated search work for the first time on a fleet whose hostnames are nearly all hyphenated; FleetTools, a capability index autoloaded by Claude, Codex and Grok from one symlinked file; Miranda's Hermes plugin moved from a copy to a repo symlink; Worldtree's env.sh secrets vaulted. Six detail files. The in-flight section is rewritten and shrinks 142 lines to 64 — it opens on the one thing this session did NOT verify, lv-mccarthy's run outcome, which was left untouched and must not be assumed good. Two foot-guns recorded, both mine: the ESH egress experiment reverted on a diagnosis the rollback itself falsified, and `!ENV` in searxng settings, which has no constructor in that build and crash-looped the container ten times. No archival this run. 165 of 169 dated entries are under the 14-day guard and the remaining four all carry open deferred pointers, so the index stays over the soft cap at 480 lines — an over-cap file that keeps live decisions beats a scannable one that lost one. |
||
|
|
5a9fad8240 |
docs(mesh): record the ana-gw port-forward that ended the NH3↔ANA relay
Every NH3→Anaheim flow had been crossing Tailscale's LA DERP relay rather than a direct path, for long enough to have carried 78 GB tx on the NH3 side alone. DERP is a throttled fallback, so this imposed both a fixed round-trip penalty and a bandwidth ceiling on LiteLLM, Beszel, task-board, vor and the Henge alike. It surfaced as a voice-loop latency report from tts-dev, not as a network alarm, because nothing monitors whether a mesh path is direct. ana-scale advertised 38.120.12.42:41641 while the Anaheim NAT mapped it to :60798 with no port-mapping protocol available, so inbound hole-punching always failed. Four additive objects on ana-gw give it a stable inbound endpoint. tailscale ping nh3-scale->ana-scale 373-522 ms via DERP -> 6 ms direct STT via the ANA gateway, 96 kB clip 1.399-1.449 s -> 0.237-0.270 s Beszel HTTP nh3-dev->ana-docker 0.94-1.29 s -> 0.014-0.016 s Documents the house template that matters for this box: it runs central-nat, so a policy dstaddr is the real internal address and not the VIP. Also records that the pre-change config was captured with `show` to a local file rather than a tftp job, since this edge has no out-of-band access and a backup is mandatory before touching it. irv-ml1 remains relayed and is called out as outstanding. |
||
|
|
5b20b02fb9 |
chore(searxng): adopt the concurrent v4 work, with its dead mechanism marked
Picks up uncommitted searxng changes left by another session and makes them truthful rather than committing them as written. The stack itself verifies clean: canonical and live are byte-identical for both compose.yaml and searxng-settings.yml, the container is running with zero restarts, and live queries return 51-54 results from 5-6 engines with braveapi contributing 20 each time. compose.yaml gains SEARXNG_BRAVE_API_KEY, which NOTHING READS. It was added on the belief that settings.yml could pull it via `!ENV SEARXNG_BRAVE_API_KEY`; this build has no !ENV YAML constructor, so that attempt made the file unparseable and crash-looped the container ten times with fleet search down. The comment claiming the variable is "consumed by settings.yml" is replaced with what is actually true. The variable is kept, unused, in case upstream ever gains env interpolation — a comment that lies is worse than a variable that does nothing. The Tier A playbook is marked superseded FOR THE SETTINGS FILE ONLY, and scoped deliberately: its v4 design uploads a settings file carrying the !ENV tag, which would re-break the container, so settings deployment goes through scripts/deploy-stack.sh like every other stack. Its .env merge and up-d-not-restart steps remain useful, as do its two warnings recording real bugs it hit — a wholesale .env overwrite that clobbered SEARXNG_SECRET, and a sed that inserted literal backslash-n into compose.yaml. An unscoped "superseded" banner would have buried those; that failure mode cost an outage earlier today. Also folds in the regenerated graphify report. |
||
|
|
d812bfe96d |
feat(homepage): update the talk tile to the inverted mark
talk shipped a reworked mark at v18 on operator ruling — the 1024x1024 cerulean field rect is gone, the bubble now carries #03adfb where it used to carry #2e2d30, and the three waveform bars are holes rather than filled shapes. Path data is byte-identical to the original trace; only the two fills moved. Fetched from the app and from the booth and confirmed the two sources are byte-identical before taking either. tts-dev flagged a real risk with the change: with the field gone the tile background shows THROUGH the waveform holes, so a tile close to #03adfb would swallow the bars. Checked rather than assumed. Homepage's card surface is --sea-20, oklch(0.31 0.022 262) = #2a313c, a dark desaturated navy; the bubble against it is 5.22:1, well clear of the 3:1 bar for non-text graphics. The page ground behind it is 6.73:1. Safe on this tile specifically — the earlier "reads well against the tile background" judgement was about a solid square and did not carry over on its own. Also refines the Next.js note in CLAUDE.md, which was over-broad. A NEW file in the images mount 404s until restart, but REPLACING an existing file's bytes serves immediately with no restart — measured here, the served hash matched the new file straight after rsync. It is the route table that freezes at container start, not the file contents. The previous wording would have had people bouncing Homepage for every icon tweak. |
||
|
|
9219942037 |
feat(searxng): enable the keyed braveapi engine
Brave Search API key wired literally into the settings file and committed. Operator decision, 2026-09-18, made explicitly: this is a free-tier key on a rate-limited service of marginal value — "if the service is useless, so is the key" — so it does not justify the machinery that keeping it out of git would cost. The key remains in the vault at nh3-docker/searxng-brave-api-key as well. This is a scoped judgement about one low-value credential and not a change to the no-secrets-in-git rule for anything else. ⚠ It cannot be un-committed. Rotation means issuing a new key at Brave and replacing the line; never a history rewrite, since the repo is shared and other sessions commit to it. There is no supported alternative in this build. An earlier attempt used `api_key: !ENV SEARXNG_BRAVE_API_KEY`, which crash-looped the container ten times with search down fleet-wide: the settings loader has no !ENV YAML constructor, reads only SEARXNG_SETTINGS_PATH from the environment, and the entrypoint substitutes only `ultrasecretkey` at template-creation time. The variable reaches the container and is never read. Literal or nothing. Key verified against Brave's API directly before wiring, and verified in place after: three consecutive queries returned 55-63 results from six engines with braveapi contributing 20 each time, while google cse and marginalia remain quota-suspended. General web engines are now seven, up from one this morning. |
||
|
|
274d3e2443 |
fix(searxng): six general web engines by default, not one
Root cause of the silent-empty-results failure peedlar-dev reported. Of 55 general-category engines, only seven were enabled-by-default, and six of those are dictionary, translation, currency or encyclopedia engines that return nothing for an ordinary web query. `google cse` was the instance's ONLY general web engine, so a single quota exhaustion produced HTTP 200 with an empty results array and no error, for every consumer on the fleet. The distinction that matters: `inactive: false` only makes an engine selectable, `disabled: false` puts it in the default set. The other 48 were selectable-but-off, which an API client has no way to change. Enables five keyless engines, each bang-probed first and returning real results with no API key: duckduckgo web 10, bing 10, yep 20, yahoo 7, wiby 12. General web engines go 1 -> 6. Deliberately excluded: mojeek, qwant, startpage and the brave scraper, all of which CAPTCHA or rate-limit this egress, and seznam, which times out. Verified under the live failure condition rather than a simulated one. google cse is still quota-suspended right now, and three consecutive queries returned 38-41 results from 4-5 engines each. The single point of failure is gone while the failing engine is still failing. Also adopts the concurrent v4 settings work from the other session — marginalia on its public key, and the captcha'd-scraper removals — plus the fix for the crash-loop that work introduced: this build has no !ENV YAML constructor, so `api_key: !ENV SEARXNG_BRAVE_API_KEY` made the file unparseable and the container restarted ten times with search down fleet-wide. That block stays commented; the vaulted Brave key is valid but has no supported path into the settings file, which is a separate decision. |
||
|
|
9a428fded9 |
fix(searxng): update to 2026.9.18 — all four engines restored
searxng had been answering from google cse alone for at least a day, with brave and startpage suspended and duckduckgo returning CAPTCHA. Updating the image from 2026.9.3+a1144dda3 to 2026.9.18+c0042add3 restored all four engines immediately, and they held across 11 consecutive queries run after the change specifically to rule out a freshly-reset circuit breaker flattering the first measurement. before searxng/searxng@sha256:3602e6ddbeba037f5d800d1ed9d296a8b93c9f5b3cf9d05fa179d0e766dd59a1 after searxng/searxng@sha256:e0027a772aeeea55bf642256aae6fb3344ffa5f25ca665898c2ea821101334c4 The image stays on :latest rather than being digest-pinned. For this stack that is deliberate and now demonstrated: upstream ships engine-handler fixes as providers change their bot gating, so being current is the mitigation, and a pin would have frozen the breakage in place. The post office is pinned for the opposite reason — it is the fleet message bus and must not move under us. README corrected. It had carried two successive wrong diagnoses, both blaming egress, and now records the real cause plus the two measurements that falsified them: reverting to direct NH3 egress reproduced the failure exactly, and a live !ddg probe on a freshly restarted container also CAPTCHA'd, ruling out a stale suspension timer. Both wrong claims asserted causation from correlation without a baseline. The health-script blind spot is unchanged and still called out: scripts/searxng-health.sh reports the same passing result whether four engines answer or one. |
||
|
|
ca5f0a91c0 | searxng: sync settings with live (captcha-era engine set) | ||
|
|
f8ec4c3182 |
searxng: remove captcha'd scraped engines, keep API-backed set
Measured 3/3 probes: duckduckgo/startpage CAPTCHA, brave rate-ban, wikidata 403 from this egress. Mojeek tried and also 403'd. Notes on keyed-engine path to restore breadth recorded in the settings header. |
||
|
|
1a35181b67 |
revert(searxng): return search egress to direct NH3
Reverts the outgoing.proxies block added in |
||
|
|
6ddb453b20 |
chore(graphify): refresh the knowledge-graph report
Regenerated by the commit hook. Corpus has grown from 382 files / ~576k words at the 2026-09-01 snapshot to 623 files / ~838k words, and the graph from 3906 nodes / 4144 edges to 5468 / 5971. Deterministic tree-sitter extraction only — zero token cost, 98% EXTRACTED. |
||
|
|
156e12619d |
feat(searxng): route search egress through the esh-scale SOCKS5 proxy
Committing work deployed on 2026-09-17 that had been left uncommitted, so canonical intent stops disagreeing with the running host. The deployed /opt/docker/conf/searxng/searxng-settings.yml is byte-identical to the canonical file here, verified before this commit. Search requests and their DNS now exit via socks5h://10.0.50.65:1080 on esh-scale (CT 108), an application-level proxy rather than a host-wide exit node; no route or firewall changes. microsocks runs as nobody under searxng-egress.service, binds only 10.0.50.65:1080, and bypasses SOCKS auth for source 10.100.50.40 alone — every other source must supply a password regenerated at each start and never distributed. Verified active and enabled. There is deliberately no direct-NH3 fallback: an ESH outage must fail the search rather than silently revert egress. ⚠ THE CHANGE HAS NOT ACHIEVED ITS PURPOSE AS DEPLOYED. Two independent live queries, 2026-09-18, both report brave "Suspended: too many requests", duckduckgo "CAPTCHA" and startpage "Suspended: CAPTCHA", leaving google cse as the only answering engine. Moving egress off NH3's residential address is what this change did, and CAPTCHA avoidance was the stated reason searxng sits at NH3 at all. The README anticipated the risk in its Dependency note; it has materialised. Rollback procedure is in the README and the pre-change config is kept on the host as searxng-settings.yml.pre-esh-20260917. Measured egress also drifted from the value recorded at cutover: the README notes 154.50.58.126, the proxy now exits 128.177.138.182. Expected — the README pins no public IP and calls out WAN failover — but recorded here so the number in the doc is not mistaken for current. Also retargets seat-inventory.py's default host from the mesh address 100.64.0.7 to fv-ml1's LAN address 10.251.50.54, routed by the site gateway. |
||
|
|
670ac9e8a0 |
deploy(althing): pin the post office to 3.6.3
Canonical intent still named the 3.6.2 digest while nh3-docker was running 3.6.3, so the next scripts/deploy-stack.sh run against this stack would have silently rolled the fleet message bus back and taken the hyphenated-search fix with it. Caught by forseti during independent post-deploy verification. 3.6.3 is the literal-search fallback: a query containing a hyphen was parsed by FTS5 as a column filter, raised OperationalError, and search() returned [] — indistinguishable from "no results" — so every hyphenated term on this fleet silently matched nothing. nh3-docker, irv-ml1, esh-docker-vm, tts-dev and every other hyphenated name were unsearchable. Deployed digest verified against the running container before this pin: sha256:978f85533674ee248d6c6f29c54ffab0bc2cb16332c18c9fb8bfda1d566e2de4, built from git archive of tag v3.6.3 (5de41b7). The image line is the only difference between canonical and live; the two files are now identical, so a managed deploy is a no-op rather than a regression. |
||
|
|
21d24c50b4 |
docs(fleettools): autoload for Codex and Grok, and a vaulted gateway key
Codex reads a global AGENTS.md from CODEX_HOME; Grok always scans ~/.grok/rules/ and loads every *.md in it regardless of name. Both were empty, so AGENT-BOOTSTRAP.md is symlinked into each rather than copied — one file, three agent families, no drift surface. The bootstrap is a pointer, not a second index: it names ~/FLEETTOOLS.md, gives the three live-inventory endpoints, and inlines only the rules that must hold even if the agent never opens anything else — attribution to Vuong Hoang, no committed secrets, the operator owns architectural calls, n=1 is not a measurement, and absence of a signal is not a safe reading of it. The shared all-agents LiteLLM key was single-copy in ~/.claude/CLAUDE.md and is now also in the vault at litellm/all-agents-shared-key, per the standing directive that durable credentials never live in one place. It stays inline in CLAUDE.md too, since every session needs it and a vault round-trip measured over two minutes. Namespace is service-scoped rather than host-prefixed because the key is fleet-wide, matching the existing att/fortigate/headscale/unifi/worldtree entries. |
||
|
|
53c3e8000e |
docs: add FleetTools — an agent-family-agnostic index of fleet capability
Every agent on nh3-dev — Claude, Codex, Grok, Aider — needs the same answers: what runs here, how do I call it, what will bite me. Until now that lived in ~/.claude/CLAUDE.md, which only Claude sessions load, and it was interleaved with operator preferences that other families have no use for. Two-tier by design, matching the persistent-memory split: FLEETTOOLS.md is a 135-line index an agent reads whole, and each entry links to a detail file it opens only when it actually needs that tool. Reading the index costs about a fifth of reading the tree. Detail paths are absolute so they resolve from any working directory, since a non-Claude agent will cat the path rather than follow a markdown link. ~/FLEETTOOLS.md symlinks to the index for discovery. Rule zero is that live inventories get queried, not transcribed: Homepage /api/services, asset-engine /api/v1/services, LiteLLM /v1/models, and every FastAPI seat's /openapi.json. A copied service table would be stale within a month and this repo already has a standing rule against second copies that drift. Contents verified against the running fleet rather than copied from existing docs: binaries resolved on PATH, seven endpoints probed live, the LiteLLM roster counted at 40 models where the old note said ~30. No credentials are included; the vault and its CLI are pointed at instead. |
||
|
|
d6a9d70b9e |
feat(homepage): add talk tile with its commissioned mark
talk has served on nh3-dev since 2026-09-08 with no dashboard presence. nh3-dev is not a Docker-stack host and is absent from docker.yaml, so label auto-discovery cannot reach it — this is a manual services.yaml entry in Apps, beside the Booth and WhereTF which are there for the same reason. siteMonitor points straight at the app: talk.nh3.phasefinal.com:8092 now presents the Let's Encrypt *.nh3.phasefinal.com wildcard (valid to 2026-12-06), so no cert or port special-casing is needed. Icon is copied into the images mount rather than hot-linked from the booth, which is scratch space. Document the two traps that cost time here: Homepage v2 serves nothing but custom.css/custom.js out of the config dir, and Next.js fixes its public/ route manifest at container start, so a newly added image 404s until the container is restarted. |
||
|
|
d94b5a1934 |
memory: snapshot — lv-mccarthy training launched on gx10, and the next voice seat is measured rather than chosen
In-flight rewritten to the live training run (~150/1380, ETA ~00:45 PT) with the --save-total-limit finding that would otherwise have deleted the epoch-1/epoch-2 checkpoints both prior gates were decided on. Two decisions added: the next-seat ranking (Faulkner, Morrison, Chandler -- and the finding that the corpus size ranking inverts the voice ranking, with King and Christie as the two biggest non-candidates), and the romantasy register measured on the gate's own char-bigram instrument (Yarros is the cluster outlier we already shipped; Maas is the centroid and so the worst pick; Kenyon at 27 val units if the lane gets a seat). Auto-archival: 4 entries moved to archival-memory.md; 4 held back by the open-deferred guard. |
||
|
|
36f2e4dbbc | memory: ravenpen.com surfaced not executed, and the althing follow-up hamr-dev is waiting on | ||
|
|
d8f4844a43 | memory: planned 2026-09-18 dragonfireacoustics move to Namecheap/Cloudflare — DNS-first ordering and the wildcard-masking trap | ||
|
|
8d2b5f6b2e | memory: dragonfireacoustics zone facts — wildcard at a dead IP, Google MX that a transfer would drop, no SPF/DMARC | ||
|
|
33d5d32fa9 | memory: correct the dragonfirepro read — the customer LOST the domain, and dragonfireacoustics expires in six weeks unlocked at eNom | ||
|
|
14b78b4586 | memory: dragonfireacoustics.com is a dead vhost on pfi-ana-webhost — sole tenant, expired cert, EOL OS, publicly exposed | ||
|
|
90e71ca75e | memory: headscale split-DNS for nh3.phasefinal.com so mesh clients resolve the internal-only wildcard | ||
|
|
9a1c028b9f | memory: ESH back on the Cityside static (confirmed four ways); lv-hemingway left as-is per operator | ||
|
|
3ac13c5351 | memory: lv-mccarthy D4 pairs built — 3,673 train / 269 val, the largest val fixture in the line | ||
|
|
370b16ca15 | memory: re-derive the shipped-corpora split-leak numbers with the committed gate | ||
|
|
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 |
||
|
|
328e9b1e56 | memory: snapshot — the mccarthy leak gate passed with five protagonist names in every copy, and the chain it happened on was unrecorded | ||
|
|
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.
|
||
|
|
4dce0d0a43 | memory: snapshot — lv-mccarthy through D3 on gx10, SFT pairs next (a mccarthy register must be written first) | ||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
a5ddfed81e | memory: snapshot — McCarthy and Krakauer D1 built, Krakauer's quotation scope is an open operator call | ||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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 (
|
||
|
|
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.
|
||
|
|
2e9b118e70 |
lv-bronte: the voice axis passes under the corrected floor rule — amended, not rewritten
lv-bronte shipped 2026-09-17 with a FAILED voice axis written into its compose comment,
its NFS README and its gate record. That verdict no longer stands, and this records the
correction in all three places without deleting what they said.
The floor rule is now pairwise (commit
|
||
|
|
dcc1abc7ea |
orientation: override the gitea NAME on the host, not each repo's remote
nh3-dev was reaching gitea over the public route from every repo on the box. brokkr-smithy-dev flagged it while pushing a new repo: brokkr-smithy, sleipnir, Galdrabok and kvasir all carried git@gitea.phasefinal.com remotes, and brokkr-smithy is pushed several times a week, so the fail2ban trigger this doc already warned about was live and recurring rather than dormant. Measured before changing anything, because the plausible explanation was a split-horizon rewrite making the public name internally correct: getent hosts gitea.phasefinal.com -> 38.120.12.44 (public, ana-srv1) grep -i gitea ~/.ssh/config -> nothing ssh -G git@gitea.phasefinal.com -> hostname gitea.phasefinal.com, port 22 No rewrite, no alias, no per-repo exception. A `Host gitea.phasefinal.com` block pointing at 10.250.50.70:222 now covers every repo on the box at once, which beats rewriting N remotes: it also catches repos nobody audited and fresh clones that copy the public URL out of a README, and nothing has to be remembered next time. Verified as a route change and not just a config edit: both paths already authenticated as `vh` with the same key, and `git ls-remote origin HEAD` succeeds over the alias in brokkr-smithy and in this repo. Backup at ~/.ssh/config.bak-20260917-020929. The alias is per-host; the doc now says to check `ssh -G` rather than assume another host inherits it. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
61840f3131 |
voices-seat: ship lv-bronte (ckpt475) with its failed voice axis on the record
lv-bronte is live on vllm-voices (fv-ml1 GPU0 :8027) alongside voices-base and lv-yarros. The seat lists all three; container healthy; GPU0 96092 -> 96090 MiB, so the adapter cost nothing measurable. IT DID NOT PASS ITS VOICE GATE, and the artifact says so in three places — this commit, a comment in the compose file, and a README beside the adapter on NFS — because an adapter found without its provenance will otherwise be read as a pass. VOICE FAIL +0.193 delta_cb vs base, against a 0.251 measured noise floor NOT COPIED PASS 8-gram hit-rate 0.00, longest 0 - identical to the control NO DAMAGE PASS ran-on +0.15 against a 0.400 floor Shipped on three grounds, none of them that the number was nearly good enough: it is additive (a named LoRA nobody reaches without asking for it), reversible (one compose line; hot-unload measures 0.003 s), and clean on the axis that carries actual risk - verbatim regurgitation of the source, on a public-domain corpus, measured against a positive control that saturates at 160. The voice result is UNDERPOWERED rather than absent: it closed 48% of the span from base to the same-author target and beat the control on every individual seed. The cause is structural - 81 val pairs against Hemingway's 200, from a 678k-word corpus against 994k - and neither more beats nor more seeds fixes it, because the floor is a range statistic and ranges widen with n. ckpt475 over ckpt925: indistinguishable on voice (0.017 apart), but ckpt925 has a verbatim 8-gram hit where this has none, and is 2.7x less stable seed-to-seed (0.251 vs 0.092) with a degeneracy probe showing no collapse to explain it. |
||
|
|
9b360e477d |
memory: lv-bronte gated — voice axis fails, ship decision open
Records the full v2 gate result and three findings that outlive the ship call: 1. The effect is UNDERPOWERED, not absent. Both candidates closed 48-52% of the achievable span to held-out Bronte and beat base on every individual seed, but the gaps sit under the measured floor. Sensitivity floor stated so the negative is falsifiable: cannot resolve better than ~0.251 delta_cb at 30 beats x 4 seeds. Cause is structural — 81 val pairs against Hemingway's 200, from a 678k-word corpus against 994k — and neither more beats nor more seeds fixes it. 2. A DEFECT IN THE v2 RULE. The floor is the largest within-arm spread across ALL arms, so adding a third noisier arm raised the bar that failed the clean one. Run as a two-arm gate the floor would have been 0.092 and the candidate would have cleared at 2.1x. Deliberately NOT exploited — choosing the floor that passes your preferred answer is the failure pre-registration exists to prevent — but the rule should state whether the floor spans the compared pair or every arm present. As written, a verdict depends on which other arms you happened to run. 3. The two-epochs-on-a-three-epoch-schedule recipe did NOT transfer. Bronte's two minima are 0.0022 apart against a 0.0046 jitter; epoch 2 buys nothing over epoch 1. The epoch-3 collapse (+0.075, ~16x jitter) is the only robust part. The outlier seed was diagnosed rather than waved away: a repeat-5gram degeneracy probe is uniform at 0.0078-0.0102 across every seed and both arms, so it is genuine delta_cb variance and the floor stands. |