Skip to content

[refactor] preserve-numbers: delete the two dead year tiers and close the doc gaps #919 left #928

Description

@s-annam

Follow-up to #876 / #919 (merged as b4d3a2d). Four items, none behavioural, all
inside src/lib/webllm/preserve-numbers.ts and its test. They were the surviving
non-blocking nits on that PR's approval plus one measured simplification found
afterwards.

Nothing here changes what the gate does. This is a drive-by: fold it into
whatever PR is next in src/lib/webllm/, rather than opening a PR of its own.

1. Tiers 1b and 1c in isYearContext are dead — delete them and their two regexes

isYearContext (preserve-numbers.ts:438) ends with two "verb" tiers:

// 1b. Bare past-tense verb followed by a function word, punctuation, or end of bullet (#876)
if (YEAR_PREFIX_VERB_CUE.test(before) && FUNCTION_WORD_FOLLOWS.test(after)) {
  return "verb";
}

// 1c. Weak prepositions, qualifiers, or bullet-initial attributive years (#876)
if (YEAR_PREFIX_WEAK_CUE.test(before) || leadingText.length === 0) {
  return "verb";
}

Both are shadowed by the inverted gate's own fallback in bareIntegerClaim
(preserve-numbers.ts:510), which already returns year_verb for every 4-digit
integer in 1900–2099 that nothing else claimed. Anything 1b or 1c catches, the
fallback catches one branch later with the same answer.

The only way they could matter is ordering: bareIntegerClaim consults
isRangeEndpoint before the fallback, so a 4-digit range endpoint carrying a
weak cue gets year_verb from 1c where it would otherwise get range — and
range is claimed while year_verb is counted as unclaimed by
countUnclaimedByKey. That is a real difference in principle. It does not show
up in practice.

Measured, not assumed. Replacing both tiers with return "none":

  • npx vitest run src/lib/webllm/preserve-numbers.test.ts → 97/97 still pass.
  • Differential against the submitted implementation over 136 cases — 16 bullets
    covering 4-digit range endpoints ("Shipped 2000-3000 units per quarter.",
    "Handled 1900 to 2100 requests per second.", "Reduced errors from 2000 to 1900 per week."), weak-cue shapes (from / by / for / of / over / the /
    our / early / mid-), bare past-tense verbs, and bullet-initial attributive
    years, each × identity / year-dropped / year-dropped-with-same-value-decoy /
    appended-clause, each × with and without an unrelated decoy bullet on the input
    side — comparing ok, dropped and added: 0 diffs.

Deleting both tiers also removes YEAR_PREFIX_WEAK_CUE (:362) and
YEAR_PREFIX_VERB_CUE (:371), which have no other reference. That is roughly
4 cyclomatic off isYearContext, which fallow currently reports at CRAP 63.6 /
15 cyclomatic / 13 cognitive / 52 lines
— the highest in the module and the
reason it shows up in fallow audit at all.

Before deleting, re-run the two checks above rather than trusting this issue:
the claim is "no observable difference", and it is only as good as the corpus it
was measured on.

2. bareIntegerClaim's docblock still says "Three ways to qualify"

preserve-numbers.ts:495:

Three ways to qualify — a headcount, a year, or one endpoint of a tight range.

There are four readings now that year_verb exists. One word, but the docblock is
the thing a reader trusts to know how many claim kinds there are, and the
lenient/strict split between year and year_verb is the whole point of #876.

(If item 1 lands, the count is still four — year_verb comes from the fallback,
not from tiers 1b/1c.)

3. The residual false-revert families are not disclosed in code

#919's PR description listed the accepted residual, and the squash commit body
carries it, but nothing in the module does — and a PR description is not where
someone debugging a surprise revert will look.

The module docblock should name the mechanism and all four shapes. A strict year
cue immediately before the digit — , / ( / a leading dash / a month name —
means that if an unrelated quantity of the same value is merged away in another
bullet, strict count parity reports a drop. Confirmed shapes:

Input Output Result
["Shipped in March 2000 units.", "Counted 2000 units."] ["Counted 2000 units shipped in March."] { ok: false, dropped: ["2000"] }
["Speaker (2019) at the internal summit.", "Handled 2019 support tickets."] ["Handled 2019 support tickets; spoke at the internal summit."] { ok: false, dropped: ["2019"] }
["B.S. CS, 2019.", "Closed 2019 issues."] ["Closed 2019 issues after earning a B.S. in CS."] { ok: false, dropped: ["2019"] }

All three were ok: true before #919. The trade is deliberate — the cost is one
discarded rewrite with a visible warning, and the alternative is losing the
degree/talk/award year defence — so this is disclosure, not a bug. The month-name
shape in particular was never written down anywhere.

4. The residual test's title is wider than its body

preserve-numbers.test.ts:1121:

it("pins accepted residual: comma/parenthesis cues across two occurrences (#876 review)", ...)

The body exercises only the comma ("Cut infra spend, 2000 servers decommissioned.").
Either narrow the title to what it pins, or add the parenthesis case from the table
above so the title becomes true.

Why these were not fixed in #919

#919 was approved after six review rounds by an outside contributor. main has
dismiss_stale_reviews on, so any push to the branch would have dismissed the
approval and bought a seventh round over comment wording. Landing it and folding
these in later was the cheaper trade.

Decisions (2026-09-27)

  • Ship as its own PR. Do not wait to fold this into another webllm change.
  • Item 4: add the parenthesis case from the table above to the test body so the existing title becomes true. Do not narrow the title.
  • Item 5 (from the comments) is in scope: delete the dead |${OPEN_ENDED_WORDS} alternation and correct the "six sites" docblock count.
  • The 136-case / 432-case differential harnesses mentioned above are not committed. The existing preserve-numbers.test.ts suite passing is the gate; say so in the PR body.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationgaalHand this issue to Gaal, the repo's coding agentrefactorCode restructuring without behavior change

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions