Skip to content

Propagate the docstring gate entry guard fix so an unresolvable entry cannot pass the gate - #32

Merged
unbraind merged 6 commits into
mainfrom
fix-docstring-gate-entry-guard
Aug 10, 2026
Merged

unbraind merged 6 commits into
mainfrom
fix-docstring-gate-entry-guard

Conversation

@unbraind

@unbraind unbraind commented Aug 10, 2026 •

Copy link
Copy Markdown
Owner

Propagate the docstring gate entry guard fix

Summary

The isMainInvocation guard in scripts/docstring-gate.ts caught realpathSync
errors and returned false. When argv[1] could not be resolved, the top-level
selector called the no-op placeholder instead of main, so npm run docstring
exited 0 having scanned nothing — a mandatory release gate reporting success
without doing its job.

The corrected implementation propagates the realpathSync error. A broken
environment must not silently satisfy a gate; crashing loudly is the safe outcome.

Changes

  • scripts/docstring-gate.ts: isMainInvocation now propagates realpathSync
    errors instead of catching them and returning false. The JSDoc is updated to
    document the new @throws contract and the rationale.
  • test/docstring-gate.test.ts: the test asserting false for an unresolvable
    argv[1] is replaced with one asserting assert.throws(..., /ENOENT/).

Verification

  • npm test — 249 tests pass
  • npm run docstring — exits 0, prints the "N file(s), N declaration(s)" line
  • Manual check: isMainInvocation(["node","/nonexistent/x.ts"],"file:///x")
    throws ENOENT (printed "GOOD: threw ENOENT")

pm item

pm-github-wb4q

Summary by Sourcery

Ensure the docstring gate process entry guard fails loudly when the entry script path cannot be resolved so a broken environment cannot silently skip the mandatory docstring check.

Bug Fixes:

  • Change the isMainInvocation check to propagate realpathSync failures when argv[1] is unresolvable, preventing the docstring gate from exiting successfully without scanning.
  • Update the docstring gate tests to assert that an unresolvable entry path throws an error instead of returning false.

Enhancements:

  • Simplify isMainInvocation to compare the resolved entry path as a file URL directly against import.meta.url and clarify its JSDoc around safety and failure behavior.

Documentation:

  • Document the corrected behavior and rationale of the docstring gate entry guard in JSDoc and add a corresponding entry to the changelog.

Chores:

  • Record the associated product management item in the pm history and issues directories.

Summary by cubic

Fixes the docstring gate to fail loudly on missing or invalid entry paths and makes the main check robust to symlink-preserving runtime flags. Clarifies comments and removes an unused pathToFileURL import.

  • Bug Fixes
    • Canonicalize both argv[1] and import.meta.url with realpathSync before comparing, so --preserve-symlinks* cannot cause a silent skip.
    • Tests cover a symlinked moduleUrl; use process.execPath and assert error.code === "ENOENT" for an unresolvable entry; clarify a test comment.

Written for commit 7992d51. Summary will update on new commits.

Review in cubic

…ing the gate

The isMainInvocation guard caught realpathSync errors and returned false. When
argv[1] could not be resolved, the top-level selector called the no-op
placeholder instead of main, so npm run docstring exited 0 having scanned
nothing — a mandatory release gate reporting success without doing its job.

The corrected implementation propagates the realpathSync error. The case
requires argv[1] to stop resolving after Node has already loaded this file, so
in practice it means the environment is broken, and a broken environment must
not silently satisfy a gate. Crashing loudly is the safe outcome.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @unbraind, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@sourcery-ai

sourcery-ai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Reviewer's Guide

Propagates the docstring gate main-invocation guard fix so that an unresolvable entry path now crashes loudly instead of silently skipping the mandatory docstring gate, aligning runtime behavior, tests, and changelog with the new contract.

Sequence diagram for docstring gate main invocation guard behavior

sequenceDiagram
  participant Process
  participant DocstringGate as scripts_docstring_gate
  participant FS as node_fs_realpathSync
  participant URL as node_url_pathToFileURL

  Process->>DocstringGate: isMainInvocation(process.argv, import.meta.url)
  alt [argv[1] resolves]
    DocstringGate->>FS: realpathSync(argv[1])
    FS-->>DocstringGate: resolvedEntry
    DocstringGate->>URL: pathToFileURL(resolvedEntry)
    URL-->>DocstringGate: href
    DocstringGate-->>Process: href === moduleUrl
    opt [isMainInvocation returns true]
      Process->>DocstringGate: main(root)
    end
  else [argv[1] cannot be resolved]
    DocstringGate->>FS: realpathSync(argv[1])
    FS-->>DocstringGate: ENOENT
    DocstringGate-->>Process: ENOENT propagates
  end
