From 31b267ae2d713770f5d74aa1b885179e3b8c481f Mon Sep 17 00:00:00 2001 From: Tabish Bidiwale Date: Thu, 25 Dec 2025 22:26:35 +1100 Subject: [PATCH] fix(artifact-graph): normalize paths for cross-platform glob compatibility Add FileSystemUtils.toPosixPath() utility and consolidate scattered path normalization patterns. This fixes Windows test failures where fast-glob couldn't match paths containing backslashes. Root cause: path.join() uses backslashes on Windows, but fast-glob requires forward slashes for glob patterns on all platforms. Changes: - Add toPosixPath() to FileSystemUtils for cross-platform path handling - Update artifact-graph/state.ts to normalize glob patterns - Consolidate path normalization in update.ts, validator.ts, and json-converter.ts to use the new utility --- src/core/artifact-graph/state.ts | 5 ++++- src/core/converters/json-converter.ts | 3 ++- src/core/update.ts | 2 +- src/core/validation/validator.ts | 7 ++++--- src/utils/file-system.ts | 8 ++++++++ 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/core/artifact-graph/state.ts b/src/core/artifact-graph/state.ts index 449bb723..55c6fd08 100644 --- a/src/core/artifact-graph/state.ts +++ b/src/core/artifact-graph/state.ts @@ -3,6 +3,7 @@ import * as path from 'node:path'; import fg from 'fast-glob'; import type { CompletedSet } from './types.js'; import type { ArtifactGraph } from './graph.js'; +import { FileSystemUtils } from '../../utils/file-system.js'; /** * Detects which artifacts are completed by checking file existence in the change directory. @@ -54,8 +55,10 @@ function isGlobPattern(pattern: string): boolean { /** * Checks if a glob pattern has any matches. + * Normalizes Windows backslashes to forward slashes for cross-platform glob compatibility. */ function hasGlobMatches(pattern: string): boolean { - const matches = fg.sync(pattern, { onlyFiles: true }); + const normalizedPattern = FileSystemUtils.toPosixPath(pattern); + const matches = fg.sync(normalizedPattern, { onlyFiles: true }); return matches.length > 0; } diff --git a/src/core/converters/json-converter.ts b/src/core/converters/json-converter.ts index 162b4e7b..b8468c2a 100644 --- a/src/core/converters/json-converter.ts +++ b/src/core/converters/json-converter.ts @@ -3,6 +3,7 @@ import path from 'path'; import { MarkdownParser } from '../parsers/markdown-parser.js'; import { ChangeParser } from '../parsers/change-parser.js'; import { Spec, Change } from '../schemas/index.js'; +import { FileSystemUtils } from '../../utils/file-system.js'; export class JsonConverter { convertSpecToJson(filePath: string): string { @@ -43,7 +44,7 @@ export class JsonConverter { } private extractNameFromPath(filePath: string): string { - const normalizedPath = filePath.replaceAll('\\', '/'); + const normalizedPath = FileSystemUtils.toPosixPath(filePath); const parts = normalizedPath.split('/'); for (let i = parts.length - 1; i >= 0; i--) { diff --git a/src/core/update.ts b/src/core/update.ts index 6d75898e..41fd7720 100644 --- a/src/core/update.ts +++ b/src/core/update.ts @@ -107,7 +107,7 @@ export class UpdateCommand { if (updatedSlashFiles.length > 0) { // Normalize to forward slashes for cross-platform log consistency - const normalized = updatedSlashFiles.map((p) => p.replace(/\\/g, '/')); + const normalized = updatedSlashFiles.map((p) => FileSystemUtils.toPosixPath(p)); summaryParts.push(`Updated slash commands: ${normalized.join(', ')}`); } diff --git a/src/core/validation/validator.ts b/src/core/validation/validator.ts index c15b70c2..e6928cbd 100644 --- a/src/core/validation/validator.ts +++ b/src/core/validation/validator.ts @@ -5,12 +5,13 @@ import { SpecSchema, ChangeSchema, Spec, Change } from '../schemas/index.js'; import { MarkdownParser } from '../parsers/markdown-parser.js'; import { ChangeParser } from '../parsers/change-parser.js'; import { ValidationReport, ValidationIssue, ValidationLevel } from './types.js'; -import { +import { MIN_PURPOSE_LENGTH, MAX_REQUIREMENT_TEXT_LENGTH, - VALIDATION_MESSAGES + VALIDATION_MESSAGES } from './constants.js'; import { parseDeltaSpec, normalizeRequirementName } from '../parsers/requirement-blocks.js'; +import { FileSystemUtils } from '../../utils/file-system.js'; export class Validator { private strictMode: boolean; @@ -359,7 +360,7 @@ export class Validator { } private extractNameFromPath(filePath: string): string { - const normalizedPath = filePath.replaceAll('\\', '/'); + const normalizedPath = FileSystemUtils.toPosixPath(filePath); const parts = normalizedPath.split('/'); // Look for the directory name after 'specs' or 'changes' diff --git a/src/utils/file-system.ts b/src/utils/file-system.ts index 364b5cda..bce759d5 100644 --- a/src/utils/file-system.ts +++ b/src/utils/file-system.ts @@ -42,6 +42,14 @@ function findMarkerIndex( } export class FileSystemUtils { + /** + * Converts a path to use forward slashes (POSIX style). + * Essential for cross-platform compatibility with glob libraries like fast-glob. + */ + static toPosixPath(p: string): string { + return p.replace(/\\/g, '/'); + } + private static isWindowsBasePath(basePath: string): boolean { return /^[A-Za-z]:[\\/]/.test(basePath) || basePath.startsWith('\\'); }