融合觀測台點陣視覺語彙
All checks were successful
CD Pipeline / deploy (push) Successful in 57s

This commit is contained in:
OoO
2026-05-13 18:51:46 +08:00
parent 5a21e2394e
commit f947469a36
4 changed files with 218 additions and 7 deletions

View File

@@ -3,7 +3,7 @@
* Rendered visual contract for AI observability pages.
*
* This complements the static template guard by checking computed CSS in a
* browser: typography, warm-token surfaces, radius, hero backgrounds, and
* browser: typography, warm-token surfaces, dot-matrix texture, radius, and
* mobile density across the 10 observability pages.
*/
@@ -195,6 +195,11 @@ function contrastRatio(fg, bg) {
return (lighter + 0.05) / (darker + 0.05);
}
function isDotMatrixBackground(backgroundImage) {
const value = String(backgroundImage || '');
return value.includes('radial-gradient') && !value.includes('linear-gradient');
}
async function collectMetrics(page) {
return page.evaluate(({ heroSelectors, titleSelectors, surfaceSelectors }) => {
const parseRgbValue = (value) => {
@@ -234,6 +239,7 @@ async function collectMetrics(page) {
const title = document.querySelector(titleSelectors);
const badRadius = [];
const badBackground = [];
const missingMatrix = [];
const badChipContrast = [];
for (const el of document.querySelectorAll(`.momo-observability-mode :is(${surfaceSelectors})`)) {
@@ -246,13 +252,24 @@ async function collectMetrics(page) {
text: String(el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 80),
});
}
if ((el.matches(heroSelectors) || el.matches('.obs-panel,.agent-panel,.biz-panel,.runtime-panel,.calls-panel,.gov-panel,.gate-panel,.rag-panel,.qa-panel,.quality-panel,.ppt-panel')) && style.backgroundImage !== 'none') {
const mustUseMatrix = el.matches(heroSelectors)
|| el.matches('.obs-panel,.agent-panel,.biz-panel,.runtime-panel,.calls-panel,.gov-panel,.gate-panel,.rag-panel,.qa-panel,.quality-panel,.ppt-panel')
|| el.matches('.obs-signal,.agent-signal,.biz-signal,.runtime-signal,.calls-signal,.gov-signal,.gate-signal,.rag-signal,.qa-signal,.quality-signal,.ppt-signal');
const hasRadialMatrix = style.backgroundImage.includes('radial-gradient');
const hasLegacyGradient = style.backgroundImage.includes('linear-gradient');
if (mustUseMatrix && (!hasRadialMatrix || hasLegacyGradient)) {
badBackground.push({
className: String(el.className || '').slice(0, 100),
backgroundImage: style.backgroundImage.slice(0, 100),
});
}
if (badRadius.length >= 8 && badBackground.length >= 8) break;
if (mustUseMatrix && !hasRadialMatrix) {
missingMatrix.push({
className: String(el.className || '').slice(0, 100),
text: String(el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 80),
});
}
if (badRadius.length >= 8 && badBackground.length >= 8 && missingMatrix.length >= 8) break;
}
for (const el of document.querySelectorAll('.momo-observability-mode :is(.badge,.obs-pill,[class$="-pill"],.biz-badge)')) {
@@ -292,6 +309,7 @@ async function collectMetrics(page) {
} : null,
badRadius,
badBackground,
missingMatrix,
badChipContrast,
};
}, { heroSelectors: HERO_SELECTORS, titleSelectors: TITLE_SELECTORS, surfaceSelectors: SURFACE_SELECTORS });
@@ -323,13 +341,16 @@ function issuesFor(metrics, viewport) {
issues.push('missing hero selector');
} else {
if (px(metrics.hero.radius) > 8.5) issues.push(`hero radius ${metrics.hero.radius}`);
if (metrics.hero.backgroundImage !== 'none') issues.push('hero background image is not none');
if (!isDotMatrixBackground(metrics.hero.backgroundImage)) {
issues.push('hero missing tokenized dot-matrix background');
}
if (metrics.hero.height > viewport.maxHeroHeight) {
issues.push(`hero too tall ${metrics.hero.height}px > ${viewport.maxHeroHeight}px`);
}
}
if (metrics.badRadius.length) issues.push(`surface radius offenders ${metrics.badRadius.length}`);
if (metrics.badBackground.length) issues.push(`surface background-image offenders ${metrics.badBackground.length}`);
if (metrics.badBackground.length) issues.push(`surface non-matrix background offenders ${metrics.badBackground.length}`);
if (metrics.missingMatrix.length) issues.push(`surface missing dot-matrix offenders ${metrics.missingMatrix.length}`);
if (metrics.badChipContrast.length) issues.push(`chip contrast offenders ${metrics.badChipContrast.length}`);
return issues;
}
@@ -385,7 +406,7 @@ async function main() {
const hero = result.metrics?.hero ? `${result.metrics.hero.height}px r=${result.metrics.hero.radius}` : 'n/a';
console.log(`${status} ${result.viewport} ${result.route} title=${title} hero=${hero}${result.issues.length ? ` issues=${result.issues.join('; ')}` : ''}`);
if (!result.passed && result.metrics) {
for (const bucket of ['badRadius', 'badBackground', 'badChipContrast']) {
for (const bucket of ['badRadius', 'badBackground', 'missingMatrix', 'badChipContrast']) {
for (const offender of result.metrics[bucket] || []) {
console.log(` ${bucket}: ${JSON.stringify(offender)}`);
}