fix(extract): stop asserting works_at from bare people/->companies/ adjacency (#3466) (#3642)

Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain.

19,497 false `works_at` edges were being asserted from bare people/→companies/ directory adjacency. This is the exact fix prescribed when #3495 was closed — bare `mentions` plus an extractor version bump — and stubbing the old behavior back fails the new test. Retroactive cleanup of already-written rows is explicitly out of scope; filing that follow-up.

Verified before merge: the PR's own tests fail when the production change is reverted (11 of the previous 32 PRs failed exactly there — one had 7 of 8 new tests passing on master); typecheck clean; MERGEABLE/CLEAN with 22/22 checks green on the current base, not a stale one.
This commit is contained in:
Masa
2026-08-01 03:11:55 +08:00
committed by GitHub
parent bf7d706bb4
commit 7e21e47c15
3 changed files with 27 additions and 7 deletions
+7 -1
View File
@@ -349,7 +349,13 @@ function inferTypeByDir(fromDir: string, toDir: string, frontmatter?: Record<str
const to = toDir.split('/')[0];
if (from === 'people' && to === 'companies') {
if (Array.isArray(frontmatter?.founded)) return 'founded';
return 'works_at';
// #3466: bare people/ -> companies/ adjacency is not evidence of
// employment, so it gets the neutral 'mentions' verb instead of
// 'works_at'. Real works_at edges still come from the two paths that
// read actual evidence: the company:/companies: frontmatter fields
// (FRONTMATTER_LINK_MAP) and employment phrasing in prose
// (inferLinkType in link-extraction.ts).
return 'mentions';
}
if (from === 'people' && to === 'deals') return 'involved_in';
if (from === 'deals' && to === 'companies') return 'deal_for';
+5 -4
View File
@@ -28,10 +28,11 @@ import { ensureWellFormed } from './text-safe.ts';
* OR updated_at > links_extracted_at`. It is an ISO-8601 string (NOT a number) —
* the column is TIMESTAMPTZ and the predicate binds it as `::timestamptz`.
*/
// 2026-07-10: bumped for the #2576 --stale nullResolver fix — sweeps before it
// stamped pages with their bare wikilinks silently dropped; the bump re-flags
// them so the fixed sweep re-extracts.
export const LINK_EXTRACTOR_VERSION_TS = '2026-07-10T00:00:00Z';
// 2026-07-30: bumped for the #3466 inferTypeByDir fix — unevidenced
// people/ -> companies/ adjacency now infers 'mentions' instead of
// 'works_at'; the bump re-flags stamped pages so the next --stale sweep
// re-extracts them under the corrected inference.
export const LINK_EXTRACTOR_VERSION_TS = '2026-07-30T00:00:00Z';
// ─── Entity references ──────────────────────────────────────────
+15 -2
View File
@@ -97,11 +97,24 @@ describe('extractLinksFromFile', () => {
expect(links).toEqual([]);
});
it('infers link type from directory structure', async () => {
it('people -> companies adjacency without evidence infers mentions, not works_at (#3466)', async () => {
// The content carries no employment language, so the directory pair alone
// must not assert a specific employment claim. Evidence-based works_at
// still flows through the company: frontmatter path (covered above) and
// prose inference in link-extraction.ts.
const content = 'See [Brex](../companies/brex.md).';
const allSlugs = new Set(['people/pedro', 'companies/brex']);
const links = await extractLinksFromFile(content, 'people/pedro.md', allSlugs);
expect(links[0].link_type).toBe('works_at');
expect(links).toHaveLength(1);
expect(links[0].link_type).toBe('mentions');
});
it('people -> companies with founded frontmatter infers founded', async () => {
const content = '---\nfounded: [brex]\n---\nSee [Brex](../companies/brex.md).';
const allSlugs = new Set(['people/pedro', 'companies/brex']);
const links = await extractLinksFromFile(content, 'people/pedro.md', allSlugs);
expect(links).toHaveLength(1);
expect(links[0].link_type).toBe('founded');
});
it('infers deal_for type for deals -> companies', async () => {