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 1/2] 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 }), From 3d5041fb2f8964c30f0273615b4f039620eef0bf Mon Sep 17 00:00:00 2001 From: N0zoM1z0 <1705004398@qq.com> Date: Tue, 14 Jul 2026 21:32:23 +0000 Subject: [PATCH 2/2] test(reference): cover failure normalization --- src/application/ReferenceSourceImport.ts | 38 ++++++++++++++---------- tests/referenceSourceImport.test.ts | 26 +++++++++++++++- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/src/application/ReferenceSourceImport.ts b/src/application/ReferenceSourceImport.ts index 5521767b1..c0048ae60 100644 --- a/src/application/ReferenceSourceImport.ts +++ b/src/application/ReferenceSourceImport.ts @@ -67,6 +67,27 @@ const deduplicateRelationships = ( return result; }; +/** Sort parse failures deterministically while retaining distinct reasons. */ +export const normalizeHistoricalSourceParseFailures = ( + failures: HistoricalSourceGraphInput["parse_failures"], +): HistoricalSourceGraphInput["parse_failures"] => + [...failures] + .sort((left, right) => + byCodePoint( + historicalSourceParseFailureKey(left), + historicalSourceParseFailureKey(right), + ), + ) + .filter((value, index, array) => { + if (index === 0) return true; + const previous = array[index - 1]; + return ( + previous === undefined || + historicalSourceParseFailureKey(value) !== + historicalSourceParseFailureKey(previous) + ); + }); + const buildProvenance = ( options: ReferenceSourceImportOptions, ): HistoricalSourceGraphInput["provenance"] => ({ @@ -205,22 +226,7 @@ export const importReferenceSource = async ( if (isAborted(options.signal)) return err(cancelled()); const uniqueRelationships = deduplicateRelationships(relationships); - const uniqueFailures = [...parseFailures] - .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 ( - historicalSourceParseFailureKey(value) !== - historicalSourceParseFailureKey(previous) - ); - }); + const uniqueFailures = normalizeHistoricalSourceParseFailures(parseFailures); const sortedEntries = [...entries].sort((left, right) => byCodePoint(left.path, right.path), diff --git a/tests/referenceSourceImport.test.ts b/tests/referenceSourceImport.test.ts index 957387450..ccba32cc4 100644 --- a/tests/referenceSourceImport.test.ts +++ b/tests/referenceSourceImport.test.ts @@ -6,7 +6,10 @@ import { join } from "node:path"; import { add, commit, init } from "isomorphic-git"; import { describe, expect, it } from "vitest"; -import { importReferenceSource } from "../src/application/ReferenceSourceImport.js"; +import { + importReferenceSource, + normalizeHistoricalSourceParseFailures, +} from "../src/application/ReferenceSourceImport.js"; import { projectReferenceSourceEntryFailure } from "../src/application/ReferenceSourceImportEntries.js"; import { projectReferenceSourceImportError, @@ -52,6 +55,27 @@ const importTree = ( }); describe("reference source import", () => { + it("deduplicates exact parse failures without collapsing distinct reasons", () => { + const malformed = { + path: "src/main.ts", + parser: "babel", + reason: "Malformed input", + }; + const unexpected = { + path: "src/main.ts", + parser: "babel", + reason: "Unexpected token", + }; + + expect( + normalizeHistoricalSourceParseFailures([ + unexpected, + malformed, + unexpected, + ]), + ).toEqual([malformed, unexpected]); + }); + it("projects entry failures without low-level reader diagnostics", () => { for (const [kind, code] of [ ["directory", "io"],