aboutsummaryrefslogtreecommitdiff
path: root/tests/gallery-probes/audit-extraction.mjs
blob: 3a69f2b5dc143e0ac63d9b9adc37848ae77b47b1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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);