T-ENG01: test the scripts, and validate what they export - #36
Merged
Conversation
The project shipped OSCAL and STIX for other tools to consume and never read either back. It turns out 13 of the 25 frameworks were emitting OSCAL catalogs that no OSCAL tool would load — 750 control ids that are not valid OSCAL tokens — and nothing had ever noticed, because nothing looked. Test suites (3 tests → 50): stats.test.mjs invariants on the numbers every headline renders from generate.test.mjs determinism, round-trip, entry and schema-v2 integrity reports.test.mjs the three report CLIs, through their CLIs ingest.test.mjs the framework ingest path — mostly that it rejects exports.test.mjs OSCAL and STIX validated against schema Export validation uses ajv against three schemas in data/schemas/. They are deliberate subsets, written and owned here, and the filenames and the README say so: passing proves the export has not lost its shape, not that it conforms to OSCAL or STIX. data/schemas/README.md records why fetching or vendoring the upstream schemas costs more than it returns, and how to run the real validators when that is the question. Fixes found by the new tests: - `oscalToken()` in compliance-report.js coerces control ids into valid OSCAL tokens. Real ids routinely are not: `Art. 5`, `SR 1.1`, `4.1`, `V8 Data Protection`. The original is preserved verbatim in the control title and a `source-control-id` prop, and the same rule runs in both the catalog and the component-definition renderer so the two still join. Non-token ids across all 25 frameworks: 750 → 0. - ingest-framework.mjs now rejects an empty control set. It validated cleanly before, so a registry with no controls could land in data/frameworks/ and count as a framework covering nothing. Not fixed: 576 mapping rows still hold prose in `control_id` because the Markdown columns are swapped — the root cause of the invalid ids. That is a parser fix touching 576 rows across 25 frameworks and it deserves its own review, so it is issue #35. exports.test.mjs pins the per-framework counts as a regression fence, with a second test that fails if a baseline entry outlives the framework it names. CI gains a `Unit tests` job that runs `npm ci --ignore-scripts`, the suite, and then `git diff --exit-code` — the suite re-runs the generator to prove determinism, so it has to leave the tree clean. `npm run ci` runs the suite too, between build and reports. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`node --test "scripts/**/*.test.mjs"` fails in CI: bash does not expand `**` without globstar, and Node 20 does not glob the pattern itself. The suite had never run in CI before this PR, so nothing caught it. Unquoted `scripts/*.test.mjs` works both ways — a POSIX shell expands it to a file list, and on Windows, where cmd does not glob, Node expands it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The project shipped OSCAL and STIX for other tools to consume and never read either back. It turns out 13 of the 25 frameworks were emitting OSCAL catalogs that no OSCAL tool would load — 750 control ids that are not valid OSCAL tokens — and nothing had noticed, because nothing looked.
Test suites — 3 tests → 50
stats.test.mjsmapped ≤ registries,draft_onlyreally has no authored row, disk matches a fresh computationgenerate.test.mjsDRAFTnever leaks into a stored enum fieldreports.test.mjssummaryagreeing with the array it ships, an unknown--frameworkfailing loudlyingest.test.mjsexports.test.mjsExport validation
Uses ajv against three schemas in
data/schemas/. They are deliberate subsets, written and owned here — the filenames anddata/schemas/README.mdsay so. Passing proves the export has not lost its shape; it does not prove OSCAL or STIX conformance. The README records why fetching or vendoring the upstream schemas costs more than it returns, and how to run the real validators when that is the question.Two fixes the tests found
1.
oscalToken()incompliance-report.js. OSCAL ids are NCName-like: no leading digit, no whitespace. Real control ids routinely are neither —Art. 5(DORA),SR 1.1(ISA 62443),4.1(ISO 42001),V8 Data Protection(ASVS). They were emitted verbatim. The original is now preserved in the controltitleand asource-control-idprop, and the same coercion runs in both the catalog and component-definition renderers so the two still join.2.
ingest-framework.mjsnow rejects an empty control set. It validated cleanly before, so a registry with zero controls could land indata/frameworks/and count as a framework covering nothing.Not fixed here — issue #35
576 mapping rows still hold prose in
control_id, because some Markdown files put the identifier and the requirement in swapped columns andparseControlTable()falls through to the wrong one. That is the root cause of the invalid ids. It is a parser fix touching 576 rows across 25 frameworks and deserves its own review.exports.test.mjspins the per-framework counts as a regression fence — SOC 2 168, EU AI Act 118, OWASP NHI 103, and so on — so the problem cannot spread, plus a second test that fails if a baseline entry outlives the framework it names, so the fence cannot rot into dead permission.CI
New Unit tests job:
npm ci --ignore-scripts, the suite, thengit diff --exit-code. The suite re-runs the generator to prove determinism, so it has to leave the tree clean.npm run cinow runs the suite too, between build and reports. Path filters gainscripts/*.test.mjsandpackage.json.Verification
npm run test:scripts— 50 pass, 0 failvalidate.js0 errors / 312 passed ·stats:checkgreen ·markdownlint0 errorsnpm run complianceandnpm run incidentsboth regenerate cleanajv+ajv-formats, devDependencies only🤖 Generated with Claude Code