@miller-tech/uap 1.64.4 → 1.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/bin/cli.js +15 -0
  3. package/dist/bin/cli.js.map +1 -1
  4. package/dist/cli/design.d.ts +9 -0
  5. package/dist/cli/design.d.ts.map +1 -0
  6. package/dist/cli/design.js +187 -0
  7. package/dist/cli/design.js.map +1 -0
  8. package/dist/cli/react.d.ts.map +1 -1
  9. package/dist/cli/react.js +4 -0
  10. package/dist/cli/react.js.map +1 -1
  11. package/dist/cli/setup.d.ts.map +1 -1
  12. package/dist/cli/setup.js +36 -0
  13. package/dist/cli/setup.js.map +1 -1
  14. package/dist/coordination/reactor.d.ts.map +1 -1
  15. package/dist/coordination/reactor.js +25 -2
  16. package/dist/coordination/reactor.js.map +1 -1
  17. package/dist/design/gate.d.ts +16 -0
  18. package/dist/design/gate.d.ts.map +1 -0
  19. package/dist/design/gate.js +79 -0
  20. package/dist/design/gate.js.map +1 -0
  21. package/dist/design/interrogate.d.ts +17 -0
  22. package/dist/design/interrogate.d.ts.map +1 -0
  23. package/dist/design/interrogate.js +258 -0
  24. package/dist/design/interrogate.js.map +1 -0
  25. package/dist/design/reactor-inject.d.ts +8 -0
  26. package/dist/design/reactor-inject.d.ts.map +1 -0
  27. package/dist/design/reactor-inject.js +37 -0
  28. package/dist/design/reactor-inject.js.map +1 -0
  29. package/dist/design/tokens.d.ts +62 -0
  30. package/dist/design/tokens.d.ts.map +1 -0
  31. package/dist/design/tokens.js +185 -0
  32. package/dist/design/tokens.js.map +1 -0
  33. package/dist/mcp-router/tools/react.d.ts.map +1 -1
  34. package/dist/mcp-router/tools/react.js +2 -0
  35. package/dist/mcp-router/tools/react.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  38. package/src/policies/enforcers/design_token_gate.py +145 -0
  39. package/src/policies/schemas/policies/design-token-gate.md +38 -0