Loading

File-Level Changes

Change Details Files
Change isMainInvocation to compare URLs and propagate realpathSync failures so an unresolvable argv[1] cannot silently bypass the docstring gate.
  • Replace fileURLToPath+realpathSync(self) with pathToFileURL(realpathSync(entry)) URL comparison against moduleUrl.
  • Remove try/catch around realpathSync(entry) so ENOENT and similar errors throw instead of returning false.
  • Update isMainInvocation JSDoc to describe URL-based comparison, changed failure mode, and gate rationale.
scripts/docstring-gate.ts
Update tests to assert that an unresolvable entry path throws, and to exercise the real docstring gate script URL.
  • Remove test case that expected false for a non-existent entry path and the separate self-path-unresolvable test.
  • Add a focused test that builds the real docstring-gate.ts URL and asserts isMainInvocation throws ENOENT when argv[1] points to a missing file.
  • Keep existing positive/negative matching behavior and argv[1] undefined behavior assertions intact.
test/docstring-gate.test.ts
Record the fix and its tracking artifacts in project metadata.
  • Add a fixed-item line to CHANGELOG describing propagation of the docstring gate entry guard fix and linking the pm issue.
  • Add pm history JSONL entry for pm-github-wb4q.
  • Add pm issue descriptor file pm-github-wb4q.toon.
CHANGELOG.md
.agents/pm/history/pm-github-wb4q.jsonl
.agents/pm/issues/pm-github-wb4q.toon

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

Summary by CodeRabbit

  • Bug Fixes

    • Improved command entry detection so unresolved paths now report an error instead of silently skipping validation.
    • Added consistent path resolution for entry points and module paths, including symlinked paths.
    • Preserved expected behavior when no entry path is provided or when a different path is invoked.
  • Documentation

    • Added an Unreleased changelog entry describing the fix.

Walkthrough

The docstring gate now canonicalizes both argv[1] and the module URL path. Unresolved entry paths propagate ENOENT. Tests, changelog content, and PM records document the updated behavior.

Changes

Docstring gate entry guard

Layer / File(s) Summary
Entry guard behavior and verification
scripts/docstring-gate.ts, test/docstring-gate.test.ts, CHANGELOG.md, .agents/pm/...
isMainInvocation canonicalizes both paths with realpathSync. Symlinked module URLs are recognized as direct invocations. Unresolved argv[1] paths now propagate ENOENT. Tests and project records document the fix.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the main change: preventing unresolved entry paths from silently bypassing the docstring gate.
Description check ✅ Passed The description directly explains the entry-guard fix, its rationale, tests, verification, and related documentation changes.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-docstring-gate-entry-guard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 10, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR fixes isMainInvocation in the docstring release gate so that an unresolvable argv[1] throws instead of returning false. The previous try/catch caused npm run docstring to exit 0 silently when the entry path could not be resolved, making the mandatory gate appear to succeed without scanning anything.

  • scripts/docstring-gate.ts: isMainInvocation is reduced to a single comparison realpathSync(entry) === realpathSync(fileURLToPath(moduleUrl)), with both sides now canonicalised. The pathToFileURL import (no longer needed in the script) is removed, and the JSDoc documents the new @throws contract and its rationale.
  • test/docstring-gate.test.ts: The assertion that an absent argv[1] returns false is removed; a new test asserts ENOENT is thrown instead. A second new test exercises the --preserve-symlinks path by placing the symlink in moduleUrl rather than argv[1].

Confidence Score: 5/5

  • Safe to merge. The change is a targeted, well-understood removal of a try/catch that was hiding a failure mode; both the implementation and its tests are correct.
  • The implementation change is a single-line simplification whose correctness is easy to verify. The new ENOENT test directly exercises the removed catch branch, confirming the propagation behaviour. The symlink test correctly validates the both-sides canonicalisation. No regressions in the unmodified code paths.
  • No files require special attention.

Important Files Changed

Filename Overview
scripts/docstring-gate.ts isMainInvocation simplified to a one-liner that canonicalises both argv[1] and moduleUrl through realpathSync, removing the try/catch that silently swallowed ENOENT and let the gate exit 0. Unused pathToFileURL import removed. JSDoc updated with accurate @throws contract.
test/docstring-gate.test.ts Removed the now-incorrect assertion that an absent argv[1] returns false; replaced with an ENOENT-throws assertion that directly exercises the catch removal. Added a symlink test for the --preserve-symlinks case. Both new tests are structurally sound.
CHANGELOG.md Adds a changelog entry for this fix under the current unreleased section, consistent with existing entries.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[process.argv and import.meta.url] --> B[isMainInvocation]
    B --> C{argv1 undefined?}
    C -- yes --> D[return false]
    C -- no --> E[realpathSync of argv1]
    E -- throws --> F[propagate error - non-zero exit]
    E -- resolves --> G[realpathSync of fileURLToPath moduleUrl]
    G -- throws --> F
    G -- resolves --> H{paths equal?}
    H -- yes --> I[call main - gate runs]
    H -- no --> J[return false - test import skips gate]
Loading

Reviews (11): Last reviewed commit: "test(gate): drop a cross-reference to a ..." | Re-trigger Greptile

@unbraind

Copy link
Copy Markdown
Owner Author

@coderabbitai full review
@greptileai

Reviewer context — this is a small change with an inverted premise behind it, so please read
the reasoning rather than just the diff:

isMainInvocation used to catch a realpathSync failure and return false. The top-level
selector reads false as "not the entry point" and calls the no-op placeholder, so
npm run docstring exited 0 having scanned nothing — a required release check reporting
success without doing its job, which is the single failure this gate exists to prevent.
Letting realpathSync throw turns that into a loud non-zero exit.

The original comment and its test both used the phrase "fail closed" to mean "does not
crash". That inverts it: here the crash is the safe outcome. Because this launcher was
copied between repositories rather than published as a dependency, the wrong reasoning was
copied with it and appeared independently confirmed in every adopting repo — code, comment
and test all agreeing with each other and all wrong.

Specific things worth checking:

  1. The replacement test asserts the throw (assert.throws(..., /ENOENT/)) rather than
    accepting either outcome.
  2. A genuinely different entry path must still return false — that is how a test importing
    the module declines to run the gate. Only an unresolvable entry should propagate.
  3. Coverage thresholds are untouched; please confirm nothing was weakened to accommodate the
    removed branch.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Rate Limit Exceeded

@unbraind have exceeded the limit for the number of chat messages per hour. Please wait 44 minutes and 56 seconds before sending another message.

…gate

Greptile and CodeRabbit independently flagged the same hole in the fix from
the previous commit, on two different repositories.

Comparing `pathToFileURL(realpathSync(entry)).href` against a raw
`moduleUrl` resolves only one side. That is sufficient under Node's
defaults, where the ESM loader realpaths a module before recording
`import.meta.url`. Under `--preserve-symlinks` or
`--preserve-symlinks-main` it is not: `moduleUrl` keeps the symlink while
`realpathSync(entry)` resolves it, so a direct invocation through a symlink
compares unequal, the selector calls the placeholder, and `npm run
docstring` exits 0 without scanning. That is the exact silent skip this
function exists to prevent, reintroduced by a launch flag.

Measured rather than argued. With `moduleUrl` holding the symlink path:

  both-sides (new): true
  one-sided (old):  false

Canonicalising both sides costs one syscall and removes the dependence on
how Node was launched.

The tests also now use `process.execPath` rather than the literal "node",
so the argv matches a real invocation on systems where the binary is named
differently, and assert on `error.code === "ENOENT"` rather than matching
the message text, which is not part of Node's contract.
@unbraind

Copy link
Copy Markdown
Owner Author

@coderabbitai full review
@greptileai

New commits since the last pass. All previous threads on this PR are replied to, voted on and
resolved.

What changed and why, because the second round came from you rather than from me:

Greptile (on pm-beads) and CodeRabbit (on pm-slack) independently found that the fix in the
first round was itself incomplete. It compared pathToFileURL(realpathSync(entry)).href
against a raw moduleUrl — resolving one side only. That is sufficient under Node's
defaults, where the ESM loader realpaths a module before recording import.meta.url. It is
not sufficient under --preserve-symlinks / --preserve-symlinks-main, where moduleUrl
keeps the symlink while realpathSync(entry) resolves it. The comparison then returns false
on a direct invocation, the selector calls the no-op placeholder, and npm run docstring
exits 0 without scanning — the exact silent skip this PR exists to remove, reachable via a
launch flag.

