fix(prompt-hook): close the segment-vocab integrity gaps (#1141, #1142, #1144, #1145, #1146) (#1150)

Five hardening fixes to the #1136 MEDIUM (graph-derived) tier:

- #1141: updateNode() now writes the segment vocabulary like insertNode()
  does — framework post-extract renames (NestJS route prefixing) left the
  new name permanently unsearchable (the old rows orphaned, the backfill
  gated on an EMPTY vocab, so even a full re-index re-created the drift).
- #1142: new CodeGraph.healSegmentVocabIfEmpty() — the hook opens the
  graph without sync, so a database migrated from pre-vocab schema kept
  the MEDIUM tier dormant until some unrelated sync ran. The hook heals
  on first use (one SELECT when populated; lock-aware, defers to a
  running sync) and records noop-vocab-empty when it can't.
- #1144: a name whose only nodes are file/import kind is skipped instead
  of falling back to surfacing an import statement as a matched symbol;
  import specifiers no longer enter the vocab at all (shared
  isSegmentableKind gate across insertNode/updateNode/rebuild page query)
  since they can never be surfaced and only inflate rarity statistics.
- #1145: plural variant folding is keyed on English plural spelling —
  bare-s plurals no longer mint a bogus -es sibling (services→servic),
  unambiguous sibilant-es plurals no longer mint a bogus -s sibling
  (classes→classe), trailing -ss singulars no longer strip (class→clas);
  genuinely ambiguous endings (caches/databases) still emit both keys.
- #1146: getSegmentCoOccurrence folds variants to their original word
  inside the SQL (CASE mapping + COUNT(DISTINCT word)) so a plural pair
  of ONE word can't tie with a genuine two-word match and crowd it past
  the pre-fold ORDER BY/LIMIT; the JS re-check stays as the honesty layer.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-02 17:23:54 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent be55b93d02
commit 35611b92bb
7 changed files with 233 additions and 20 deletions
+22
View File
@@ -73,9 +73,31 @@ describe('segmentLookupVariants — light plural folding', () => {
it('folds trailing s/es so plurals hit singular segments', () => {
expect(segmentLookupVariants('services')).toContain('service');
expect(segmentLookupVariants('machines')).toContain('machine');
expect(segmentLookupVariants('classes')).toContain('class');
});
it('bare-s plurals no longer mint a bogus -es sibling (#1145)', () => {
expect(segmentLookupVariants('services')).toEqual(['services', 'service']);
expect(segmentLookupVariants('machines')).toEqual(['machines', 'machine']);
});
it('unambiguous sibilant-es plurals no longer mint a bogus -s sibling (#1145)', () => {
expect(segmentLookupVariants('classes')).toEqual(['classes', 'class']);
expect(segmentLookupVariants('hashes')).toEqual(['hashes', 'hash']);
});
it('a trailing -ss is a singular, not a plural — no strip (#1145)', () => {
expect(segmentLookupVariants('class')).toEqual(['class']);
expect(segmentLookupVariants('process')).toEqual(['process']);
});
it('ambiguous endings emit BOTH candidate keys — a wrong exclusive guess would lose the real match', () => {
expect(segmentLookupVariants('caches')).toEqual(['caches', 'cach', 'cache']); // cache + s
expect(segmentLookupVariants('databases')).toEqual(['databases', 'databas', 'database']); // database + s
});
it('never strips a word below the minimum', () => {
expect(segmentLookupVariants('bus')).toEqual(['bus']);
expect(segmentLookupVariants('boxes')).toEqual(['boxes']); // -es strip would go sub-minimum
});
});