@@ -0,0 +1,62 @@
1
+ export interface TypographyToken {
2
+ fontFamily?: string;
3
+ fontSize?: string;
4
+ fontWeight?: string | number;
5
+ lineHeight?: string | number;
6
+ letterSpacing?: string;
7
+ fontFeature?: string;
8
+ fontVariation?: string;
9
+ }
10
+ export interface DesignTokens {
11
+ version?: string;
12
+ name?: string;
13
+ description?: string;
14
+ colors?: Record<string, string>;
15
+ typography?: Record<string, TypographyToken>;
16
+ rounded?: Record<string, string | number>;
17
+ spacing?: Record<string, string | number>;
18
+ components?: Record<string, Record<string, string>>;
19
+ }
20
+ export interface ParsedDesign {
21
+ tokens: DesignTokens;
22
+ /** Markdown body after the front matter. */
23
+ prose: string;
24
+ /** Section heading -> body text, keyed by lowercased `##` title. */
25
+ sections: Record<string, string>;
26
+ }
27
+ /** Flat, JSON-serializable allow-list consumed by the Python gate. */
28
+ export interface TokenAllowList {
29
+ name: string;
30
+ colors: string[];
31
+ spacing: string[];
32
+ radii: string[];
33
+ fontSizes: string[];
34
+ fontFamilies: string[];
35
+ generatedFrom: string;
36
+ }
37
+ /** Locate a DESIGN.md in `dir` (project root). Returns absolute path or null. */
38
+ export declare function findDesignFile(dir: string): string | null;
39
+ /** Split a DESIGN.md into YAML front matter + markdown body. */
40
+ export declare function parseDesignMd(content: string): ParsedDesign;
41
+ /** Resolve a `{path.to.token}` reference against the token tree; returns the
42
+ * raw string for non-references unchanged. */
43
+ export declare function resolveRef(tokens: DesignTokens, value: unknown): string;
44
+ /** Normalize a CSS color for set-membership comparison: lowercase, trim, and
45
+ * expand `#rgb`/`#rgba` shorthand to the 6/8-digit form. */
46
+ export declare function normalizeColor(c: string): string;
47
+ /** Derive the flat allow-list from a parsed DESIGN.md. */
48
+ export declare function buildAllowList(parsed: ParsedDesign, designPath: string, projectDir: string): TokenAllowList;
49
+ export declare function allowListPath(projectDir: string): string;
50
+ /** Write `.uap/design-tokens.json` (the gate's source of truth). */
51
+ export declare function writeAllowList(projectDir: string, allow: TokenAllowList): string;
52
+ /** Load + parse the project DESIGN.md, or null if none exists. */
53
+ export declare function loadDesign(projectDir: string): {
54
+ parsed: ParsedDesign;
55
+ path: string;
56
+ } | null;
57
+ /**
58
+ * Compact guidance string injected by the reactor when UI/UX work is detected.
59
+ * Keeps the agent on-token for new UI without re-reading the whole DESIGN.md.
60
+ */
61
+ export declare function summarizeForReactor(parsed: ParsedDesign, maxChars?: number): string;
62
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/design/tokens.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACrD;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,YAAY,CAAC;IACrB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,sEAAsE;AACtE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAID,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMzD;AAED,gEAAgE;AAChE,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CA2B3D;AAED;8CAC8C;AAC9C,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAcvE;AAED;4DAC4D;AAC5D,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAQhD;AAQD,0DAA0D;AAC1D,wBAAgB,cAAc,CAC5B,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,cAAc,CA2ChB;AAED,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,MAAM,CAKhF;AAED,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAI5F;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,SAAM,GAAG,MAAM,CA0BhF"}
@@ -0,0 +1,185 @@
1
+ /**
2
+ * DESIGN.md token model.
3
+ *
4
+ * Parses a project's DESIGN.md (the Google Labs DESIGN.md format: YAML token
5
+ * front matter + markdown prose) into a structured token set, resolves
6
+ * `{token.refs}`, and derives a flat allow-list that the (pure-stdlib) Python
7
+ * design-token gate consumes from `.uap/design-tokens.json`.
8
+ *
9
+ * Spec: https://github.com/google-labs-code/design.md (docs/spec.md).
10
+ */
11
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
12
+ import { join, dirname, relative } from 'path';
13
+ import yaml from 'js-yaml';
14
+ const DESIGN_FILENAMES = ['DESIGN.md', 'design.md', 'Design.md'];
15
+ /** Locate a DESIGN.md in `dir` (project root). Returns absolute path or null. */
16
+ export function findDesignFile(dir) {
17
+ for (const name of DESIGN_FILENAMES) {
18
+ const p = join(dir, name);
19
+ if (existsSync(p))
20
+ return p;
21
+ }
22
+ return null;
23
+ }
24
+ /** Split a DESIGN.md into YAML front matter + markdown body. */
25
+ export function parseDesignMd(content) {
26
+ let tokens = {};
27
+ let prose = content;
28
+ // Front matter must open and close with a line that is exactly `---`.
29
+ const fm = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
30
+ if (fm) {
31
+ try {
32
+ tokens = yaml.load(fm[1]) ?? {};
33
+ }
34
+ catch {
35
+ tokens = {};
36
+ }
37
+ prose = content.slice(fm[0].length);
38
+ }
39
+ const sections = {};
40
+ // Split on `##` headings (not `###`).
41
+ const parts = prose.split(/^##\s+(?!#)/m);
42
+ for (let i = 1; i < parts.length; i++) {
43
+ const block = parts[i];
44
+ const nl = block.indexOf('\n');
45
+ const heading = (nl === -1 ? block : block.slice(0, nl)).trim().toLowerCase();
46
+ const body = nl === -1 ? '' : block.slice(nl + 1).trim();
47
+ sections[heading] = body;
48
+ }
49
+ return { tokens, prose: prose.trim(), sections };
50
+ }
51
+ /** Resolve a `{path.to.token}` reference against the token tree; returns the
52
+ * raw string for non-references unchanged. */
53
+ export function resolveRef(tokens, value) {
54
+ if (typeof value !== 'string')
55
+ return String(value ?? '');
56
+ const m = value.match(/^\{([^}]+)\}$/);
57
+ if (!m)
58
+ return value;
59
+ const path = m[1].split('.');
60
+ let cur = tokens;
61
+ for (const key of path) {
62
+ if (cur && typeof cur === 'object' && key in cur) {
63
+ cur = cur[key];
64
+ }
65
+ else {
66
+ return value; // unresolved — leave as-is for the linter to flag
67
+ }
68
+ }
69
+ return typeof cur === 'string' ? cur : value;
70
+ }
71
+ /** Normalize a CSS color for set-membership comparison: lowercase, trim, and
72
+ * expand `#rgb`/`#rgba` shorthand to the 6/8-digit form. */
73
+ export function normalizeColor(c) {
74
+ let s = c.trim().toLowerCase();
75
+ const short = s.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])?$/);
76
+ if (short) {
77
+ const [, r, g, b, a] = short;
78
+ s = `#${r}${r}${g}${g}${b}${b}${a ? a + a : ''}`;
79
+ }
80
+ return s.replace(/\s+/g, '');
81
+ }
82
+ function dimStr(v) {
83
+ if (v === undefined || v === null)
84
+ return null;
85
+ if (typeof v === 'number')
86
+ return `${v}px`;
87
+ return String(v).trim();
88
+ }
89
+ /** Derive the flat allow-list from a parsed DESIGN.md. */
90
+ export function buildAllowList(parsed, designPath, projectDir) {
91
+ const t = parsed.tokens;
92
+ const colors = new Set();
93
+ const spacing = new Set();
94
+ const radii = new Set();
95
+ const fontSizes = new Set();
96
+ const fontFamilies = new Set();
97
+ for (const v of Object.values(t.colors ?? {})) {
98
+ const resolved = resolveRef(t, v);
99
+ if (resolved)
100
+ colors.add(normalizeColor(resolved));
101
+ }
102
+ for (const v of Object.values(t.spacing ?? {})) {
103
+ const d = dimStr(v);
104
+ if (d)
105
+ spacing.add(d);
106
+ }
107
+ for (const v of Object.values(t.rounded ?? {})) {
108
+ const d = dimStr(v);
109
+ if (d)
110
+ radii.add(d);
111
+ }
112
+ for (const ty of Object.values(t.typography ?? {})) {
113
+ if (ty?.fontSize)
114
+ fontSizes.add(dimStr(ty.fontSize) ?? '');
115
+ if (ty?.fontFamily)
116
+ fontFamilies.add(String(ty.fontFamily).trim().toLowerCase());
117
+ }
118
+ // Component tokens may inline literal colors/dimensions too.
119
+ for (const comp of Object.values(t.components ?? {})) {
120
+ for (const v of Object.values(comp ?? {})) {
121
+ const resolved = resolveRef(t, v);
122
+ if (/^#|^rgb|^hsl|^oklch|^oklab|^lab|^lch|^hwb|^color-mix/i.test(resolved.trim())) {
123
+ colors.add(normalizeColor(resolved));
124
+ }
125
+ }
126
+ }
127
+ return {
128
+ name: t.name ?? 'Untitled',
129
+ colors: [...colors].filter(Boolean).sort(),
130
+ spacing: [...spacing].filter(Boolean).sort(),
131
+ radii: [...radii].filter(Boolean).sort(),
132
+ fontSizes: [...fontSizes].filter(Boolean).sort(),
133
+ fontFamilies: [...fontFamilies].filter(Boolean).sort(),
134
+ generatedFrom: relative(projectDir, designPath) || 'DESIGN.md',
135
+ };
136
+ }
137
+ export function allowListPath(projectDir) {
138
+ return join(projectDir, '.uap', 'design-tokens.json');
139
+ }
140
+ /** Write `.uap/design-tokens.json` (the gate's source of truth). */
141
+ export function writeAllowList(projectDir, allow) {
142
+ const out = allowListPath(projectDir);
143
+ mkdirSync(dirname(out), { recursive: true });
144
+ writeFileSync(out, JSON.stringify(allow, null, 2) + '\n');
145
+ return out;
146
+ }
147
+ /** Load + parse the project DESIGN.md, or null if none exists. */
148
+ export function loadDesign(projectDir) {
149
+ const p = findDesignFile(projectDir);
150
+ if (!p)
151
+ return null;
152
+ return { parsed: parseDesignMd(readFileSync(p, 'utf-8')), path: p };
153
+ }
154
+ /**
155
+ * Compact guidance string injected by the reactor when UI/UX work is detected.
156
+ * Keeps the agent on-token for new UI without re-reading the whole DESIGN.md.
157
+ */
158
+ export function summarizeForReactor(parsed, maxChars = 900) {
159
+ const t = parsed.tokens;
160
+ const lines = [];
161
+ lines.push(`Apply the project design system "${t.name ?? 'DESIGN.md'}". Use ONLY these tokens for new UI:`);
162
+ const colorRoles = Object.entries(t.colors ?? {})
163
+ .slice(0, 8)
164
+ .map(([k, v]) => `${k}=${resolveRef(t, v)}`);
165
+ if (colorRoles.length)
166
+ lines.push(` colors: ${colorRoles.join(', ')}`);
167
+ const fams = [...new Set(Object.values(t.typography ?? {}).map((x) => x.fontFamily).filter(Boolean))];
168
+ if (fams.length)
169
+ lines.push(` fonts: ${fams.join(', ')}`);
170
+ const sp = Object.values(t.spacing ?? {}).map((v) => (typeof v === 'number' ? `${v}px` : v));
171
+ if (sp.length)
172
+ lines.push(` spacing: ${sp.join(', ')}`);
173
+ const donts = parsed.sections["do's and don'ts"] || parsed.sections['dos and donts'];
174
+ if (donts) {
175
+ const firstDont = donts.split('\n').find((l) => /don'?t|avoid|never/i.test(l));
176
+ if (firstDont)
177
+ lines.push(` rule: ${firstDont.replace(/^[-*]\s*/, '').trim()}`);
178
+ }
179
+ lines.push(' Hardcoding off-token colors/spacing in UI files is BLOCKED by the design gate.');
180
+ let out = lines.join('\n');
181
+ if (out.length > maxChars)
182
+ out = out.slice(0, maxChars - 1) + '…';
183
+ return out;
184
+ }
185
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/design/tokens.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../../src/mcp-router/tools/react.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,+BAA+B,CAAC;AAEvC,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuBjC,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,eAAe,CAqB5D"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../../src/mcp-router/tools/react.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,+BAA+B,CAAC;AAEvC,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuBjC,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,eAAe,CAuB5D"}
@@ -41,6 +41,8 @@ export function handleReact(args) {
41
41
  promptText: args.promptText,
42
42
  changedFiles: args.changedFiles,
43
43
  surfaced: args.surfaced,
44
+ // Locate the project DESIGN.md for design-system injection.
45
+ cwd: process.cwd(),
44
46
  };
