Skip to content

build: fix and consolidate the skill and catalog generators - #880

Merged
allxsmith merged 9 commits into
mainfrom
build/skill-generation-cleanup
Oct 3, 2026
Merged

allxsmith merged 9 commits into
mainfrom
build/skill-generation-cleanup

Conversation

@allxsmith

@allxsmith allxsmith commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Three bug fixes, each with a regression test:

  • An untitled API page passed the catalog's completeness guard but was left out of the catalog. Both catalogs now share one page list and guard.
  • gen-mcp-index listed .DS_Store and untracked skill files that the sync scripts never ship.
  • An empty SKILL.md name: was read from the next line.

The sync scripts, catalog code, frontmatter reader and pasted helpers now live in scripts/lib. Generated files are byte-identical except the catalog header, which now says pnpm gen:catalog. Two coverage floors drop slightly because covered lines moved or were deleted. Each commit says why.

Summary by CodeRabbit

  • Documentation
    • Updated component catalog regeneration instructions to use pnpm gen:catalog.
    • Clarified skill synchronization guidance.
  • Maintenance
    • Improved API catalog generation and validation, including checks for missing documentation.
    • Strengthened skill validation and synchronization, including detection of untracked files and reuse of up-to-date copies.
    • Added automated coverage for catalog generation, API documentation, and skill synchronization.

The skills-roster check parsed SKILL.md with its own regex, and its
`^name:\s*` let `\s` run across the newline, so an empty `name:` took
the next line as the name. It was also the third frontmatter parser in
these scripts.

gen-mcp-index's reader moves to lib/api-page.mjs as `frontmatter`, and
both it and the check use it. The Agent Skills spec requires `name`, so
a missing or empty one is now its own violation instead of a confusing
mismatch.
gen-component-catalog and gen-mcp-index each carried their own category
order, UNDOCUMENTED_EXPORTS, page walk, barrel parser and completeness
guard. These move to scripts/lib/api-catalog.mjs, and both generators
now read one list of titled pages from it.

That also makes the two guards agree. The catalog generator counted a
page as documented before checking its title, so an untitled page
passed its guard while the catalog dropped it. gen-mcp-index counted
only titled pages. One list now feeds both the output and the guard.

gen-mcp-index's message said to add a page "whose frontmatter title:
matches the export", but the guard matches on the file name. The shared
message states the real rule.

Both artifacts are byte-identical.
Both catalog generators kept a copy of the prose-line filter and the
160-character budget, and gen-component-catalog kept its own
first-sentence rule while lib/api-page.mjs claimed firstSentence
mirrored it. The copy had drifted: its lookahead after the period was
`(?=\s)` where firstSentence has `(?=\s|$)`. Both run on a collapsed,
trimmed line, so `$` only adds a match when the line ends in its only
period, and that match is the whole line, which the old code returned
anyway. No adjustment was needed.

lib/api-catalog.mjs now holds MAX_PURPOSE, firstProseLine and both
clips, named for the rule each applies. The generators still pick
different clips and the MCP one still prefers the description, so both
artifacts stay byte-identical.
byCodePoint, frontmatterTitle and mdFiles were pasted into several
scripts. Each now has one home: byCodePoint in lib/skills.mjs,
frontmatterTitle in lib/api-page.mjs on top of the shared frontmatter
reader, and mdFiles in lib/api-catalog.mjs. mdFiles takes a pattern, so
check-conformance keeps reading .mdx too.

gen-api-sources's function floor drops from 30 to 28. Its covered
byCodePoint moved to lib/skills.mjs, which leaves 2 of 7 functions
covered instead of 3 of 10. Nothing that ran before stopped running.

Every artifact is byte-identical and conformance output is unchanged.
gen-component-catalog ran main() on import, exported nothing and had
no test or coverage row. It now has the import.meta guard the other
generators use, exports overviewSentence, renderCatalog and build, and
gets a node:test sibling and a floor.

It also formats its output through prettier before writing, like the
other generators. Prettier leaves the catalog unchanged, so the
.prettierignore entry that kept format:check off it goes.

