feat(docs): resolve version-matched Spring docs to a shared cache - #13
Conversation
The skill takes a project and a version, resolves them through the spring-docs catalog, and hands back a filesystem path. Nothing is written into the user's project: an archive is unpacked once under ~/.cache/pleaseai-spring/docs/<tag>/, so a project keeps no documentation in version control, CLAUDE.md is never rewritten, and two projects on the same Spring version share one copy. The cache is keyed by release tag rather than by version, because a corrected archive is republished as <project>-<version>+rebuild.N and a version-keyed cache would serve the superseded tree forever. The catalog is consulted on every run for the same reason — it is a few kilobytes, and it is the only thing that reports the move; only the archive download is skipped on a hit. Downloads are checked against the .sha256 published beside them, and unpacked through a staging directory renamed into place, so neither a corrupt download nor an interrupted run leaves a tree under the name callers read from.
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| ErrorProne | 2 high |
| Security | 5 critical 68 high |
🟢 Metrics 168 complexity · 25 duplication
Metric Results Complexity 168 Duplication 25
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Code Review
This pull request implements version-matched Spring reference documentation resolution by downloading, verifying, and unpacking archives into a shared cache instead of installing them directly into the project. It introduces the docs.ts script, cache helpers, tests, and the spring-docs skill. The review feedback focuses on critical security and robustness improvements in scripts/docs.ts, specifically recommending input validation for project, version, and cached tags to prevent path traversal vulnerabilities, as well as adding defensive checks on Bun.spawnSync's stderr to avoid potential runtime TypeErrors.
…nstall
The skill ships through two channels. As a Claude Code plugin it gets
${CLAUDE_PLUGIN_ROOT} and an automatic bun install of the root package.json.
Installed with `npx skills` it gets neither: only skills/spring-docs/ is
copied, so nothing above it exists and no dependency install runs.
SKILL.md addressed its scripts with a bare relative path, which resolves
against the user's project in both channels — the scripts were never reachable
from an installed skill. It now uses ${CLAUDE_SKILL_DIR}, the one variable
substituted at the personal, project and plugin level, matched by allowed-tools
rules so the commands run without a prompt.
scripts/build-skill.ts bundles docs.ts and detect.ts into dependency-free files
under the skill directory. The output is committed because `npx skills` copies
straight from the repository, and `bun run build:skill:check` byte-compares it
in CI so it cannot drift.
Two runtime defects surfaced while verifying the bundles under plain node:
- docs.ts extracted archives with Bun.spawnSync, unavailable outside bun. Now
node:child_process, verified against a real download and extraction.
- import.meta.main does not survive bundling. Bun lowers it to a __require
comparison that resolves only when a dependency happens to pull in the CJS
interop helper, so detect.js worked by accident while docs.js threw
ReferenceError. The build pins it with define; a bundle is always the
entrypoint.
Also drops consola (zero call sites, TD-001; it would have been inlined into
every bundle) and the empty commands/ placeholder, and pins CI to the bun that
produced the committed bundles.
|
…mically Applies the gemini, greptile, gpt and ocr review findings on #13. Every segment joined into a cache path is now checked at the boundary: `project` and `version` from the CLI, the tag a pointer file names, and the tag `catalog.json` maps a version to. The last one is remote input that names the directory `publish` renames aside and `rmSync` deletes, and a charset test alone does not cover it — `.` and `..` are spelled entirely in allowed characters, so `isSafeSegment` rejects both by name. `catalog.json` is shape-checked before use. A valid-JSON body like `null` or `{"version":"1"}` used to throw a TypeError inside the lookup and surface as an internal error instead of the documented unavailable result; an array where a keyed map belongs now fails the same way rather than reading as empty. Publication swaps rather than clears: `renameSync` refuses a non-empty target outright, so remove-then-rename lost a concurrent race with ENOTEMPTY after a good download. The loser of that race discards its displaced tree instead of orphaning it, deleting the replaced tree can no longer turn a published tree into a failed download, and debris from a run killed mid-publication is reclaimed on the next download once it is too old to still have an owner. A cached tree counts as ready only when it carries `_index.md`, so an archive that checksums correctly but is mispackaged fails instead of caching a path that does not resolve, and an incomplete tree re-downloads instead of reporting ready. Replaces `Bun.spawnSync` with `node:child_process` so the bundled skill runs under plain node, and reports `result.error` rather than reading `stderr` off a spawn that never started.
There was a problem hiding this comment.
All reported issues were addressed across 5 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
Review completed against the latest diff
Not reviewed (too large): skills/spring-docs/scripts/detect.js (~4,587 lines) - if these are generated or fixture files, add them to ignored paths to exclude them from future reviews.
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…ecks Applies the cubic review findings on #13. `sweepLeftovers` moves from `unpack` to `resolveDocs`, before the cache-hit return. A refresh that died while the previous tree was still usable leaves debris that every later run skipped past, because a cache hit never reaches the download the sweep was attached to — the one case the sweep exists for. `isUsableTree` requires a regular file. `existsSync` is equally true of a directory named `_index.md`, and a tree published on that answer is served as ready forever while no caller can read the index out of it. `isCatalog` validates `released_at`. `CatalogEntry` promises `string | null` and `lookupTag` hands the value straight to callers, so an absent key put `undefined` behind a type that excludes it. The index-less archive test asserted on the archive built before the tree was stripped rather than on the bytes it serves, so an empty fixture would still have passed.
Applies the second round of cubic review findings on #13. The bundles are ESM and the skill directory carries no `package.json` to say so, which makes a `.js` bundle a CommonJS file to every Node that does not detect module syntax on its own — unflagged only since 22.7. On anything older `docs.js` died on its first `import` before resolving any docs, and the standalone channel is exactly the one where no `package.json` can be added. `.mjs` says it in the name instead. A catalog entry with a null `released_at` is a tag reserved without assets, so it now reports `unpublished` with the issue-tracker suggestion rather than downloading from it and surfacing the 404 as an unreachable network. `engines.bun` rises to the version CI pins. The `build:skill:check` gate byte-compares against CI's Bun, so a floor of 1.3.0 invited a contributor to regenerate bundles that fail the gate on an untouched tree. Test fixes: `respond()` slices its Buffer to the view rather than handing over a pooled slab far larger than the payload, and the staging-leftovers assertion reads `DOCS_CACHE_SUBDIR` instead of rebuilding the path by hand, where a changed constant would have made the check pass by missing the directory. Docs: `tech-stack.md` no longer claims "No bundler" beside the section describing the bundler, and SKILL.md's frontmatter gives the same Boot range as its own Coverage section and the README.
There was a problem hiding this comment.
All reported issues were addressed across 5 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 13 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Applies the third round of cubic review findings on #13. `sweepLeftovers` guards its own `readdirSync` instead of testing the directory first. The cache path can be absent on a first run, be a file, be unreadable, or vanish under a concurrent delete, and an `existsSync` check answers only the first of those while still losing the race to the last. Throwing there rejected `resolveDocs` before it could return the `unavailable` result it documents — for a cache it would otherwise have repaired by re-downloading.
Applies a cubic review finding on #13. `engines.bun` rose to `>=1.4.2` for the bundle gate, but two knowledge-base lines still told contributors the floor was `>=1.3.0`. Both now say which constraint sets which number: the lockfile needs 1.3, and the byte-compared bundles need CI's Bun.
Summary
Implements the
spring-docsskill: given a project and a version (boot 3.5.16), it resolves thepublished archive from
pleaseai/spring-docs, unpacks itonce into
~/.cache/pleaseai-spring/docs/<tag>/, and hands Claude the path.Nothing is written into the user's project — no
.claude/skills/spring-*/tree, noCLAUDE.mdblock. One version is 150-250 files (2.4 MB for Boot 3.5.16, 3.6 MB for 4.1.1); in a project tree
that is a permanent diff, a
.gitignoreentry and a branch-switch hazard, paid again by everyproject. A cache path costs none of that and is shared across projects and branches.
What landed
skills/spring-docs/SKILL.md<project> <version>, reads from the resolved pathscripts/docs.tsscripts/lib/docs-cache.tsscripts/__tests__/docs.test.tsscripts/__tests__/docs-cache.test.tsDesign points worth reviewing
<project>-<version>+rebuild.N; a version-keyed cache would serve the superseded tree forever.only thing that reports a rebuild having moved a version. Only the archive download — the
expensive half — is skipped.
--no-fetchserves the last resolution from a pointer file instead.(project, version), not from the tag.boot-4.1.1+rebuild.1stillships
boot-4.1.1.tar.gz; deriving the filename from the tag would 404 on every rebuild.under the name callers read. A checksum mismatch writes nothing at all (asserted in tests).
kind: "unavailable"with theissue tracker in
suggestion; the skill is explicitly told not to answer from a different minor.Verification
bun test— 194 pass, 0 fail (13 new);bun run typecheck,bun run lintclean.boot 3.5.16→ 146 pages / 2.4 MB unpacked, second run acache hit in 65 ms;
boot 3.2.0→unavailablewith exit 1; missing argument → exit 2.Docs
README.mdrewritten for what the plugin now is. The previous text described an unimplementedproduct —
/spring:list,/spring:update,/spring:remove,/spring:add,.spring-skill.json,a live HTML→Markdown conversion fallback, and an eval results table citing an
evals/spring/directory that does not exist. Those claims are dropped rather than carried forward.
ARCHITECTURE.mdgets a status note: the install/resolve/convert pipeline it describes is theearlier design and is superseded for the documentation path. A full rewrite belongs with the
remaining stages, not with this change.
Follow-ups (not here)
/spring:docsslash command;commands/is still empty.spring-docspublishes a second project. Today thecatalog holds
bootonly, so there is nothing to resolve a Boot version into.Summary by cubic
Implements the
spring-docsskill, which resolves a project's declared Spring version to the matching reference documentation: the published archive is downloaded once into a shared cache and Claude is given the path to read. Nothing is written into the user's project, and answers come from the declared version, not the newest release.The skill also ships as a standalone
npx skillsinstall, which copies only the skill directory and runs no dependency install, so its scripts are bundled into dependency-free committed.mjsfiles underskills/spring-docs/scripts/and executed with plainnode.README.mdandARCHITECTURE.mdare updated to match what exists, dropping earlier claims about slash commands, a config file, and an eval suite that were never implemented.Distribution
bun run build:skillbundlesscripts/docs.tsandscripts/detect.ts; CI'sbuild:skill:checkfails on a stale bundle..mjsbecause the skill directory has nopackage.jsonto declare ESM, and.jswould be treated as CommonJS on Node versions that don't auto-detect module syntax.import.meta.mainviadefinebecause Bun lowers it to a__requirecomparison that throws under plain node.consola(TD-001) and the emptycommands/placeholder;engines.bunand CI are pinned to Bun 1.4.2 while the lockfile itself only needs 1.3.Caching and correctness
boot-4.1.1+rebuild.1) supersedes the old tree instead of serving stale bytes.catalog.jsonis shape-checked (includingreleased_at), and a cached tree counts as ready only when it contains a regular_index.mdfile.kind: "unavailable"with a suggestion; a nullreleased_atis treated as unpublished rather than downloading toward a 404.Written for commit b3deb20. Summary will update on new commits.