view design_system/test/design_system_policy_test.js @ 258:60a876c4587a

[ui] Add semantic primitive ownership Build a layered Zenbu token and sizing system, make authored controls use native-underneath primitives, migrate mrjunejune without imposing visual surfaces, and document/enforce HTML ownership in the catalog and wiki. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Wed, 05 Aug 2026 05:25:40 -0700
parents 2b6e732087ff
children
line wrap: on
line source

const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');

const RUNFILES = process.env.JS_BINARY__RUNFILES;
const WORKSPACE = process.env.JS_BINARY__WORKSPACE;
assert.ok(RUNFILES);
assert.ok(WORKSPACE);

const designRoot = path.join(RUNFILES, WORKSPACE, 'design_system');
const sourceRoot = path.join(designRoot, 'src');
const files = [];

function walk(directory) {
  for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
    const entryPath = path.join(directory, entry.name);
    if (entry.isDirectory()) walk(entryPath);
    else if (/\.(?:css|html|js)$/.test(entry.name)) files.push(entryPath);
  }
}

walk(sourceRoot);
function listMarkdown(directory) {
  const markdown = [];
  for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
    const entryPath = path.join(directory, entry.name);
    if (entry.isDirectory()) markdown.push(...listMarkdown(entryPath));
    else if (entry.name.endsWith('.md')) markdown.push(entryPath);
  }
  return markdown;
}
const documentationFiles = [
  ...fs.readdirSync(designRoot)
    .filter(name => name.endsWith('.md'))
    .map(name => path.join(designRoot, name)),
  ...listMarkdown(path.join(designRoot, 'wiki')),
];
const namingFiles = [
  ...files,
  ...documentationFiles,
  path.join(designRoot, 'main.c'),
];

function validateCallableDocumentation(
  relative,
  source,
  index,
  name,
  parameterSource,
  failures,
) {
  const prefix = source.slice(0, index);
  const comment = prefix.match(/\/\*\*([\s\S]*?)\*\/\s*$/);
  const body = comment?.[1] || '';
  const parameters = parameterSource
    .split(',')
    .map(parameter => parameter.trim()
      .replace(/^\.\.\./, '')
      .split('=')[0].trim())
    .filter(Boolean);
  if (!/@return\s+\{[^}]+\}/.test(body)) {
    failures.push(`${relative} exported callable ${name} lacks @return`);
  }
  const documented = [...body.matchAll(
    /@param\s+\{[^}]+\}\s+([A-Za-z0-9_$]+)/g,
  )].map(parameter => parameter[1]);
  for (const parameter of parameters) {
    if (!documented.includes(parameter)) {
      failures.push(
        `${relative} exported callable ${name} lacks @param for ${parameter}`,
      );
    }
  }
}

const rawColor = new RegExp([
  '#[0-9a-fA-F]{3,8}\\b',
  '\\b(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\\s*\\(',
].join('|'), 'gi');
const forbiddenGlyph = /[×←→↑↓+◆◇✓✔✕✖★☆⚠]/gu;
const pictographic = /\p{Extended_Pictographic}/gu;
const borrowedProductName =
  /(?:shadcn|storybook|sonner|radix|tailwind|react)/gi;
const failures = [];

for (const file of files) {
  const relative = path.relative(sourceRoot, file);
  const source = fs.readFileSync(file, 'utf8');

  if (![
    'styles/reference.css',
    'styles/tokens.css',
  ].includes(relative)) {
    for (const match of source.matchAll(rawColor)) {
      failures.push(
        `${relative}:${source.slice(0, match.index).split('\n').length} ` +
        `raw color ${JSON.stringify(match[0])}`,
      );
    }
    if (relative.endsWith('.css')) {
      const css = source.replace(/\/\*[\s\S]*?\*\//g, '');
      const allowedWords = new Set([
        'none',
        'transparent',
        'currentcolor',
        'inherit',
        'initial',
        'unset',
        'revert',
        'solid',
        'dashed',
        'dotted',
        'double',
        'groove',
        'ridge',
        'inset',
        'outset',
        'canvas',
        'canvastext',
        'linktext',
        'visitedtext',
        'activetext',
        'buttonface',
        'buttontext',
        'field',
        'fieldtext',
        'highlight',
        'highlighttext',
        'graytext',
        'mark',
        'marktext',
        'selecteditem',
        'selecteditemtext',
        'accentcolor',
        'accentcolortext',
        'color-mix',
        'in',
        'srgb',
        'linear-gradient',
        'radial-gradient',
        'conic-gradient',
        'to',
        'top',
        'right',
        'bottom',
        'left',
        'at',
        'circle',
        'ellipse',
        'px',
        'rem',
        'em',
        'vh',
        'vw',
        'vi',
        'vb',
        'vmin',
        'vmax',
        'deg',
        'rad',
        'turn',
      ]);
      for (const declaration of css.matchAll(
        /(^|[;{])\s*([-\w]+)\s*:\s*([^;{}]+)/gm,
      )) {
        const property = declaration[2].toLowerCase();
        const value = declaration[3];
        const colorProperty = [
          'accent-color',
          'background',
          'background-color',
          'box-shadow',
          'caret-color',
          'color',
          'fill',
          'outline',
          'outline-color',
          'scrollbar-color',
          'stroke',
          'text-shadow',
        ].includes(property) ||
          (property.startsWith('border') &&
           !/(?:collapse|image|radius|spacing|style|width)/.test(property));
        if (!colorProperty) continue;
        const withoutTokens = value.replace(
          /var\(\s*--(?:zenbu|zen)-[\w-]+\s*\)/g,
          '',
        );
        const words = withoutTokens.toLowerCase()
          .match(/[a-z][a-z-]*/g) || [];
        if (words.some(word => !allowedWords.has(word)) &&
            !/^\s*(?:0|none|transparent|currentColor|inherit)\s*$/i.test(
              value,
            )) {
          failures.push(
            `${relative}:${css.slice(0, declaration.index).split('\n').length} ` +
            `${property} must use a --zenbu-sys-* token`,
          );
        }
      }
    }
  }

  for (const pattern of [forbiddenGlyph, pictographic]) {
    for (const match of source.matchAll(pattern)) {
      failures.push(
        `${relative}:${source.slice(0, match.index).split('\n').length} ` +
        `icon glyph ${JSON.stringify(match[0])}; use <zen-icon>`,
      );
    }
  }
  if (relative !== 'components/icon.js' &&
      /<svg\b|createElementNS\([^)]*["']svg["']/i.test(source)) {
    failures.push(`${relative} defines SVG outside components/icon.js`);
  }
  if (relative.endsWith('.js')) {
    if (/\bimport\s*\/[/*]/.test(source)) {
      failures.push(`${relative} places comments inside import syntax`);
    }
    const importPattern =
      /(?:import\s+(?:[^'"]+\s+from\s+)?|export\s+[^'"]+\s+from\s+)["']([^"']+)["']/g;
    for (const match of source.matchAll(importPattern)) {
      if (!match[1].startsWith('.')) {
        failures.push(
          `${relative} imports external runtime ${JSON.stringify(match[1])}`,
        );
      }
    }
    if (/\bimport\s*\(/.test(source)) {
      failures.push(`${relative} uses forbidden dynamic import()`);
    }
  }
  if (relative.endsWith('.css') &&
      /@font-face|(?:font|font-family)\s*:[^;]*(?:icon|awesome|material)/i.test(
        source,
      )) {
    failures.push(`${relative} declares an icon font`);
  }
  if (relative.endsWith('.css')) {
    for (const match of source.matchAll(
      /@import\s+(?:url\(\s*)?["']?([^"')\s;]+)["']?\s*\)?/g,
    )) {
      if (!match[1].startsWith('.')) {
        failures.push(
          `${relative} imports external stylesheet ${JSON.stringify(match[1])}`,
        );
      }
    }
  }
}

const cssFiles = files.filter(file => file.endsWith('.css'));
const zenbuDeclarations = new Set();
for (const file of cssFiles) {
  const source = fs.readFileSync(file, 'utf8');
  for (const match of source.matchAll(/(--zenbu-[\w-]+)\s*:/g)) {
    zenbuDeclarations.add(match[1]);
  }
}
for (const file of cssFiles) {
  const relative = path.relative(sourceRoot, file);
  const source = fs.readFileSync(file, 'utf8');
  for (const match of source.matchAll(/var\(\s*(--zenbu-[\w-]+)/g)) {
    if (!zenbuDeclarations.has(match[1])) {
      failures.push(`${relative} references unknown token ${match[1]}`);
    }
  }
  if (![
    'styles/reference.css',
    'styles/semantic.css',
    'styles/themes.css',
    'styles/tokens.css',
  ].includes(relative)) {
    for (const match of source.matchAll(
      /var\(\s*(--zenbu-ref-color-[\w-]+)/g,
    )) {
      failures.push(
        `${relative} consumes reference color ${match[1]} directly`,
      );
    }
  }
  if (![
    'styles/themes.css',
    'styles/tokens.css',
  ].includes(relative) && /var\(\s*--zen-/.test(source)) {
    failures.push(`${relative} consumes deprecated --zen-* tokens`);
  }
}

for (const file of namingFiles) {
  const source = fs.readFileSync(file, 'utf8');
  for (const match of source.matchAll(borrowedProductName)) {
    failures.push(
      `${path.relative(designRoot, file)} uses borrowed product name ` +
      JSON.stringify(match[0]),
    );
  }
}

for (const file of files.filter(file =>
  file.endsWith('.js')
)) {
  const relative = path.relative(sourceRoot, file);
  const source = fs.readFileSync(file, 'utf8');
  const declaration =
    /^export\s+(?:default\s+)?(?:async\s+)?(class|const|let|var|function)\s+([A-Za-z0-9_]+)/gm;
  for (const match of source.matchAll(declaration)) {
    const prefix = source.slice(0, match.index);
    const comment = prefix.match(/\/\*\*([\s\S]*?)\*\/\s*$/);
    const line = prefix.split('\n').length;
    if (!comment) {
      failures.push(`${relative}:${line} ${match[2]} lacks Google-style JSDoc`);
      continue;
    }
    const body = comment[1];
    if (match[1] === 'class' && !/@extends\s+\{[^}]+\}/.test(body)) {
      failures.push(`${relative}:${line} ${match[2]} lacks @extends`);
    } else if (['const', 'let', 'var'].includes(match[1]) &&
               !/@(?:const|type)\b/.test(body)) {
      failures.push(`${relative}:${line} ${match[2]} lacks @const/@type`);
    } else if (match[1] === 'function' &&
               (!/@param\s+\{[^}]+\}/.test(body) ||
                !/@return\s+\{[^}]+\}/.test(body))) {
      failures.push(`${relative}:${line} ${match[2]} lacks @param/@return`);
    } else if (match[1] === 'function') {
      const signature = source.slice(match.index).match(
        /^export\s+(?:default\s+)?(?:async\s+)?function\s+[A-Za-z0-9_]+\s*\(([^)]*)\)/,
      );
      const parameters = (signature?.[1] || '')
        .split(',')
        .map(parameter => parameter.trim()
          .replace(/^\.\.\./, '')
          .split('=')[0].trim())
        .filter(Boolean);
      const documented = [...body.matchAll(
        /@param\s+\{[^}]+\}\s+([A-Za-z0-9_$]+)/g,
      )].map(parameter => parameter[1]);
      for (const parameter of parameters) {
        if (!documented.includes(parameter)) {
          failures.push(
            `${relative}:${line} ${match[2]} lacks @param for ${parameter}`,
          );
        }
      }
    }
  }
  for (const match of source.matchAll(
    /^export\s+const\s+([A-Za-z0-9_]+)\s*=\s*(?:async\s*)?(?:\(([^)]*)\)|([A-Za-z0-9_$]+))\s*=>/gm,
  )) {
    validateCallableDocumentation(
      relative,
      source,
      match.index,
      match[1],
      match[2] || match[3] || '',
      failures,
    );
  }
  for (const match of source.matchAll(
    /^export\s+const\s+([A-Za-z0-9_]+)\s*=\s*(?:async\s*)?function(?:\s+[A-Za-z0-9_]+)?\s*\(([^)]*)\)/gm,
  )) {
    validateCallableDocumentation(
      relative,
      source,
      match.index,
      match[1],
      match[2],
      failures,
    );
  }
  for (const match of source.matchAll(
    /export\s*\{[\s\S]*?\}\s*(?!from\b)(?:;|$)/g,
  )) {
    failures.push(
      `${relative}:${source.slice(0, match.index).split('\n').length} ` +
      "uses an undocumented local export list",
    );
  }
}

assert.deepEqual(
  failures,
  [],
  `Design-system policy violations:\n${failures.join('\n')}`,
);