Skip to content

fix(ledger): signature hygiene — reject entity-name tokens, count only signable types - #4123

Merged
idoshamun merged 5 commits into
mainfrom
fix/ledger-signable-change-types
Aug 21, 2026
Merged

fix(ledger): signature hygiene — reject entity-name tokens, count only signable types#4123
idoshamun merged 5 commits into
mainfrom
fix/ledger-signable-change-types

Conversation

@idoshamun

@idoshamun idoshamun commented Aug 21, 2026

Copy link
Copy Markdown
Member

Two defects in the signature layer, both found while verifying the ledger is ready for the rot-bench eval.

1. Signature tokens that are just an entity name

extractClaimSignatures already dropped a token repeating the claim's own entity, because it matches every plan that mentions the technology rather than the change. The same argument applies to every other entity the ledger knows, and nothing enforced it.

Production carries 1,103 distinct signature tokens that are exactly an entity name (1,610 instances across 1,358 claims). The most repeated: Claude Code, Python 3.10, React 19, Redis, Docker, Node.js 18, GitHub Actions.

Tier A is meant to say your code touches the thing that changed. These say your plan mentions this technology — which the entity tiers already say, version-gated and therefore more precisely. The rule moves signal to a better tier rather than dropping it. Same failure as the generic word list one layer up: the token is specific, but still not a code surface. It is also the same shape as the 2026-08-20 incident, where unfiltered generic tokens gave rot-bench's first harness pilot 15 identical Tier A false findings.

  • New src/common/ledgerEntityNames.ts builds the prose-name set (canonical unless codeOnlyCanonical, plus aliases), cached an hour so it is one query per process.
  • codeOnlyAliases and code-only canonicals are excluded from the reject set — those ARE code tokens (browser_toolset_20260801).
  • The live extraction worker never applied the bar's entity half at all. It does now.

2. The unsigned-claims alert could never stop firing

ledger-hygiene counted every consumable claim with a NULL signaturesBackfilledAt against a threshold of 500, and read 34,320 in production. But 34,275 of those are release/new_capability rows the backfill is designed never to stamp (signatures make nothing stale for those types — measured at 0/45 and 1/49 fill). The count could not drop below the threshold no matter what anyone did, and an alert that always fires is the one a reader learns to skim.

The real number awaiting the pass was 45; they have since been stamped in production (45/45, 26 carrying tokens, 0 failed).

  • SIGNABLE_CHANGE_TYPES moves from bin/backfillClaimSignatures.ts into src/common/claimSignatures.ts so the counter and the backfill share one list.
  • Threshold 500 → 200, now that it measures a population that can shrink.

Follow-up

The 1,610 existing entity-name tokens are cleaned in production separately; this PR stops new ones being written.

The hygiene cron counted every consumable claim with a NULL
signaturesBackfilledAt against a threshold of 500, and read 34,320 in
production. 34,275 of those are release/new_capability rows the backfill
is designed never to stamp, so the count could not fall below the
threshold no matter what anyone did.

An alert that always fires is the one a reader learns to skim, which is
the failure this cron exists to prevent. The change-type list moves from
bin/backfillClaimSignatures.ts to src/common/claimSignatures.ts so the
counter and the backfill cannot drift apart. Real figure: 45.
@pulumi

pulumi Bot commented Aug 21, 2026

Copy link
Copy Markdown

🍹 The Update (preview) for dailydotdev/api/prod (at ee80dcf) was successful.

Resource Changes

    Name                                                    Type                           Operation
