Repository navigation
feat: replace validate-output.sh with a parsed-HTML validator - #29
ChristianMurphy wants to merge 4 commits into
Conversation
adb6d48 to
c22d5d3
Compare
|
Thanks so much for this, @ChristianMurphy ! Going with Node and adding a test page for each case covers everything I asked about in #21. It's a big change, so it'll take me some time to review properly. I'll get back to you once I've been through it. |
|
Hey @ChristianMurphy, #18 is merged now (squashed as 16ed6df), so this is ready for the rebase you mentioned. One behavior from that round needs to come across too. #18 now requires the extension to start with a letter, so |
|
Hey @ChristianMurphy, are you still up for the rebase now that #18 is in? If it's easier, I can resolve the conflicts on your branch. Which would you prefer? |
Problem: validate-output.sh read HTML with grep, sed, and awk. Some checks passed pages that had the problem, and some failed pages that were fine, as the cases in malav2110#21 show. Goal: read the page as the tree a browser builds, and query it with CSS selectors. Keep the script's arguments, exit codes, and `pass:` and `FAIL:` lines. Guard the behavior with regression tests in CI. Changes: - Add `scripts/validate-output.ts`, which Node 24.12 or later runs directly. It parses with parse5 and queries with css-select. Each check is a named function that returns its result. Text checks use string parsing and Intl.Segmenter in place of regular expressions. - Add `scripts/package.json` and its lockfile with exact pins. Every runtime package is MIT, BSD-2-Clause, or ISC. - Delete validate-output.sh. - Add tests/validate-output/. A page in pass/ must pass every check. A page in fail/<check id>/ must fail only that check. Node's built-in test runner runs them. - Add a root package.json with TypeScript 7, oxlint, and oxfmt, and a `tsconfig.json` for type stripping. - Add the Checks workflow. It type-checks, lints, and checks formatting, runs the regression tests, and runs actionlint and zizmor on the workflows. The samples workflow now installs the validator's dependencies. - Update `SKILL.md`, `references/validation.md`, `README.md`, and `CONTRIBUTING.md` for the new command and the Node 24.12 requirement. Notes: on all nine samples the output matches the shell script byte for byte. The intended differences: - The unlinked-reference failure lists each reference. - A Markdown heading anchor such as #license no longer fails. - The quiz checks read only the visible text of the quiz section. Refs: malav2110#21
c22d5d3 to
47caf93
Compare
|
Thanks for the rebase and the letter rule, @ChristianMurphy! I'll go through the full review next and get back to you. |
malav2110
left a comment
There was a problem hiding this comment.
Thanks, @ChristianMurphy. I re-ran everything under How I checked it and got the same results, and the output matches the shell script on all nine samples. A fresh npx skills add from this branch ships the lockfile, and the validator runs from the installed copy.
I've left some comments inline. The main one is the open question from #21 about bundling the validator, since the answer changes a few of the others. Two more are blocking: where the install puts node_modules, and what step 7 tells the agent after a usage error. The rest are questions and small suggestions.
Problem: the skill installed the validator's packages on first use. Each installed copy needed the npm registry once. It also left an untracked `scripts/node_modules` package folder in the user's repository. A copy updated by hand could keep stale packages. Step 7 read a usage error as a setup failure. No API the script uses needs Node 24.12. A span with the `srcref` class counted as a link. The check read a `#l1cache` heading anchor and a `#L3C5` column anchor the wrong way round. Goal: ship one self-contained file with its license notices. Run the toolchain through Vite+, and support Node 22.18 and later. Changes: - `vite.config.ts`: `vp pack` bundles `src/validate-output.ts` and its packages into `scripts/validate-output.js`. An inline plugin writes the license notices to `THIRD-PARTY-LICENSES.txt` beside it. The build replaces `import.meta.main` with `true`. The bundle then never exits 0 without running. `vp check` runs oxfmt, oxlint with warnings denied, and type checks. - `package.json`: vite-plus 1.1.0 replaces typescript, oxlint, and oxfmt. The runtime packages become dev dependencies. `@types/node` follows the 22 line. The `scripts/` manifest and lockfile go away. - `src/validate-output.ts`: only `a.srcref` counts as a link. A line anchor may carry a `C` and a column. `#l1cache` stays a heading anchor. - Tests: Vitest runs the suite against the bundle. The suite checks the `pass:`, `FAIL:`, and `usage:` lines, not only exit codes. New case pages cover the branches the review listed and each fix above. - CI: tests run on Node 22.18.0 and 24. A step fails when the committed bundle or notices differ from a fresh build. The samples job installs nothing. - Docs: step 7 runs a single `node` command and explains a `usage:` line. `SKILL.md`, `references/validation.md`, README, and CONTRIBUTING name the 22.18 floor. Notes: vite-plus 1.1.0 is 13 days old. Version 1.0.0 carries the advisory `GHSA-r825-w8mm-5pxw` in a built-in launch-editor copy. The pin takes 1.1.0 for that fix. An inline plugin writes the notices. The unplugin-license package has a single maintainer and little use. The rollup-plugin-license package needs rollup as a peer. Refs: malav2110#21
|
Thanks taking a look, @malav2110. I pushed 8b176d6, which answers every comment below. The main change follows the bundling question. The skill now ships one built file, |
malav2110
left a comment
There was a problem hiding this comment.
Thanks, @ChristianMurphy. The bundle addresses every comment from the last round, and a clean build reproduces the committed bundle and notices byte for byte.
One blocking issue is left, inline: the bundle fails to load in a project whose package.json says "type": "commonjs". The rest are non-blocking. Could you also update the PR description? It still describes the first-use npm ci, scripts/package.json, and the Node 24.12 floor.
Problem: the bundle is a `.js` file with `import` statements, and Node takes its module type from the nearest `package.json`. An installed copy has none of its own, so it inherits the user's project. A project set to `"type": "commonjs"` makes the bundle fail with "Cannot use import statement outside a module", and step 7 reads that as a missing Node. Goal: the bundle loads in any project, and the suite covers the cases the second review found untested. Changes: - `scripts/package.json` sets `"type": "module"` for the shipped folder. - A test copies the skill into a temporary CommonJS project and runs the bundle there. - Tests check that each `fail/` folder holds a page, plus a missing page, a short commit over 40 characters, the advisory block, and the counts line. - New case pages cover a host and port, `second`, `third`, a curly possessive, an uppercase `.MD` path, a non-hex commit, and `<style>` and `<template>` inside the quiz. - The root `engines` range matches the one vite-plus supports, and CONTRIBUTING says the same. The floor for the shipped skill stays at 22.18. - `references/validation.md` lists `<template>` among the skipped tags. Notes: removing `template` from the skipped tags fails no test. The tree adapter puts template content under a fragment node, which the text walk already skips. The page still pins the behavior. Refs: malav2110#21
|
Thanks, @malav2110. 8644489 fixes the CommonJS load and picks up the rest of the suggestions. I also rewrote the PR description, so it now covers the bundle, the |
malav2110
left a comment
There was a problem hiding this comment.
Thanks, @ChristianMurphy. The CommonJS fix works, and everything from the last round checks out. A clean build reproduces the committed bundle, and all 57 tests and nine samples pass. I've left a few non-blocking comments, mostly on SKILL.md text that still describes the old validator, plus an optional test for the advisory list. Once those are addressed, this is good to merge from my side.
| | ------ | ---------------------------------------- | -------------------------------------------------------------------------- | | ||
| | `git` | Every run | Resolving the base, fetching the ref, reading the diff | | ||
| | `gh` | Explaining a pull request | Fetching the pull request title, body, and URL | | ||
| | `node` | Every run, version 22.18 or later | Validating the page in step 7, and Mermaid sources in step 4 through `npx` | |
There was a problem hiding this comment.
suggestion (non-blocking): Lines 110-111 still say a run with no Mermaid diagram "needs no Mermaid, and therefore no Node". That sentence was true when this table row said Node was only for pages with a Mermaid diagram. Now step 7 runs the validator on every page, and the check at line 52 asks for Node 22.18 on every run. Something like:
A run whose change warrants none of those four shapes needs no Mermaid. The
hand-built families cover it. Step 7 still needs Node to run the validator.
| function getQuizText(document: Document): string { | ||
| const quiz = selectOne<AnyNode, Element>("main #quiz", document); | ||
| return quiz ? getVisibleText(quiz).replace(/\s+/g, " ").trim() : ""; | ||
| } |
There was a problem hiding this comment.
suggestion (non-blocking): Lines 590-591 of SKILL.md still describe the shell script. They say it reads the quiz "from its tag to </main>", so anything after the quiz is checked as quiz text. This function reads only main #quiz, so a section after the quiz now gets no quiz check. A page with "The first option is wrong." after the quiz fails on main and passes here. The advice to keep the quiz last still holds; only the reason changed:
Keep the quiz as the last section in `<main>`, where the template puts it, and
add no section after it. The validation script reads only the element whose `id`
is `quiz`, so text placed after it gets none of the quiz checks.
validation.md already says this, so only SKILL.md needs it.
| test("a run prints the advisory block", () => { | ||
| expect(runScript(join(passFolder, "baseline.html"), sha).stdout).toMatch(/^advisory: /m); | ||
| }); |
There was a problem hiding this comment.
suggestion (non-blocking): The advisory list itself has no test. The test at this line only matches the advisory: header. No pass page has an ordinal in its visible quiz text, so a change that always printed none would keep the suite green. A pass page whose quiz says "Which is the second step in the flow? Pick one." prints the second step in the flow? Pick one under the header:
test("the advisory lists each ordinal with the rest of its sentence", () => {
const { stdout } = runScript(join(passFolder, "advisory-ordinal.html"), sha);
expect(stdout).toMatch(/^the second step in the flow\? Pick one$/m);
});
Replaces
validate-output.shwith a validator that reads the page as the tree a browser builds and queries it with CSS selectors. It keeps the arguments, the exit codes, and thepass:andFAIL:lines. Closes #21.The skill ships the validator as one built file,
scripts/validate-output.js, with its packages inlined, so there's nonpm installstep and nothing to download.What changes
src/validate-output.ts: the source. It parses withparse5and queries withcss-select. Each check is a named function that returns its result. Text checks use string parsing andIntl.Segmenter, not regular expressions.skills/explain-diff-html/scripts/: what the skill ships.validate-output.jsis the bundle, built bynpm run build. It runs on Node 22.18 or later.THIRD-PARTY-LICENSES.txtcarries the license of each inlined package. The build writes it too.package.jsonholds only{ "type": "module" }. Without it, Node takes the module type from the user's project, and a CommonJS project can't load the bundle.vite.config.ts: Vite+ config.vp packbuilds the bundle, and a small inline plugin writes the license notices.vp checkruns oxfmt, oxlint with warnings denied, and type checks.vp testruns Vitest.tests/validate-output/: a page inpass/must pass every check, and a page infail/<check id>/must fail that check and no other. To add a case, add a page. The command-line tests run the bundle and check thepass:,FAIL:, andusage:lines, not only the exit code. One test copies the skill into a CommonJS project and runs it there..github/workflows/checks.yml: check, then rebuild and fail whenscripts/differs from what's committed. Run the tests on Node 22.18.0 and 24. Lint the workflows with actionlint and audit them with zizmor. The samples workflow doesn't install anything.references/validation.md, README, and CONTRIBUTING name the singlenodecommand and the 22.18 floor. SKILL.md checks the Node version up front. Step 7 explains ausage:line, and treats output with nopass:,FAIL:, orusage:line as a setup failure.Working on the validator needs a Node version Vite+ supports, so the root
enginesfield says^22.18.0 || ^24.11.0 || >=26.0.0. The shipped bundle doesn't need Vite+, so the skill's own floor stays at 22.18.Behavior
On every sample, the output matches the shell script byte for byte, including the exit codes and the advisory list.
These cases change on purpose. The shell script got each of them wrong, and each one has a test page:
<p class="filename wide">b.py:9</p>, unlinkeda.srcreflink elsewhere?notplain=1README.md#L3?plain=1<p data-bare="outside-repo" class="filename">, with the attribute before the class<script>inside the quiz that holds "the first option"<p class="filename" data-bare="outside-repo"><code>main.py:8</code></p>README.md#licenseReview turned up a couple more, also with test pages:
a.srcrefcounts as a link. A<span class="srcref">around a reference fails.#L3C5-L4C9and no?plain=1fails. A heading anchor such as#l1cachestill passes.The unlinked-reference failure lists each reference after #18's line, so the agent can see what to fix.
Dependencies
The bundle inlines these packages, and their licenses ship in
scripts/THIRD-PARTY-LICENSES.txt:parse5parse5-htmlparser2-tree-adaptercss-selectdomutilsdomhandlercss-whatdomelementtypeentitiesnth-checkdom-serializerboolbaseThe root
package.jsonholds the development tools, which never reach the installed skill:vite-plus@types/nodevite-plus1.1.0 brings oxlint 1.87.0, oxfmt 0.72.0, and Vitest 5.0.3. It's pinned at 1.1.0 rather than 1.0.0 because 1.0.0 carries the advisoryGHSA-r825-w8mm-5pxwin its built-in launch-editor copy.npm auditis clean.How I checked it
npm run checkpasses.npm testpasses all 57 tests on Node 22.18.0 and on Node 24.20.0.sh .github/scripts/validate-samples.shpasses every sample on both versions..github/workflows/.templatein the quiz's skipped tags. The tree adapter puts template content under a fragment node, which the text walk already skips, so that entry can't change the result. Its page still pins the behavior.scripts/package.json, the CommonJS test fails with "Cannot use import statement outside a module".pass:orFAIL:line.npx skills addinstall. In the first round, @malav2110 confirmed it copies the files inscripts/, including the lockfile that was there at the time.Follow-ups
pre.mermaidblock.