The generated header, the script's doc comment and skills/README.md
said to run `npm run gen:catalog`. They now say `pnpm gen:catalog`.
That shortens the catalog by three bytes, which moves its byte count
in bestax-mcp/data/skills.json from 18545 to 18542.
readSkills in gen-mcp-index walked references/ and examples/ and listed
every regular file, including .DS_Store and untracked or gitignored
files. Both sync scripts leave .DS_Store out of the copy and refuse to
bundle untracked files, so a local run could write a skills.json naming
a file the server never ships.

lib/skills.mjs gains isDsStore, skillFiles (the walk that skips it) and
assertSkillsVetted (the untracked-file refusal), and readSkills uses
all three. skills.json is byte-identical on a clean tree.
The two sync-skills.mjs scripts were near-twins: the source check, the
roster read, the empty-roster check, the untracked gate, the copy loop
and the .DS_Store filter, plus a lock and fingerprint in bestax-mcp's.
These move to scripts/lib/sync-skills.mjs as syncSkills, and both
scripts become thin callers.

bestax-mcp passes a stateDir, so it keeps its lock, freshness stamp,
signal release and data/.sync-skills location. create-bestax passes
none and still copies afresh on every run. Adopting the lock there
would put state under templates/, which ships, for callers that never
overlap. It now copies with node:fs cp instead of fs-extra.

The fingerprint now walks only the files a copy ships, so a .DS_Store
change no longer forces a recopy. Output, messages and the copied trees
are identical, and npm pack lists the same files for both packages.
check-conformance re-exported SKILL_DIR_NAME and rosterSkillNames, and
its readSkillDirs and readSkillNames only forwarded to lib/skills.mjs.
The check now imports from lib directly, and skills-roster.test.mjs
takes each function from lib rather than from both places.

checkSkillsRoster rebuilt readSkillNames inline from the dirs it had
already read. lib/skills.mjs gains bundledSkillNames for that, and
readSkillNames uses it too. The stale "local copy" comment in
gen-skills-rosters.mjs goes.

gen-skills-rosters's line floor drops from 72 to 71. The deleted
comment counted as four covered lines, so the file moves from 88 of
121 lines to 84 of 117. Nothing that ran before stopped running.
The MCP manifest took name and promptName from SKILL.md frontmatter
(`fm.name || name`) while every roster used the directory name, and
the `replace(/^bestax-/, '')` slug rule was copied five times.

lib/skills.mjs gains skillSlug, and gen-mcp-index and the
skills-roster check use it. The manifest now uses the directory name,
like the rosters. frontmatterNameViolations stays, because the Agent
Skills spec still requires `name` to match the directory, and agents
load a skill by that name. Its rationale and message now say so.

skills.json is byte-identical, since every name already equals its
directory.
@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

🧰 Additional context used
📚 Code guidelines (4)
skills/CLAUDE.md — auto-discovered
skills/README.md — configured
bestax-mcp/CLAUDE.md — auto-discovered
bulma-ui/CLAUDE.md — auto-discovered

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: allxsmith/bestax/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: bca1ce1d-f0b0-425e-a930-6508a209d650
📥 Commits

Reviewing files that changed from the base of the PR and between 3032ac6 and 402af3c.

📒 Files selected for processing (23)
  • .prettierignore
  • bestax-mcp/CLAUDE.md
  • bestax-mcp/data/skills.json
  • bestax-mcp/scripts/sync-skills.mjs
  • create-bestax/scripts/sync-skills.mjs
  • scripts/api-catalog.test.mjs
  • scripts/check-conformance.mjs
  • scripts/coverage-floors.json
  • scripts/gen-api-docs.mjs
  • scripts/gen-api-sources.mjs
  • scripts/gen-component-catalog.mjs
  • scripts/gen-component-catalog.test.mjs
  • scripts/gen-mcp-index.mjs
  • scripts/gen-mcp-index.test.mjs
  • scripts/gen-skills-rosters.mjs
  • scripts/lib/api-catalog.mjs
  • scripts/lib/api-page.mjs
  • scripts/lib/skills.mjs
  • scripts/lib/sync-skills.mjs
  • scripts/skills-roster.test.mjs
  • scripts/sync-skills.test.mjs
  • skills/README.md
  • skills/bestax-custom-component/references/component-catalog.md
