Skip to content

fix(pipeline): four silent production failures — dead sources, notification storm, lobotomised reranker, lying metrics - #423

Merged
runyourempire merged 5 commits into
mainfrom
worktree-pipeline-silent-failures
Aug 14, 2026
Merged

fix(pipeline): four silent production failures — dead sources, notification storm, lobotomised reranker, lying metrics#423
runyourempire merged 5 commits into
mainfrom
worktree-pipeline-silent-failures

Conversation

@runyourempire

@runyourempire runyourempire commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Four independent failures found by reading the live app's debug console, then confirmed against the running process, the live SQLite DB, data/usage.json, and live HTTP calls to the failing endpoints. Every one of them was invisible in the logs — three reported success, one reported the wrong cause.

What was broken

1. Lobste.rs — 100% dead, zero rows ever ingested.
submitter_user changed upstream from {"username": "..."} to a bare string "fzakaria". The binding required the object form. #[serde(default)] did not help: the field is present with the wrong type, and default only covers absent fields. Because the batch decoded as Vec<LobstersStory>, one drifted record zeroed every fetch on both endpoints. The unit test kept passing the whole time — it asserted the stale shape. The DB confirms it: 19 source types present, lobsters is not one of them.

2. Stack Overflow — a self-inflicted 12.9h IP ban it could not escape.
Stack Exchange signals throttling as HTTP 400 with the reason in the body, not 429. The old code classified on status alone and returned before reading the body, so: the ban was logged as "Bad Request"; quota_remaining could never be re-read once throttled, so it never backed off, so the next cycle renewed the ban; and the backoff field returned on success had no binding at all. Reproduced independently outside the app:

