From 8dbe92a4a3827135f88ae26700be95b60dacf8c9 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:11:44 +0800 Subject: [PATCH] refactor(reference): centralize source inventory rules --- src/application/ReferenceSourceImport.ts | 51 +++++---------------- src/domain/referenceSourceGraph.test.ts | 4 ++ src/domain/referenceSourceGraph.ts | 57 +++++++++++++++--------- 3 files changed, 50 insertions(+), 62 deletions(-) diff --git a/src/application/ReferenceSourceImport.ts b/src/application/ReferenceSourceImport.ts index 0a7c272658..f58043b0a5 100644 --- a/src/application/ReferenceSourceImport.ts +++ b/src/application/ReferenceSourceImport.ts @@ -1,9 +1,11 @@ import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; import { createHistoricalSourceGraph, + historicalSourceInventoryHasPartialFacts, historicalSourceLanguages, historicalSourceManifests, historicalSourceParseFailureKey, + historicalSourceRelationshipKey, type HistoricalSourceGraph, type HistoricalSourceGraphInput, } from "../domain/referenceSourceGraph.js"; @@ -45,18 +47,13 @@ const cancelled = ( const isAborted = (signal?: AbortSignal): boolean => signal?.aborted === true; -const relationshipKey = ( - relationship: HistoricalSourceGraphInput["relationships"][number], -): string => - `${relationship.from_path}\u0000${relationship.to}\u0000${relationship.kind}\u0000${relationship.resolution}\u0000${relationship.parse_state}`; - const deduplicateRelationships = ( relationships: HistoricalSourceGraphInput["relationships"], ): HistoricalSourceGraphInput["relationships"] => { const seen = new Set(); const result: HistoricalSourceGraphInput["relationships"] = []; for (const relationship of relationships) { - const key = relationshipKey(relationship); + const key = historicalSourceRelationshipKey(relationship); if (seen.has(key)) continue; seen.add(key); result.push(relationship); @@ -93,37 +90,6 @@ const buildProvenance = ( caller: options.caller, }); -const deriveInventoryState = ( - input: Pick< - HistoricalSourceGraphInput, - | "entries" - | "relationships" - | "parse_failures" - | "exclusions" - | "limitations" - >, -): "complete" | "partial" | "unknown" => { - if (input.limitations.length > 0) return "partial"; - if (input.exclusions.length > 0) return "partial"; - if (input.parse_failures.length > 0) return "partial"; - const partialEntry = input.entries.some( - (entry) => - entry.limitations.length > 0 || - (entry.kind === "file" && entry.content_state !== "hashed") || - (entry.kind === "directory" && entry.tree_state !== "enumerated") || - (entry.kind === "symlink" && entry.target_state !== "internal") || - entry.kind === "unknown", - ); - if (partialEntry) return "partial"; - const partialRelationship = input.relationships.some( - ({ parse_state, resolution }) => - parse_state !== "parsed" || - ["unresolved", "unknown"].includes(resolution), - ); - if (partialRelationship) return "partial"; - return "complete"; -}; - const sortExclusions = ( exclusions: HistoricalSourceGraphInput["exclusions"], ): HistoricalSourceGraphInput["exclusions"] => @@ -261,16 +227,21 @@ export const importReferenceSource = async ( schema: "HistoricalSourceGraph", authority: "historical-reference", root_alias: "$REFERENCE_ROOT", - inventory_state: deriveInventoryState({ + inventory_state: historicalSourceInventoryHasPartialFacts({ entries: sortedEntries, relationships: uniqueRelationships, parse_failures: uniqueFailures, exclusions: sortedExclusions, limitations: sortedLimitations, - }), + }) + ? "partial" + : "complete", entries: sortedEntries, relationships: uniqueRelationships.sort((left, right) => - compareUnicodeCodePoints(relationshipKey(left), relationshipKey(right)), + compareUnicodeCodePoints( + historicalSourceRelationshipKey(left), + historicalSourceRelationshipKey(right), + ), ), parse_failures: uniqueFailures, exclusions: sortedExclusions, diff --git a/src/domain/referenceSourceGraph.test.ts b/src/domain/referenceSourceGraph.test.ts index 5458b984ac..0d571774fb 100644 --- a/src/domain/referenceSourceGraph.test.ts +++ b/src/domain/referenceSourceGraph.test.ts @@ -337,6 +337,10 @@ describe("partial historical source graphs", () => { expect(createHistoricalSourceGraph(partial).inventory_state).toBe( "partial", ); + expect( + createHistoricalSourceGraph({ ...partial, inventory_state: "unknown" }) + .inventory_state, + ).toBe("unknown"); for (const change of [ { entries: partial.entries }, diff --git a/src/domain/referenceSourceGraph.ts b/src/domain/referenceSourceGraph.ts index a10213e87e..3179d8f219 100644 --- a/src/domain/referenceSourceGraph.ts +++ b/src/domain/referenceSourceGraph.ts @@ -237,13 +237,43 @@ export type HistoricalSourceManifest = z.infer< typeof historicalSourceManifestBaseSchema >; +/** Whether source facts prevent an inventory from being claimed complete. */ +export const historicalSourceInventoryHasPartialFacts = ( + input: Pick< + HistoricalSourceGraphInput, + | "entries" + | "relationships" + | "parse_failures" + | "exclusions" + | "limitations" + >, +): boolean => + input.limitations.length > 0 || + input.exclusions.length > 0 || + input.parse_failures.length > 0 || + input.entries.some( + (entry) => + entry.limitations.length > 0 || + (entry.kind === "file" && entry.content_state !== "hashed") || + (entry.kind === "directory" && entry.tree_state !== "enumerated") || + (entry.kind === "symlink" && entry.target_state !== "internal") || + entry.kind === "unknown", + ) || + input.relationships.some( + ({ parse_state, resolution }) => + parse_state !== "parsed" || + resolution === "unresolved" || + resolution === "unknown", + ); + /** 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 relationshipKey = ( - relationship: z.infer, +/** Stable identity used to sort and deduplicate source relationships. */ +export const historicalSourceRelationshipKey = ( + relationship: HistoricalSourceGraphInput["relationships"][number], ): string => `${relationship.from_path}\u0000${relationship.to}\u0000${relationship.kind}\u0000${relationship.resolution}\u0000${relationship.parse_state}`; @@ -397,26 +427,9 @@ const checkCompleteInventory = ( graph: GraphStructure, context: z.RefinementCtx, ): void => { - if (graph.inventory_state !== "complete") return; - const partialEntry = graph.entries.some( - (entry) => - entry.limitations.length > 0 || - (entry.kind === "file" && entry.content_state !== "hashed") || - (entry.kind === "directory" && entry.tree_state !== "enumerated") || - (entry.kind === "symlink" && entry.target_state !== "internal") || - entry.kind === "unknown", - ); - const partialRelationship = graph.relationships.some( - ({ parse_state, resolution }) => - parse_state !== "parsed" || - ["unresolved", "unknown"].includes(resolution), - ); if ( - !partialEntry && - !partialRelationship && - graph.parse_failures.length === 0 && - graph.exclusions.length === 0 && - graph.limitations.length === 0 + graph.inventory_state !== "complete" || + !historicalSourceInventoryHasPartialFacts(graph) ) return; context.addIssue({ @@ -445,7 +458,7 @@ function checkGraphInvariants( checkSortedUnique(graph.languages, "languages", context); checkSortedUnique(graph.manifests, "manifests", context); checkSortedUnique( - graph.relationships.map(relationshipKey), + graph.relationships.map(historicalSourceRelationshipKey), "relationships", context, );