💤 Files with no reviewable changes (2)
  • .prettierignore
  • scripts/gen-skills-rosters.mjs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


Walkthrough

The changes add shared API catalog and skill utilities, update generators and conformance checks to use them, and centralize skill synchronization for the bestax-mcp and create-bestax scripts.

Changes

API catalog pipeline

Layer / File(s) Summary
Shared API-page and catalog helpers
scripts/lib/api-page.mjs, scripts/lib/api-catalog.mjs, scripts/api-catalog.test.mjs
Shared modules provide frontmatter parsing, API-page discovery, category ordering, description extraction and clipping, export parsing, and missing-page checks. Tests cover parsing, ordering, clipping, and completeness.
Shared API-page discovery in consumers
scripts/check-conformance.mjs, scripts/gen-api-docs.mjs, scripts/gen-api-sources.mjs, scripts/gen-mcp-index.mjs, scripts/gen-mcp-index.test.mjs, scripts/coverage-floors.json
API generators and conformance checks use shared page discovery and frontmatter parsing. The MCP index uses shared API-page and completeness helpers. Coverage floors are updated.
Component catalog generation
scripts/gen-component-catalog.mjs, scripts/gen-component-catalog.test.mjs, .prettierignore, skills/README.md, skills/bestax-custom-component/references/component-catalog.md, bestax-mcp/data/skills.json
The generator builds and formats the catalog using shared API-page data and checks for missing pages. Tests validate rendering and formatting. The catalog is no longer ignored by Prettier, its regeneration instructions use pnpm, and its recorded byte count changes.

Skill roster and synchronization

Layer / File(s) Summary
Shared skill naming and roster checks
scripts/lib/skills.mjs, scripts/check-conformance.mjs, scripts/gen-mcp-index.mjs, scripts/gen-mcp-index.test.mjs, scripts/skills-roster.test.mjs, scripts/gen-skills-rosters.mjs
Shared helpers provide skill naming, slug conversion, file enumeration, and vetting. Roster checks and MCP index skill records use these helpers. Tests cover frontmatter names, file lists, and untracked files.
Shared skill synchronization
scripts/lib/sync-skills.mjs, bestax-mcp/scripts/sync-skills.mjs, create-bestax/scripts/sync-skills.mjs, scripts/sync-skills.test.mjs, bestax-mcp/CLAUDE.md, scripts/coverage-floors.json
Both sync scripts call syncSkills. The helper validates and copies skills, and supports optional fingerprint-based reuse, locking, and signal cleanup. Tests cover these behaviors. The bestax-mcp note identifies the shared implementation and its synchronization requirements.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 402af

No actionable issue is established; the change is mergeable after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 402af

The shared implementation preserves the inspected callers’ destinations and coordination choices, and extends source vetting to skill indexing. No introduced security concern was established. Existing synchronization assumptions and incomplete exposure coverage leave limited uncertainty.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • observed — The inspected delivery scope is canonical repository skill content copied into MCP package data and scaffolder templates, with metadata generated for MCP consumption. Caller destination paths remain package-local constants, not identified request-controlled parameters.

Trust Boundaries and Controls

  • observed — The generator now refuses detected untracked skill files before indexing them, aligning with the existing bundlers. Required frontmatter-name validation remains in conformance checks, where an empty parsed name is reported as missing rather than accepted from the next line.