45
47
  const opts = {
46
48
  injectThreshold: args.injectThreshold,
@@ -1 +1 @@
1
- {"version":3,"file":"react.js","sourceRoot":"","sources":["../../../src/mcp-router/tools/react.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,OAAO,IAAI,cAAc,GAG1B,MAAM,+BAA+B,CAAC;AAavC,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,IAAI,EAAE,OAAO;IACb,WAAW,EAAE;;yVAE0U;IACvV,WAAW,EAAE;QACX,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE;YACV,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+BAA+B,EAAE;YAC5E,YAAY,EAAE;gBACZ,IAAI,EAAE,OAAO;gBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBACzB,WAAW,EAAE,iCAAiC;aAC/C;YACD,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,yCAAyC,EAAE;YACjF,QAAQ,EAAE;gBACR,IAAI,EAAE,OAAO;gBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBACzB,WAAW,EAAE,uEAAuE;aACrF;SACF;QACD,QAAQ,EAAE,CAAC,YAAY,CAAC;KACzB;CACF,CAAC;AAQF,MAAM,UAAU,WAAW,CAAC,IAAe;IACzC,IAAI,CAAC;QACH,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC;YAC5E,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,wBAAwB,EAAE,CAAC;QACxD,CAAC;QACD,MAAM,GAAG,GAAmB;YAC1B,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,aAAa;YAClC,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,QAAQ,EAAE,IAAI,CAAC,QAAQ;SACxB,CAAC;QACF,MAAM,IAAI,GAAmB;YAC3B,eAAe,EAAE,IAAI,CAAC,eAAe;YACrC,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;YAC3C,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;YAC3C,cAAc,EAAE,IAAI,CAAC,cAAc;SACpC,CAAC;QACF,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC;IACzD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1E,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"react.js","sourceRoot":"","sources":["../../../src/mcp-router/tools/react.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,OAAO,IAAI,cAAc,GAG1B,MAAM,+BAA+B,CAAC;AAavC,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,IAAI,EAAE,OAAO;IACb,WAAW,EAAE;;yVAE0U;IACvV,WAAW,EAAE;QACX,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE;YACV,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+BAA+B,EAAE;YAC5E,YAAY,EAAE;gBACZ,IAAI,EAAE,OAAO;gBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBACzB,WAAW,EAAE,iCAAiC;aAC/C;YACD,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,yCAAyC,EAAE;YACjF,QAAQ,EAAE;gBACR,IAAI,EAAE,OAAO;gBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBACzB,WAAW,EAAE,uEAAuE;aACrF;SACF;QACD,QAAQ,EAAE,CAAC,YAAY,CAAC;KACzB;CACF,CAAC;AAQF,MAAM,UAAU,WAAW,CAAC,IAAe;IACzC,IAAI,CAAC;QACH,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC;YAC5E,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,wBAAwB,EAAE,CAAC;QACxD,CAAC;QACD,MAAM,GAAG,GAAmB;YAC1B,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,aAAa;YAClC,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,4DAA4D;YAC5D,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;SACnB,CAAC;QACF,MAAM,IAAI,GAAmB;YAC3B,eAAe,EAAE,IAAI,CAAC,eAAe;YACrC,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;YAC3C,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;YAC3C,cAAc,EAAE,IAAI,CAAC,cAAc;SACpC,CAAC;QACF,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC;IACzD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1E,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.64.4",
3
+ "version": "1.65.0",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ """design-token gate: UI edits must stay on the project's DESIGN.md token system.
3
+
4
+ A Write/Edit/MultiEdit to a UI file (.css/.scss/.tsx/.jsx/.vue/.svelte/.html/...)
5
+ whose proposed content introduces a hardcoded color or spacing value NOT present
6
+ in the project's design tokens is **blocked**. The agent must use a DESIGN.md
7
+ token (or a CSS var), add the value to DESIGN.md (`uap design sync`), or bypass.
8
+
9
+ The token allow-list is generated by `uap design interrogate|sync` into
10
+ `.uap/design-tokens.json` (so this enforcer needs no YAML parser). If that file
11
+ is absent, the gate is INACTIVE and fails open — DESIGN.md is opt-in per project.
12
+
13
+ Mirrors src/design/gate.ts. Escape hatch: UAP_DESIGN_GATE_OFF=1.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import re
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ sys.path.insert(0, str(Path(__file__).parent))
24
+ from _common import emit, parse_cli, repo_root, worktree_root # noqa: E402
25
+
26
+ PATH_WRITE_OPS = {"Write", "Edit", "MultiEdit", "write", "edit", "multiedit"}
27
+ UI_EXT = {".css", ".scss", ".sass", ".less", ".tsx", ".jsx", ".vue", ".svelte", ".html", ".astro"}
28
+ SPACING_IGNORE = {"0px", "1px", "2px", "3px", "4px"}
29
+
30
+ HEX_RE = re.compile(r"#[0-9a-fA-F]{3,8}\b")
31
+ FUNC_COLOR_RE = re.compile(r"\b(?:rgba?|hsla?|hwb|oklch|oklab|lch|lab)\([^)]*\)", re.IGNORECASE)
32
+ PX_RE = re.compile(r"\b\d{1,4}px\b")
33
+ VAR_RE = re.compile(r"var\(--[^)]*\)")
34
+ # A DESIGN.md token reference is a dotted path in braces (e.g. {colors.primary}).
35
+ # Must NOT match CSS declaration blocks like `{color:#fff;}` (they contain
36
+ # colons/semicolons), or the scanner would strip whole rules before checking.
37
+ REF_RE = re.compile(r"\{[\w.-]+\}")
38
+
39
+
40
+ def _norm_color(c: str) -> str:
41
+ s = c.strip().lower()
42
+ m = re.match(r"^#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])?$", s)
43
+ if m:
44
+ r, g, b, a = m.groups()
45
+ s = f"#{r}{r}{g}{g}{b}{b}{(a + a) if a else ''}"
46
+ return re.sub(r"\s+", "", s)
47
+
48
+
49
+ def _allow_list() -> dict | None:
50
+ for root in (worktree_root(), repo_root()):
51
+ p = root / ".uap" / "design-tokens.json"
52
+ if p.exists():
53
+ try:
54
+ return json.loads(p.read_text())
55
+ except Exception: # noqa: BLE001
56
+ return None
57
+ return None
58
+
59
+
60
+ def _target_path(args: dict) -> str:
61
+ for key in ("file_path", "path", "notebook_path", "filePath", "target"):
62
+ if args.get(key):
63
+ return str(args[key])
64
+ return ""
65
+
66
+
67
+ def _proposed_content(op: str, args: dict) -> str:
68
+ # Write: full content. Edit: the inserted text. MultiEdit: all inserts.
69
+ if args.get("content"):
70
+ return str(args["content"])
71
+ if args.get("new_string"):
72
+ return str(args["new_string"])
73
+ edits = args.get("edits")
74
+ if isinstance(edits, list):
75
+ return "\n".join(str(e.get("new_string", "")) for e in edits if isinstance(e, dict))
76
+ return ""
77
+
78
+
79
+ def _scan(content: str, allow: dict) -> tuple[list[str], list[str]]:
80
+ allow_colors = {_norm_color(c) for c in allow.get("colors", [])}
81
+ allow_spacing = set(allow.get("spacing", [])) | set(allow.get("radii", [])) | set(allow.get("fontSizes", []))
82
+ off_colors: list[str] = []
83
+ off_spacing: list[str] = []
84
+
85
+ for raw in content.split("\n"):
86
+ t = raw.strip()
87
+ if t.startswith("//") or t.startswith("*") or t.startswith("/*"):
88
+ continue
89
+ line = REF_RE.sub("", VAR_RE.sub("", raw))
90
+ for m in HEX_RE.finditer(line):
91
+ if _norm_color(m.group(0)) not in allow_colors:
92
+ off_colors.append(m.group(0))
93
+ for m in FUNC_COLOR_RE.finditer(line):
94
+ if re.sub(r"\s+", "", m.group(0).lower()) not in allow_colors:
95
+ off_colors.append(m.group(0))
96
+ for m in PX_RE.finditer(line):
97
+ px = m.group(0)
98
+ if px not in SPACING_IGNORE and px not in allow_spacing:
99
+ off_spacing.append(px)
100
+
101
+ return off_colors, off_spacing
102
+
103
+
104
+ def main() -> None:
105
+ op, args = parse_cli()
106
+
107
+ if os.environ.get("UAP_DESIGN_GATE_OFF") == "1":
108
+ emit(True, "UAP_DESIGN_GATE_OFF override set")
109
+ if op not in PATH_WRITE_OPS:
110
+ emit(True, "not a file-write operation")
111
+
112
+ target = _target_path(args)
113
+ if Path(target).suffix.lower() not in UI_EXT:
114
+ emit(True, "not a UI file")
115
+
116
+ allow = _allow_list()
117
+ if not allow:
118
+ emit(True, "no DESIGN.md token allow-list (.uap/design-tokens.json) — gate inactive")
119
+
120
+ content = _proposed_content(op, args)
121
+ if not content:
122
+ emit(True, "no proposed content to scan")
123
+
124
+ off_colors, off_spacing = _scan(content, allow)
125
+ if not off_colors and not off_spacing:
126
+ emit(True, "on-token UI edit")
127
+
128
+ name = allow.get("name", "DESIGN.md")
129
+ palette = ", ".join(allow.get("colors", [])[:5])
130
+ spacing_scale = ", ".join(allow.get("spacing", []))
131
+ lines = [f'design-token gate: this UI edit introduces values not in the "{name}" design system.']
132
+ if off_colors:
133
+ uniq = list(dict.fromkeys(off_colors))[:6]
134
+ lines.append(f" off-token colors: {', '.join(uniq)}")
135
+ lines.append(f" → use a DESIGN.md color token ({palette}{', …' if len(allow.get('colors', [])) > 5 else ''}) or a CSS var.")
136
+ if off_spacing:
137
+ uniq = list(dict.fromkeys(off_spacing))[:6]
138
+ lines.append(f" off-scale spacing: {', '.join(uniq)}")
139
+ lines.append(f" → use a spacing token ({spacing_scale or 'define spacing in DESIGN.md'}).")
140
+ lines.append(" Add the value to DESIGN.md (then `uap design sync`), reference a token, or bypass with UAP_DESIGN_GATE_OFF=1.")
141
+ emit(False, "\n".join(lines))
142
+
143
+
144
+ if __name__ == "__main__":
145
+ main()
@@ -0,0 +1,38 @@
1
+ # design-token-gate
2
+
3
+ **Category**: quality
4
+ **Level**: REQUIRED
5
+ **Enforcement Stage**: pre-exec
6
+ **Tags**: design, ui, ux, tokens, design-system
7
+
8
+ ## Rule
9
+
10
+ A `Write`, `Edit`, or `MultiEdit` to a UI file (`.css`, `.scss`, `.tsx`, `.jsx`,
11
+ `.vue`, `.svelte`, `.html`, `.astro`) whose proposed content hardcodes a color
12
+ or spacing value that is NOT part of the project's DESIGN.md token system is
13
+ **blocked**. The agent must use an existing design token (or CSS var), add the
14
+ value to `DESIGN.md` and re-sync, or bypass explicitly.
15
+
16
+ On-token (allowed): values present in the token allow-list, `var(--…)` usages,
17
+ `{token.ref}` references, and structural hairlines (`0–4px`). The gate is
18
+ **inactive** (fails open) when a project has no `DESIGN.md` / token allow-list.
19
+
20
+ ## Why
21
+
22
+ DESIGN.md gives agents a persistent, structured understanding of a project's
23
+ visual identity. Without enforcement, new UI drifts off-system — ad-hoc hex
24
+ colors and off-scale spacing accumulate until the design system is fiction. The
25
+ gate keeps new UI on-token so the design system stays the real source of truth.
26
+
27
+ ## Enforcement
28
+
29
+ Python enforcer `design_token_gate.py` reads `.uap/design-tokens.json` (generated
30
+ by `uap design interrogate|sync` from the project DESIGN.md — no YAML parser
31
+ needed) and scans the proposed content for off-token colors/spacing. Mirrors
32
+ `src/design/gate.ts`. Escape hatch: `UAP_DESIGN_GATE_OFF=1`.
33
+
34
+ ```rules
35
+ - title: "New UI must use DESIGN.md design tokens, not hardcoded colors/spacing"
36
+ keywords: [css, color, theme, ui, ux, button, component, style, tailwind, design]
37
+ antiPatterns: ["#hex", "rgb(", "hardcoded color", "off-scale spacing"]
38
+ ```