Measured with moduleUrl holding the symlink path:

both-sides (new): true
one-sided (old):  false

Both sides are now canonicalised. Also applied from Sourcery on sibling PRs: process.execPath
in place of the literal "node", and an error-code predicate (error.code === "ENOENT")
rather than a message regex, since the message text is not part of Node's contract and a regex
would match any error whose text happens to contain ENOENT.

What is most worth your attention this round:

  1. Does canonicalising both sides break the negative case? A genuinely different entry path
    must still return false — that is how a test import declines to run the gate. Only an
    unresolvable path should propagate.
  2. Coverage thresholds are untouched in every repo. If you see a threshold moved, that is a
    finding I want.
  3. Is there any remaining path by which this guard can return false for a real direct
    invocation? That is the failure mode with teeth; a spurious throw is loud and harmless by
    comparison.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== PR diff summary =='
git diff --stat HEAD^ HEAD
echo
echo '== Changed paths =='
git diff --name-status HEAD^ HEAD
echo
echo '== Current PR diff =='
git diff --unified=80 HEAD^ HEAD -- scripts/docstring-gate.ts test/docstring-gate.test.ts package.json CHANGELOG.md
echo
echo '== Source outline =='
ast-grep outline scripts/docstring-gate.ts --items all || true
echo
echo '== Test outline =='
ast-grep outline test/docstring-gate.test.ts --items all || true
echo
echo '== Package scripts and coverage-related configuration =='
if [ -f package.json ]; then
  sed -n '/"scripts"[[:space:]]*:/,/^[[:space:]]*},\{0,1\}[[:space:]]*$/p' package.json
  rg -n -i -C 3 'coverage|threshold|docstring' package.json
fi
echo
echo '== Repository coverage configuration files =='
fd -HI -t f '^(vitest|jest|nyc|c8|\.nycrc|package)\..*|^\.nycrc.*$|^tsconfig.*\.json$' . \
  -E node_modules -E .git | while IFS= read -r file; do
    if rg -q -i 'coverage|threshold' "$file"; then
      echo "--- $file"
      rg -n -i -C 3 'coverage|threshold' "$file"
    fi
  done

Length of output: 20679


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Invocation tests with line numbers =='
nl -ba test/docstring-gate.test.ts | sed -n '60,105p'
echo
echo '== Symlink fixture and invocation-case searches =='
rg -n -i -C 3 'symlink|isMainInvocation\s*\(' test scripts \
  -g '!node_modules/**' -g '!dist/**'
echo
echo '== Coverage threshold diff confirmation =='
git diff --unified=8 HEAD^ HEAD -- package.json

Length of output: 784


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Invocation tests with line numbers =='
awk 'NR >= 60 && NR <= 105 { printf "%6d  %s\n", NR, $0 }' test/docstring-gate.test.ts
echo
echo '== Symlink fixture and invocation-case searches =='
rg -n -i -C 3 'symlink|isMainInvocation\s*\(' test scripts \
  -g '!node_modules/**' -g '!dist/**' || true
echo
echo '== Coverage threshold diff confirmation =='
git diff --unified=8 HEAD^ HEAD -- package.json

Length of output: 6396


@unbraind: I found one test-coverage gap.

P2 — Add a symlink regression case for isMainInvocation.
test/docstring-gate.test.ts Line 94 tests only a regular path. The previous one-sided comparison also passes that test because pathToFileURL(realpathSync(script)).href equals the regular-path URL. Therefore, the test suite does not detect the --preserve-symlinks failure that this change fixes.

Add a fixture where both argv[1] and moduleUrl use a symlink to the same script. The assertion must require true. That case fails with the previous implementation and passes only when both sides are canonicalised.

The implementation in scripts/docstring-gate.ts Line 127 otherwise has the required behavior. A different resolvable path returns false. An unresolved entry propagates its error. The PR does not change the coverage thresholds.


✏️ Learnings added
Learnt from: unbraind
URL: https://github.com/unbraind/pm-github/pull/32

