diff --git a/understand-anything-plugin/packages/core/src/__tests__/schema.test.ts b/understand-anything-plugin/packages/core/src/__tests__/schema.test.ts index e2ffe69..0504557 100644 --- a/understand-anything-plugin/packages/core/src/__tests__/schema.test.ts +++ b/understand-anything-plugin/packages/core/src/__tests__/schema.test.ts @@ -593,6 +593,7 @@ describe("permissive validation", () => { const result = validateGraph(validGraph); expect(result.success).toBe(true); expect(result.issues).toEqual([]); + expect(result.errors).toBeUndefined(); }); it("auto-corrects and loads graph that would have failed strict validation", () => { @@ -634,4 +635,30 @@ describe("permissive validation", () => { expect.objectContaining({ level: "auto-corrected", category: "type-coercion" }) ); }); + + it("returns fatal when edges is present but not an array", () => { + const graph = structuredClone(validGraph) as any; + graph.edges = { source: "node-1", target: "node-1" }; + + const result = validateGraph(graph); + expect(result.success).toBe(false); + expect(result.fatal).toContain('"edges" must be an array'); + expect(result.errors).toContain('"edges" must be an array when present'); + expect(result.issues).toContainEqual( + expect.objectContaining({ + level: "fatal", + category: "invalid-collection", + path: "edges", + }) + ); + }); + + it("preserves deprecated errors for dropped-item callers", () => { + const graph = structuredClone(validGraph); + graph.edges[0].target = "non-existent-node"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.errors).toContain('edges[0]: target "non-existent-node" does not exist in nodes — removed'); + }); }); diff --git a/understand-anything-plugin/packages/core/src/schema.ts b/understand-anything-plugin/packages/core/src/schema.ts index 68d3507..18ccbb6 100644 --- a/understand-anything-plugin/packages/core/src/schema.ts +++ b/understand-anything-plugin/packages/core/src/schema.ts @@ -328,10 +328,27 @@ export interface GraphIssue { export interface ValidationResult { success: boolean; data?: z.infer; + /** @deprecated Use issues/fatal instead */ + errors?: string[]; issues: GraphIssue[]; fatal?: string; } +function buildInvalidCollectionIssue(name: string): GraphIssue { + return { + level: "fatal", + category: "invalid-collection", + message: `"${name}" must be an array when present`, + path: name, + }; +} + +function buildErrors(issues: GraphIssue[], fatal?: string): string[] | undefined { + const messages = issues.map((issue) => issue.message); + if (fatal && !messages.includes(fatal)) messages.unshift(fatal); + return messages.length > 0 ? messages : undefined; +} + export function normalizeGraph(data: unknown): unknown { if (typeof data !== "object" || data === null) return data; @@ -372,7 +389,8 @@ export function normalizeGraph(data: unknown): unknown { export function validateGraph(data: unknown): ValidationResult { // Tier 4: Fatal — not even an object if (typeof data !== "object" || data === null) { - return { success: false, issues: [], fatal: "Invalid input: not an object" }; + const fatal = "Invalid input: not an object"; + return { success: false, issues: [], fatal, errors: buildErrors([], fatal) }; } const raw = data as Record; @@ -386,11 +404,27 @@ export function validateGraph(data: unknown): ValidationResult { // Tier 2: Auto-fix defaults and coercion const { data: fixed, issues } = autoFixGraph(normalized); + // Tier 4: Fatal — malformed top-level collections + const requiredCollections = ["nodes", "edges", "layers", "tour"] as const; + for (const collection of requiredCollections) { + if (collection in fixed && fixed[collection] !== undefined && !Array.isArray(fixed[collection])) { + const issue = buildInvalidCollectionIssue(collection); + issues.push(issue); + return { + success: false, + errors: buildErrors(issues, issue.message), + issues, + fatal: issue.message, + }; + } + } + // Tier 4: Fatal — missing project metadata const projectResult = ProjectMetaSchema.safeParse(fixed.project); if (!projectResult.success) { return { success: false, + errors: buildErrors(issues, "Missing or invalid project metadata"), issues, fatal: "Missing or invalid project metadata", }; @@ -420,6 +454,7 @@ export function validateGraph(data: unknown): ValidationResult { if (validNodes.length === 0) { return { success: false, + errors: buildErrors(issues, "No valid nodes found in knowledge graph"), issues, fatal: "No valid nodes found in knowledge graph", }; @@ -514,5 +549,5 @@ export function validateGraph(data: unknown): ValidationResult { tour: validTour, }; - return { success: true, data: graph, issues }; + return { success: true, data: graph, issues, errors: buildErrors(issues) }; }