aboutsummaryrefslogtreecommitdiff
path: root/tests/gallery-probes/audit-extraction.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/gallery-probes/audit-extraction.mjs')
-rw-r--r--tests/gallery-probes/audit-extraction.mjs77
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);