Timestamp: 2026-08-10T06:54:05.454Z
Learning: In `scripts/docstring-gate.ts`, `isMainInvocation` must canonicalise both `argv[1]` and `fileURLToPath(moduleUrl)` with `realpathSync`. Under Node.js `--preserve-symlinks` or `--preserve-symlinks-main`, `import.meta.url` can retain a symlink path; canonicalising only the entry path can make a direct docstring-gate invocation return `false` and silently skip the required gate.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/docstring-gate.test.ts`:
- Around line 102-112: Add a regression test alongside the existing
isMainInvocation tests that creates a real script and symlink, passes the
symlink path as argv[1] and its file URL as moduleUrl, and asserts
isMainInvocation returns true. Ensure the test cleans up the temporary script
and symlink, covering canonicalization of both invocation arguments.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1d1acb8c-a099-4d54-9ded-d60a1ef891f8

📥 Commits

Reviewing files that changed from the base of the PR and between c14ff2b and 0bc2477.

📒 Files selected for processing (5)
  • .agents/pm/history/pm-github-wb4q.jsonl
  • .agents/pm/issues/pm-github-wb4q.toon
  • CHANGELOG.md
  • scripts/docstring-gate.ts
  • test/docstring-gate.test.ts

Comment thread test/docstring-gate.test.ts
…ails

The existing test (isMainInvocation resolves matching and non-matching
scripts) did not use symlinks at all, so it could not distinguish the
fixed implementation from the broken one.

The new test puts the symlink in moduleUrl (pathToFileURL(link).href),
which is what Node records in import.meta.url under --preserve-symlinks /
--preserve-symlinks-main. The old comparison resolves argv[1] to the real
path and compares it to the symlink URL, which is false, so the gate
silently skips. The canonicalized comparison resolves both sides through
realpathSync and returns true.

Measured for this repo: reverting to the old one-sided comparison makes
the new test fail (fail 1), restoring the canonicalization makes it pass
(fail 0).
@unbraind

Copy link
Copy Markdown
Owner Author

@greptileai

New commit since the last pass. (Deliberately not @-mentioning CodeRabbit this round —
mentioning it on many PRs at once exhausted its hourly chat quota earlier and produced green
checks whose body read "Review rate limited", which is a review that never happened. Its
automatic incremental review covers the new commit.)

What changed: CodeRabbit found that the symlink regression test added in the previous
round could not distinguish the fixed implementation from the broken one. It passed the link
as argv[1] and the real path as moduleUrl — and since realpathSync(link) resolves to
that same real path, the old one-sided comparison satisfied it too. A regression test that
passes against the bug it guards is not a regression test.

The added test puts the symlink in moduleUrl, which is what Node records in
import.meta.url under --preserve-symlinks. Verified by reverting the one-line
implementation change and re-running the gate test file:

one-sided (old):  fail 1
both-sides (new): fail 0

The original symlink test is kept — argv[1] through an npm bin shim is a real and separate
case.

Worth your attention: this is the second time in this series that something which looked
like a working check was not one — the gate exited 0 without scanning, and then the test
guarding that fix passed without discriminating. Same shape, one level up. If you see any
other assertion in this file that would still pass with the implementation reverted, that is
the finding I want most.

CodeRabbit flagged that the comment said canonicalising both sides costs one
syscall while the function calls realpathSync twice, and each resolution can
itself require several filesystem operations. The claim was mine and it was
copied into every adopting repository along with the fix.

The accurate statement is that it adds a second realpathSync. What the
comment is actually justifying is the removal of a dependence on how Node
was launched, and that argument does not need a cost figure to stand.
@unbraind

Copy link
Copy Markdown
Owner Author

@greptileai

One more commit: a comment-only correction, no behaviour change.

CodeRabbit found that the JSDoc claimed canonicalising both sides "costs one syscall" while
the function calls realpathSync twice, and each resolution can itself require several
filesystem operations. The number was invented and wrong in both directions. It now reads
"adds a second realpathSync", which is what is actually true and is all the argument needs.

Worth noting how far it travelled: the claim was mine and was copied verbatim into 14
repositories
along with the fix. One invented number became fourteen wrong comments —
the same failure mode this PR series exists to fix, where a copied launcher carried the
comment justifying its own bug into every adopting repo, so the wrong reasoning looked
independently confirmed everywhere.

Nothing else changed in this commit. If you see any other claim in this file that is stated
with more precision than it can support, that is the finding I want.

Comment thread test/docstring-gate.test.ts
@unbraind

Copy link
Copy Markdown
Owner Author

@greptileai

Final review pass — no further changes are planned for this PR.

Since your last look the only delta is a comment-only correction: the JSDoc claimed
canonicalising both sides "costs one syscall" while the function calls realpathSync twice.
CodeRabbit caught it; the number was mine and had been copied into 14 repositories along with
the fix.

State of this PR:

  • every review thread replied to, voted on and resolved
  • all required checks green
  • gates verified locally: tests, docstring gate, coverage (thresholds unchanged), and
    changelog:check
  • the regression test is revert-proofed — reverting the implementation makes it fail

One correction to this PR's own description, raised by you on pm-github#32 and applicable
across the series: six repositories were already both-sides canonicalized on main
(pm-beads, pm-gantt-chart, pm-github, pm-jira, pm-slack, pm-slack-standup), so for those the
net change is only the catch removal and the commit subject overstated it. Nine genuinely
moved one-sided → both-sided. Details are in a comment on this PR where it applies.

If you have no further findings, this is ready to merge.

DeepScan flagged one new issue on these PRs and this is it: switching to
`realpathSync(entry) === realpathSync(fileURLToPath(moduleUrl))` removed the
last use of `pathToFileURL` in this file, but the import stayed.

Nothing else caught it. These packages have no lint script, and typecheck
does not enable noUnusedLocals, so the only gate that saw it was the
advisory one.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/docstring-gate.test.ts`:
- Around line 103-107: Update the explanatory comment before the
symlink-preservation regression test to reference the preceding regular-path
test, which uses regular files and does not pass a symlink as argv[1]. Keep the
explanation focused on why the current moduleUrl-as-symlink case distinguishes
the two implementations.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fe874d04-f2b7-4742-ae52-e70aa773a992

