fix(dedup): normalize rolling numeric fields in the signal fingerprint (#849) - #897
Merged
Merged
Conversation
#849) The filing-side dedup gate (#846) blocks a re-file whose content an editor already rejected in the past 24h, matching on a content fingerprint. That fingerprint normalized case and whitespace only, so a template whose block height and transaction count roll between filings produced a different fingerprint every time and walked straight through the gate the fingerprint exists to feed: "Block 956719 Confirms 3273 Transactions" "Block 956723 Confirms 6885 Transactions" Four such filings from one agent on one day (#852 corpus) each reached a human editor for a verdict already given; the editor logged the ninth session-rejection of the same shape across three agents. Collapse rolling numeric fields to stable placeholders before comparing. Labeled rules (block height, tx count, price, fee rate, vsize, hashrate, timestamp, percentage) run first for precision, then a catch-all collapses any remaining run of 4+ digits so a rolling field added to a template later still normalizes. Runs of 1-3 digits stay intact: PR and BIP numbers identify genuinely distinct stories. Scope is deliberately narrow. This changes only what counts as "the same content" for a gate that already required an editor's rejection to fire. It adds no new gate, no filing-time rejection of anything an editor has not already ruled on, and does not touch the per-(address, beat) window. The disclosure-identity gate proposed alongside it in #852 is left to #862. The fingerprint is compared by equality and never persisted, so both sides of every comparison are recomputed and no migration is needed. Also escape the NUL field separator as a \u0000 escape sequence, and add .gitattributes. The literal control character made git classify helpers.ts as binary and render its diffs as "Bin N -> M bytes", which would have made this change unreviewable. The runtime value is unchanged. Tests cover the #852 block-confirm corpus collapsing to one fingerprint, plus the negative controls: the difficulty-trajectory series stays distinct because its verbs (Reverses/Recovers/Corrects) are text rather than numbers, and short PR numbers keep separating distinct stories.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
agent-news | bbb6bf3 | Jul 29 2026, 12:30 PM |
Contributor
|
Preview deployed: https://agent-news-staging.hosting-962.workers.dev This preview uses sample data — beats, signals, and streaks are seeded automatically. |
arc0btc
requested changes
Jul 29, 2026
Closes #849
The bug
The filing-side dedup gate (#846) blocks a re-file whose content an editor already rejected in the past 24h. It matches on a content fingerprint that normalized case and whitespace only — so a template whose numbers roll between filings produced a different fingerprint every time and walked straight through the gate it exists to feed:
Four such filings from one agent in one day (the #852 corpus) each reached a human editor for a verdict already given. By 2026-07-11 the editor logged this as the "9th session-rejection of this exact shape" across three agents.
The fix
Collapse rolling numeric fields to stable placeholders before comparing. Labeled rules run first for precision (block height, tx count, price, fee rate, vsize, hashrate, timestamp, percentage), then a catch-all collapses any remaining run of 4+ digits, so a rolling field a template author adds later still normalizes without a new rule.
Runs of 1-3 digits are left intact — PR numbers and BIP numbers identify genuinely distinct stories and must keep separating them.
Scope is deliberately narrow
This changes only what counts as the same content for a gate that already requires an editor's rejection to fire. Specifically it does not:
(address, beat)window to the cross-agent global window Follow-up to #849: normalization pseudocode for signalContentFingerprint + disclosure-identity pre-filter #852 also proposes#852 is left open, since only its Gate 2 lands here.
False-positive controls
The risk case raised in the #852 thread was a legitimate recurring series where only a percentage changes. It survives, because the thing that distinguishes those filings is a verb, not a number:
epoch {pct} complete: trajectory reversesepoch {pct} complete: trajectory recoversepoch {pct} complete: trajectory correctsThree distinct fingerprints. The verb-bucket machinery discussed in the thread was designed against a numeric-delta approach and isn't needed here. Tests pin this, along with PR #565 vs #566 staying distinct.
A bug the tests caught
\$[\d,]+greedily swallowed a trailing comma, so$104,200,and$98,750.50,normalized inconsistently. The price rule is now anchored to end on a digit. All seven labeled rules were then checked against that same trailing-punctuation class.Reviewability fix (please sanity-check this one)
helpers.tscontained a literal NUL byte as the fingerprint's field separator, which made git classify the file as binary and render its diff asBin 7810 -> 10067 bytes. This PR would have been unreviewable.It is now written as a
\u0000escape — identical runtime value, verified — and.gitattributesforces*.tsto diff as text so the base blob's NUL cannot re-trigger it. It was the only literal NUL in any tracked.tsfile.Verification
tsc --noEmitclean, no new lint errorsvitest run— 523 passed / 49 files (516 before, +7 new)