diff options
Diffstat (limited to 'tests/gallery-probes/audit-extraction.mjs')
| -rw-r--r-- | tests/gallery-probes/audit-extraction.mjs | 77 |
1 files changed, 77 insertions, 0 deletions
diff --git a/tests/gallery-probes/audit-extraction.mjs b/tests/gallery-probes/audit-extraction.mjs new file mode 100644 index 0000000..3a69f2b --- /dev/null +++ b/tests/gallery-probes/audit-extraction.mjs @@ -0,0 +1,77 @@ +// audit-extraction.mjs — report-only grader for the extraction-readiness bar. +// +// Every DUPRE builder should be liftable out of the gallery without archaeology: +// a contract comment documenting opts + handle, its CSS in DUPRE_CSS rather than +// the page stylesheet, and no page code reaching past a handle into instrument +// DOM. This audit is static (no browser): it reads the sources and reports a +// worklist. It gates nothing — the gate flips on only when the sweep reaches +// 100% (the VSTATUS/policy ratchet pattern). Exit 0 always, unless the audit +// itself cannot parse the sources. +// +// Checks: +// C1 contract comment — the block comment directly above DUPRE.<name> = +// function mentions both opts and the handle (the split-flap shape). +// C2 page-styled internals — class selectors in the page <style> that also +// appear in DUPRE_CSS (the page overriding component internals). +// C3 page reach-ins — page script querySelectors descending into a card's +// stage (past the handle) instead of using the .dupre surface. +// +// Usage: node audit-extraction.mjs [--verbose] + +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const kitSrc = readFileSync(join(root, 'docs/prototypes/widgets.js'), 'utf8'); +const page = readFileSync(join(root, 'docs/prototypes/panel-widget-gallery.html'), 'utf8'); +const verbose = process.argv.includes('--verbose'); + +// ---- C1: contract comments ------------------------------------------------- +// Builders are `DUPRE.<name> = function`; the comment block that ends directly +// above (allowing blank lines) is its documentation. +const builders = []; +const re = /DUPRE\.(\w+)\s*=\s*function/g; +for (let m; (m = re.exec(kitSrc)); ) { + const name = m[1]; + const head = kitSrc.slice(0, m.index); + // Only the LAST comment block directly above counts — a lazy match from the + // file start would hand every builder the whole file's text and grade + // everything compliant (it did, on the audit's first run). + const at = head.lastIndexOf('/*'); + const tail = at >= 0 ? head.slice(at) : ''; + const comment = /^\/\*[\s\S]*\*\/\s*$/.test(tail) ? tail : ''; + const contract = /\bopts\b/i.test(comment) && /\bhandle\b/i.test(comment); + builders.push({ name, contract, commentLen: comment.length }); +} + +// ---- C2: page-styled instrument internals ------------------------------------- +const classSels = src => new Set([...src.matchAll(/\.([a-zA-Z][\w-]*)/g)].map(x => x[1])); +const cssBlock = kitSrc.match(/DUPRE_CSS\s*=\s*\[([\s\S]*?)\]\.join|DUPRE_CSS\s*=\s*`([\s\S]*?)`/); +const dupreCss = cssBlock ? (cssBlock[1] || cssBlock[2]) : ''; +if (!dupreCss) { console.error('AUDIT ERROR: DUPRE_CSS not found'); process.exit(1); } +const pageStyle = [...page.matchAll(/<style>([\s\S]*?)<\/style>/g)].map(x => x[1]).join('\n'); +const dupreClasses = classSels(dupreCss); +// Rig and page chrome legitimately live in the page; only instrument-internal +// classes (defined in DUPRE_CSS) styled by the page are violations. +const pageOverrides = [...classSels(pageStyle)].filter(c => dupreClasses.has(c)); + +// ---- C3: page reach-ins ---------------------------------------------------- +// Page script (inline <script> bodies only; widgets.js is external). +const pageScript = [...page.matchAll(/<script>([\s\S]*?)<\/script>/g)].map(x => x[1]).join('\n'); +const RIG = /\.(famchips|fc|fgroup|lab|opts|wrd|stagew|winfo|wnote|wname|no)\b/; +const reachIns = [...pageScript.matchAll(/querySelector(?:All)?\(\s*['"](#card-[\w-]+\s+[^'"]+)['"]/g)] + .map(x => x[1]).filter(sel => !RIG.test(sel)); + +// ---- report ---------------------------------------------------------------- +const good = builders.filter(b => b.contract); +console.log(`extraction audit — ${builders.length} builders`); +console.log(`C1 contract comments: ${good.length}/${builders.length} compliant`); +if (verbose || true) { + const bad = builders.filter(b => !b.contract).map(b => b.name); + if (bad.length) console.log(` missing (${bad.length}): ${bad.join(' ')}`); +} +console.log(`C2 page-styled instrument internals: ${pageOverrides.length}${pageOverrides.length ? ' — ' + [...new Set(pageOverrides)].join(' ') : ' (clean)'}`); +console.log(`C3 page reach-ins past handles: ${reachIns.length}${reachIns.length ? '' : ' (clean)'}`); +reachIns.forEach(r => console.log(' ' + r)); +process.exit(0); |