📥 Commits

Reviewing files that changed from the base of the PR and between 0bc2477 and 1f83a7b.

📒 Files selected for processing (4)
  • .agents/pm/history/pm-github-wb4q.jsonl
  • .agents/pm/issues/pm-github-wb4q.toon
  • scripts/docstring-gate.ts
  • test/docstring-gate.test.ts

Comment thread test/docstring-gate.test.ts Outdated
@unbraind

Copy link
Copy Markdown
Owner Author

@greptileai

One more commit, and it came from DeepScan — the advisory check that was reporting
1 new and 0 fixed issues on this PR.

Switching the entry guard to realpathSync(entry) === realpathSync(fileURLToPath(moduleUrl))
removed the last use of pathToFileURL in scripts/docstring-gate.ts, but the import stayed.
Removed now, across all 15 repositories carrying the change.

Nothing mandatory caught it. These packages have no lint script, typecheck does not
enable noUnusedLocals, and the docstring gate checks for the presence of documentation
rather than for dead code. Tests, coverage and changelog:check all passed with the unused
import in place. The only gate positioned to see it was the advisory, paywalled one — and its
detail is not readable through the API, so the count alone (1 new, identical on two PRs
carrying the identical change) is what made the hypothesis testable.

Tracked as a fleet gap rather than patched here: every package should either have a lint
script that flags unused imports, or noUnusedLocals in its typecheck config. Not attempted
in this PR because it would touch every repository while the review queue is already
saturated.

Verified after the change: typecheck, tests, docstring gate and changelog:check all pass.

@greptile-apps

greptile-apps Bot commented Aug 10, 2026

Copy link
Copy Markdown

The cleanup is clean. The file confirms the imports are now exactly right: realpathSync and fileURLToPath from node:url (still needed for fileURLToPath(moduleUrl)), pathToFileURL gone.

The fleet gap analysis is accurate and the deferral is correct. The problem has two independent failure modes:

  1. No lint script — the most direct fix; tsc with --noUnusedLocals or an ESLint no-unused-vars/@typescript-eslint/no-unused-imports rule would catch this at typecheck time with zero advisory tooling required.
  2. noUnusedLocals not enabled in tsconfig.json — a one-liner fix per repo, but it can surface pre-existing latent issues in the same pass, which is why batching it separately when the review queue is saturated is the right call.

One thing worth noting for when the fleet fix lands: noUnusedLocals only fires on locally-declared names that are never read. It would have caught pathToFileURL here. But it will not catch an import that is re-exported or used only in a type position — for that you need verbatimModuleSyntax (or importsNotUsedAsValues: "error") alongside it. If either of those patterns shows up across the fleet, the lint approach with @typescript-eslint/no-unused-imports catches all three cases without the re-export caveat.