Resilience and Maintainability Implications

  • observed — The stateful synchronization path establishes lock ownership through atomic directory creation, releases only a lock it considers held, removes signal handlers after completion, and reports stale locks instead of automatically reclaiming them. These controls serialize cooperating writers but do not isolate independent readers from partial destination contents.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 79.63% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 54 functions across 16 files. (5 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: fixing and consolidating the skill and catalog generators.
Description check ✅ Passed The description gives a specific summary of the three fixes, notes regression tests, and describes the shared-helper refactor and generated-file changes. It does not identify the affected package(s) o…
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 79.63% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 54 functions across 16 files. (5 skipped: 5 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

bestax-mcp/scripts/sync-skills.mjs

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

create-bestax/scripts/sync-skills.mjs

ESLint skipped: the matched ESLint configuration already failed (missing-dependency).

scripts/api-catalog.test.mjs

ESLint skipped: the matched ESLint configuration already failed (missing-dependency).

  • 13 others

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.

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://3bb13a78.bestax.pages.dev

@claude claude 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.

Deep review — 0 blocking · 4 advisory

# Severity Area Finding Location
1 🔵 Advisory Robustness Both sync callers print err.message only, so an unexpected error (fs, or a programming error inside syncSkills) is now reported as a single untagged line with no stack — where the old bestax-mcp script let it surface as an unhandled rejection with a full trace. The exit code is still 1, so no gate goes green. bestax-mcp/scripts/sync-skills.mjs:33, create-bestax/scripts/sync-skills.mjs:43
2 🔵 Advisory Correctness skillFiles pushes only e.isFile() entries, so the bestax-mcp fingerprint no longer notices a change behind a symlinked skill file — which cp (default verbatimSymlinks: false) would still dereference and ship. The old fingerprintSource hashed any non-directory entry. The listing side is unchanged (the old walk already had if (!e.isFile()) continue). find skills -type l returns nothing today. scripts/lib/skills.mjs:125
3 🔵 Advisory Robustness The shared frontmatter reader is documented as flat ("Nested YAML is not parsed"), so a valid folded or block scalar name (name: >- on its own line) reads as empty and now fails conformance as "frontmatter has no name" rather than being read. All seven skills use plain scalars, so this is latent, not live. scripts/lib/api-page.mjs:232
4 🔵 Advisory API The module header now lists "frontmatter reads" under "Everything here is FENCE-AWARE … Every scan therefore runs through fenceMask()", but frontmatter() and frontmatterTitle() never call it — they anchor at the --- at byte 0, which is why they are correct anyway. Deleting "Every scan therefore runs through fenceMask()." leaves the rest true. scripts/lib/api-page.mjs:5

Overall: Sound, and unusually well evidenced for a refactor of this size — all three claimed bugs are real in the pre-PR code, and I confirmed the byte-identical-artefacts claim rather than taking it: gen:catalog:check and gen:mcp:check are both clean, check:conformance is green except the pre-existing shallow-clone version-regression rule, prettier --check passes on the now-unignored catalog, and the twelve new test cases all pass. The riskiest part is the widened blast radius of scripts/lib/sync-skills.mjs and scripts/lib/skills.mjs — four consumers now share one copy loop, one vetting gate and one frontmatter reader, so a future edit there lands everywhere at once; the compensating test files are the thing to read first. Second focus: pnpm gen:mcp is now a hard failure on any untracked or gitignored file under skills/ (new behaviour, deliberate, matching the sync gate) — worth knowing before it bites someone mid-authoring.

Residual risk:

  • Coverage floors — refuted empirically. The suite is red locally only on the documented "build bulma-ui/dist first" assertions, but the spec reporter still prints the table: lib/api-catalog.mjs 100.00/100.00/100.00, lib/sync-skills.mjs 97.93/92.50/100.00, gen-component-catalog.mjs 91.21/88.89/75.00, gen-api-sources.mjs functions 28.57, gen-skills-rosters.mjs lines 71.79. Every new row and both lowered rows clear, and the two drops match their commit-message arithmetic exactly (2 of 7 functions; 84 of 117 lines). The 100 branch floor on api-catalog.mjs has no slack by design (coverage-floors.mjs: a metric at 100% keeps a floor of 100) and matches shell-words.mjs and api-sources.mjs.
  • firstSentence drift — refuted. The deleted copy used (?=\s) where the lib uses (?=\s|$); because .*? is lazy and the input is a collapsed single line, the two can only differ when no period is followed by whitespace and the line ends in one, where the match is the whole line the old code returned anyway. The catalog regenerating byte-identical is the empirical half.
  • The guard still runs from a different page list than check-conformance does. apiComponents() keys on the frontmatter title and includes .mdx; missingApiPages keys on the file name and .md only. Refuted as drift: they answer different questions (which titles must appear on the listing surfaces, versus which exports have a page), and missingApiPagesMessage now states the file-name rule it actually applies.
  • STORY_EXEMPT at scripts/check-conformance.mjs:193 still hand-duplicates the six Table sub-parts that UNDOCUMENTED_EXPORTS lists. Open, and the same shape of drift this PR exists to end: a new Table sub-part needs adding to both. Not consolidated here, and arguably should not be (a story exemption is not a page exemption), but nothing holds the two in step.
  • bestax-mcp/src/server.ts:682 keeps its own bestax- prefix strip. Refuted as a sixth roster copy: it normalises a caller-supplied name argument before matching against promptName or name, so it derives no shipped roster.
  • git-less trees still generate. untrackedSkillPaths returns an empty list when git is missing, when the tree is outside any repository, or when the resolved toplevel does not own skills/ — so adding assertSkillsVetted to readSkills cannot hard-fail an exported tarball. The new gen-mcp-index test drives both the no-repo and the in-repo paths.
  • New format:check coupling on the catalog. Refuted: the generator's prettier.resolveConfig call and the CLI agree here because the repo has no .editorconfig at all (the one divergence between the API and the CLI), the generator matches every other generator in this repo, and gen-component-catalog.test.mjs asserts the fixpoint directly.
  • Not yet observed: Build and Test was still in_progress on 402af3c when this review ran, so the green coverage-floor judgement and format:check are my local runs, not CI's.

🏄 Nine commits of pure gnarly cleanup and every one of them paddles out clean — three real bugs caught, the artefacts come back byte-for-byte identical, and the duplicate copies that were quietly drifting apart now ride one board. Nothing here to wipe out on. Ship it.

@github-actions github-actions Bot added the review-converged Deep review converged: no blocking findings, all review threads resolved, checks green label Oct 3, 2026
@allxsmith
allxsmith merged commit 448aeda into main Oct 3, 2026
51 checks passed
@allxsmith
allxsmith deleted the build/skill-generation-cleanup branch October 3, 2026 21:41
allxsmith added a commit that referenced this pull request Oct 4, 2026
* build: refuse symlinked skill files and keep sync crash traces

Follow-ups from the deep review of #880.

skillFiles now refuses a symbolic link instead of skipping it, and
syncSkills runs that check for every caller before copying. cp would
follow a link and ship its target while the bestax-mcp fingerprint never
saw a change behind it.

Both sync callers print syncFailureText, which keeps the message for a
tagged refusal and the full stack for an unexpected error.

The api-page header no longer says every scan goes through fenceMask,
since the frontmatter readers anchor at the first line instead.

* build: tag only skill refusals and refuse a symlinked skill directory

A permission error during the vetting walk was wrapped with the
[sync-skills] tag and printed without its stack. Untracked files and
symbolic links now carry an ESKILLREFUSAL code, and syncSkills tags only
those, so any other error keeps its own message and stack.

readSkillDirs now lists a symlinked skill directory instead of skipping
it, and skillFiles refuses it, so a linked skill fails loudly rather than
going missing from every bundle. A symlink named .DS_Store is refused
too, and the comments now say cp would recreate a link pointing at this
checkout rather than follow it.

* build: share the symlink refusal and copy only the vetted files

readSkillDirs lists a symbolic link only when it points at a directory,
and marks it with isSymlink, so a linked file such as AGENTS.md is no
longer read as a skill. The conformance check reports a linked skill
directory, and gen-mcp-index walks each skill once from its root, so it
refuses a linked skill directory or SKILL.md just as the sync does.
bestax-mcp/data is unchanged.

syncSkills now checks each skill after taking the lock and copies
exactly the files it checked, so a link added while a run waited can no
longer ship. The fingerprint and the copy share that list, which also
drops the second walk. On the real tree the copied files and the
bestax-mcp fingerprint match what cp produced.

A .DS_Store symlink is skipped again instead of refused, since the copy
ships only listed files and the listing never includes .DS_Store.

Every expected failure carries ESKILLREFUSAL, and failureText in
skills.mjs prints its message, or the stack for anything else. Both
sync scripts and gen-mcp-index print through it, which replaces
syncFailureText and its prefix check.
@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 5.26.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 2.25.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 4.2.13 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 1.14.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deep-review released review-converged Deep review converged: no blocking findings, all review threads resolved, checks green

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant