140 lines
7.8 KiB
JavaScript
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;
|