No action needed in this PR — the dead import is gone, the gate is correct, and the gap is tracked.

@unbraind

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Final pass — no further changes planned. The only delta since your last review is the
comment-only correction you asked for (the JSDoc claimed the extra resolution "costs one
syscall" while realpathSync is called twice). All threads are replied to, voted on and
resolved, all required checks are green, and the regression test is revert-proofed.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== PR changes against base =='
git diff --name-status "$(git merge-base HEAD origin/main)" HEAD
git diff --unified=40 "$(git merge-base HEAD origin/main)" HEAD -- \
  scripts/docstring-gate.ts test/docstring-gate.test.ts package.json CHANGELOG.md

echo '== Entry-guard source map =='
ast-grep outline scripts/docstring-gate.ts --items all
echo '== isMainInvocation implementation =='
rg -n -A45 -B12 'function isMainInvocation|const isMainInvocation|isMainInvocation\s*=' scripts/docstring-gate.ts

echo '== Invocation tests =='
ast-grep outline test/docstring-gate.test.ts --items all
rg -n -A35 -B10 'isMainInvocation\s*\(|symlinkSync|ENOENT|preserve-symlinks' test/docstring-gate.test.ts

echo '== Coverage-related diff and current configuration =='
git diff --unified=20 "$(git merge-base HEAD origin/main)" HEAD -- package.json
rg -n -i -C3 'coverage|threshold' package.json . \
  -g '!node_modules/**' -g '!dist/**' -g '!build/**' \
  -g 'package.json' -g 'vitest.config.*' -g 'vite.config.*' -g 'jest.config.*' -g '.nycrc*' || true

echo '== Unused pathToFileURL references in relevant source/test files =='
rg -n -C2 '\bpathToFileURL\b' scripts test -g '!node_modules/**' -g '!dist/**' || true

Length of output: 32257


@unbraind I found one documentation-only issue.

P3 — Correct the stale test comment.
In test/docstring-gate.test.ts Line 103, the comment says that “the symlink test above” passes a link as argv[1]. The preceding test at Line 86 uses only regular paths. There is no such symlink test above this test.

Update or remove that sentence. The current regression assertion at Line 118 is valid and discriminates against the previous one-sided implementation.

I confirmed the following:

  • isMainInvocation canonicalises both paths.
  • A different resolvable entry path returns false.
  • An unresolvable entry propagates ENOENT.
  • The symlinked moduleUrl test fails with the old one-sided comparison.
  • Coverage thresholds are unchanged.
⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

CodeRabbit caught that the comment opens with "The symlink test above" while
this file has no preceding symlink test - the regression test is the first
and only one here. The wording was copied from a repository that does have
both, so the rationale read as describing a test that is not present.

Rewritten to state the property directly rather than by reference: a case
that passes the link as argv[1] and the real path as moduleUrl cannot
distinguish the two implementations, because realpathSync(link) resolves to
the real path either way.
@unbraind

Copy link
Copy Markdown
Owner Author

Merging. CodeRabbit's review is current against this head, all required checks are green, and every thread is replied to and resolved.

A correction to this PR's own description, which Greptile raised on pm-github#32 and which applies here: main was already both-sides canonicalized, so the net change is the catch removal — an unresolvable argv[1] now propagates instead of making npm run docstring exit 0 having scanned nothing. The commit subject mentioning canonicalization overstates it for this repository.

Six repos were in that position (pm-beads, pm-gantt-chart, pm-github, pm-jira, pm-slack, pm-slack-standup); nine genuinely moved one-sided → both-sided. Worth recording: the intermediate commit here temporarily replaced the stronger implementation with the weaker "canonical" one before a later commit restored it. Net zero and it never shipped, but merging mid-series would have introduced the --preserve-symlinks hole into a repo that never had it.

The symlink regression test is kept deliberately. It does not discriminate this diff, but it pins the both-sided property so a future refactor toward the one-sided reference cannot land silently — which is exactly the regression that just happened here mid-PR.

@unbraind
unbraind merged commit 28180cf into main Aug 10, 2026
7 checks passed
@unbraind
unbraind deleted the fix-docstring-gate-entry-guard branch August 10, 2026 09:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant