ProDocs 1.1: production adoption and assurance - #15
Conversation
📝 WalkthroughWalkthroughProDocs 1.1 adds ChangesAdoption and continuous assurance
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant CLI
participant diagnoseProject
participant ProjectScanner
participant GeneratedManifest
CLI->>diagnoseProject: request readiness report
diagnoseProject->>ProjectScanner: scan project in read-only mode
ProjectScanner-->>diagnoseProject: indexed files and symbols
diagnoseProject->>GeneratedManifest: compare source and knowledge hashes
GeneratedManifest-->>diagnoseProject: freshness result
diagnoseProject-->>CLI: readiness and check results
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🤖 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 @.github/workflows/publish-npm.yml:
- Line 57: Update the checksum verification command in the publish workflow to
resolve archive paths relative to the release directory and fail when any
expected archive is missing. Remove the ineffective ignore-missing behavior so
modified or absent release archives prevent npm publishing.
In `@docs/BENCHMARKS.md`:
- Around line 7-10: Update the benchmark metadata in the ProDocs repository
section to use a documented supported Node.js runtime (20, 22, or 24) and re-run
the published baseline, or explicitly label the existing Node.js 26.4.0 result
as an unsupported-runtime measurement.
In `@docs/prodocs/KNOWLEDGE_HEALTH.md`:
- Around line 7-8: Update the renderer’s unsupported-knowledge empty-state text
to say “No unsupported knowledge is available.”, then regenerate
docs/prodocs/KNOWLEDGE_HEALTH.md using the repository’s documented generation
tooling rather than editing the generated file directly.
In `@docs/RELEASING.md`:
- Around line 63-65: Update the checksum verification command in the releasing
documentation to run from the workspace root and reference the downloaded file
as release/SHA256SUMS, matching the documented release/ path. Alternatively,
explicitly document the required working directory and prefix rewrite, but keep
the command consistent with the checksum entries and the verification example in
TABLETOPS.md.
In `@examples/getting-started/README.md`:
- Around line 3-4: Update the getting-started README instructions to avoid
implying the checked-in example is identical to the output of `prodocs
tutorial`. Align the checked-in example with the generated tutorial from
`src/tutorial.js`, or revise the documented entry points so both paths provide
equivalent tutorial artifacts without suggesting they are interchangeable when
their contents differ.
In `@examples/getting-started/test/delivery.test.js`:
- Around line 5-8: The test for retryDelivery should cover the intermediate
second attempt. In the existing “delivery retries stop after three attempts”
test, add an assertion that retryDelivery(2) returns “retry”, while preserving
the current assertions for attempts 1 and 3.
In `@src/benchmark.js`:
- Around line 11-39: Update benchmarkProject so the cold scan starts from an
isolated or explicitly cleared index, then reuse that same index for the warm
scan to measure cache reuse accurately. Preserve the existing timing and
evaluation output, and add a regression test that invokes benchmarkProject twice
and verifies the second run does not contaminate the first run’s cold
measurement.
In `@src/tutorial.js`:
- Around line 80-86: Wrap the file-creation loop in the tutorial-writing flow
with failure cleanup: if fs.mkdir() or fs.writeFile() throws, call fs.rm() on
destination before rethrowing the original error. Preserve the written tracking
and successful return behavior while ensuring partial files and directories are
removed.
In `@test/adoption.test.js`:
- Around line 90-99: Update the symlink setup in the “tutorial refuses a
symlinked output parent” test to use a junction on Windows and a directory
symbolic link on other platforms, while preserving the existing outside-root
rejection assertion.
In `@test/roadmap.test.js`:
- Around line 491-496: Update the “empty request” test case in the roadmap tests
to explicitly assert that evaluating the missing source returns zero files,
rather than relying only on precision === 1. Add the assertion against the
returned file list or packet using the existing result structure, and apply the
same correction to the related case around the second referenced range.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 69692dab-8af3-4fb0-9453-71f7483abd6a
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (51)
.github/ISSUE_TEMPLATE/adopter-feedback.yml.github/workflows/mutation.yml.github/workflows/publish-npm.yml.gitignoreCHANGELOG.mdREADME.mddocs/ADOPTION.mddocs/BENCHMARKS.mddocs/MATURITY.mddocs/MUTATION_TESTING.mddocs/OPERATIONS.mddocs/RELEASING.mddocs/ROADMAP.mddocs/SUPPLY_CHAIN.mddocs/TABLETOPS.mddocs/TROUBLESHOOTING.mddocs/VALIDATION.mddocs/knowledge/features/adoption-assurance.mddocs/prodocs/CODE_MAP.mddocs/prodocs/FEATURE_MAP.mddocs/prodocs/KNOWLEDGE_HEALTH.mddocs/prodocs/SYSTEM_OVERVIEW.mddocs/prodocs/knowledge.jsondocs/prodocs/manifest.jsondocs/prodocs/views/coding-agents.mddocs/prodocs/views/operations.mddocs/prodocs/views/product.mddocs/prodocs/views/security.mddocs/prodocs/views/support.mddocs/prodocs/views/technical.mdexamples/getting-started/CODEOWNERSexamples/getting-started/README.mdexamples/getting-started/docs/knowledge/features/delivery-retries.mdexamples/getting-started/package.jsonexamples/getting-started/prodocs.config.jsonexamples/getting-started/src/delivery.jsexamples/getting-started/test/delivery.test.jsfixtures/evaluation/agents.jsonpackage.jsonscripts/complexity.jssrc/benchmark.jssrc/cli.jssrc/constants.jssrc/doctor.jssrc/evaluate.jssrc/tutorial.jsstryker.config.jsontest/adoption.test.jstest/cli-roadmap.test.jstest/package.test.jstest/roadmap.test.js
| --pattern "SHA256SUMS" \ | ||
| --dir release | ||
| (cd release && sha256sum --check SHA256SUMS --ignore-missing) | ||
| sha256sum --check release/SHA256SUMS --ignore-missing |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
Restore effective checksum verification.
Line 57 checks archive names from the workspace root. The downloaded archives are in release/. With --ignore-missing, missing archives do not fail verification. A modified release archive can then reach npm publish.
Proposed fix
- sha256sum --check release/SHA256SUMS --ignore-missing
+ (
+ cd release
+ sha256sum --check SHA256SUMS
+ )📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| sha256sum --check release/SHA256SUMS --ignore-missing | |
| ( | |
| cd release | |
| sha256sum --check SHA256SUMS | |
| ) |
🤖 Prompt for 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.
In @.github/workflows/publish-npm.yml at line 57, Update the checksum
verification command in the publish workflow to resolve archive paths relative
to the release directory and fail when any expected archive is missing. Remove
the ineffective ignore-missing behavior so modified or absent release archives
prevent npm publishing.
| ## ProDocs repository — 2026-08-09 | ||
|
|
||
| Environment: macOS, Node.js 26.4.0. These are directional local measurements, | ||
| not latency guarantees. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Run the published baseline on a documented supported runtime.
Line 9 records the benchmark on Node.js 26.4.0. The README documents support for Node.js 20, 22, and 24. Re-run the benchmark on a supported runtime, or label this result as an unsupported-runtime measurement.
🤖 Prompt for 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.
In `@docs/BENCHMARKS.md` around lines 7 - 10, Update the benchmark metadata in the
ProDocs repository section to use a documented supported Node.js runtime (20,
22, or 24) and re-run the published baseline, or explicitly label the existing
Node.js 26.4.0 result as an unsupported-runtime measurement.
| | Authored knowledge | 9 | | ||
| | Supported | 9 | |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Fix the unsupported-knowledge empty state before regenerating.
The changed metrics report 9 authored and 9 supported items, with 0 unsupported items. Line [15] still says _No authored knowledge is available._ under Unsupported knowledge. This contradicts the report. Update the renderer to say _No unsupported knowledge is available._, then regenerate this file.
As per path instructions, generated files under docs/prodocs/** must be regenerated through repository tooling and must not be hand-edited.
🤖 Prompt for 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.
In `@docs/prodocs/KNOWLEDGE_HEALTH.md` around lines 7 - 8, Update the renderer’s
unsupported-knowledge empty-state text to say “No unsupported knowledge is
available.”, then regenerate docs/prodocs/KNOWLEDGE_HEALTH.md using the
repository’s documented generation tooling rather than editing the generated
file directly.
Source: Path instructions
| The checksum file records paths relative to the release build workspace. When | ||
| downloading into a local directory, preserve the `release/` directory or | ||
| rewrite only that known prefix before verification. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Make the checksum command match the documented release/ path.
If the downloaded checksum entries retain the release/ prefix, run verification from the workspace root with shasum -a 256 -c release/SHA256SUMS. The command at Line 60 still uses SHA256SUMS without the prefix and does not define a working directory. Update that command or state the required directory and prefix rewrite explicitly.
The reproducible verification in docs/TABLETOPS.md uses release/SHA256SUMS from the workspace root.
Proposed documentation fix
-shasum -a 256 -c SHA256SUMS
+shasum -a 256 -c release/SHA256SUMS🤖 Prompt for 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.
In `@docs/RELEASING.md` around lines 63 - 65, Update the checksum verification
command in the releasing documentation to run from the workspace root and
reference the downloaded file as release/SHA256SUMS, matching the documented
release/ path. Alternatively, explicitly document the required working directory
and prefix rewrite, but keep the command consistent with the checksum entries
and the verification example in TABLETOPS.md.
| Run `prodocs tutorial --output prodocs-tutorial` to create a fresh copy, or use | ||
| this checked-in example directly: |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline src/tutorial.js --items all
rg -n -C 4 'FILES|retryDelivery|prodocs\.config\.json|delivery-retries|getting-started' src/tutorial.js
for file in \
examples/getting-started/README.md \
examples/getting-started/prodocs.config.json \
examples/getting-started/src/delivery.js \
examples/getting-started/test/delivery.test.js \
examples/getting-started/docs/knowledge/features/delivery-retries.md
do
sha256sum "$file"
doneRepository: boyeesu/ProDocs
Length of output: 2861
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the exact generated file contents and the checked-in content in parallel.
sed -n '1,120p' src/tutorial.js
printf '\n--- examples/getting-started files ---\n'
for file in \
examples/getting-started/README.md \
examples/getting-started/prodocs.config.json \
examples/getting-started/src/delivery.js \
examples/getting-started/test/delivery.test.js \
examples/getting-started/docs/knowledge/features/delivery-retries.md
do
printf '\n!!! %s !!!\n' "$file"
sed -n '1,140p' "$file"
done
# Programmatic text comparison: compare generated file templates against checked-in file content for each entry, ignoring only trailing newline differences.
python3 - <<'PY'
import json, pathlib, re
contents = {}
for rel in contents_getting_started = []:
pass
PYRepository: boyeesu/ProDocs
Length of output: 5696
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import json, pathlib, re, sys
def normalize(s):
s = s.replace("\r\n", "\n")
return s.rstrip()
def normalized_json_object(s):
s = s.strip()
if not s.startswith("{") or not s.endswith("}"):
return ("literal", s)
try:
return ("json", json.dumps(json.loads(s), indent=2, ensure_ascii=False, sort_keys=True))
except Exception as exc:
return ("literal", s)
checks = {
"src/delivery.js": pathlib.Path("src/tutorial.js"),
"test/delivery.test.js": pathlib.Path("src/tutorial.js"),
"docs/knowledge/features/delivery-retries.md": pathlib.Path("src/tutorial.js"),
"README.md": pathlib.Path("src/tutorial.js"),
"package.json": pathlib.Path("src/tutorial.js"),
}
def read_file(path):
if path == pathlib.Path("src/tutorial.js"):
text = path.read_text("utf8")
match = re.search(r'const FILES = \{(?P<body>.*?)\n\};', text, re.S)
if not match:
print("MISSING FILES", file=sys.stderr)
sys.exit(1)
body = "[ " + match.group("body") + " ]"
# Strip surrounding whitespace, use eval rather than running repo code.
# The template uses simple JSON-compatible strings and JSON.stringify for package.json.
return json.loads(body, strict=False)
else:
return {str(path): normalize(path.read_text("utf8"))}
generated = read_file(checks["src/delivery.js"])
generated["prodocs.config.json"] = generated.pop("package.json")["name"]
paths = {
"src/delivery.js": "examples/getting-started/src/delivery.js",
"test/delivery.test.js": "examples/getting-started/test/delivery.test.js",
"docs/knowledge/features/delivery-retries.md": "examples/getting-started/docs/knowledge/features/delivery-retries.md",
"README.md": "examples/getting-started/README.md",
"package.json": "examples/getting-started/package.json",
"prodocs.config.json": "examples/getting-started/prodocs.config.json",
"CODEOWNERS": "examples/getting-started/CODEOWNERS",
}
def gen(path, kind=None):
value = generated.get(path)
if value is None:
raise SystemExit(f"Generated missing {path}")
return ("json" if kind == "json" else "literal", normalize(value.strip()))
def check(path):
print(f"\n== {path} ==")
gen_kind, gen_text = gen(path, kind=(path == "package.json"))
check_kind, check_text = gen(path, kind=(path == "package.json"))
if gen_text == check_text:
print("MATCH")
return True
print(f"GENERATED {gen_kind}:")
print(gen_text)
print(f"CHECKED-IN {check_kind}:")
print(check_text)
return False
ok = True
for path in sorted(paths, key=lambda p: paths[p]):
if not check(path):
ok = False
sys.exit(0 if ok else 1)
PYRepository: boyeesu/ProDocs
Length of output: 677
Avoid presenting this example as the generated tutorial.
prodocs tutorial writes files from src/tutorial.js directly; this checked-in example has different content, including CODEOWNERS and extra package metadata, while the generated README is shorter. Users following either documented entry point should get equivalent tutorial artifacts.
🤖 Prompt for 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.
In `@examples/getting-started/README.md` around lines 3 - 4, Update the
getting-started README instructions to avoid implying the checked-in example is
identical to the output of `prodocs tutorial`. Align the checked-in example with
the generated tutorial from `src/tutorial.js`, or revise the documented entry
points so both paths provide equivalent tutorial artifacts without suggesting
they are interchangeable when their contents differ.
| test("delivery retries stop after three attempts", () => { | ||
| assert.equal(retryDelivery(1), "retry"); | ||
| assert.equal(retryDelivery(3), "failed"); | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Test the last retry attempt.
The test does not check retryDelivery(2). An implementation that retries only attempt 1 would pass the current assertions. Add the attempt-2 assertion.
Proposed test update
test("delivery retries stop after three attempts", () => {
assert.equal(retryDelivery(1), "retry");
+ assert.equal(retryDelivery(2), "retry");
assert.equal(retryDelivery(3), "failed");
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| test("delivery retries stop after three attempts", () => { | |
| assert.equal(retryDelivery(1), "retry"); | |
| assert.equal(retryDelivery(3), "failed"); | |
| }); | |
| test("delivery retries stop after three attempts", () => { | |
| assert.equal(retryDelivery(1), "retry"); | |
| assert.equal(retryDelivery(2), "retry"); | |
| assert.equal(retryDelivery(3), "failed"); | |
| }); |
🤖 Prompt for 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.
In `@examples/getting-started/test/delivery.test.js` around lines 5 - 8, The test
for retryDelivery should cover the intermediate second attempt. In the existing
“delivery retries stop after three attempts” test, add an assertion that
retryDelivery(2) returns “retry”, while preserving the current assertions for
attempts 1 and 3.
| export async function benchmarkProject(root, config, manifest, suite) { | ||
| const cold = await timed(() => scanProject(root, config, { indexMode: "write" })); | ||
| const warm = await timed(() => scanProject(root, config, { indexMode: "write" })); | ||
| const evaluation = evaluateContextSuite(warm.value, manifest, suite, { | ||
| maxFiles: config.limits.maxContextFiles, | ||
| maxTokens: config.limits.maxContextTokens | ||
| }); | ||
| const misses = warm.value.runtime.index.cacheMisses; | ||
| const hits = warm.value.runtime.index.cacheHits; | ||
| return { | ||
| schemaVersion: 1, | ||
| kind: "prodocs.product-benchmark", | ||
| privacy: "local-only; no telemetry or repository content is transmitted", | ||
| repository: { | ||
| files: warm.value.stats.files, | ||
| symbols: warm.value.stats.symbols, | ||
| relationships: warm.value.stats.edges, | ||
| knowledge: warm.value.stats.knowledge.total | ||
| }, | ||
| indexing: { | ||
| coldMilliseconds: cold.milliseconds, | ||
| warmMilliseconds: warm.milliseconds, | ||
| warmCacheHits: hits, | ||
| warmCacheMisses: misses, | ||
| warmCacheHitRate: hits + misses === 0 ? 1 : hits / (hits + misses) | ||
| }, | ||
| context: evaluation | ||
| }; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Establish an empty index before the cold scan.
scanProject reads existing index entries. Both calls use indexMode: "write", which does not clear those entries. A later benchmark run can therefore report cache-assisted work as coldMilliseconds.
Use an isolated or cleared index for the first scan. Reuse that index only for the warm scan. Add a regression test that runs the benchmark twice.
🤖 Prompt for 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.
In `@src/benchmark.js` around lines 11 - 39, Update benchmarkProject so the cold
scan starts from an isolated or explicitly cleared index, then reuse that same
index for the warm scan to measure cache reuse accurately. Preserve the existing
timing and evaluation output, and add a regression test that invokes
benchmarkProject twice and verifies the second run does not contaminate the
first run’s cold measurement.
| const written = []; | ||
| for (const [file, contents] of Object.entries(files)) { | ||
| const target = path.join(destination, file); | ||
| await fs.mkdir(path.dirname(target), { recursive: true }); | ||
| await fs.writeFile(target, contents, { flag: "wx", encoding: "utf8" }); | ||
| written.push(path.posix.join(output, file)); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate src/tutorial.js =="
fd -a 'tutorial\.js$' .
echo "== relevant file outline =="
ast-grep outline src/tutorial.js || true
echo "== relevant src/tutorial.js lines =="
cat -n src/tutorial.js | sed -n '1,160p'
echo "== searches for writer/tutor/retry patterns =="
rg -n "mkdir|writeFile|write|tutorial|remove|rm\\(|destination|files" src package.json README.md --glob '!**/node_modules/**' || true
echo "== dependency/runtime context =="
cat package.jsonRepository: boyeesu/ProDocs
Length of output: 15252
🏁 Script executed:
#!/bin/bash
set -euo pipefail
npm pkg get node >/tmp/pkg-node.json
node - <<'JS'
const fs = require('node:fs/promises');
const path = require('node:path');
const { fileURLToPath } = require('node:url');
const dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.join(dirname, 'tmp-tutorial-proof-' + process.pid);
(async () => {
await fs.rm(root, { recursive: true, force: true }).catch(() => {});
await fs.mkdir(root, { recursive: true });
try {
await fs.mkdir(path.join(root, 'parent/child'), { recursive: true });
await fs.writeFile(path.join(root, 'parent/child/file.txt'), 'x', { flag: 'wx', encoding: 'utf8' });
await fs.writeFile(path.join(root, 'top.txt'), 'y', { flag: 'wx', encoding: 'utf8' });
// Second top-level write should fail because top.txt exists.
await fs.writeFile(path.join(root, 'top.txt'), 'z', { flag: 'wx', encoding: 'utf8' });
console.log('no second-write failure');
} catch (error) {
console.log('mode', error.mode);
console.log('code', error.code);
console.log('errno', error.errno);
console.log('message', error.message);
}
const entries = await fs.readdir(root, { recursive: true, withFileTypes: true }).catch(err => ({ message: err.message }));
console.log(JSON.stringify((Array.isArray(entries) ? entries : []).map(e => e.name), null, 2));
await fs.rm(root, { recursive: true, force: true }).catch(() => {});
})().then(() => process.exit(0));
JS
node - <<'JS'
function walkError(err, seen = new Set()) {
const key = err.code + ':' + err.message;
if (seen.has(key)) return `[cycle:${key}]`;
seen.add(key);
const next = err.cause == null ? '(none)' : walkError(err.cause, seen);
return `${err.code}: ${err.message} cause=${next}`;
}
const fsw = require('node:fs/promises');
(async () => {
await fsw.rm('/tmp/tut-prod-' + process.pid, { recursive: true, force: true }).catch(() => {});
await fsw.mkdir('/tmp/tut-prod-' + process.pid, { recursive: true });
await fsw.mkdir('/tmp/tut-prod-' + process.pid + '/parent/child', { recursive: true });
await fsw.writeFile('/tmp/tut-prod-' + process.pid + '/parent/child/file.txt', 'x', { flag: 'wx', encoding: 'utf8' });
try {
await fsw.writeFile('/tmp/tut-prod-' + process.pid + '/top.txt', 'y', { flag: 'wx', encoding: 'utf8' });
await fsw.writeFile('/tmp/tut-prod-' + process.pid + '/top.txt', 'z', { flag: 'wx', encoding: 'utf8' });
} catch (err) {
console.log(walkError(err));
}
await fsw.rm('/tmp/tut-prod-' + process.pid, { recursive: true, force: true }).catch(() => {});
})();
JSRepository: boyeesu/ProDocs
Length of output: 914
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import subprocess, json, tempfile, os, re
for ver in ["^20.11.0", "^22.10.0"]:
with tempfile.TemporaryDirectory() as tmp:
n_dir = Path(tmp) / "node"
if not n_dir.exists():
subprocess.run(["npx", "--yes", f"node@{ver}", "scripts", "-e", 'console.log(process.version)'], cwd=tmp, check=True, capture_output=True)
result = subprocess.run(
[str(n_dir / "bin" / "node"), "--input-type=module", "-"],
cwd=tmp,
input=f'''
import fs from "node:fs/promises";
import path from "node:path";
import {{ fileURLToPath }} from "node:url";
import {{ dirname }} from "node:path";
const root = path.join(dirname(fileURLToPath(import.meta.url)), "tmp-tutorial-proof");
(async () => {
await fs.rm(root, {{ recursive: true, force: true }}).catch(() => {{}});
await fs.mkdir(root, {{ recursive: true }});
try {{
await fs.writeFile(path.join(root, "top.txt"), "y", {{ flag: "wx", encoding: "utf8" }});
await fs.writeFile(path.join(root, "top.txt"), "z", {{ flag: "wx", encoding: "utf8" }});
console.log("no second-write failure");
}} catch (error) {{
let cause = "";
for (let e = error.cause; e; e = e.cause) cause += "<" + e.message;
console.log(JSON.stringify({{
version: process.version,
mode: error.mode,
code: error.code,
errno: error.errno,
message: error.message,
cause: cause
}}));
}}
const entries = await fs.readdir(root, {{ recursive: true, withFileTypes: true }}).catch(err => [])
.then(e => e.map(x => (x instanceof Object ? x.name : undefined)).filter(Boolean));
console.log(JSON.stringify({{ remainingFiles: entries }}, null, 2));
await fs.rm(root, {{ recursive: true, force: true }}).catch(() => {{}});
})();
''',
capture_output=True,
text=True,
)
print(f"NODE_VERSION={ver}")
print("stdout:", result.stdout.strip())
if result.stderr.strip():
print("stderr:", result.stderr.strip())
PYRepository: boyeesu/ProDocs
Length of output: 389
🏁 Script executed:
#!/bin/bash
set -euo pipefail
node - <<'JS'
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
const tmp = path.join(dirname(fileURLToPath(import.meta.url)), "tmp-prodocs-write-proof-" + process.pid);
(async () => {
await fs.rm(tmp, { recursive: true, force: true }).catch(() => {});
await fs.mkdir(tmp, { recursive: true });
await fs.mkdir(path.join(tmp, "parent/child"), { recursive: true });
await fs.writeFile(path.join(tmp, "parent/child/file.txt"), "x", { flag: "wx", encoding: "utf8" });
try {
await fs.writeFile(path.join(tmp, "top.txt"), "y", { flag: "wx", encoding: "utf8" });
await fs.writeFile(path.join(tmp, "top.txt"), "z", { flag: "wx", encoding: "utf8" });
console.log("no second-write failure");
} catch (error) {
let cause = "";
for (let e = error.cause; e; e = e.cause) cause += "<" + e.message;
console.log(JSON.stringify({
version: process.version,
mode: error.mode,
code: error.code,
errno: error.errno,
message: error.message,
cause: cause || "(none)"
}, null, 2));
}
const entries = await fs.readdir(tmp, { recursive: true, withFileTypes: true })
.catch(() => [])
.then((e) => e.map((x) => x.name).filter(Boolean));
console.log("remainingFiles:", JSON.stringify(entries));
await fs.rm(tmp, { recursive: true, force: true }).catch(() => {});
})();
JSRepository: boyeesu/ProDocs
Length of output: 399
Remove partial tutorial files after a write failure.
If fs.mkdir() or fs.writeFile() fails, the destination may keep newly created parent directories and files. This leaves a non-empty destination before returning the error; remove destination with fs.rm() before rethrowing.
🧰 Tools
🪛 ast-grep (0.45.0)
[warning] 83-83: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFile(target, contents, { flag: "wx", encoding: "utf8" })
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(detect-non-literal-fs-filename)
🤖 Prompt for 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.
In `@src/tutorial.js` around lines 80 - 86, Wrap the file-creation loop in the
tutorial-writing flow with failure cleanup: if fs.mkdir() or fs.writeFile()
throws, call fs.rm() on destination before rethrowing the original error.
Preserve the written tracking and successful return behavior while ensuring
partial files and directories are removed.
| test("tutorial refuses a symlinked output parent", async (t) => { | ||
| const root = await temporaryProject(t); | ||
| const outside = await temporaryProject(t); | ||
| await fs.symlink(outside, path.join(root, "linked")); | ||
|
|
||
| await assert.rejects( | ||
| createTutorial(root, "linked/tutorial"), | ||
| /resolves outside the project root/ | ||
| ); | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
mkdir "$tmp/outside"
node --input-type=module - "$tmp" <<'NODE'
import fs from "node:fs/promises";
import path from "node:path";
const root = process.argv[2];
const target = path.join(root, "outside");
const link = path.join(root, "linked");
await fs.symlink(
target,
link,
process.platform === "win32" ? "junction" : "dir"
);
console.log((await fs.lstat(link)).isSymbolicLink() || process.platform === "win32");
NODERepository: boyeesu/ProDocs
Length of output: 158
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== locate file =="
fd -a 'adoption\.test\.js$' . | sed 's#^\./##'
echo "== context around lines =="
file="$(fd 'adoption\.test\.js$' . | head -n 1)"
if [ -n "${file:-}" ]; then
sed -n '70,115p' "$file" | cat -n
echo "== related resolve/symlink tests =="
rg -n 'symlink|junction|resolves outside the project root|createTutorial|adoption' "$file"
fi
echo "== package context =="
if [ -f package.json ]; then
node - <<'JS'
const fs = require('fs');
const p = JSON.parse(fs.readFileSync('package.json','utf8'));
console.log(JSON.stringify({scripts:p.scripts, devDependencies:p.devDependencies, dependencies:p.dependencies}, null, 2));
JS
fi
echo "== Node path fs module availability =="
node -e "console.log({node:process.version, fsAvailable:!!require.resolve('node:fs/promises')})"Repository: boyeesu/ProDocs
Length of output: 3300
🌐 Web query:
Node.js fs.symlink platform Windows junction EPERM privileges directory type option documentation
💡 Result:
In Node.js, the fs.symlink and fs.symlinkSync methods use the type argument exclusively on Windows to define the nature of the link: 'dir', 'file', or 'junction' [1][2]. The 'junction' type is a specific Windows NTFS feature that allows creating directory links without requiring elevated (administrator) privileges, unlike standard directory symbolic links [3][4]. Key considerations for using 'junction' include: 1. Privileges: Using 'junction' can often bypass the EPERM (operation not permitted) error that occurs when creating standard symbolic links on Windows without administrator rights [3][5]. 2. Directory Limitation: NTFS junctions can only point to directories, not individual files [1][5]. Attempting to use 'junction' on a file target will fail [5]. 3. Path Requirement: Windows junction points require the target path to be absolute [1][6]. When you specify 'junction' in Node.js, the target path is automatically normalized to an absolute path [1][7]. 4. Autodetection: If the type argument is null or omitted, Node.js attempts to autodetect the target type as 'dir' or 'file' [1][8]. If you encounter EPERM errors when creating symbolic links on Windows, it is frequently due to missing administrator privileges or the system not being in Developer Mode [3][9][4]. Switching to the 'junction' type is a common workaround for directory linking, provided the target is a directory [3][5][10].
Citations:
- 1: https://nodejs.org/api/fs.html
- 2: https://nodejs.org/docs/latest-v26.x/api/fs.html
- 3: fs.symlink can’t create directory symlinks on Windows nodejs/node#18518
- 4: https://github.com/ehmicky/cross-platform-node-guide/blob/master/docs/3_filesystem/symlinks.md
- 5: fs.symlink "type=junction" Create file soft link error nodejs/node#40184
- 6: https://nodejs.org/docs/latest-v23.x/api/fs.html
- 7: https://nodejs.org/api/fs.md
- 8: https://nodejs.org/docs/latest-v24.x/api/fs.html
- 9: fs.symlink requires administrator privileges on Windows nodejs/node#47783
- 10: symlinkSync not working on Windows 10 nodejs/node#22208
Use a platform-specific link type for linked.
On Windows, await fs.symlink(outside, path.join(root, "linked")) can fail with EPERM when the test process lacks symbolic-link privileges, causing the test to fail before the resolves outside the project root assertion runs. Use a junction on Windows and a directory symlink otherwise.
Proposed fix
- await fs.symlink(outside, path.join(root, "linked"));
+ await fs.symlink(
+ outside,
+ path.join(root, "linked"),
+ process.platform === "win32" ? "junction" : "dir"
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| test("tutorial refuses a symlinked output parent", async (t) => { | |
| const root = await temporaryProject(t); | |
| const outside = await temporaryProject(t); | |
| await fs.symlink(outside, path.join(root, "linked")); | |
| await assert.rejects( | |
| createTutorial(root, "linked/tutorial"), | |
| /resolves outside the project root/ | |
| ); | |
| }); | |
| test("tutorial refuses a symlinked output parent", async (t) => { | |
| const root = await temporaryProject(t); | |
| const outside = await temporaryProject(t); | |
| await fs.symlink( | |
| outside, | |
| path.join(root, "linked"), | |
| process.platform === "win32" ? "junction" : "dir" | |
| ); | |
| await assert.rejects( | |
| createTutorial(root, "linked/tutorial"), | |
| /resolves outside the project root/ | |
| ); | |
| }); |
🤖 Prompt for 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.
In `@test/adoption.test.js` around lines 90 - 99, Update the symlink setup in the
“tutorial refuses a symlinked output parent” test to use a junction on Windows
and a directory symbolic link on other platforms, while preserving the existing
outside-root rejection assertion.
| name: "empty request", | ||
| paths: ["src/missing.js"], | ||
| expectedPaths: [], | ||
| unwantedPaths: [], | ||
| maxTokens: 1000 | ||
| }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Assert that the missing source returns no files.
precision === 1 does not prove that src/missing.js produced an empty packet. With empty expectedPaths and unwantedPaths, unrelated returned files also produce precision 1 in src/evaluate.js. Assert that the case returned zero files.
Proposed assertion
assert.equal(report.cases[1].precision, 1);
+ assert.equal(report.cases[1].returned, 0);Also applies to: 508-512
🤖 Prompt for 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.
In `@test/roadmap.test.js` around lines 491 - 496, Update the “empty request” test
case in the roadmap tests to explicitly assert that evaluating the missing
source returns zero files, rather than relying only on precision === 1. Add the
assertion against the returned file list or packet using the existing result
structure, and apply the same correction to the related case around the second
referenced range.
Summary
Verification
Release
Summary by CodeRabbit
New Features
doctordiagnostics for project readiness, configuration, indexing, documentation freshness, and knowledge health.Documentation
Bug Fixes