HTTP 400 {"error_id":502,"error_name":"throttle_violation",
          "error_message":"too many requests from this IP,
                           more requests available in 46472 seconds"}

3. Chain notifications — 2,880 critical toasts/day.
maybe_notify_escalating_chains sat bare in the scheduler loop. Its comment said "hourly"; the loop ticks every 60 seconds. Every sibling job is interval-gated with a mark_job_complete; this one had no gate, no interval constant, no persisted state, and no gate_policy check — so it also ignored the battery/power throttle. Observed live at 01:57:29, 01:58:30, 01:59:28, 02:00:28, 02:01:28, 02:02:28, 02:03:29, 02:04:28Z, notified=2 every time. Critical toasts display for 8s, so one was on screen ~27% of all waking hours.

4. LLM rerank — dead 22.4 of every 24 hours, reported as success.
Five early-return paths all yielded a bare None, three with no log line at all; the caller printed "LLM rerank phase complete" elapsed_ms=0 regardless. Live data/usage.json at the time:

tokens_today      102677   limit 100000   EXCEEDED
cost_today_cents      51   limit     50   EXCEEDED

The budget resets 00:00Z and was exhausted by 01:36Z — the analysis loop runs every ~10.5min at ~11.4k tokens/rerank, so ~9 passes burned the day's allowance in 96 minutes.

5. Metrics that lie. coverage_pct=9.5 / not_scored=9171 claimed 9,171 items "were never scored". Checked against the live DB: all 10,172 items were scored and stamped at the current PIPELINE_VERSION. Real coverage was 100%. The line would send a reader hunting a recall crisis that does not exist.

What changed

Area Fix
Lobsters untagged enum accepts both shapes; per-record decode so one bad story costs one story, not the source; records-arrived-but-none-decoded is an error, not a silent empty feed; byte-faithful live-payload fixture pinned
Stack Overflow read the body before classifying; parse the deadline; process-global circuit breaker persisted atomically to data/.stackoverflow_throttle so all three pipeline drivers share it; clamp 24h, only ever extend; honour backoff; abort remaining tags (the ban is per-IP, not per-tag)
Chain notifications CHAIN_NOTIFY_INTERVAL=3600 + persisted chain_notify scheduler job + JobPriority::Normal gate; per-chain ledger re-arming on genuine phase change or after 24h
Rerank RerankOutcome::{Reranked, Skipped} with 9 tagged skip reasons carrying real numbers; all 3 call sites log the truth; budget paced proportionally across the UTC day
Metrics selection ratio relabelled as per-pass throughput, not coverage; summary logs scored/survivors/removed_by_dedup

Note on the rebase onto #421: that PR centralised status handling into sources::classify_http_status. Lobsters adopts it. Stack Overflow deliberately does not, and carries an in-line comment saying why — it is the one upstream where the status code does not carry the meaning, and centralising the call there reintroduces this exact bug.

Deliberately not done

Caching judgments so unchanged items aren't re-judged. That is the real ~10x token saving, but it rewires the calibration-sample and provenance write path — the same subsystem quarantined three days ago for a poisoned curve (AD-029). It deserves its own change with its own verification, not a rider on this one.

Verification

  • cargo clippy -- -D warnings clean on both CI matrix legs (default and --features experimental)
  • cargo fmt --check clean
  • full lib suite: 4,290 passed / 0 failed / 10 ignored (post-rebase)
  • 23 new tests, including negative cases: a genuine bad_parameter 400 must not arm the breaker; a shorter throttle must not shorten an active pause; pacing must have blocked the exact observed burn
  • CI green: Fresh clone ubuntu + windows, Rust default + experimental, Hermetic Success, Validate Success

Not yet live-verified in-app. Per .claude/rules/victauri-verification.md these changes still need a rebuild + restart and a Victauri pass before anyone calls them confirmed in production. The GUI process exited during this session, so that verification is outstanding.

cargo clippy --all-features fails on team_sync_crypto.rs / webhooks/mod.rs — pre-existing, untouched here, owned by the feature-rot lane. CI does not lint --all-features.

🤖 Generated with Claude Code

runyourempire and others added 4 commits August 14, 2026 17:13
… in production

Two source adapters were returning zero items on every cycle. Neither failure
was visible as a failure: one looked like a parse hiccup, the other like a
malformed query.

Lobste.rs — total blackout from a single field's type change.
`submitter_user` now arrives as a bare string ("fzakaria"); the binding
required an object ({"username": ...}). `#[serde(default)]` did not help:
the field is PRESENT with the wrong type, and `default` only covers ABSENT
fields. Because the whole batch decoded as `Vec<LobstersStory>`, one bad
record zeroed every fetch on both endpoints. The unit test kept passing
because it asserted the stale object shape — the suite was green while the
source had contributed zero rows to the corpus.
  - accept both shapes via an untagged enum
  - decode per-record so one drifted story costs one story, not the source
  - records-arrived-but-none-decoded is now an error, not a silent empty feed
  - pin a byte-faithful fixture of the live payload so drift fails the suite

Stack Overflow — self-inflicted IP ban it could never escape.
Stack Exchange signals throttling as HTTP 400 with the reason in the BODY,
not as 429. The old code returned on status alone, before the body was ever
read, so:
  - a 12.9h IP ban was logged as "Bad Request"
  - `quota_remaining` could never be re-read once throttled, so it never
    backed off, so the ban kept being renewed by the next cycle
  - the `backoff` field Stack Exchange returns on SUCCESS had no binding
    at all and was silently discarded
Live capture 2026-08-14:
  {"error_id":502,"error_name":"throttle_violation",
   "error_message":"too many requests from this IP,
                    more requests available in 46472 seconds"}
  - read the body before classifying; throttle != bad request
  - parse the deadline and arm a process-global circuit breaker (per-instance
    state forgets it instantly — a fresh source is built every cycle)
  - clamp to 24h, only ever extend, never shorten an active pause
  - honour `backoff` on the success path
  - abort the remaining tags on a throttle: it applies to the IP, not the tag
  - drop the unread `has_more`/`error_id` bindings (doctrine rule 8) rather
    than annotate them into permanence

Verified: cargo check clean; 6/6 lobsters tests, 10/10 stackoverflow tests,
including negative cases (a genuine bad_parameter 400 must NOT arm the
breaker) and clamp/monotonicity of the deadline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmAB6P1r22WBwdM6eUyGgk
…oasts/day

`maybe_notify_escalating_chains` sat BARE in the scheduler loop. Its own
comment said "hourly"; the loop ticks every 60 SECONDS. Every sibling job in
that loop is wrapped in `if now - last_X >= effective_interval(...)` with a
`mark_job_complete`. This one had no gate, no interval constant, no persisted
state, and no `gate_policy` check — so it also ignored the power/battery
throttle that governs everything else.

Measured on the live app before the fix (2026-08-14, ~11:57-12:04 local):
the sweep ran at 01:57:29, 01:58:30, 01:59:28, 02:00:28, 02:01:28, 02:02:28,
02:03:29, 02:04:28Z — once a minute, `notified=2` every time. That is two
`priority=critical` toasts per minute (~2,880/day), each displayed for 8s, so
a critical toast was on screen roughly 27% of all waking hours. Each sweep
also ran a full chain detection (topic extraction over ~900 items into ~2,210
topics) plus a `temporal_events` write.

Two layers, both required:
  - schedule gate: CHAIN_NOTIFY_INTERVAL = 3600, persisted via a new
    `chain_notify` scheduler_state job so a restart cannot reset the cadence
    to fire-immediately, and gated on JobPriority::Normal like its siblings
  - per-chain ledger: a chain sits in Escalating/Peak for DAYS, so an hourly
    sweep with no memory would still fire the identical toast 24x/day.
    Notifications now re-arm on a genuine phase change, or after 24h,
    whichever comes first. Entries older than 7d are pruned.

Neither `maybe_notify_escalating_chains` nor
`send_chain_prediction_notification` had any dedup, cooldown, or
already-notified check, and `notification_window::show_notification` calls
`window.show()` unconditionally — so nothing downstream was absorbing this.
The user's `notification_threshold` is `high_and_above`, so the `critical_only`
branch that would have masked non-security chains never engaged either.

Verified: cargo check clean; 4/4 new dedup tests (first-fire, repeat
suppressed, phase-change re-arm, timeout re-arm, cross-chain independence);
9/9 scheduler tests still green including jobs_are_lowercase_underscores,
which validates the new constant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmAB6P1r22WBwdM6eUyGgk
…dget

The reranker was dead for 22.4 of every 24 hours and the logs called it a
success.

`apply_llm_reranking` had FIVE early-return paths that all yielded a bare
`None`, three of them with no log line at all. The caller then printed
"LLM rerank phase complete elapsed_ms=0" regardless. On the live app
(2026-08-14T01:57Z) that line appeared while `data/usage.json` read:

    tokens_today       102677   limit 100000   EXCEEDED
    cost_today_cents       51   limit     50   EXCEEDED

so the "completed" phase had in fact returned at the first branch without
doing anything. `elapsed_ms=0` was the only tell.

Honesty:
  - `apply_llm_reranking` now returns `RerankOutcome::{Reranked, Skipped}`
  - nine distinct `RerankSkip` variants, each with a stable machine-readable
    tag and a detail string carrying the actual numbers, so budget exhaustion
    is legible in the log instead of requiring a trip to usage.json
  - all three call sites (cached_full, differential, deep_scan) log the real
    outcome; a skip is now a WARN that states items carry pipeline scores only
  - `is_rerank_enabled()` and `within_daily_limits()` are no longer collapsed
    into one boolean, so "disabled" and "out of budget" are distinguishable

Pacing:
The budget resets at 00:00Z and was being consumed as fast as the scheduler
could spend it — the analysis loop runs every ~10.5min at ~11.4k tokens per
rerank, so ~9 passes exhausted the day's allowance by 01:36Z. That is not a
budget, it is a 96-minute sprint followed by 22 hours of silent degradation.
`budget_allowance_by_now` releases the daily limit proportionally to elapsed
UTC time (plus 5% up-front headroom so the first pass after the rollover can
still run), converting "9 passes clustered at dawn" into "~9 passes spread
across the day". Zero limit still means unlimited.

Deliberately NOT done here: caching judgments so unchanged items are not
re-judged. That is the real 10x saving, but it rewires the calibration-sample
and provenance write path — the same subsystem quarantined three days ago for
a poisoned curve (AD-029). It needs its own change with its own verification,
not a rider on this one.

Also fixed, same theme — metrics that lie (scoring/analyzer.rs):
  - "Pre-score coverage — items not selected this pass were never scored"
    reported coverage_pct=9.5 / not_scored=9171. Checked against the live DB:
    all 10,172 items were scored AND stamped at the current PIPELINE_VERSION,
    so true coverage was 100%. Unselected items keep an earlier pass's score;
    the old line would send a reader hunting a recall crisis that isn't there.
    Relabelled to what it actually measures (per-pass selector throughput).
  - "Cache analysis summary" reported only the post-dedup survivor count next
    to a rejection rate, reading as "we scored 654" when the scorer saw 961.
    Now logs scored / survivors / removed_by_dedup. The persisted
    `total_scored` column keeps its existing meaning on purpose: 179 historical
    rows were written under it and redefining the denominator would make every
    stored rejection_rate incomparable with its own history. Documented in situ.

Verified: cargo clippy clean on BOTH CI matrix legs (default and
--features experimental) with -D warnings; cargo fmt clean; full lib suite
4460 passed / 0 failed / 10 ignored, including 8 new pacing+skip tests, one
of which asserts the pacing would have blocked the exact observed burn.

Note: `cargo clippy --all-features` fails on team_sync_crypto.rs and
webhooks/mod.rs. Those are pre-existing, untouched by this change, and owned
by the `feature-rot` lane (fix/feature-gated-build-rot). CI does not lint
--all-features.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmAB6P1r22WBwdM6eUyGgk
…sses

The circuit breaker landed in c4ebddb is per-process, but three processes
drive this pipeline against one data dir on this machine:

  1. the GUI (`fourda.exe`, long-lived)
  2. `4DA Background Refresh` -> `fourda.exe --engine-once`, repetition PT30M
  3. `4DA-Ledger-Cycle` -> node run-cycle.mjs -> `fourda-engine.exe --once`

Stack Exchange throttles by IP, so the ban is shared but the knowledge of it
was not: each short-lived `--once` process started with an empty breaker and
had to spend a request to rediscover the ban — and every such request can
push the deadline further out. With 4 tags per cycle across three drivers the
combined volume structurally exceeds the unauthenticated 300/day quota, which
is how the IP ended up 12.9 hours deep in `throttle_violation`.

The deadline now persists to `data/.stackoverflow_throttle`, so a ban learned
by any process is honoured by all. Fail-soft throughout: a missing, garbage,
or unreadable file simply means "no known throttle", never an error. Only ever
extends, matching the in-memory semantics.

Persistence is compiled out under cfg(test) so the unit tests exercise the
in-memory logic hermetically and never touch a real data directory.

Verified: 10/10 stackoverflow tests still green; clippy -D warnings clean;
gitignore entry confirmed with git check-ignore.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmAB6P1r22WBwdM6eUyGgk
@runyourempire
runyourempire force-pushed the worktree-pipeline-silent-failures branch from 3526aa0 to 5a4ab8a Compare August 14, 2026 07:23
Self-audit follow-up to 5a4ab8a. Three processes can arm the breaker
concurrently and `fs::write` is not atomic, so a reader could observe a
half-written file. That parses as garbage, is treated as "no throttle"
(fail-soft), and costs a wasted request straight into an active ban — a small
hole, but exactly the class of thing this change exists to close.

Write to `<file>.tmp<pid>` then rename: a reader now sees either the previous
deadline or the new one, never a torn value. Rename failure cleans up the temp
file rather than leaving litter. Gitignore widened to the glob so the temp
files cannot be staged.

Verified: clippy -D warnings clean; 10/10 stackoverflow tests; both marker
paths confirmed ignored via git check-ignore.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmAB6P1r22WBwdM6eUyGgk
@runyourempire
runyourempire merged commit a6ece84 into main Aug 14, 2026
10 checks passed
@runyourempire
runyourempire deleted the worktree-pipeline-silent-failures branch August 14, 2026 12:55
runyourempire added a commit that referenced this pull request Aug 14, 2026
…mmit gate (fleet cannot commit) (#430)

## Why this is urgent

`src-tauri/src/analysis_rerank.rs` landed at **1032 lines** in #423,
over the **1000-line hard error threshold** in
`scripts/check-file-sizes.cjs`. That script exits `1`, and
`.husky/pre-commit:38-41` treats a non-zero exit as blocking:

```sh
node scripts/check-file-sizes.cjs || {
    echo "File size check failed. Split large files or add justified exceptions."
    exit 1
}
```

**Every developer on this repo currently cannot commit anything
locally.** This PR restores that.

### How it got in

#423 was Rust-only, so CI's `Frontend` job — the only place the
file-size check runs — was skipped by the path filter and the gate never
fired. The CI path-filter fix is a **separate change owned by another
agent**; this PR deliberately does not touch `.github/workflows/`.

## What this does

Option A (split), not an exception-list entry. The file had a
self-contained 169-line inline `#[cfg(test)] mod rerank_breaker_tests`
block at the bottom, which is the cleanest possible seam — it touches
**zero production logic**.

Moved it into `src-tauri/src/analysis_rerank_tests.rs`, declared with:

```rust
#[cfg(test)]
#[path = "analysis_rerank_tests.rs"]
mod rerank_breaker_tests;
```

This is the established pattern in this crate — **45 other modules**
already use `#[cfg(test)] #[path = "*_tests.rs"] mod ...;` with the test
file holding the module body directly (e.g. `adversarial.rs:495-497`).
Test files get 2x the size limit, so the extracted file sits well inside
it.

| File | Before | After | Status |
|---|---|---|---|
| `analysis_rerank.rs` | 1032 | **866** | warn only (non-blocking) |
| `analysis_rerank_tests.rs` | — | 166 | test file, exempt from warnings
|

The module name is unchanged, so test paths stay
`analysis_rerank::rerank_breaker_tests::*` — no test-name churn.

## Verification

Gate, before and after:

```
# before
1 file(s) exceed error threshold. Split large files or add justified exceptions.
EXIT=1

# after
43 file(s) approaching size limits (warnings only).
EXIT=0
```

| Check | Result |
|---|---|
| `node scripts/check-file-sizes.cjs` | **exit 0** (was exit 1) |
| `cargo fmt --check` | clean |
| `cargo clippy -- -D warnings` (default) | clean |
| `cargo clippy --features experimental -- -D warnings` | clean |
| `cargo test --lib` | **4290 passed, 0 failed, 10 ignored** |

The test count is **identical** to the pre-change baseline measured on
the same tree. Beyond the count, a full test-**name** set diff between
the two runs shows **4300 unique names on both sides, 0 added, 0
removed** — proving nothing was orphaned by the module move (a missed
`#[path]` declaration would silently drop tests, which a passing run
alone would not catch).

The production diff is a single hunk starting at line 862; everything
above it is byte-for-byte untouched. The extracted body was verified as
a byte-exact dedent of the original, with the only difference being
rustfmt's re-wrap of the `use super::{...}` block at the reduced indent
level.

## Notes for the reviewer

- Rebased onto latest `main` (`4e8e197b`, #428). No overlap — #428 does
not touch `analysis_rerank.rs`.
- Local pre-commit and pre-push hooks both ran in full and passed. No
`--no-verify` was used anywhere. (Hooks were invoked via `git -c
core.hooksPath=.husky ...` so the worktree ran **its own tracked hooks**
rather than the shared tree's `core.hooksPath` absolute path — that is
the correct branch's gate, not a bypass.)
- `cargo audit` reports a pre-existing `RUSTSEC-2026-0221`
(`event-listener` unsound) on main; unrelated to this change and
warn-only in the hook.
- The exception list in `scripts/check-file-sizes.cjs` was **not**
touched. Worth a separate cleanup pass: it reportedly carries 5 stale
entries for deleted files and 3 with false justifications.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01AUeKTKwNmdow8yUk3q8RB2

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
runyourempire added a commit that referenced this pull request Aug 14, 2026
Today's outage made this concrete. #423 was a Rust-only PR, so the path filter
skipped its Frontend job — and `scripts/check-file-sizes.cjs --ci` ran ONLY
inside that job, even though it scans BOTH trees (SCAN_DIRS = ['src',
'src-tauri/src']). A 1032-line src-tauri/src/analysis_rerank.rs merged past the
1000-line hard error threshold with the gate never executing. check-file-sizes
then exited 1 on main, and because .husky/pre-commit treats that as blocking,
every developer in the fleet was unable to commit until #430 landed.

A gate that guards Rust files must not be reachable only through a filter that
excludes Rust.

Adds a `repo-guards` job with NO path filter and NO `needs:`, so it runs on
every pull request and dispatch whatever changed. It carries check-file-sizes,
check-no-window-spawns, check-release-channel and the guard self-tests, which
are removed from `frontend` — they were never frontend-specific. It needs no
pnpm install (all three guards use only node builtins) and runs hosted in ~40s.

It is in `validate-success`'s needs, so it actually gates the merge: since it
never skips, it is the only leg guaranteed to have run.

Also folds in `pnpm run test:scripts` — 53 tests across 6 files that verify the
guards still detect what they claim, and which previously executed in no hook
and no workflow. package.json is deliberately untouched: a peer holds a claim
on it and #418 edits the same line, and wiring it here achieves the same goal.

Rust job timeout 30 -> 45: that job now compiles the integration test targets,
and Swatinem's cache is only saved from main, so until this lands every PR run
pays a cold link for 5 extra binaries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUeKTKwNmdow8yUk3q8RB2
runyourempire added a commit that referenced this pull request Aug 14, 2026
… email (#427)

## What does this PR do?

Closes an unauthenticated licence-key disclosure in the live Signal
licence endpoint, without breaking licence recovery for real customers.

> **Relationship to #421:** #421 deleted the *stale Vercel duplicate* of
this handler (`site/api/streets/activate.js`) as part of its dead-code
sweep. It did not touch the live Cloudflare Pages handler, which is
where the vulnerability actually lives — `git log` shows
`site/functions/api/streets/activate.js` last changed in #357. This PR
is rebased on top of #421 and #423 and fixes the live one.

### The vulnerability

`site/functions/api/streets/activate.js` serves two GET paths. The
`session_id` path is correct: a Stripe checkout session id is
high-entropy, unguessable, and re-verified against Stripe, so holding
one is proof of purchase.

The `email` path was not. It took the address straight off the query
string, looked the customer up, and returned that customer's full
licence key in the response body — no authentication, no proof the
caller owned the address, nothing:

```js
// site/functions/api/streets/activate.js:373-406 (before)
} else { customerEmail = email; }        // caller-supplied, never verified
const customers = await stripe.customers.list({ email: customerEmail.toLowerCase(), limit: 1 });
return json({ license_key: license, tier, issued_at, expires_at, status }, 200, headers);
```

Impact, in order of severity:

1. **Anyone who knows or guesses a customer's email gets a working
licence.** The keys are Ed25519-signed and verified **offline** against
the public key embedded in the desktop app, so a stolen key keeps
working indefinitely — there is no revocation channel that could take it
back.
2. **Customer-list oracle.** `200` vs `404` answered "is this person a
paying 4DA subscriber?" for any address the caller cared to try.
3. **Nothing stood in front of it.** CORS is not an access control — a
plain HTTP client sends no `Origin` header at all. There is no
`_middleware.js`, no Turnstile, and `site/wrangler.toml` declares zero
KV/D1 bindings, so no rate limiter existed or could have existed without
new infrastructure.

### The fix

The `email` path now **never puts the key on the wire to an unverified
caller**. It mails the key to the address on file and returns the same
`202` either way.

- **`site/functions/api/streets/activate.js:356-470`** — the GET handler
splits into two named functions with explicitly different trust
properties. `handleSessionLookup` is byte-for-byte the old verified
behaviour (untouched on purpose). `handleEmailRecovery` validates the
address shape, checks that outbound mail is provisioned, then **responds
before doing the Stripe lookup** — the lookup and send are scheduled on
`waitUntil`. That makes the response constant in *both* body and
latency, so the oracle is closed on every path rather than just the
obvious one.
- **`site/lib/recovery-email.js`** (new) — Resend delivery via raw
`fetch`, the same provider and pattern
`paddle-webhook/api/paddle.ts:363` already uses; no new dependency. It
only ever mails an address that is **already a Stripe customer holding a
licence**, so the endpoint cannot be turned into an open relay against
arbitrary third parties. Expired licences get a "renew at 4da.ai/signal"
notice instead of a key. It never throws and never returns the key to
the caller.
- **Honest degradation.** Delivery needs `RESEND_API_KEY` and
`RESEND_FROM_EMAIL` in the Cloudflare Pages environment (see operator
actions below). If they are unset, the endpoint returns `503` with
"contact support@4da.ai from your purchase email" — uniformly, before
any Stripe call. It does **not** fall back to returning the key; that
fallback is the vulnerability.
- **`json()` now sets `Cache-Control: no-store`** so no browser, proxy
or CDN retains a key-bearing response from the `session_id` path either.

### Consumers updated to the new contract

Legitimate recovery had three consumers and all three were carried
across rather than broken:

- **`src-tauri/src/settings_commands_license.rs:326-443`** —
`recover_license_by_email` now handles `202` (`reason: "emailed"`),
`400` and `503`. It no longer auto-activates, because the server no
longer hands it a key; the doc comment explains why the 200 arm is now
unreachable.
- **`src/components/settings/LicenseSection.tsx`**,
**`src/components/LicenseRecoveryBanner.tsx`** — `emailed` renders as an
informational (gold) state, not a red error. It is the success case.
- **`src/locales/*/ui.json`** — 3 new keys across all 13 locales, plus
updated recovery copy that says the key is emailed and is never shown
in-app.
- **`site/src/signal/success.njk`** — the public form's button is now
"Email me my key", with a note explaining why, and it handles
`202`/`503`.

### Privacy disclosure

This change introduces **Resend** as a processor of a customer's email
address and licence key, so it is added to the third-party tables in
`site/src/privacy.njk` and `docs/legal/PRIVACY-POLICY.md`. #421 had
already corrected the Vercel→Cloudflare drift in both, so that part is
not re-done here.

## Type of change

- [x] Bug fix (security)

## Checklist

- [x] `pnpm run lint` — 0 errors
- [x] `tsc --noEmit` — clean
- [x] `pnpm run test` — 1362 tests / 125 files pass
- [x] `cargo fmt --check` and `cargo clippy --lib` — clean
- [x] `node scripts/validate-translations.cjs` — 0 errors
- [x] `node scripts/check-file-sizes.cjs` — passes (see the note below
about #423)
- [x] `npx @11ty/eleventy` — site builds
- [x] No secrets or API keys committed

## Testing

**Live-verified** against `wrangler pages dev` with throwaway
credentials (a fake Stripe key is sufficient — the whole point is that
the response does not depend on the lookup):

| Request | Result |
|---|---|
| `?email=` a plausible address | `202`, body
`{"delivery":"email","message":"If that address has a 4DA licence…"}` |
| `?email=` a different address | `202`, **byte-identical body** |
| `?email=` malformed | `400`, rejected on shape alone |
| no parameters | `400` |
| any of the above | `Cache-Control: no-store`, no `license_key` field
anywhere |

`site/test-e2e-stripe.mjs` previously *asserted the vulnerable
behaviour* (`data.license_key === licenseKey`). Those assertions are now
regression tests for the fix, covering the two properties that matter:

- the email path's response contains **no** licence key and no
`license_key` field, in the live, cancelled and expired cases;
- a known customer address and a random non-existent address produce a
**byte-identical body and identical status**, so there is no customer
oracle.

That script needs live Stripe test keys, so it is for the operator to
run against a preview deployment — it is not wired into CI.

## Operator actions required before this is fully live

1. **Set `RESEND_API_KEY` and `RESEND_FROM_EMAIL`** (e.g. `4DA
<licenses@4da.ai>`, on a Resend-verified domain) in Cloudflare Pages →
`4da-site` → Settings → Environment variables. Until they are set, email
recovery honestly returns "contact support" instead of working.
2. **Deploy.** Cloudflare is direct-upload, not git-auto-deploy —
merging does not ship this. It needs `wrangler pages deploy`.
3. **Confirm the dormant Vercel project `4da-home` is not still serving
the old copy.** #421 removed `site/api/**` from the repo, but if that
project still has a live deployment on a `*.vercel.app` URL with
`STRIPE_SECRET_KEY` and `LICENSE_PRIVATE_KEY_HEX` populated, the old
vulnerable endpoint is still reachable there regardless of what the repo
says.
4. **Consider rotating `LICENSE_PRIVATE_KEY_HEX`** if there is reason to
think keys were harvested while the endpoint was open. This invalidates
every issued key and forces re-issue, so it is a judgement call, not
automatic.

## Note for the #423 author — file-size gate was left red

`src-tauri/src/analysis_rerank.rs` went from 739 → **1032 lines** in
#423, past the 1000-line hard error in `scripts/check-file-sizes.cjs`,
with no exception entry added. That left `main` red on the gate and
blocked *every* local commit via the pre-commit hook. I added an
explicitly **TEMPORARY** entry so this PR could be committed at all,
with a comment naming #423 as owing the split and instructing that the
line be deleted afterwards. It is not a blessing to keep growing the
file — please split the reranker and remove the entry.

## Residual risk, deliberately not fixed here

- **No rate limiting.** The email path is still unauthenticated and
unmetered, so it can be driven in a loop to repeatedly mail an existing
customer their own key. It cannot mail anyone who is *not* already a
customer, which bounds this to inbox nuisance rather than open-relay
abuse. A real limiter needs per-IP counters and this Pages project
declares no KV or D1 binding to hold them. The zero-code mitigation is a
**Cloudflare WAF rate-limiting rule on `/api/streets/activate`**,
configurable from the dashboard. This is noted in a comment at the top
of the email path.
- **The address still travels in a query string**, so it lands in
Cloudflare access logs and browser history. Moving recovery to `POST`
would fix that but would break already-shipped desktop builds that call
`GET`, so the compatible shape was kept.
- **Stripe is still absent from both privacy processor tables**, even
though it has processed Signal subscriptions for some time. That is a
pre-existing gap, not one this change introduces, so it is flagged
rather than silently rewritten — legal copy should be your call.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01AUeKTKwNmdow8yUk3q8RB2

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
runyourempire added a commit that referenced this pull request Aug 14, 2026
…ncovered paths (#429)

## What this fixes

Four CI gates reported success without doing the work they claim. Each
was verified against live run data before anything was changed.

### 1. The integration tests had never run in CI — on any workflow

Every `cargo test` in `.github/workflows/` was `--lib`-scoped
(`validate.yml:303/308`, `hermetic.yml:167/176`). No `--tests`, no
`--all-targets`. So **all 154 real integration tests in
`src-tauri/tests/` had never executed in CI**, including all 12
migration tests and the repo's only forward-migration coverage — against
a `TARGET_VERSION = 103` migration chain that has no checksums and no
downgrade path.

**I measured before changing anything.** Full `cargo test --tests` run
on this branch's base (`a6ece843`), isolated data dir, exit code 0:

| Target | Result |
|---|---|
| lib unittests | **4,290 passed**, 0 failed, 10 ignored |
| `4da` (cli bin) | 13 passed, 0 failed |
| `fourda` / `fourda-engine` bins | 0 tests each |
| `migration_tests` | **12 passed**, 0 failed |
| `pipeline_integration` | **13 passed**, 0 failed |
| `source_resilience` | **5 passed**, 0 failed |
| `stack_simulation` | **124 passed**, 0 failed |
| `victauri_dogfood` | 157 passed, 3 ignored (self-skips without
`VICTAURI_E2E=1`) |
| **Total** | **4,614 passed across 9 binaries, 0 failed** |

**Nothing was broken and nothing had to be excluded.** They are also
hermetic by construction, which I verified rather than assumed:
`pipeline_integration` uses an in-memory DB (`test_utils::test_db()` →
`:memory:`) and `migration_tests` uses `tempfile::tempdir()`. Both
workflows now run `--tests` (lib + bins + integration) under the same
throwaway-data-dir isolation `hermetic.yml` already used.

Two follow-on fixes were required to avoid landing a red gate:
- The count floor took `tail -1` of the `test result:` lines. With
`--tests` there are **9** test binaries, so it would have read the
*last* binary's total (157) and tripped the 2000 floor on every run. It
now **sums** all binaries.
- A new assertion fails if fewer than 5 test binaries report — so if
this is ever re-scoped to `--lib`, it fails loudly instead of silently
dropping the integration suite again.

> ⚠️ **Non-obvious trap for reviewers:** the isolation directory name
must keep containing the substring `data`.
`src/state.rs::test_get_db_path_points_to_data_dir` asserts the resolved
DB path contains `"data"`. Pointing `FOURDA_DATA_DIR` at e.g.
`/tmp/4da-hermetic` makes that lib test fail; `…/4da-hermetic-data`
passes. I hit this during measurement. Both call sites are commented.

### 2. The hermetic fresh-clone canary never ran outside PRs

`fresh-clone` needs the PR-only `changes` job. GitHub skips any job
whose `needs` was skipped **unless its `if:` contains a status
function** — and `hermetic.yml:94` had none. So push-to-main, the
nightly cron and manual dispatch all skipped the clone and reported
success:

| Trigger | Duration | Result |
|---|---|---|
| push → main (08-14 03:52) | **9s** | "success" |
| push → main (08-13 16:42) | **7s** | "success" |
| schedule (08-13 08:21) | **7s** | "success" |
| pull_request (real work) | ~19min | success |

The file's own comment at `:56-60` claimed these paths "ALWAYS run the
full canary". **The nightly cron had never built a single fresh clone.**
Fixed with `!cancelled()` — not `always()`, so a cancelled run doesn't
spawn a 45-minute cold build.

The **identical defect** silently disabled `workflow_dispatch` for
Frontend, MCP Server and the entire Rust matrix in `validate.yml`: the
`github.event_name == 'workflow_dispatch'` clause on those three jobs
had never once fired, while `Validate Success` (`if: always()`) still
went green. Same fix.

### 3. A path-filter hole took the whole fleet down today

This stopped being hypothetical while this PR was being written:

- **#423 was a Rust-only PR.** Its `Frontend` job was skipped by the
path filter.
- `scripts/check-file-sizes.cjs --ci` ran **only inside the Frontend
job** — but it scans `SCAN_DIRS = ['src', 'src-tauri/src']`, i.e. it
guards Rust files too.
- So #423 merged a **1032-line `src-tauri/src/analysis_rerank.rs`** past
the 1000-line hard error threshold, with the gate never executing.
- `check-file-sizes.cjs` then exited 1 on `main`, and
`.husky/pre-commit:38-41` treats that as blocking — **every developer in
the fleet was unable to commit.**

A gate that guards Rust files must not be reachable only through a
filter that excludes Rust. This PR adds a **`repo-guards` job with no
path filter and no `needs:`** — it runs on every PR and dispatch, and
carries `check-file-sizes`, `check-no-window-spawns`,
`check-release-channel` and the guard self-tests. They are removed from
`Frontend` (they were never frontend-specific). Hosted, ~40s, no `pnpm
install` needed: all three guards use only node builtins.

It is also in `validate-success`'s `needs`, so it actually gates the
merge — `repo-guards` is now the only leg guaranteed to have run.

**Additionally, "no filter matched" now means RUN, not PASS.** `site/`,
`paddle-webhook/`, `mcp-memory-server/`, `editors/vscode/` and `.husky/`
matched no filter, while `Validate Success` is the only required check
and auto-merge is enabled repo-wide — so a Dependabot bump into the
payment webhook, or a PR weakening `.husky/` itself, could merge with
nothing run. Added `.github/**` and `.husky/**` explicitly, plus an
`uncovered` fail-safe filter that catches anything unrecognised
**including directories added in future**.

> The `uncovered` filter uses `predicate-quantifier: 'every'`, which is
**required** — the default `'some'` ORs the patterns, and a list of
negations OR'd together matches every file. Verified against the
action's source at the pinned SHA (`src/filter.ts:110-113` →
`patterns.every(...)`; `MatchOptions = {dot: true}`, so `.husky/**`
matches).

### 4. The guards' own self-tests ran nowhere

`pnpm run test:scripts` (53 tests across 6 files) executed in **no hook
and no workflow** — nothing verified the guards still detect what they
claim. `pnpm run validate` isn't run by CI either (the Frontend job runs
its steps individually), so wiring it into `package.json` alone would
not have gated it. It is now a step in `repo-guards`.

## Deliberately NOT done

- **No branch-protection or ruleset change.** Making `Hermetic Success`
required is the correct end state — currently `Validate Success` is the
*only* required check in the active `main-protection` ruleset (verified
via the API; classic branch protection is disabled). But with ~30 open
PRs and Hermetic historically failing on #421, flipping it now would
block the fleet. **Recommended as an explicit follow-up** once this
lands and Hermetic is observed green on push-to-main for a few days —
which, note, is the first time that signal will ever have existed.
- **`package.json` untouched.** `test:scripts` was going to be added to
the `validate` chain, but a peer worktree agent holds a claim on that
file and #418 also edits that exact line. Wiring it into `repo-guards`
achieves the real goal (it now runs in CI) without touching the claimed
file.
- **`analysis_rerank.rs` / `check-file-sizes.cjs` untouched** — a
separate agent owns the immediate unblock. This PR fixes the structural
cause only.
- **No per-package jobs for `site/`, `paddle-webhook/`,
`mcp-memory-server/`, `editors/vscode/`.** The `uncovered` fail-safe
means they now trigger the generic gate instead of passing silently, but
that gate does not *build* them. Dedicated jobs are the right follow-up
and belong in their own PR.
- **Rust job timeout raised 30 → 45 min.** Not cosmetic: this job now
compiles the integration test targets, and Swatinem's cache is only
saved from `main`, so until this lands there every PR run pays a cold
link for 5 extra binaries. 30 was too tight for that first window, and a
timeout on a required gate is a red gate.

## Conflicts with open PRs

Checked `gh pr diff --name-only` on every PR touching these files:

| PR | Overlap | Notes |
|---|---|---|
| #387, #350 | none | Dependabot `actions/checkout` SHA pins only —
different lines |
| #388 | none | `taiki-e/install-action` SHA pin only |
| #424 | none | Adds 3 matrix legs at `validate.yml:236-255`; my edits
are at 301+ and inside `steps:`. I deliberately did **not** add a matrix
key — an integration-floor key would have had to be added to its new
legs. Verified `cargo test --tests --features experimental` compiles
clean, so its `test-floor: 0` compile-gate legs are unaffected. |
| #418 | **1 line** | Both edit `validate-success`'s `needs:`. #418 adds
`pr-metadata`, this adds `repo-guards`. Resolution is a union of the two
lists — whoever merges second takes both. Flagged rather than
pre-empted. |

`uncovered` was also deliberately placed *before* the main filter step,
to stay clear of the end-of-job boundary #418 inserts a job into.

## Live CI evidence from this PR's own run

The first run of this branch already proves the fix, on both platforms:

| Check | Result |
|---|---|
| Fresh clone (ubuntu-22.04) | **pass**, 12m22s |
| Fresh clone (windows-latest) | **pass**, 18m21s |
| Hermetic Success | **pass** |
| Rust (default) | **pass**, 12m41s |
| Rust (experimental) | **pass**, 12m02s |

The hermetic job log shows **all 9 test binaries executing on both
legs** — `migration_tests` 12, `pipeline_integration` 13,
`source_resilience` 5, `stack_simulation` 124, `victauri_dogfood` 157,
plus lib (4,290 windows / 4,284 ubuntu — a 6-test platform delta, far
above the 2000 floor) and the 3 bin targets. **Zero failures.** That is
the first time any of those integration tests has run in CI.

Rust finished in ~12min against the old 30min cap, so the 45min bump is
headroom for the first cold-cache window rather than a response to an
observed timeout.

## Verification

- Both workflows parse as YAML; all `if:` expressions and filter blocks
inspected post-rebase.
- `cargo test --tests` measured green in full **before** any workflow
edit, and **re-run green after** rebasing onto current `main` (table
above) — #421 removed ~54k lines and #423 changed pipeline code between
those two runs.
- `dorny/paths-filter` negation + `every` semantics confirmed from
source at the pinned SHA, not assumed.
- `check-no-window-spawns`, `check-release-channel`, `test:scripts` all
verified exit 0 locally; `check-file-sizes` correctly exits 1 (the live
outage above).
- Rebased onto latest `main`; only the two workflow files differ.

The `analysis_rerank.rs` unblock (#430) has landed, so `repo-guards`
passes; this branch is rebased on top of it.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
runyourempire added a commit that referenced this pull request Aug 15, 2026
…pply-chain blind spot (#433)

## The premise, verified first

An audit lane claimed the quick-xml suppression in `deny.toml:96-112` /
`.cargo/audit.toml:15-29` had gone stale. It rested on this
justification:

> "NO consumer in our tree has a released version against >=0.41 yet"

**Confirmed false.** Read straight out of the registry index
(`rust_version` and `deps` per published version):

| consumer | we had | latest | quick-xml req | zip req |
|---|---|---|---|---|
| `calamine` | **0.25.0** | 0.36.1 | `^0.31` → **`^0.41`** | `^1.0` →
`^8.6` |
| `docx-rs` | **0.4.20** | 0.4.22 | `^0.36` → **`^0.41`** (since 0.4.21)
| `^0.6.3` → `^8.6` |
| `plist` | **1.9.0** | 1.10.0 | `^0.39.2` → **`^0.41`** | — |

All three shipped support. The ignores were suppressing a live, fixable
advisory pair on a parser that reads **user-supplied `.xlsx` /
`.docx`**.

## What changed

**quick-xml (RUSTSEC-2026-0194 / -0195) — resolved, not re-justified.**
Bumping the three consumers collapses `quick-xml` **0.31.0 + 0.36.2 +
0.39.4 → a single 0.41.0**. Both advisories stop firing on their own, so
both ignores are **deleted** from `deny.toml` *and* `.cargo/audit.toml`
(they had diverged; both were checked). `RUSTSEC-2023-0071` (`rsa`) left
alone as instructed.

**`office.rs` needed no edit** — and that is a verified claim, not an
absence of errors:
- `sheet_names()` and `worksheet_range()` have byte-identical signatures
in 0.25 and 0.36.
- `Data` still has exactly the same nine variants with the same
payloads. `cell_to_string` matches it **exhaustively with no wildcard
arm**, so an added variant could not have compiled.
- `ExcelDateTime`'s `Display` impl is byte-identical (`write!(f, "{}",
self.value)`), so `DateTime` cells format the same.
- Same for `docx-rs`: `TableChild` / `TableRowChild` are destructured
irrefutably, so a new variant there could not have compiled either.

The documented decompression-bomb weakness (the 100 MB cap is on the
**compressed** size) is untouched — separate work, not regressed.

**zip — partial.** `zip 1.1.4` retired as hoped. **`zip 0.6.6` did not**
— it is our own direct `zip = "0.6"`, so retiring it is an 8-major API
migration across `osv/cache.rs`, `extractors/archive.rs` and
`embeddings_providers/fastembed.rs`. No advisory attaches to it, so it
is staleness, not exposure. Left as follow-up rather than smuggled into
a security PR. Tree is now `zip` 0.6.6 (ours) + 4.6.1
(tauri-plugin-updater) + 8.6.0 (calamine/docx-rs).

**`relay/` — 5 vulnerabilities → 0.** A TLS-terminating server with no
Dependabot entry, no cargo-audit, no CI.

| crate | change | advisory |
|---|---|---|
| `rustls-webpki` | 0.103.9 → **0.103.14** | RUSTSEC-2026-0049 / -0098 /
-0099 / -0104 (cert validation) |
| `spin` | 0.9.8 → **0.9.9** | 0.9.8 was **yanked** |
| `anyhow` | 1.0.102 → 1.0.104 | RUSTSEC-2026-0190 |
| `event-listener` | 5.4.1 → 5.4.2 | RUSTSEC-2026-0221 |
| `rand` | 0.8.5 → 0.8.7 | RUSTSEC-2026-0097 |

`rsa 0.9.10` remains with no fix available, and is recorded in a new
`relay/.cargo/audit.toml` with evidence that it is **not in the build
graph**: it reaches `Cargo.lock` only via sqlx's optional `mysql`
backend, which relay never enables — `cargo tree -i rsa` and `cargo tree
-i sqlx-mysql` both report *nothing to print*.

**Coverage, so it stops recurring.** `dependabot.yml` gains a `cargo`
entry for `/relay` (not a `src-tauri` workspace member, so the existing
entry never saw it), and `nightly-audit.yml`'s cargo-audit step now
loops every `Cargo.lock` in the repo. **Workflow footprint is
deliberately limited to those two files** — `validate.yml` is being
reshaped by peer PRs and is untouched here.

**`relay/Dockerfile`.** `cargo build --release --locked 2>/dev/null ||
cargo build --release` silently dropped lockfile enforcement and
swallowed the reason. Fallback removed. Its base image also had to move
**1.82 → 1.95**: the lockfile already required 1.88 via `time 0.3.47`
(`jsonwebtoken` → `simple_asn1`), so that image could not have built
this crate at all — the fallback was hiding a hard failure, not
surviving a soft one.

## Two things found on the way

**1. `main` was un-committable — independently confirmed, now fixed by
#430.** `scripts/check-file-sizes.cjs` exits 1 on
`src-tauri/src/analysis_rerank.rs` (**1032 lines against a 1000 hard
limit**, arrived with #423). The gate scans the whole repo rather than
staged paths, so `.husky/pre-commit` failed for *every* terminal on
*every* commit — including this one. I hit it, diagnosed it, and fixed
it the same way a peer did in **#430** (lift the test module into a
sibling `analysis_rerank_tests.rs` via `#[path]`, 1032 → 866). #430
landed first, so **that commit has been dropped from this branch by
rebase** — this PR now contains only the dependency work. Recording it
here as an independent second confirmation of both the diagnosis and the
chosen fix.

**2. `cargo clippy --all-targets -- -D warnings` does not pass on
`main`** (255 pre-existing errors at my branch point, ~all
`unwrap_used`/`expect_used` in test code). This is **not** the gate — CI
runs `cargo clippy ${{ matrix.cargo-features }} -- -D warnings`
*without* `--all-targets`, so the numbers below are from the
CI-equivalent invocation. Reported as an observation, not touched.

## Verification

| check | result |
|---|---|
| `src-tauri` `cargo audit` | **exit 0** — zero vulnerabilities, zero
warnings |
| `src-tauri` `cargo deny check` | **exit 0** — `advisories ok, bans ok,
licenses ok, sources ok` |
| `relay` `cargo audit` | **exit 0** (was 5 vulns + 4 warnings + 1
yanked) |
| `relay` `cargo check --locked --all-targets` | clean |
| `cargo clippy -- -D warnings` (CI-equivalent, default) | **exit 0** |
| `cargo clippy --features experimental -- -D warnings` | **exit 0** |
| `cargo fmt --check` | **exit 0** |
| `cargo test --lib` | **4300 passed, 0 failed, 10 ignored** |

All re-run after rebasing onto `c1fd348c` (#425, #426, #427, #429, #430,
#431 all landed mid-flight).

`--features team-sync` and `--features enterprise` fail to compile —
**pre-existing rot on `main`** (`chacha20poly1305::aead::OsRng`
unresolved, then cascading `__cmd__*` macro failures), which is what
#424 exists to repair. My lockfile diff touches no crypto crate. #424 is
still open as of this push, and the CI clippy matrix on `main` still
carries only the `default` and `experimental` legs — so the two legs
verified above are exactly the gate.

### The extractor tests were `#[ignore]`d and had never run

There are no `.xlsx`/`.docx` fixtures anywhere in the repo, so
`test_real_docx_extraction` / `test_real_xlsx_extraction` were no-ops
that returned early. To gain real confidence in an 11-minor-version
parser bump I generated **real OOXML documents** — shared strings, an
inline string, numeric and boolean cells, paragraphs and a table —
confirmed both `#[ignore]`d tests pass against them, and separately
asserted the extracted text matches the pre-bump formatting contract
exactly:

```
=== Sheet: Budget ===        Hello from 4DA
Item | Cost                  Second paragraph
Widget | 42                  A1 | B1
Gadget | 3.50 | TRUE
```

That exercises every arm of `cell_to_string` that a document can reach
(shared/inline string, integral float → `42`, fractional float → `3.50`,
bool → `TRUE`) plus the docx paragraph and table paths. The scratch
harness was deleted; **no test-file changes ship in this PR**.

## Deliberately left

- **`zip 0.6.6`** — direct dep, 8-major API migration, no advisory.
Follow-up.
- **`office.rs` decompression bomb** — the 100 MB cap is on the
compressed size. Out of scope, not regressed.
- **`--all-targets` clippy backlog** — pre-existing, not the CI gate.
- **`validate.yml`** — peer-owned right now, untouched on purpose.
- **`--features team-sync` / `enterprise`** — pre-existing rot, #424's
job.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
runyourempire added a commit that referenced this pull request Aug 15, 2026
…eal warnings (#438)

## Why

`EXCEPTIONS` in `scripts/check-file-sizes.cjs` is consulted **before**
any size comparison:

```js
if (EXCEPTIONS[normalized]) continue;
```

So an entry suppresses the **warn** tier as well as the **error** tier.
An entry for a file that is no longer over the *error* threshold is not
a harmless leftover — it silently hides a legitimate warning, and it
disarms the hard limit on a file that may keep growing.

Seven entries were in that state. All line counts below were measured
with the gate's **own** `countLines()`, not `wc`.

## Removed

| Entry | Lines | Warn | Error | Why it's stale |
|---|---:|---:|---:|---|
| `src-tauri/src/analysis_rerank.rs` | 866 | 700 | 1000 | Entry was
self-described `TEMPORARY`, owed a split back to #423. **#430
(`02c105d9`) did the split (1032 → 866) but only touched the two `.rs`
files — it never removed the exception it was owed.** |
| `src/components/preemption/PreemptionCard.tsx` | 388 | 350 | 500 |
Justification claimed *"9 lines over"*. It is 38 over **warn** and 112
**under** error — the stated reason was false. |
| `src/components/enterprise/SsoConfigPanel.tsx` | 355 | 350 | 500 |
Justification claimed *"5 lines over"*. 5 over **warn**, 145 under
error. |
| `src-tauri/src/scoring/pipeline_tests.rs` | *deleted* | 700 | 1000 |
File was **deleted in #421**. Same class as the five dead entries #421
already swept — it missed this one. |
| `src-tauri/src/settings/types.rs` | 908 | 700 | 1000 | Under error;
was hiding a warning. |
| `src/store/slice-types.ts` | 446 | 300 | 500 | Under error; was hiding
a warning. |
| `src-tauri/src/sources/adapter_resilience_tests.rs` | 1802 | *n/a* |
2000 | Test file, so warn-exempt — removing it changes no output today,
but it left the 2000-line hard limit silently unenforceable on a file
that is actively grown. |

## Kept

Every other entry is genuinely over its **error** threshold and is doing
its job — those were left alone.

One deliberate keep that looks like a miss:
**`src/types/i18n-resources.d.ts` does not resolve on disk**, but it is
gitignored and generated by `pnpm run i18n:types`, which `validate:all`
runs *before* this gate. That is already documented inline; the entry
stays.

`src-tauri/src/briefing_pipeline_tests.rs` is a different file from the
deleted `scoring/pipeline_tests.rs` and was never in the map.

## Verification

```
before:  42 file(s) approaching size limits (warnings only).   exit 0
after:   47 file(s) approaching size limits (warnings only).   exit 0
```

The 5 new warnings are exactly the suppressed files now reporting
honestly (`settings/types.rs` 908, `analysis_rerank.rs` 866,
`slice-types.ts` 446, `PreemptionCard.tsx` 388, `SsoConfigPanel.tsx`
355).

**No file crosses an ERROR threshold**, so the pre-commit gate cannot
block the fleet — the failure mode that took every developer offline on
2026-08-14 (#423#430). Every consumer of this script
(`.husky/pre-commit`, the `repo-guards` CI job, `build-guardian.cjs`,
`compound-quality-check.cjs`, `sentinel-scan.cjs`) keys on exit code or
`ERROR` lines only, so warnings are safe everywhere.

Also verified: after this change every remaining entry passes the "file
exists **and** is over its error threshold" test, except the documented
generated-file case above.

## Scope

`scripts/check-file-sizes.cjs` only — 12 deletions, no source file
touched, nothing split.

4 of the 7 (`pipeline_tests.rs`, `settings/types.rs`, `slice-types.ts`,
`adapter_resilience_tests.rs`) were found by auditing all 38 entries
rather than being named up front. Each is an independent line and can be
dropped in review without affecting the others.

## Hook note

This worktree has no `node_modules`, so `.husky/_` does not exist and
`core.hooksPath` resolves to nothing — git silently ran **no** hooks.
`--no-verify` was **not** used. The gates were run manually instead, all
green:

`check-file-sizes` · `check-doc-location` · `check-llm-gate-honesty` ·
`check-vanity-metrics` · `check-release-channel` · `i18n-guard` ·
`validate-boundary-calls` · `compound-quality-check` · push-range
`scan-secrets --diff-added` · `private-asset-guard`

Not runnable here (no `node_modules`): `tsc`, ESLint, and the frontend
suite. This change is a Node tooling script with no TypeScript or
frontend surface; CI's required gates cover them.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01AUeKTKwNmdow8yUk3q8RB2

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant