Skip to content

fix(company): state the diacritic gap honestly, and restore the family-key floor - #280

Merged
sroussey merged 3 commits into
mainfrom
claude/keen-knuth-hxoj5s-company-identity-keys
Aug 14, 2026
Merged

fix(company): state the diacritic gap honestly, and restore the family-key floor#280
sroussey merged 3 commits into
mainfrom
claude/keen-knuth-hxoj5s-company-identity-keys

Conversation

@sroussey

Copy link
Copy Markdown
Contributor

Two corrections to the company identity story — one in the docs, one in behavior.

1. generateCompanyHash is a derived slug, not the company identity key

It folds diacritics. Nothing persists it. grep company_hash_id outside tests hits only CompanyNormalization.ts and src/eval/scoreExtraction.ts. The key the company tier actually matches on is company_observations.normalized_name, written by normalizeCompanyName — the function the fold commit deliberately left unchanged, and the column CompanyResolver's name fallback and canonical_company are keyed on.

normalizeCompanyName("Søren Skou Holdings LLC") → "Søren Skou Holdings LLC"
normalizeCompanyName("Soren Skou Holdings LLC") → "Soren Skou Holdings LLC"

Two spellings of one company still mint two canonical_company rows and two identity links, while CLAUDE.md said "both identity tiers use it".

Decision: do not fold the persisted key. There is no rebuild path for a company re-key — normalized_name is written only by the extraction path, and sec resolve re-resolves from that column rather than recomputing it, so a fold would take effect only by re-extracting every company-observing form and re-paying the AI cost of all of them. The prerequisite for closing the gap is teaching ResolveObservationsTask to re-normalize as it re-partitions.

