theam / theam/facility

markdown-links reports every local link as missing on Windows

Open
#191 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
71
Forks
64
Avg merge
15h 38m
Merged PRs (30d)
66

Description

What happens

On a Windows checkout, node guards/run.mjs fails markdown-links with a violation for
every local link in the repository — 56 of them, including targets that plainly exist:

✗ markdown-links
    CONTRIBUTING.md:12   local Markdown target "SECURITY.md" does not exist
    README.md:53         local Markdown target "CONTRIBUTING.md" does not exist
    README.md:521        local Markdown target "LICENSE" does not exist
    ... 53 more
3 guards ran, 1 failed.

actions-pinned passes in the same run, and the same checkout passes cleanly on Linux.

Because pnpm verify runs pnpm guards as a step, a Windows contributor cannot get a green
local verify
, and the failure looks like a broken checkout rather than a platform bug.

Cause

guards/markdown-links.mjs takes the repository root from git:

const root = execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();

git rev-parse --show-toplevel reports POSIX separators on every platform, so on Windows
this is C:/projects/facility. The containment check then compares it against a
path.resolve result, which uses native separators:

const exact = resolve(root, dirname(source), target);          // C:\projects\facility\SECURITY.md
if (exact !== root && !exact.startsWith(`${root}${sep}`)) return false;
//                                       ^^^^ "C:/projects/facility" + "\"

"C:\projects\facility\SECURITY.md".startsWith("C:/projects/facility\") is false, so the
function returns false before existsSync is ever called. Every in-repo target is reported
missing, and only on Windows.

Reproducible from any platform:

import { win32 } from "node:path";
const root = "C:/projects/facility";                                  // what git returns
const exact = win32.resolve(root, win32.dirname("README.md"), "SECURITY.md");
console.log(exact);                                    // C:\projects\facility\SECURITY.md
console.log(exact.startsWith(`${root}${win32.sep}`));  // false  <-- bails here
console.log(exact.startsWith(`${win32.resolve(root)}${win32.sep}`)); // true

Fix

Normalise the root with path.resolve before comparing. The containment rule itself is
unchanged, so targets that escape the repository are still rejected.

I have a PR ready: the resolution step is extracted as resolveWithinRoot with an injectable
path module so the Windows behaviour is testable from any host, plus five tests covering the
git-reported root, a native Windows root, POSIX parity, and traversal rejection on both
platforms.

Verified on the Windows machine where the failure was found — the same checkout goes from 56
violations to 2 guards ran, 0 failed — and node guards/run.mjs still passes on Linux.

Context

This is the third Windows papercut on the quickstart path, alongside #182 (pnpm dev fails
with spawn EINVAL) and #167 (Node 25+ no longer bundles corepack). Individually each is
small; together they mean a Windows contributor's first pnpm verify cannot go green. It might
be worth a Windows job in CI, even a minimal one that runs pnpm guards and pnpm test:dev
all three of these would have been caught by it. Happy to open that separately if it's wanted.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with guards/markdown-links.mjs and inspect the containment check and the proposed resolveWithinRoot resolution step. Run node guards/run.mjs and the existing guard tests; done means local links pass on Windows and Linux while traversal targets remain rejected.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
tooling
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.