From 543510f62fd257b42e6a7dfc876112dda0e0ea01 Mon Sep 17 00:00:00 2001 From: N0zoM1z0 <1705004398@qq.com> Date: Tue, 14 Jul 2026 20:27:31 +0000 Subject: [PATCH] fix(reference): preserve distinct parse failures --- src/application/ReferenceSourceImport.ts | 17 +++++++++++------ src/domain/referenceSourceGraph.ts | 7 ++++++- tests/referenceSourceGraph.test.ts | 24 ++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/application/ReferenceSourceImport.ts b/src/application/ReferenceSourceImport.ts index d68a651a1..5521767b1 100644 --- a/src/application/ReferenceSourceImport.ts +++ b/src/application/ReferenceSourceImport.ts @@ -1,6 +1,7 @@ import { isSecretLikePath } from "../domain/referenceSourceClassification.js"; import { createHistoricalSourceGraph, + historicalSourceParseFailureKey, type HistoricalSourceGraph, type HistoricalSourceGraphInput, } from "../domain/referenceSourceGraph.js"; @@ -205,16 +206,20 @@ export const importReferenceSource = async ( const uniqueRelationships = deduplicateRelationships(relationships); const uniqueFailures = [...parseFailures] - .sort((left, right) => { - const byPath = byCodePoint(left.path, right.path); - if (byPath !== 0) return byPath; - return byCodePoint(left.parser, right.parser); - }) + .sort((left, right) => + byCodePoint( + historicalSourceParseFailureKey(left), + historicalSourceParseFailureKey(right), + ), + ) .filter((value, index, array) => { if (index === 0) return true; const previous = array[index - 1]; if (previous === undefined) return true; - return value.path !== previous.path || value.parser !== previous.parser; + return ( + historicalSourceParseFailureKey(value) !== + historicalSourceParseFailureKey(previous) + ); }); const sortedEntries = [...entries].sort((left, right) => diff --git a/src/domain/referenceSourceGraph.ts b/src/domain/referenceSourceGraph.ts index b9689c217..3c5413199 100644 --- a/src/domain/referenceSourceGraph.ts +++ b/src/domain/referenceSourceGraph.ts @@ -225,6 +225,11 @@ export type HistoricalSourceManifest = z.infer< typeof historicalSourceManifestBaseSchema >; +/** Stable identity used to sort and deduplicate source parse failures. */ +export const historicalSourceParseFailureKey = ( + failure: HistoricalSourceGraphInput["parse_failures"][number], +): string => `${failure.path}\u0000${failure.parser}\u0000${failure.reason}`; + const canonicalJson = (value: unknown): string => { const serialized = canonicalize(value); if (serialized === undefined) @@ -400,7 +405,7 @@ function checkGraphInvariants( context, ); checkSortedUnique( - graph.parse_failures.map(({ path, parser }) => `${path}\u0000${parser}`), + graph.parse_failures.map(historicalSourceParseFailureKey), "parse_failures", context, ); diff --git a/tests/referenceSourceGraph.test.ts b/tests/referenceSourceGraph.test.ts index e00e8c66c..bf3a35953 100644 --- a/tests/referenceSourceGraph.test.ts +++ b/tests/referenceSourceGraph.test.ts @@ -5,6 +5,7 @@ import { createHistoricalSourceGraph, createHistoricalSourceManifest, historicalSourceGraphSchema, + historicalSourceParseFailureKey, parseHistoricalSourceGraph, parseHistoricalSourceManifest, type HistoricalSourceGraphInput, @@ -192,6 +193,29 @@ describe("historical source graph", () => { ).toThrow(/Complete inventory/u); }); + it("keeps distinct parse failure reasons for the same path and parser", () => { + const input = graphInput(); + const malformed = { + path: "src/main.ts", + parser: "typescript", + reason: "Malformed input", + }; + const unexpected = { + path: "src/main.ts", + parser: "typescript", + reason: "Unexpected token", + }; + input.inventory_state = "partial"; + input.parse_failures = [malformed, unexpected]; + + expect(createHistoricalSourceGraph(input).parse_failures).toEqual( + input.parse_failures, + ); + expect(historicalSourceParseFailureKey(malformed)).not.toBe( + historicalSourceParseFailureKey(unexpected), + ); + }); + it("rejects strict unknown fields, invalid VCS, dangling internal edges, and false manifests", () => { expect(() => createHistoricalSourceGraph({ ...graphInput(), surprise: true }),