So instead: the fold stays where it is (the eval scorer's company key uses it), generateCompanyHash gains a JSDoc saying plainly that it is a derived slug, not persisted, not the company identity key; CLAUDE.md is corrected to say the company tier does not fold, with sec canonical company alias as today's remedy and the re-normalizing resolver named as the prerequisite; and two tests pin the gap, so a future "fix" cannot land as a silent one-line re-key with no migration.

2. companyFamilyName — the deleted vehicle-word list took a floor with it

Removing VEHICLE_WORDS also removed the guard they provided. Verified by running the shipped function on main:

name main this PR
Fund III fund fund-iii
Fund II, L.P. fund fund-ii
Partners III LLC / Partners IV LLC both partners partners-iii / partners-iv
Ventures 2021 / Ventures 2022 both ventures ventures-2021 / ventures-2022

Unrelated sponsor vehicles named only by a generic word plus a series marker shared one family — the over-merge the design notes call the worse error, since it silently attributes one house's deals to another and leaves no trace, while an under-merge is visible as two families and costs one alias.

The vocabulary comes back as GENERIC_VEHICLE_WORDS with the opposite polarity: never a strip list, only a floor. Nothing in it is ever dropped — Chardan Capital Markets still keeps every token. It answers exactly one question: would the name that survives stripping still name a house?

isDroppableTail splits into isLegalTail (always drops) and isSeriesTail (drops unless dropping it would leave a single generic vehicle word as the whole house name). Unchanged behavior where a real house name survives:

  • Churchill Sponsor XIII LLCchurchill-sponsor (= Churchill Sponsor XIV LLC)
  • WAVE Equity Fund II, L.P.wave-equity-fund
  • Curnes Fund 2001curnes-fund
  • Bluerock Acquisition Holdings II, LLCbluerock-acquisition-holdings

Test and doc corrections

  • CompanyFamilyName.test.ts:110 asserted "fund" under a comment saying the name "is kept whole" — the contract was unpinned in the test and the doc at once. Replaced with the real contract, plus a companion test that the series marker still drops when a house name survives it.
  • The "token-exact" test's Fundamental / Fund rationale went vacuous when fund stopped being droppable. Re-anchored on a legal form, using real names from the committed golden labels: DirectorCo ends in co and Cambridge Quantum in ua, and a substring rule would gut both.
  • CLAUDE.md's companyFamilyName paragraph now describes the shipped rule, and carries a warning that changing the function re-keys the family tier.

Verified

npx vitest run src/storage/company src/resolver src/sec/forms/registration-statements/Form_S_1.storage.underwriters.test.ts src/sec/forms/registration-statements/s1/spacSponsor.e2e.test.ts — 92 pass, 0 fail (16 files). npx tsc --noEmit clean.

The three new/changed assertions were confirmed red against the current code before the fix (Fund III returned "fund", expected "fund-iii").

One pre-existing failure on main, untouched by this PR and present before it: componentRegistry.test.ts expects 20 registered components and finds 21.

⚠️ This changes family keys for the affected names. Family keys are derived from the legal name every observation carries, so they are rebuildable in principle — but no batch family resolve exists yet, so in practice a deployment holding such rows re-extracts the affected S-1/424 filings.


🤖 Generated with Claude Code


Generated by Claude Code

Two corrections to the company identity story, one in the docs and one
in behavior.

**generateCompanyHash is a derived slug, not the company identity key.**
It folds diacritics; nothing persists it. No table stores
`company_hash_id`, and its one in-repo consumer is the eval scorer's
company match key. The key the company tier actually matches on is
`company_observations.normalized_name`, written by `normalizeCompanyName`
— which does not fold. So `Søren Skou Holdings LLC` and
`Soren Skou Holdings LLC` still mint two canonical companies and two
identity links, and CLAUDE.md said the opposite ("both identity tiers
use it").

The fold is deliberately NOT extended to the persisted key. There is no
rebuild path for a company re-key: `normalized_name` is written only by
the extraction path, and `sec resolve` re-resolves FROM that column
rather than recomputing it, so folding would take effect only by
re-extracting every company-observing form and re-paying the AI cost of
all of them. The prerequisite is a re-normalizing
`ResolveObservationsTask`. Until then the remedy is an explicit
`sec canonical company alias`, and a test pins the gap so the fold cannot
land later as a silent one-line re-key.

**companyFamilyName keeps a series marker that would leave a bare
vehicle word.** Removing the vehicle-word vocabulary also removed the
floor it provided: `Fund III`, `Fund II, L.P.` and `Fund I` all collapsed
to `fund`, as did `Partners III LLC` / `Partners IV LLC` to `partners`
and `Ventures 2021` / `Ventures 2022` to `ventures`. Unrelated sponsor
vehicles named only by a generic word plus a series marker shared one
family — the over-merge the design notes call the worse error, since it
silently attributes one house's deals to another.

The vocabulary comes back as `GENERIC_VEHICLE_WORDS` with the OPPOSITE
polarity: never a strip list, only a floor. `isDroppableTail` splits into
`isLegalTail` (always drops) and `isSeriesTail` (drops unless dropping it
would leave a single generic vehicle word as the whole house name), so
`Churchill Sponsor XIII LLC` → `churchill-sponsor` and
`WAVE Equity Fund II, L.P.` → `wave-equity-fund` are unchanged while
`Fund III` → `fund-iii` ≠ `fund-ii`.

The test that was meant to pin this asserted `"fund"` under a comment
saying the name "is kept whole" — the contract was unpinned in the test
and the doc at once. The "token-exact" test re-anchors on a legal form
(`DirectorCo` ends in `co`, `Cambridge Quantum` in `ua`); its old
`Fundamental` / `Fund` rationale went vacuous when `fund` stopped being
droppable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKvKnyVeQQQCm6FhtaLyMa
claude added 2 commits August 14, 2026 16:31
`companyFamilyName` only dropped a trailing series marker, so a sponsor
that serializes a vehicle in the MIDDLE of the name got one family per
vehicle — the opposite of what the family tier is for.

Three real names in the committed golden labels do exactly that:

  Southern Cross Acquisition I  Sponsor Corp. -> ...-acquisition-i-sponsor
  Southern Cross Acquisition II Sponsor Corp. -> ...-acquisition-ii-sponsor
  Osprey Acquisition III, Sponsor LLC         -> osprey-acquisition-iii-sponsor
  CGC III Sponsor DirectorCo LLC              -> cgc-iii-sponsor-directorco

The first two are the same sponsor and did not meet. Across the 129
company-shaped names in the golden labels, 4 keys carried a surviving
interior marker; after this change, 0. `Samara Acquisition Sponsor VI`
consolidated before only because its numeral happened to land last.

Mid-name the rule has to be stricter than at the tail, because position
stops being evidence there:

- Well-formed roman numerals only. The tail test accepts any run of
  `ivxlcdm`, which is safe at the end of a name and not in the middle:
  `civil`, `dim`, `mild`, `vivid` and `lid` all pass it, and deleting one
  of those from inside a name deletes a real word.
- No bare numbers. An interior number belongs to the name — `Route 66
  Ventures` would become `route-ventures` — while a TRAILING year is a
  vintage (`Curnes Fund 2001`) and keeps its existing treatment.
- Never the first or last token. A leading numeral is the house's own
  name (`V Capital`), and the last position already answered to the tail
  rule including its generic-vehicle floor, so `Fund III` cannot be
  stripped by the back door. Those two bounds also make the result safe
  by construction: an interior index has a token on each side, so the
  name can be neither emptied nor reduced to a single word.

The interior test fails against the previous code; the three guard tests
(real words, bare numbers, first/last position) pass before and after,
which is what scopes the change. Full suite green, tsc clean.
@sroussey
sroussey merged commit 7aff5a6 into main Aug 14, 2026
1 check passed
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.

2 participants