Skip to content

Fix docref issues #1–#4 and open-review findings#5

Merged
PaulDotterer merged 8 commits into
mainfrom
fix/docref-issues-review-findings
Jul 6, 2026
Merged

Fix docref issues #1–#4 and open-review findings#5
PaulDotterer merged 8 commits into
mainfrom
fix/docref-issues-review-findings

Conversation

@PaulDotterer

Copy link
Copy Markdown
Contributor

Resolves the four open issues on this repo plus the actionable open-review findings.

Issues

Open-review findings

  • security: SCP_LIKE allowed an option-shaped host/path (git@-oProxyCommand=…:x, the CVE-2017-1000117 ssh RCE class). Reject a leading - on host and path, and whitespace in the path.
  • correctness: update persisted the lock before refresh; addRepo wrote docref.toml before validating reachability. Both reordered so disk writes happen only after the fetch/refresh succeed (with in-memory rollback in addRepo).
  • memory: bounded the extension's driftDocs map — evict each virtual diff doc when its tab closes.
  • tests: selfupdate.test no longer crashes collection on unsupported platforms; symbols.test's code() helper re-throws non-DocrefErrors instead of masking them.
  • lint: install.sh SC2015 + SC2086.
  • The parserFor double-load finding was verified as a false positive (synchronous get/set) and left unchanged.

Verification

