From d0f227e944b31eee52893f209d3f3e13d9859fb1 Mon Sep 17 00:00:00 2001 From: Nagi-Inaba <221035391+Nagi-Inaba@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:01:19 +0900 Subject: [PATCH 1/2] fix: treat artifact paths that differ only in case as duplicates on macOS Duplicate registered artifact paths were folded to lower case only on win32, but the default macOS filesystem is case-insensitive too. Add artifactPathKey with an injectable platform that folds case on win32 and darwin, and use it only for the two duplicate-path checks. Symlink, junction and containment comparisons keep their exact semantics on every platform. Co-Authored-By: Claude Sonnet 5.5 --- release-files.json | 1 + .../scripts/lib/audit-run-validation.mjs | 4 +-- shared/skill/scripts/lib/audit-run.mjs | 4 +-- shared/skill/scripts/lib/safe-file-io.mjs | 5 +++ tests/artifact-path-identity.test.mjs | 31 +++++++++++++++++++ 5 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 tests/artifact-path-identity.test.mjs diff --git a/release-files.json b/release-files.json index a7247ab..13cd3d6 100644 --- a/release-files.json +++ b/release-files.json @@ -346,6 +346,7 @@ "tests/agent-role-contracts.test.mjs", "tests/architecture-doc.test.mjs", "tests/artifact-authoring.test.mjs", + "tests/artifact-path-identity.test.mjs", "tests/assessment-output-contract.test.mjs", "tests/assessment-review-provenance.test.mjs", "tests/attestation-verifier.test.mjs", diff --git a/shared/skill/scripts/lib/audit-run-validation.mjs b/shared/skill/scripts/lib/audit-run-validation.mjs index 53d47e1..8c49a4a 100644 --- a/shared/skill/scripts/lib/audit-run-validation.mjs +++ b/shared/skill/scripts/lib/audit-run-validation.mjs @@ -10,7 +10,7 @@ import { validateParticipantBindings, collectParticipantEvidence } from "./parti import { validateDeclaredChangeBindings, collectDeclaredChangeEvidence } from "./declared-change.mjs"; import { collectFixExecutionEvidence } from "./fix-execution-evidence.mjs"; import { targetBindingErrors } from "./run-targets.mjs"; -import { pathKey, parseJsonBytes, readStableFile, assertStableFile, resolveInside } from "./safe-file-io.mjs"; +import { artifactPathKey, parseJsonBytes, readStableFile, assertStableFile, resolveInside } from "./safe-file-io.mjs"; import { defaultSkillRoot, loadAuditResources } from "./audit-resources.mjs"; import { auditRunRegistryCompatibility, auditRunEnvelopeCompatibility } from "./orchestration-contracts.mjs"; import { artifactRootFor, validateArtifact, registeredArtifactPath, canonicalPermissions, remediationPermissionError, validateRegisteredArtifactEntries, validateArtifactEnvelopeSemantics, validateHumanQueueBindings, validateScreeningProfileBindings, validateDeclaredHumanBindings, validateRemediationBindings } from "./audit-artifact-validation.mjs"; @@ -158,7 +158,7 @@ export function validateAuditRun(run, { skillRoot = defaultSkillRoot, runFile, r if (!artifactRoot) continue; try { const file = registeredArtifactPath(artifactRoot, entry); - const canonicalPath = pathKey(file); + const canonicalPath = artifactPathKey(file); if (canonicalArtifactPaths.has(canonicalPath)) errors.push(`Duplicate canonical artifact path: ${String(entry?.path)}.`); canonicalArtifactPaths.add(canonicalPath); const snapshot = readArtifactFile(file, { label: `registered artifact ${String(entry?.artifact_id)}` }); diff --git a/shared/skill/scripts/lib/audit-run.mjs b/shared/skill/scripts/lib/audit-run.mjs index 4bfd722..78138ba 100644 --- a/shared/skill/scripts/lib/audit-run.mjs +++ b/shared/skill/scripts/lib/audit-run.mjs @@ -16,7 +16,7 @@ import { networkPolicyErrors } from "./network-policy.mjs"; import { afterChangeRun } from "./declared-change.mjs"; import { collectFixExecutionEvidence } from "./fix-execution-evidence.mjs"; import { targetInventoryErrors, targetBindingErrors, checkLocalRunTargets, consumeRunTargetCheck, checkRunTargets } from "./run-targets.mjs"; -import { pathKey, isWithinPath, hasTraversal, inspectRealComponents, parseJsonBytes, readStableFile, assertStableFile, resolveInside } from "./safe-file-io.mjs"; +import { artifactPathKey, isWithinPath, hasTraversal, inspectRealComponents, parseJsonBytes, readStableFile, assertStableFile, resolveInside } from "./safe-file-io.mjs"; import { defaultSkillRoot, loadAuditResources } from "./audit-resources.mjs"; import { compareText } from "./text-order.mjs"; import { roleFor, validateArtifact, registeredArtifactPath, canonicalPermissions, remediationPermissionError, validateFixHandoffBinding, validateChangeRecordAuthorizationBinding, assertCurrentOperationalRun } from "./audit-artifact-validation.mjs"; @@ -173,7 +173,7 @@ export function registerArtifact(run, artifact, options = {}) { if (relativeSegments.includes(".fix-consumption") || relativeSegments.some((segment) => segment.includes("pending-change-record"))) { throw new Error(`Internal fixer runtime evidence is not registerable as a completed artifact: ${relativePath}`); } - if (run.artifacts.some((entry) => pathKey(registeredArtifactPath(validation.artifactRoot, entry)) === pathKey(artifactPath))) { + if (run.artifacts.some((entry) => artifactPathKey(registeredArtifactPath(validation.artifactRoot, entry)) === artifactPathKey(artifactPath))) { throw new Error(`Duplicate artifact path: ${relativePath}`); } const snapshot = readStableFile(artifactPath, { label: "artifact file" }); diff --git a/shared/skill/scripts/lib/safe-file-io.mjs b/shared/skill/scripts/lib/safe-file-io.mjs index b4e7dae..3bb6a30 100644 --- a/shared/skill/scripts/lib/safe-file-io.mjs +++ b/shared/skill/scripts/lib/safe-file-io.mjs @@ -11,6 +11,11 @@ export function pathKey(value) { return process.platform === "win32" ? normalized.toLowerCase() : normalized; } +export function artifactPathKey(value, platform = process.platform) { + const normalized = path.normalize(path.resolve(value)); + return platform === "win32" || platform === "darwin" ? normalized.toLowerCase() : normalized; +} + export function samePath(left, right) { return pathKey(left) === pathKey(right); } diff --git a/tests/artifact-path-identity.test.mjs b/tests/artifact-path-identity.test.mjs new file mode 100644 index 0000000..cb7f3db --- /dev/null +++ b/tests/artifact-path-identity.test.mjs @@ -0,0 +1,31 @@ +import assert from "node:assert/strict"; +import path from "node:path"; +import test from "node:test"; +import { artifactPathKey, pathKey, samePath } from "../codex/skills/information-accessibility-practice/scripts/lib/safe-file-io.mjs"; + +for (const platform of ["darwin", "win32", "linux"]) { + test(`artifact path identity ${platform === "linux" ? "preserves" : "folds"} case on ${platform}`, () => { + const upper = artifactPathKey("artifacts/Report.json", platform); + const lower = artifactPathKey("artifacts/report.json", platform); + if (platform === "linux") assert.notEqual(upper, lower); + else assert.equal(upper, lower); + assert.equal(upper, artifactPathKey(path.resolve("artifacts/Report.json"), platform)); + }); +} + +test("artifact path identity defaults to the current platform", () => { + assert.equal(artifactPathKey("artifacts/Report.json"), artifactPathKey("artifacts/Report.json", process.platform)); +}); + +test("path safety comparisons preserve case on darwin", () => { + const descriptor = Object.getOwnPropertyDescriptor(process, "platform"); + try { + Object.defineProperty(process, "platform", { value: "darwin" }); + assert.notEqual(pathKey("artifacts/Report.json"), pathKey("artifacts/report.json")); + assert.equal(samePath("artifacts/Report.json", "artifacts/report.json"), false); + assert.equal(samePath("artifacts/Report.json", path.resolve("artifacts/Report.json")), true); + assert.equal(artifactPathKey("artifacts/Report.json"), artifactPathKey("artifacts/report.json")); + } finally { + Object.defineProperty(process, "platform", descriptor); + } +}); From e86c481f54acbc899856bfb87634ed3481763440 Mon Sep 17 00:00:00 2001 From: Nagi-Inaba <221035391+Nagi-Inaba@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:13:50 +0900 Subject: [PATCH 2/2] docs: record the macOS duplicate artifact path change Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b16c5b7..932261b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -130,6 +130,7 @@ This project uses a Keep a Changelog-style structure. Until a tagged release is - Additional WCAG requirement IDs used by `jp-public-web` are accepted by screening validation. - Formal claim blockers are separated from auxiliary screening candidates. - Public report visibility (`report --visibility public`) now withholds every target host that the Web network guard refuses, and records the reason in the redaction manifest. This adds IPv4-mapped, NAT64, 6to4 and site-local IPv6 forms, `192.88.99.0/24`, and `*.localhost` / `*.localdomain` names. Earlier builds kept some of these hosts; regenerate public reports made with them and complete publication review again. +- Registering or validating artifact paths that differ only in letter case is now rejected as a duplicate on macOS as well as Windows, because the default macOS filesystem is case-insensitive. Symlink, junction and containment checks are unchanged. A case-sensitive macOS volume can report a false duplicate; that is deliberate. ## Compatibility notes