Files
HMDesign/examples/org-chart/verify-guard-p1.mjs
T

140 lines
7.8 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { createHash } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { performance } from 'node:perf_hooks';
import { parse } from '@babel/parser';
import postcss from 'postcss';
import { themeAnalysis } from '../../scripts/guard-theme-analysis.mjs';
const root = fileURLToPath(new URL('../../', import.meta.url));
const script = fileURLToPath(import.meta.url);
const rawFile = path.join(root, 'test-results/org-chart-p1/guard-storybook-menu-recheck.json');
const output = path.join(root, 'test-results/org-chart-quality/guard-reproduction.json');
const started = performance.now();
const hash = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
const rawHashBefore = hash(rawFile);
const raw = JSON.parse(fs.readFileSync(rawFile, 'utf8'));
const analyzerHashBefore = hash(path.join(root, 'scripts/guard-theme-analysis.mjs'));
function walk(node, visitor, parent = null, field = null) {
if (!node || typeof node !== 'object') return;
if (node.type) visitor(node, parent, field);
for (const [key, value] of Object.entries(node)) {
if (['loc', 'start', 'end', 'comments', 'tokens', 'extra'].includes(key)) continue;
if (Array.isArray(value)) value.forEach(item => walk(item, visitor, node, key));
else if (value && typeof value === 'object') walk(value, visitor, node, key);
}
}
/** In-memory fixtures use the function's existing injected parser interface.
* The script itself is a readable CSS-file sentinel because the real analyzer
* reads files before invoking its parser. The parser returns the supplied
* fixture CSS AST; no fixture files or production code are changed. */
function analyze(label, jsSource, cssSource) {
const jsFile = `memory://${label}.js`;
const records = [{ file: jsFile, edges: [], ast: parse(jsSource, { sourceType: 'module' }) }];
if (cssSource !== undefined) records.push({ file: script, edges: [], css: true });
const issues = [];
const result = themeAnalysis({
records, workspace: root,
postcss: { parse: () => postcss.parse(cssSource ?? '', { from: `memory://${label}.css` }) },
walk, selectedFile: () => true,
add: (severity, code, file, node, message) => issues.push({
severity, code, file: file === script ? `memory://${label}.css` : file,
line: node?.loc?.start?.line ?? node?.source?.start?.line ?? 1, message,
}),
});
return {
label, fixture: { js: jsSource, css: cssSource ?? null },
analyzerStatus: issues.some(issue => issue.severity === 'FAIL') ? 'FAIL'
: issues.length ? 'NOT VERIFIED' : 'PASS',
definitions: result.definitions, references: result.references.map(ref => ({
...ref, file: ref.file === script ? `memory://${label}.css` : ref.file,
})), dynamic: result.dynamic, incomplete: result.incomplete, issues,
};
}
const measurements = [];
const assertions = [];
function check(name, condition) {
assertions.push({ name, status: condition ? 'PASS' : 'FAIL' });
assert.ok(condition, name);
}
let failure;
try {
const graphSource = 'const background = "var(--hm-orgchart-resolved-bg-surface)";';
const excluded = analyze('shadow-css-excluded', graphSource);
const included = analyze('shadow-css-included', graphSource,
'.hm-orgchart-shell { --hm-orgchart-resolved-bg-surface: #ffffff; }');
measurements.push(excluded, included);
check('Excluded Shadow CSS reference reproduces TOKEN-001 FAIL',
excluded.issues.some(issue => issue.code === 'TOKEN-001' && issue.severity === 'FAIL'));
check('Connected CSS declaration resolves the identical reference',
included.analyzerStatus === 'PASS' && included.definitions.includes('--hm-orgchart-resolved-bg-surface'));
const setterSource = 'element.style.setProperty("--hm-orgchart-current-border", "#194d82");\n'
+ 'element.style.setProperty("--hm-orgchart-accent", "var(--hm-orgchart-current-border)");';
const setterOnly = analyze('set-property-not-collected', setterSource);
const setterWithCss = analyze('set-property-with-css-declaration', setterSource,
'.hm-orgchart-group { --hm-orgchart-current-border: #194d82; }');
measurements.push(setterOnly, setterWithCss);
check('Actual literal setProperty definition is absent from analyzer definitions',
!setterOnly.definitions.includes('--hm-orgchart-current-border') && setterOnly.analyzerStatus === 'FAIL');
check('CSS definition resolves setProperty reference without changing JS fixture',
setterWithCss.analyzerStatus === 'PASS');
const optional = analyze('optional-public-input-with-fallback', '',
'.hm-orgchart-shell { --hm-orgchart-resolved-fg-primary: var(--hm-orgchart-fg-primary, #10243b); }');
measurements.push(optional);
const optionalReference = optional.references.find(ref => ref.name === '--hm-orgchart-fg-primary');
check('Optional standalone input with valid fallback is still classified TOKEN-001 FAIL',
optionalReference?.fallback === true && optionalReference.status === 'FAIL'
&& optional.issues.some(issue => issue.code === 'TOKEN-001' && issue.severity === 'FAIL'));
// Diagnostic namespace control only. Renaming OrgChart inputs is NOT a fix.
const otherNamespace = analyze('external-input-namespace-control', '',
'.content { --resolved-fg-primary: var(--external-fg-primary, #10243b); }');
measurements.push(otherNamespace);
check('Same fallback shape outside HM namespace is NOT VERIFIED, not FAIL',
otherNamespace.analyzerStatus === 'NOT VERIFIED' && otherNamespace.references[0]?.fallback === true);
check('Historical raw report remains FAIL with the original three TOKEN-001 findings',
raw.status === 'FAIL' && raw.staticStatus === 'FAIL'
&& raw.issues.filter(issue => issue.code === 'TOKEN-001').length === 3);
check('Original Guard output and analyzer source hashes remain unchanged',
hash(rawFile) === rawHashBefore && hash(path.join(root, 'scripts/guard-theme-analysis.mjs')) === analyzerHashBefore);
} catch (error) {
failure = { name: error.name, message: error.message };
}
const report = {
schemaVersion: 1,
purpose: 'Minimal reproduction of existing analyzer behavior; assertion PASS is NOT OrgChart quality or Guard PASS',
reproductionStatus: failure ? 'FAIL' : 'PASS',
generatedAt: new Date().toISOString(), durationMs: Math.round((performance.now() - started) * 100) / 100,
assertions, measurements,
originalGuard: {
file: 'test-results/org-chart-p1/guard-storybook-menu-recheck.json',
sha256Before: rawHashBefore, sha256After: hash(rawFile), status: raw.status, staticStatus: raw.staticStatus,
relevantIssues: raw.issues.filter(issue => ['TOKEN-001', 'TOKEN-002'].includes(issue.code)),
},
analyzer: { file: 'scripts/guard-theme-analysis.mjs', sha256Before: analyzerHashBefore,
sha256After: hash(path.join(root, 'scripts/guard-theme-analysis.mjs')) },
scope: 'In-memory fixtures passed directly to the actual exported themeAnalysis; no full Guard suite or browser checks',
limitations: [
'This demonstrates analyzer limitations; it does not implement a revised Guard policy',
'Fixture graph inclusion is controlled directly; real Shadow URL dataflow is not implemented or rerun',
'The CSS declaration control is a fixture, not a production dummy declaration proposal',
'The external namespace control is diagnostic; production namespace renaming would be a bypass',
'Original Guard FAIL remains unchanged; runtime evidence cannot overwrite that FAIL',
'Common Guard changes require separate user approval',
],
...(failure ? { failure } : {}),
};
fs.mkdirSync(path.dirname(output), { recursive: true });
fs.writeFileSync(output, `${JSON.stringify(report, null, 2)}\n`);
console.log(JSON.stringify({ reproductionStatus: report.reproductionStatus, assertions: assertions.length,
measurements: measurements.length, originalGuardStatus: raw.status, output: path.relative(root, output) }));
if (failure) process.exitCode = 1;