~   vpc-native-check-analytics-report-cron                  kubernetes:batch/v1:CronJob    update
~   vpc-native-update-views-cron                            kubernetes:batch/v1:CronJob    update
~   vpc-native-validate-active-users-cron                   kubernetes:batch/v1:CronJob    update
~   vpc-native-personalized-digest-cron                     kubernetes:batch/v1:CronJob    update
~   vpc-native-channel-digests-cron                         kubernetes:batch/v1:CronJob    update
~   vpc-native-user-profile-updated-sync-cron               kubernetes:batch/v1:CronJob    update
~   vpc-native-channel-highlights-cron                      kubernetes:batch/v1:CronJob    update
~   vpc-native-refresh-tool-stack-stats-cron                kubernetes:batch/v1:CronJob    update
~   vpc-native-clean-expired-better-auth-sessions-cron      kubernetes:batch/v1:CronJob    update
~   vpc-native-post-analytics-clickhouse-cron               kubernetes:batch/v1:CronJob    update
~   vpc-native-rotate-weekly-quests-cron                    kubernetes:batch/v1:CronJob    update
~   vpc-native-update-tag-materialized-views-cron           kubernetes:batch/v1:CronJob    update
~   vpc-native-personalized-digest-deployment               kubernetes:apps/v1:Deployment  update
~   vpc-native-post-lifecycle-state-clickhouse-cron         kubernetes:batch/v1:CronJob    update
+   vpc-native-api-clickhouse-migration-acf452dd            kubernetes:batch/v1:Job        create
~   vpc-native-clean-stale-user-transactions-cron           kubernetes:batch/v1:CronJob    update
~   vpc-native-ws-deployment                                kubernetes:apps/v1:Deployment  update
~   vpc-native-generic-referral-reminder-cron               kubernetes:batch/v1:CronJob    update
+   vpc-native-api-db-migration-acf452dd                    kubernetes:batch/v1:Job        create
~   vpc-native-update-trending-cron                         kubernetes:batch/v1:CronJob    update
~   vpc-native-interest-scheduled-run-cron                  kubernetes:batch/v1:CronJob    update
~   vpc-native-update-tags-str-cron                         kubernetes:batch/v1:CronJob    update
~   vpc-native-post-analytics-history-day-clickhouse-cron   kubernetes:batch/v1:CronJob    update
~   vpc-native-update-achievement-rarity-cron               kubernetes:batch/v1:CronJob    update
~   vpc-native-subscription-anniversary-achievements-cron   kubernetes:batch/v1:CronJob    update
~   vpc-native-user-posts-analytics-refresh-cron            kubernetes:batch/v1:CronJob    update
~   vpc-native-rotate-daily-quests-cron                     kubernetes:batch/v1:CronJob    update
~   vpc-native-private-deployment                           kubernetes:apps/v1:Deployment  update
~   vpc-native-update-source-public-threshold-cron          kubernetes:batch/v1:CronJob    update
~   vpc-native-clean-old-notifications-cron                 kubernetes:batch/v1:CronJob    update
~   vpc-native-materialize-monthly-best-post-archives-cron  kubernetes:batch/v1:CronJob    update
~   vpc-native-worker-job-deployment                        kubernetes:apps/v1:Deployment  update
~   vpc-native-world-index-cron                             kubernetes:batch/v1:CronJob    update
~   vpc-native-ledger-hygiene-cron                          kubernetes:batch/v1:CronJob    update
~   vpc-native-daily-digest-cron                            kubernetes:batch/v1:CronJob    update
~   vpc-native-temporal-deployment                          kubernetes:apps/v1:Deployment  update
~   vpc-native-bg-deployment                                kubernetes:apps/v1:Deployment  update
~   vpc-native-update-current-streak-cron                   kubernetes:batch/v1:CronJob    update
~   vpc-native-calculate-top-readers-cron                   kubernetes:batch/v1:CronJob    update
~   vpc-native-user-profile-analytics-clickhouse-cron       kubernetes:batch/v1:CronJob    update
~   vpc-native-hourly-notification-cron                     kubernetes:batch/v1:CronJob    update
... and 20 other changes

extractClaimSignatures already dropped a token repeating the claim's own
entity, on the grounds that it matches every plan mentioning the
technology rather than the change. The same argument applies to every
other entity the ledger knows, and nothing enforced it: production
carries 1,103 distinct signature tokens that are exactly an entity name
-- "Claude Code", "Python 3.10", "React 19", "Redis", "Docker" among the
most repeated.

Tier A is meant to say "your code touches the thing that changed". These
say "your plan mentions this technology", which the entity tiers already
say version-gated and therefore more precisely, so the rule moves signal
to a better tier rather than dropping it. This is the same failure as
the generic word list one layer up, where the token is specific but
still not a code surface.

The live extraction worker never applied the bar's entity half at all;
it does now. codeOnlyCanonical names are excluded from the reject set --
those ARE code tokens.
@idoshamun idoshamun changed the title fix(ledger): count only signable change types as awaiting signatures fix(ledger): signature hygiene — reject entity-name tokens, count only signable types Aug 21, 2026
#4115 enforced the bar on the two extraction paths, but /claims/update
and /candidates/resolve accept affected/superseding straight from a
reviewer and applied nothing. A hand-written "name" accuses every
codebase on earth exactly as an extracted one does.

The generic-word half moves into the shared `signatures` schema, so any
route using it inherits the rule. The entity-name half needs the
ledger's own names, so the two routes apply it after parsing. Tokens are
filtered rather than rejected: the operator's other overrides in the
same call are still valid, and a change whose only symbol is generic
legitimately carries none.
The first pass rejected any token matching a known entity name, which
would have taken 1,738 tokens including `esbuild`, `curl`, `minimatch`,
`encoding/json/v2`, `System.Text.Json` and `GPT-5.3-Codex`. Those are
entity names AND literal code tokens: they are what a package.json pins,
what a file imports, what a request body sets. Dropping them deletes
real signal — the same reason the specificity bar deliberately leaves
bare lowercase words alone and lets the detector gate the match on the
claim's entity being resolved in the same input.

A name with a space in it is different in kind: nothing imports "React
Server Components" or pins "Swift Package Manager", so a plan holding
that string is describing the technology in prose, which is what the
entity tiers are for. Both halves are now required.

Production figure drops from 1,738 tokens to 434 across 397 claims.
@idoshamun
idoshamun enabled auto-merge (squash) August 21, 2026 19:35
@idoshamun
idoshamun merged commit dace46e into main Aug 21, 2026
9 checks passed
@idoshamun
idoshamun deleted the fix/ledger-signable-change-types branch August 21, 2026 19:41
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