414 tests pass (regressions added for #1#4, findRootOrNull, scp-url injection, and the multi-project logic), tsc clean, CLI/extension/standalone bundles build, and docref's own dogfood check is green.

Note: the vscode change (#4) is verified via tsc + bundle + pure-logic unit tests (tagRoot/mergeProjectTrees/aggregateStatusText live in the tested logic.ts), not runtime — a manual smoke test in a real multi-repo workspace before merge is worthwhile.

Issue #1 — bash symbol resolution crashed with "resolved is not a
function" on any script whose parse reached the external scanner (a
`[ … ]` test, extglob, or $'…'). tree-sitter-wasms' bash build imports a
libc symbol (isalpha) no web-tree-sitter runtime exports, so the
emscripten lazy stub blew up. Vendor a source-built bash grammar (pinned
CLI, sha256-checked) like proto, wire it into VENDORED_GRAMMARS and the
standalone binary's embed list.

Issue #2 — `docref check <directory>` surfaced a raw EISDIR. docFiles now
expands a directory argument to every scanned doc under it (a file is
still taken verbatim; a missing path is kept so the read fails loudly).

Issue #3 — the code-side anchor scan skipped dot-directories while the
resolver read them by path, so an unused marker under .github/ was never
flagged. anchorFiles globs with dot:true so both walk the same files.

Findings:
- gitcache SCP_LIKE let an option-shaped host/path through
  (git@-oProxyCommand=…:x, the CVE-2017-1000117 ssh RCE class). Reject a
  leading '-' on host and path, and whitespace in the path.
- ops.update persisted docref.lock before refresh; a refresh failure left
  the lock advanced past the docs. Write the lock only after refresh.
- ops.addRepo appended the [repos.<alias>] block before update fetched;
  an unreachable remote left a dangling block. Fetch first, persist last,
  roll back the in-memory alias on failure.
- selfupdate.test evaluated assetName at describe scope, crashing the whole
  file on an unsupported platform. Pin a fixed supported platform for the
  injected-IO decision tests.
- symbols.test's code() helper cast every error to DocrefError and read
  .code, masking a TypeError as undefined. Re-throw non-DocrefErrors.
- install.sh: SC2015 (A && B || C) and SC2086 (unquoted $dir/docref).

Tests cover each: bash external-scanner constructs, directory scoping,
dot-directory anchors, scp-url host/path injection, findRootOrNull.
The extension loaded a single project from the first workspace folder
(loadProject(findRoot(workspaceFolder))). In a multi-repo workspace the
docref.toml files sit in subdirectories, so findRoot walked up and found
none, loading a default config with no aliases and workspace-relative
paths — 100+ false "broken"/"undeclared alias" diagnostics on green docs.

Resolve each file's governing project by walking up from the file to its
nearest docref.toml (ESLint/EditorConfig style), independent of the
workspace folder:

- core: findRootOrNull(from) — the nullable governing-root walk; findRoot
  now delegates to it.
- rescan discovers every docref.toml under the workspace and scans each
  project against its own config; diagnostics and the actionable-at lookup
  are keyed by absolute path so two projects' docs never collide.
- per-document commands (completion, code actions, drift, approve, refresh,
  add-repo, create/stage anchor) resolve the active file's project.
- sidebar trees aggregate across projects with each node tagged with its
  root, so a node action targets the right repo; a single-repo workspace
  renders exactly as before. openLocation now opens absolute paths.

The pure decision logic — tagRoot, mergeProjectTrees, aggregateStatusText —
lives in logic.ts with unit tests, per the extension's thin-wiring rule.

Also bound driftDocs: evict each virtual diff doc when its tab closes, so
the map no longer grows for the life of the session.
open-review runs its `git diff origin/main...HEAD` inside the container
as root over the runner-owned checkout. git's dubious-ownership guard
then rejects the mount ("Not a git repository. Use --no-index"), so the
reviewer crashes with exit 2 before it can review — gating every PR.

Pass safe.directory=/repo via GIT_CONFIG_* env on the docker run, which
applies to every git call inside the container without a config file.
Reproduced the crash and verified the fix locally against the published
image (static scan then runs clean: 0 findings across the changed files).

The dogfood job doesn't hit this because `docref check` tolerates git
failing (it falls back to a glob); open-review hard-fails on the diff.
- vscode onFsEvent (WARNING, gating): the watcher's per-project match used
  `uri.fsPath.startsWith(s.root + '/')` with a hardcoded '/', so on Windows
  (backslash fsPaths) it always skipped and never rescanned on source/doc
  changes — only on-save and config events worked. Drop the buggy manual
  guard and rely on relPath, which normalizes separators and returns null
  outside root (the codebase's Windows-safe pattern, already unit-tested).
- ops.update (NOTE): it advanced project.lock in-memory before refresh but,
  unlike addRepo's project.repos rollback, did not restore it if refresh
  threw — leaving a reused Project advanced past disk. Add the symmetric
  rollback, with a regression test (red-checked) that forces refresh to
  fail mid-update and asserts both the in-memory and on-disk lock are intact.

The one discarded finding (tagRoot in-place mutation) was correctly dropped
by the judge as speculative — the docstring documents the mutation and all
callers pass freshly-built trees.
open-review flagged the synchronous statSync inside async docFiles as
event-loop-blocking. Swap both statSync sites (docFiles and anchors) for
`stat` from node:fs/promises; the anchors loop now does one stat per file
instead of two. Pure internal change — no behavioral difference — so the
anchors claim prose is re-approved unchanged.
open-review flagged two write-ordering gaps in the earlier rollback work:

- update(): writeLock ran outside the try/catch, so a lock-write failure
  (disk full) left the in-memory lock advanced while the file stayed old.
  Move writeLock inside the guarded region — on failure the in-memory lock
  is rolled back to match the unwritten file.
- addRepo(): appendRepoBlock ran after the lock-writing update, so an
  appendRepoBlock failure left a lock entry with no toml declaration.
  Reorder to: checkOnly update (reachability, no writes) → append the toml
  block → real update (lock + refresh). A failure now persists nothing, or
  at worst leaves a declared-but-unlocked alias (recoverable via update),
  never a lock without its declaration.

Also reworded the docFiles stat-catch comment: it swallows any stat error
(missing / permission / symlink), not only ENOENT, so it no longer claims
the downstream read always reports ENOENT.
… nodes

Regression from the multi-repo refactor: staged sidebar items (buildStageTree)
carry no `root`, so clicking one called openAnchor(ref, undefined) and the new
`if (!root || !refRaw) return` guard silently did nothing — where the old code
used workspaceRoot(). Fall back to the active editor's project when no root is
supplied, matching how the other staged-item actions resolve via projectForNode.

open-review surfaced this but its cascade discarded it on a wrong-line-number
technicality; re-verifying against the actual code confirmed the regression is
real, so fixing it despite the discard.
The issue #2 directory-scoping built its match prefix from the raw CLI arg,
but scanned docs are POSIX paths (tinyglobby). On Windows a `content\dir`
argument would match nothing and the directory would silently expand to an
empty set (exit 0, looking like a clean pass). Normalize the arg's
backslashes to forward slashes before building the prefix.

Surfaced (then discarded as low-risk) by open-review; it's the same
hardcoded-'/' vs Windows-'\' class as the onFsEvent fix, in a feature this
PR adds, so hardening it is in-scope.
@PaulDotterer PaulDotterer merged commit b0f56e0 into main Jul 6, 2026
4 checks passed
@PaulDotterer PaulDotterer deleted the fix/docref-issues-review-findings branch July 6, 2026 16:49
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