From ccf5d01cabcf63d1f69fdc50c1cf3db348dee8d2 Mon Sep 17 00:00:00 2001 From: Bruno Azoulay Date: Wed, 29 Jul 2026 11:20:09 +0200 Subject: [PATCH 1/2] fix(design): catch quoted font names in non-English custom properties The typo gate only recognized typography custom properties by an English keyword in the property name (--*font*/--*ff*/--*type*). Tokens named in French, like the corpus's --police-texte, carried no such keyword and slipped through unbanned even when quoting a forbidden font (e.g. refs-design/linear-recode/styles.css: --police-texte: "Inter", ...). Add FONT_QUOTED_PROP_RE: any --* custom property whose value quotes one of the forbidden names is flagged, using the quote itself as the usage signal instead of an English keyword. Trade-off: a quoted mention in a non-keyword property (e.g. --comment: "we never use Inter") is flagged too, since it's indistinguishable from real usage without prose parsing. Adds test/design-font-custom-prop.test.ts (6 cases, mutation-checked) covering the two-line real-world fixture, all four banned names, the existing English-keyword branch (regression guard), and the allowed-font negative control. --- src/policy/design/design-system-rules.ts | 18 ++++++++--- test/design-font-custom-prop.test.ts | 40 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 test/design-font-custom-prop.test.ts diff --git a/src/policy/design/design-system-rules.ts b/src/policy/design/design-system-rules.ts index 7ca6118..d4a6f6c 100644 --- a/src/policy/design/design-system-rules.ts +++ b/src/policy/design/design-system-rules.ts @@ -12,14 +12,22 @@ const OKLCH_RE = /oklch\(\s*[\d.]+%?\s+0\.0*[1-9]/; * system may SAY "Inter" ("## Interaction states", "we never use Inter"), it * may not USE it as a font. Covered forms: CSS declarations (`font:`, * `font-family:` in any case, multiline values), JS config (`fontFamily:`), - * custom properties (`--*font*`/`--*ff*`/`--*type*`), `@import` URLs, and - * Markdown table cells (`| Body | Inter |`). Explicitly OUT of scope (chosen, - * not overlooked): unquoted prose in bullet lines — distinguishing "usage" - * from "mention" there is prose parsing, not a regex's job. + * custom properties named in English (`--*font*`/`--*ff*`/`--*type*`, value + * unquoted or quoted), ANY custom property whose value quotes a forbidden + * name (`--police-texte: "Inter", …` — a non-English property name has no + * keyword to key off, so the quote itself is the usage signal: a quoted + * token in a `--*` value is a font-family entry, not prose), `@import` + * URLs, and Markdown table cells (`| Body | Inter |`). Explicitly OUT of + * scope (chosen, not overlooked): unquoted prose in bullet lines — + * distinguishing "usage" from "mention" there is prose parsing, not a + * regex's job. Accepted trade-off of the quote signal: `--comment: "we + * never use Inter"` is flagged too — a quoted mention reads identically to + * a quoted usage once the property name carries no keyword to disambiguate. */ const FONT_DECL_RE = /(?:font(?:-family)?|fontFamily|--[\w-]*(?:font|ff|type)[\w-]*)\s*:[^;\n]*(?:\n\s*)?[^;\n]*\b(?:Inter|Roboto|Arial|Open Sans)\b/i; const FONT_IMPORT_RE = /family=[^&"')]*\b(?:Inter|Roboto|Arial|Open Sans)\b/i; const FONT_TABLE_RE = /\|\s*\*{0,2}(?:Inter|Roboto|Arial|Open Sans)\*{0,2}\s*\|/; +const FONT_QUOTED_PROP_RE = /--[\w-]+\s*:[^;\n]*["'][^;\n]*(?:\n\s*)?[^;\n]*\b(?:Inter|Roboto|Arial|Open Sans)\b/i; /** * Return the requirements missing from a design-system.md (empty = valid). @@ -32,6 +40,6 @@ export function validateDesignSystem(content: string, corpusCitationOk = false): if (!content.includes("## Design Reference")) missing.push("## Design Reference section"); if (!/https?:\/\//.test(content) && !(corpusCitationOk && hasCorpusCitation(content))) missing.push("reference URL (https://…) or Corpus citation"); if (!OKLCH_RE.test(content)) missing.push("oklch() color with chroma > 0"); - if (FONT_DECL_RE.test(content) || FONT_IMPORT_RE.test(content) || FONT_TABLE_RE.test(content)) missing.push("forbidden font (Inter/Roboto/Arial/Open Sans)"); + if (FONT_DECL_RE.test(content) || FONT_IMPORT_RE.test(content) || FONT_TABLE_RE.test(content) || FONT_QUOTED_PROP_RE.test(content)) missing.push("forbidden font (Inter/Roboto/Arial/Open Sans)"); return missing; } diff --git a/test/design-font-custom-prop.test.ts b/test/design-font-custom-prop.test.ts new file mode 100644 index 0000000..f60fbd8 --- /dev/null +++ b/test/design-font-custom-prop.test.ts @@ -0,0 +1,40 @@ +import { test, expect } from "bun:test"; +import { validateDesignSystem } from "../src/policy/design/gates"; + +const FORBIDDEN_FONT_MSG = "forbidden font (Inter/Roboto/Arial/Open Sans)"; + +/** + * Change 2 only: any custom property (English-named or not) whose value + * quotes a forbidden font name must be caught. The quote is the signal — + * `--police-texte: "Inter"` has no English keyword to key off, so without + * this branch it silently escaped detection. Real-world two-line fixture + * below is inspired by the corpus's `linear-recode/styles.css` font-stack. + */ +test("quoted custom prop: two-line font-stack with a non-English name is caught", () => { + const twoLine = + '--police-texte: "Inter", "Inter Variable", "SF Pro Display", -apple-system,\n BlinkMacSystemFont, sans-serif;'; + expect(validateDesignSystem(twoLine)).toContain(FORBIDDEN_FONT_MSG); +}); + +test("quoted custom prop: single-line non-English name is caught", () => { + expect(validateDesignSystem('--police-texte: "Inter";')).toContain(FORBIDDEN_FONT_MSG); +}); + +test("quoted custom prop: all four forbidden names are caught", () => { + const names = ["Inter", "Roboto", "Arial", "Open Sans"]; + for (const name of names) { + expect(validateDesignSystem(`--police-texte: "${name}";`)).toContain(FORBIDDEN_FONT_MSG); + } +}); + +test("regression: the existing unquoted English-keyword branch still catches --font-body: Inter", () => { + expect(validateDesignSystem("--font-body: Inter")).toContain(FORBIDDEN_FONT_MSG); +}); + +test("quoted custom prop: an allowed font name is not flagged", () => { + expect(validateDesignSystem('--police-texte: "Fraunces";')).not.toContain(FORBIDDEN_FONT_MSG); +}); + +test("accepted trade-off: a quoted PROSE mention in a non-keyword custom prop is also caught", () => { + expect(validateDesignSystem(`--commentaire: "on n'utilise jamais Inter"`)).toContain(FORBIDDEN_FONT_MSG); +}); From 08aabbc642eb4cca315cdab5742a2419a9304e17 Mon Sep 17 00:00:00 2001 From: Bruno Azoulay Date: Wed, 29 Jul 2026 11:22:05 +0200 Subject: [PATCH 2/2] chore: update CHANGELOG to 0.1.85 --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c536cc..5c1fc30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to `@fusengine/harness`. Format: [Keep a Changelog](https:// ## [Unreleased] +## [0.1.85] - 2026-07-29 + +### Fixed + +- **Typo gate missed quoted font names in non-English custom properties** (`src/policy/design/design-system-rules.ts`) — the gate only recognized typography custom properties by an English keyword in the property name (`--*font*`/`--*ff*`/`--*type*`), so a token named in French like the corpus's `--police-texte: "Inter", …` (`refs-design/linear-recode/styles.css`) slipped through unbanned. `FONT_QUOTED_PROP_RE` now flags any `--*` custom property whose value quotes a forbidden name, using the quote itself as the usage signal instead of an English keyword. + ## [0.1.84] - 2026-07-29 ### Added diff --git a/package.json b/package.json index 011c9f4..49bfd09 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fusengine/harness", - "version": "0.1.84", + "version": "0.1.85", "description": "Harness-agnostic toolkit for AI coding agents: runtime harness detection (Claude Code, Codex, Cursor, Cline, Gemini, Aider...), pure policy core (env config, project/framework detection, SOLID/file-size limits, APEX freshness, guard patterns, portable prompts), cache, project memory, ref routing, state/locks, statusline, per-harness adapters (Claude/Cursor/Cline/Gemini) and a cli-mode harness-check binary. Bun-native, with a built dist for Node + bundlers.", "type": "module", "module": "src/index.ts",