agentready-design-cli 0.1.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.
@@ -0,0 +1,1207 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/rubric.ts
4
+ var RUBRIC = [
5
+ // Category 1 — Tokens & Foundations
6
+ { id: "1.1", category: 1, title: "Tokens exist as exported, versioned source files." },
7
+ { id: "1.2", category: 1, title: "Semantic tokens exist on top of primitives." },
8
+ { id: "1.3", category: 1, title: "Tokens are closed and enumerable." },
9
+ { id: "1.4", category: 1, title: "Token usage is enforced." },
10
+ // Category 2 — Component API Contract
11
+ { id: "2.1", category: 2, title: "Every component has a machine-readable manifest." },
12
+ { id: "2.2", category: 2, title: "Prop naming is consistent across the library." },
13
+ { id: "2.3", category: 2, title: "Composition and slot rules are explicit." },
14
+ { id: "2.4", category: 2, title: "Anti-patterns are documented per component." },
15
+ { id: "2.5", category: 2, title: "Accessibility metadata travels with the component." },
16
+ // Category 3 — Documentation Format
17
+ { id: "3.1", category: 3, title: "Single canonical docs site, no scattered duplicates." },
18
+ { id: "3.2", category: 3, title: "Component pages have a stable, predictable structure." },
19
+ { id: "3.3", category: 3, title: "At least 3 concrete usage examples per component, with anti-patterns." },
20
+ { id: "3.4", category: 3, title: "An llms.txt (and optionally llms-full.txt) is published." },
21
+ // Category 4 — Composition & Layout
22
+ { id: "4.1", category: 4, title: "Layout primitives exist as first-class components." },
23
+ { id: "4.2", category: 4, title: "Responsive conventions are explicit and uniform." },
24
+ { id: "4.3", category: 4, title: "A patterns or recipes layer exists for compositions agents should reuse." },
25
+ // Category 5 — Retrieval & Discoverability
26
+ { id: "5.1", category: 5, title: "AGENTS.md (or CLAUDE.md) exists and is hand-curated." },
27
+ { id: "5.2", category: 5, title: "An MCP server (or registry endpoint) exposes the system to agents." },
28
+ { id: "5.3", category: 5, title: "Always-on foundational rules are injected into every agent run." },
29
+ { id: "5.4", category: 5, title: "Components are addressable by stable, predictable identifiers." },
30
+ // Category 6 — Distribution & Versioning
31
+ { id: "6.1", category: 6, title: "The system is installable through a single, well-known mechanism." },
32
+ { id: "6.2", category: 6, title: "Breaking changes and deprecations are machine-readable." },
33
+ { id: "6.3", category: 6, title: "A clear current vs. latest version is communicated to agents." },
34
+ // Category 7 — Quality Signals
35
+ { id: "7.1", category: 7, title: "Every component has interaction tests / stories." },
36
+ { id: "7.2", category: 7, title: "Visual regression is in place." },
37
+ { id: "7.3", category: 7, title: "Accessibility checks are automated." },
38
+ { id: "7.4", category: 7, title: "Code quality and consistency within the library itself." },
39
+ // Category 8 — Evaluation & Governance for Agents
40
+ { id: "8.1", category: 8, title: "An eval suite tests agent output against the design system." },
41
+ { id: "8.2", category: 8, title: "Trust levels are defined for agent actions." },
42
+ { id: "8.3", category: 8, title: "Drift detection between docs, tokens, and code." },
43
+ { id: "8.4", category: 8, title: "Generated-code attribution and review." }
44
+ ];
45
+ var TOTAL_CRITERIA = RUBRIC.length;
46
+
47
+ // src/rollup.ts
48
+ function median(nums) {
49
+ if (nums.length === 0) return 0;
50
+ const sorted = [...nums].sort((a, b) => a - b);
51
+ const mid = Math.floor(sorted.length / 2);
52
+ return sorted.length % 2 === 0 ? ((sorted[mid - 1] ?? 0) + (sorted[mid] ?? 0)) / 2 : sorted[mid] ?? 0;
53
+ }
54
+ function scored(criteria) {
55
+ return criteria.filter((c) => c.status === "scored" && c.score !== null);
56
+ }
57
+ function categoryScores(criteria, category) {
58
+ return scored(criteria).filter((c) => c.category === category).map((c) => c.score);
59
+ }
60
+ function tierFor(floor, retrieval, overall, byCat) {
61
+ const cat8Median = byCat["8"] ?? 0;
62
+ const allCatsAtLeast3 = [1, 2, 3, 4, 5, 6, 7, 8].every((c) => (byCat[String(c)] ?? 0) >= 3);
63
+ if (allCatsAtLeast3 && cat8Median >= 3) return 4;
64
+ if (floor >= 3 && retrieval >= 3 && overall >= 3) return 3;
65
+ if (floor >= 2 && retrieval >= 2 && overall >= 2) return 2;
66
+ if (floor >= 2) return 1;
67
+ return 0;
68
+ }
69
+ function computeRollup(criteria) {
70
+ const foundationalScores = scored(criteria).filter((c) => c.category <= 4).map((c) => c.score);
71
+ const retrievalScores = categoryScores(criteria, 5);
72
+ const allScores = scored(criteria).map((c) => c.score);
73
+ const categoryFloors = {};
74
+ const categoryMedians = {};
75
+ for (const cat of [1, 2, 3, 4, 5, 6, 7, 8]) {
76
+ const s = categoryScores(criteria, cat);
77
+ if (s.length > 0) {
78
+ categoryFloors[`${cat}`] = Math.min(...s);
79
+ categoryMedians[`${cat}`] = median(s);
80
+ }
81
+ }
82
+ const floor = foundationalScores.length > 0 ? Math.min(...foundationalScores) : 0;
83
+ const retrieval = retrievalScores.length > 0 ? Math.max(...retrievalScores) : 0;
84
+ const overall = median(allScores);
85
+ const tier = tierFor(floor, retrieval, overall, categoryMedians);
86
+ return {
87
+ foundationalFloor: floor,
88
+ retrievalLevel: retrieval,
89
+ overallMedian: overall,
90
+ tier,
91
+ categoryFloors,
92
+ categoryMedians
93
+ };
94
+ }
95
+ var TIER_LABELS = {
96
+ 0: "Pre-agentic",
97
+ 1: "Human-ready, AI-hostile",
98
+ 2: "Agent-legible",
99
+ 3: "Agent-collaborative",
100
+ 4: "Self-healing"
101
+ };
102
+ var EFFORT_HINTS = {
103
+ "1.1": "days",
104
+ "1.2": "days",
105
+ "1.3": "days",
106
+ "1.4": "hours",
107
+ "2.1": "weeks",
108
+ "2.2": "weeks",
109
+ "2.3": "weeks",
110
+ "2.4": "days",
111
+ "2.5": "weeks",
112
+ "3.1": "weeks",
113
+ "3.2": "weeks",
114
+ "3.3": "weeks",
115
+ "3.4": "hours",
116
+ "4.1": "weeks",
117
+ "4.2": "days",
118
+ "4.3": "weeks",
119
+ "5.1": "hours",
120
+ "5.2": "weeks",
121
+ "5.3": "hours",
122
+ "5.4": "days",
123
+ "6.1": "weeks",
124
+ "6.2": "days",
125
+ "6.3": "hours",
126
+ "7.1": "weeks",
127
+ "7.2": "days",
128
+ "7.3": "days",
129
+ "7.4": "weeks",
130
+ "8.1": "months",
131
+ "8.2": "days",
132
+ "8.3": "weeks",
133
+ "8.4": "days"
134
+ };
135
+ function generateBacklog(criteria) {
136
+ const ranked = [...criteria].filter((c) => c.score !== null && c.score < 3).sort((a, b) => {
137
+ const aFoundational = a.category <= 4 ? 0 : 1;
138
+ const bFoundational = b.category <= 4 ? 0 : 1;
139
+ if (aFoundational !== bFoundational) return aFoundational - bFoundational;
140
+ const aScore = a.score ?? 0;
141
+ const bScore = b.score ?? 0;
142
+ if (aScore !== bScore) return aScore - bScore;
143
+ return a.category - b.category;
144
+ }).slice(0, 10);
145
+ return ranked.map((c, i) => ({
146
+ title: c.suggestion ?? `Raise ${c.id} from ${c.score}/4 toward 3+`,
147
+ rationale: c.rationale ?? `Criterion ${c.id} is at ${c.score}/4.`,
148
+ criteria: [c.id],
149
+ priority: i + 1,
150
+ estimatedEffort: EFFORT_HINTS[c.id] ?? "days"
151
+ }));
152
+ }
153
+
154
+ // src/context.ts
155
+ import { readFile, stat } from "fs/promises";
156
+ import { resolve, relative, sep } from "path";
157
+ import fg from "fast-glob";
158
+ var IGNORES = [
159
+ "**/node_modules/**",
160
+ "**/dist/**",
161
+ "**/build/**",
162
+ "**/.next/**",
163
+ "**/.astro/**",
164
+ "**/.cache/**",
165
+ "**/.turbo/**",
166
+ "**/coverage/**",
167
+ "**/.git/**"
168
+ ];
169
+ var CheckContext = class {
170
+ root;
171
+ fileCache = /* @__PURE__ */ new Map();
172
+ globCache = /* @__PURE__ */ new Map();
173
+ pkgJsonCache;
174
+ constructor(root) {
175
+ this.root = resolve(root);
176
+ }
177
+ resolve(...p) {
178
+ return resolve(this.root, ...p);
179
+ }
180
+ relative(p) {
181
+ return relative(this.root, p).split(sep).join("/");
182
+ }
183
+ async exists(rel) {
184
+ try {
185
+ await stat(this.resolve(rel));
186
+ return true;
187
+ } catch {
188
+ return false;
189
+ }
190
+ }
191
+ async existsAny(rels) {
192
+ for (const r of rels) {
193
+ if (await this.exists(r)) return r;
194
+ }
195
+ return null;
196
+ }
197
+ async readFile(rel) {
198
+ if (this.fileCache.has(rel)) return this.fileCache.get(rel) ?? null;
199
+ try {
200
+ const content = await readFile(this.resolve(rel), "utf8");
201
+ this.fileCache.set(rel, content);
202
+ return content;
203
+ } catch {
204
+ this.fileCache.set(rel, null);
205
+ return null;
206
+ }
207
+ }
208
+ async glob(pattern, opts) {
209
+ const key = JSON.stringify({ p: pattern, i: opts?.ignore ?? null });
210
+ const cached = this.globCache.get(key);
211
+ if (cached) return cached;
212
+ const matches = await fg(pattern, {
213
+ cwd: this.root,
214
+ ignore: [...IGNORES, ...opts?.ignore ?? []],
215
+ dot: true,
216
+ onlyFiles: true,
217
+ followSymbolicLinks: false
218
+ });
219
+ matches.sort();
220
+ this.globCache.set(key, matches);
221
+ return matches;
222
+ }
223
+ async firstMatch(pattern) {
224
+ const matches = await this.glob(pattern);
225
+ return matches[0] ?? null;
226
+ }
227
+ async pkgJson() {
228
+ if (this.pkgJsonCache !== void 0) return this.pkgJsonCache;
229
+ const raw = await this.readFile("package.json");
230
+ if (!raw) {
231
+ this.pkgJsonCache = null;
232
+ return null;
233
+ }
234
+ try {
235
+ this.pkgJsonCache = JSON.parse(raw);
236
+ } catch {
237
+ this.pkgJsonCache = null;
238
+ }
239
+ return this.pkgJsonCache;
240
+ }
241
+ /**
242
+ * Heuristic "component directories" — places to look for component source.
243
+ * Covers common conventions: top-level components/, ui/, lib/, src/, packages/*\/src.
244
+ */
245
+ async componentDirs() {
246
+ const candidates = [
247
+ "components",
248
+ "src/components",
249
+ "src/ui",
250
+ "ui",
251
+ "lib/components",
252
+ "packages/ui/src",
253
+ "packages/components/src",
254
+ "packages/ui/src/components"
255
+ ];
256
+ const found = [];
257
+ for (const c of candidates) {
258
+ if (await this.exists(c)) found.push(c);
259
+ }
260
+ if (found.length === 0) {
261
+ const pkgs = await this.glob("packages/*/package.json");
262
+ for (const p of pkgs) {
263
+ const base = p.replace(/\/package\.json$/, "");
264
+ for (const sub of ["src", "src/components", "components"]) {
265
+ const full = `${base}/${sub}`;
266
+ if (await this.exists(full)) found.push(full);
267
+ }
268
+ }
269
+ }
270
+ return found;
271
+ }
272
+ };
273
+
274
+ // src/checks/category-1-tokens.ts
275
+ var TOKEN_DIRS = ["tokens", "src/tokens", "design-tokens", "src/design-tokens", "packages/tokens"];
276
+ async function findTokenSources(ctx) {
277
+ const out = {
278
+ dir: null,
279
+ jsonFiles: [],
280
+ cssVarFiles: [],
281
+ dtcg: false,
282
+ styleDictionary: false
283
+ };
284
+ for (const d of TOKEN_DIRS) {
285
+ if (await ctx.exists(d)) {
286
+ out.dir = d;
287
+ break;
288
+ }
289
+ }
290
+ out.jsonFiles = await ctx.glob([
291
+ "tokens/**/*.json",
292
+ "**/*.tokens.json",
293
+ "design-tokens/**/*.json",
294
+ "packages/tokens/**/*.json"
295
+ ]);
296
+ out.cssVarFiles = await ctx.glob(["tokens/**/*.{css,scss}", "src/tokens/**/*.{css,scss}", "**/tokens.css"]);
297
+ if (await ctx.firstMatch(["style-dictionary.config.{js,cjs,mjs,ts,json}", "sd.config.{js,cjs,mjs,ts,json}"])) {
298
+ out.styleDictionary = true;
299
+ }
300
+ for (const f of out.jsonFiles.slice(0, 5)) {
301
+ const raw = await ctx.readFile(f);
302
+ if (raw && /"\$value"\s*:/.test(raw)) {
303
+ out.dtcg = true;
304
+ break;
305
+ }
306
+ }
307
+ return out;
308
+ }
309
+ var checks = {
310
+ "1.1": async (ctx) => {
311
+ const t = await findTokenSources(ctx);
312
+ if (!t.dir && t.jsonFiles.length === 0 && t.cssVarFiles.length === 0) {
313
+ return {
314
+ score: 0,
315
+ rationale: "No token directory, JSON token export, or CSS-variables file detected.",
316
+ evidence: [
317
+ { kind: "glob", path: TOKEN_DIRS.join(", "), detail: "no matches" },
318
+ { kind: "glob", path: "**/*.tokens.json", detail: "no matches" }
319
+ ],
320
+ suggestion: "Create a /tokens directory and export tokens as JSON or CSS custom properties."
321
+ };
322
+ }
323
+ if (t.dtcg && t.styleDictionary) {
324
+ return {
325
+ score: 4,
326
+ rationale: "DTCG-formatted JSON tokens detected alongside a Style Dictionary build pipeline.",
327
+ evidence: [
328
+ ...t.dir ? [{ kind: "file", path: t.dir }] : [],
329
+ ...t.jsonFiles.slice(0, 3).map((f) => ({ kind: "file", path: f })),
330
+ { kind: "note", detail: "Style Dictionary config present" }
331
+ ]
332
+ };
333
+ }
334
+ if (t.dtcg) {
335
+ return {
336
+ score: 3,
337
+ rationale: "DTCG-formatted JSON tokens detected (uses $value/$type).",
338
+ evidence: t.jsonFiles.slice(0, 5).map((f) => ({ kind: "file", path: f })),
339
+ suggestion: "Wire up Style Dictionary v4 (or equivalent) to consume the DTCG source."
340
+ };
341
+ }
342
+ if (t.jsonFiles.length > 0) {
343
+ return {
344
+ score: 2,
345
+ rationale: "JSON token export exists but not in DTCG format ($value/$type missing).",
346
+ evidence: t.jsonFiles.slice(0, 5).map((f) => ({ kind: "file", path: f })),
347
+ suggestion: "Migrate to W3C DTCG format (stable spec 2025.10)."
348
+ };
349
+ }
350
+ return {
351
+ score: 1,
352
+ rationale: "CSS variables exist but no JSON export of the token set.",
353
+ evidence: t.cssVarFiles.slice(0, 5).map((f) => ({ kind: "file", path: f })),
354
+ suggestion: "Export tokens as JSON (W3C DTCG format) so agents and tooling can enumerate them."
355
+ };
356
+ },
357
+ "1.2": async () => ({
358
+ score: null,
359
+ status: "pending",
360
+ pending: "requires-agent",
361
+ rationale: "Detecting whether semantic tokens layer on top of primitives requires reading token names and component usage; agent should verify."
362
+ }),
363
+ "1.3": async (ctx) => {
364
+ const t = await findTokenSources(ctx);
365
+ if (t.jsonFiles.length === 0 && t.cssVarFiles.length === 0) {
366
+ return {
367
+ score: 0,
368
+ rationale: "No enumerable token source found.",
369
+ evidence: [{ kind: "note", detail: "no token JSON or CSS-var files" }]
370
+ };
371
+ }
372
+ return {
373
+ score: null,
374
+ status: "pending",
375
+ pending: "requires-agent",
376
+ rationale: "Token sources exist; an agent must verify they are exhaustive and that no hidden tokens live in per-component CSS.",
377
+ evidence: [...t.jsonFiles, ...t.cssVarFiles].slice(0, 5).map((f) => ({ kind: "file", path: f }))
378
+ };
379
+ },
380
+ "1.4": async (ctx) => {
381
+ const stylelint = await ctx.firstMatch([
382
+ ".stylelintrc",
383
+ ".stylelintrc.{json,js,cjs,mjs,yaml,yml}",
384
+ "stylelint.config.{js,cjs,mjs,ts}"
385
+ ]);
386
+ const eslint = await ctx.firstMatch([
387
+ ".eslintrc",
388
+ ".eslintrc.{json,js,cjs,mjs,yaml,yml}",
389
+ "eslint.config.{js,cjs,mjs,ts}"
390
+ ]);
391
+ const compDirs = await ctx.componentDirs();
392
+ const cssFiles = await ctx.glob(compDirs.map((d) => `${d}/**/*.{css,scss}`));
393
+ const hexFindings = [];
394
+ for (const f of cssFiles.slice(0, 50)) {
395
+ const content = await ctx.readFile(f);
396
+ if (content && /#[0-9a-fA-F]{3,8}\b/.test(content)) {
397
+ hexFindings.push(f);
398
+ }
399
+ }
400
+ if (!stylelint && hexFindings.length > 0) {
401
+ return {
402
+ score: 0,
403
+ rationale: `No stylelint config detected and ${hexFindings.length} component CSS file(s) contain raw hex literals.`,
404
+ evidence: hexFindings.slice(0, 5).map((f) => ({ kind: "file", path: f })),
405
+ suggestion: "Add a stylelint rule that rejects hex literals in component CSS."
406
+ };
407
+ }
408
+ if (!stylelint) {
409
+ return {
410
+ score: 1,
411
+ rationale: "No stylelint config detected; token usage is not enforced by lint.",
412
+ evidence: [{ kind: "note", detail: "no .stylelintrc / stylelint.config.* found" }],
413
+ suggestion: "Add stylelint + a rule rejecting raw hex/px in component CSS."
414
+ };
415
+ }
416
+ return {
417
+ score: null,
418
+ status: "pending",
419
+ pending: "requires-agent",
420
+ rationale: "Stylelint or ESLint config detected; agent should verify it actually rejects raw hex/px in component CSS.",
421
+ evidence: [
422
+ ...stylelint ? [{ kind: "file", path: stylelint }] : [],
423
+ ...eslint ? [{ kind: "file", path: eslint }] : []
424
+ ]
425
+ };
426
+ }
427
+ };
428
+
429
+ // src/checks/category-2-api.ts
430
+ var checks2 = {
431
+ "2.1": async (ctx) => {
432
+ const pkg = await ctx.pkgJson();
433
+ const declaresCEM = !!pkg && typeof pkg.customElements === "string";
434
+ const cemFile = await ctx.firstMatch(["custom-elements.json", "**/custom-elements.json"]);
435
+ const sbManifest = await ctx.firstMatch([
436
+ "**/manifest/components.json",
437
+ "**/storybook-manifest.json",
438
+ ".storybook/manifest.json"
439
+ ]);
440
+ const docgen = await ctx.firstMatch(["**/docgen.json", "**/.docgen/**/*.json"]);
441
+ const enumeratedHint = await (async () => {
442
+ const files = await ctx.glob(["**/*.{ts,tsx}"], { ignore: ["**/*.test.*", "**/*.stories.*"] });
443
+ let withEnum = 0;
444
+ for (const f of files.slice(0, 50)) {
445
+ const content = await ctx.readFile(f);
446
+ if (content && /variant\?:\s*["'][^"']+["']\s*\|/.test(content)) withEnum++;
447
+ }
448
+ return withEnum;
449
+ })();
450
+ if (declaresCEM && cemFile) {
451
+ return {
452
+ score: 3,
453
+ rationale: 'package.json declares "customElements" and a custom-elements.json ships.',
454
+ evidence: [
455
+ { kind: "file", path: "package.json", detail: "customElements declared" },
456
+ { kind: "file", path: cemFile }
457
+ ],
458
+ suggestion: "Serve the CEM via an MCP tool to reach Level 4."
459
+ };
460
+ }
461
+ if (cemFile || sbManifest) {
462
+ return {
463
+ score: 2,
464
+ rationale: "Manifest file present but not declared from package.json.",
465
+ evidence: [
466
+ ...cemFile ? [{ kind: "file", path: cemFile }] : [],
467
+ ...sbManifest ? [{ kind: "file", path: sbManifest }] : []
468
+ ],
469
+ suggestion: 'Declare "customElements": "custom-elements.json" in package.json and include it in the "files" array.'
470
+ };
471
+ }
472
+ if (docgen) {
473
+ return {
474
+ score: 1,
475
+ rationale: "react-docgen output exists but is not published as a contract.",
476
+ evidence: [{ kind: "file", path: docgen }],
477
+ suggestion: "Promote to a versioned component manifest (CEM or Storybook Component Manifest)."
478
+ };
479
+ }
480
+ if (enumeratedHint > 0) {
481
+ return {
482
+ score: 1,
483
+ rationale: "TypeScript types describe component props but no machine-readable manifest is published.",
484
+ evidence: [{ kind: "note", detail: `${enumeratedHint} component(s) appear to enumerate variants in TS` }],
485
+ suggestion: "Generate a Custom Elements Manifest or Storybook Component Manifest and ship it in the package."
486
+ };
487
+ }
488
+ return {
489
+ score: 0,
490
+ rationale: "No component manifest detected (CEM, Storybook manifest, or react-docgen output).",
491
+ evidence: [
492
+ { kind: "glob", path: "custom-elements.json", detail: "no match" },
493
+ { kind: "glob", path: "**/storybook-manifest.json", detail: "no match" }
494
+ ],
495
+ suggestion: "Add a manifest (CEM via @custom-elements-manifest/analyzer is the lowest-friction path)."
496
+ };
497
+ },
498
+ "2.2": async () => ({
499
+ score: null,
500
+ status: "pending",
501
+ pending: "requires-agent",
502
+ rationale: "Cross-component prop naming consistency requires reading every component signature; agent should evaluate."
503
+ }),
504
+ "2.3": async () => ({
505
+ score: null,
506
+ status: "pending",
507
+ pending: "requires-agent",
508
+ rationale: "Whether compound-component composition rules are documented requires reading component docs."
509
+ }),
510
+ "2.4": async () => ({
511
+ score: null,
512
+ status: "pending",
513
+ pending: "requires-agent",
514
+ rationale: "Anti-pattern presence per component requires reading each component's docs."
515
+ }),
516
+ "2.5": async () => ({
517
+ score: null,
518
+ status: "pending",
519
+ pending: "requires-agent",
520
+ rationale: "Whether a11y metadata is structured (vs. prose) requires reading docs and/or the manifest."
521
+ })
522
+ };
523
+
524
+ // src/checks/category-3-docs.ts
525
+ var checks3 = {
526
+ "3.1": async () => ({
527
+ score: null,
528
+ status: "pending",
529
+ pending: "requires-agent",
530
+ rationale: "Detecting scattered/duplicate docs (Confluence vs Figma vs Storybook) requires external knowledge."
531
+ }),
532
+ "3.2": async () => ({
533
+ score: null,
534
+ status: "pending",
535
+ pending: "requires-agent",
536
+ rationale: "Structural consistency of docs pages requires reading and comparing MDX/HTML across components."
537
+ }),
538
+ "3.3": async () => ({
539
+ score: null,
540
+ status: "pending",
541
+ pending: "requires-agent",
542
+ rationale: "Counting per-component examples and identifying anti-patterns requires reading docs."
543
+ }),
544
+ "3.4": async (ctx) => {
545
+ const llmsTxt = await ctx.existsAny(["llms.txt", "public/llms.txt", "site/public/llms.txt", "docs/public/llms.txt"]);
546
+ const llmsFull = await ctx.existsAny([
547
+ "llms-full.txt",
548
+ "public/llms-full.txt",
549
+ "site/public/llms-full.txt",
550
+ "docs/public/llms-full.txt"
551
+ ]);
552
+ if (!llmsTxt && !llmsFull) {
553
+ return {
554
+ score: 0,
555
+ rationale: "No llms.txt or llms-full.txt found.",
556
+ evidence: [{ kind: "glob", path: "**/llms*.txt", detail: "no matches" }],
557
+ suggestion: "Publish /llms.txt at the docs root, per llmstxt.org spec. Look at Cloudscape, Nord, or CMS Design System for examples."
558
+ };
559
+ }
560
+ if (llmsTxt && llmsFull) {
561
+ return {
562
+ score: 3,
563
+ rationale: "Both llms.txt and llms-full.txt are published.",
564
+ evidence: [
565
+ { kind: "file", path: llmsTxt },
566
+ { kind: "file", path: llmsFull }
567
+ ]
568
+ };
569
+ }
570
+ return {
571
+ score: 2,
572
+ rationale: `${llmsTxt ?? llmsFull} is published; the companion file is not.`,
573
+ evidence: [{ kind: "file", path: llmsTxt ?? llmsFull }],
574
+ suggestion: "Publish llms-full.txt with per-page Markdown reference content."
575
+ };
576
+ }
577
+ };
578
+
579
+ // src/checks/category-4-composition.ts
580
+ var PRIMITIVE_NAMES = ["Stack", "Inline", "HStack", "VStack", "Grid", "Box", "Container", "Flex", "Cluster"];
581
+ var checks4 = {
582
+ "4.1": async (ctx) => {
583
+ const compDirs = await ctx.componentDirs();
584
+ if (compDirs.length === 0) {
585
+ return {
586
+ score: 0,
587
+ rationale: "Could not locate a components directory; cannot detect layout primitives.",
588
+ evidence: [{ kind: "note", detail: "no components/, src/components/, ui/, or packages/*/src found" }]
589
+ };
590
+ }
591
+ const found = /* @__PURE__ */ new Set();
592
+ const filePaths = /* @__PURE__ */ new Map();
593
+ for (const dir of compDirs) {
594
+ for (const name of PRIMITIVE_NAMES) {
595
+ const matches = await ctx.glob([
596
+ `${dir}/${name}.{ts,tsx,jsx,js,vue,svelte}`,
597
+ `${dir}/${name}/index.{ts,tsx,jsx,js,vue,svelte}`,
598
+ `${dir}/${name.toLowerCase()}.{ts,tsx,jsx,js,vue,svelte}`,
599
+ `${dir}/${name.toLowerCase()}/index.{ts,tsx,jsx,js,vue,svelte}`
600
+ ]);
601
+ if (matches.length > 0) {
602
+ found.add(name);
603
+ if (!filePaths.has(name)) filePaths.set(name, matches[0]);
604
+ }
605
+ }
606
+ }
607
+ if (found.size === 0) {
608
+ return {
609
+ score: 0,
610
+ rationale: "No layout primitives (Stack, Inline, Grid, Box, Container) detected.",
611
+ evidence: [{ kind: "note", detail: `searched: ${compDirs.join(", ")}` }],
612
+ suggestion: 'Introduce named layout primitives so agents can avoid raw <div className="flex">.'
613
+ };
614
+ }
615
+ const score = found.size >= 3 ? 2 : 1;
616
+ return {
617
+ score,
618
+ rationale: `Found ${found.size} layout primitive(s): ${[...found].join(", ")}.`,
619
+ evidence: [...filePaths.entries()].slice(0, 5).map(([, path]) => ({ kind: "file", path })),
620
+ suggestion: score < 2 ? "Add the rest of the standard set (Stack/Inline/Grid/Box/Container)." : "Document the primitives in your manifest so agents pick them over raw divs."
621
+ };
622
+ },
623
+ "4.2": async () => ({
624
+ score: null,
625
+ status: "pending",
626
+ pending: "requires-agent",
627
+ rationale: "Whether responsive conventions are uniform requires reading multiple components."
628
+ }),
629
+ "4.3": async (ctx) => {
630
+ const dirs = ["patterns", "recipes", "src/patterns", "src/recipes", "packages/patterns", "packages/recipes"];
631
+ const found = await ctx.existsAny(dirs);
632
+ if (!found) {
633
+ return {
634
+ score: 0,
635
+ rationale: "No patterns/ or recipes/ directory detected.",
636
+ evidence: [{ kind: "glob", path: dirs.join(", "), detail: "no matches" }],
637
+ suggestion: "Create a recipes/ layer with pre-blessed compositions (EmptyState, DataTableToolbar, ...)."
638
+ };
639
+ }
640
+ return {
641
+ score: 2,
642
+ rationale: `A recipes/patterns directory exists: ${found}.`,
643
+ evidence: [{ kind: "file", path: found }],
644
+ suggestion: "Add manifests for each recipe and serve via MCP/registry to reach Level 3+."
645
+ };
646
+ }
647
+ };
648
+
649
+ // src/checks/category-5-retrieval.ts
650
+ var AGENT_FILE_CANDIDATES = ["AGENTS.md", ".builder/AGENTS.md", "CLAUDE.md"];
651
+ var RULES_FILES = [".cursorrules", ".cursor/rules", ".windsurfrules", ".clinerules", ".roorules", ".aider.conf.yml"];
652
+ var checks5 = {
653
+ "5.1": async (ctx) => {
654
+ const found = [];
655
+ for (const f of AGENT_FILE_CANDIDATES) {
656
+ if (await ctx.exists(f)) found.push(f);
657
+ }
658
+ if (found.length === 0) {
659
+ return {
660
+ score: 0,
661
+ rationale: "No AGENTS.md or CLAUDE.md found at the repo root.",
662
+ evidence: [{ kind: "glob", path: AGENT_FILE_CANDIDATES.join(", "), detail: "no matches" }],
663
+ suggestion: "Write a hand-curated AGENTS.md (per agents.md format) \u2014 canonical import path, anti-hallucination rule, always-on token rules."
664
+ };
665
+ }
666
+ const primary = found[0];
667
+ const content = await ctx.readFile(primary) ?? "";
668
+ const looksGeneric = /generated by/i.test(content) || /this file was automatically/i.test(content) || content.length < 300;
669
+ if (looksGeneric) {
670
+ return {
671
+ score: 1,
672
+ rationale: `${primary} exists but looks auto-generated or minimal (${content.length} chars).`,
673
+ evidence: [{ kind: "file", path: primary }],
674
+ suggestion: "Hand-curate AGENTS.md: per ETH Zurich evidence, auto-generated ones reduce task success ~3%."
675
+ };
676
+ }
677
+ return {
678
+ score: null,
679
+ status: "pending",
680
+ pending: "requires-agent",
681
+ rationale: `${primary} exists; agent should verify it includes canonical import path, anti-hallucination rule, and trust levels.`,
682
+ evidence: found.map((f) => ({ kind: "file", path: f }))
683
+ };
684
+ },
685
+ "5.2": async (ctx) => {
686
+ const mcpHints = [];
687
+ const pkg = await ctx.pkgJson();
688
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
689
+ const mcpDeps = Object.keys(deps ?? {}).filter((k) => /mcp/i.test(k) || /modelcontext/i.test(k));
690
+ if (mcpDeps.length > 0) mcpHints.push(`deps: ${mcpDeps.join(", ")}`);
691
+ const registry = await ctx.firstMatch(["registry.json", "**/registry.json"]);
692
+ const sbConfig = await ctx.firstMatch([".storybook/main.{ts,tsx,js,cjs,mjs}"]);
693
+ if (sbConfig) {
694
+ const c = await ctx.readFile(sbConfig) ?? "";
695
+ if (/storybook.*mcp|@storybook\/.+mcp/i.test(c)) mcpHints.push(`Storybook MCP addon referenced in ${sbConfig}`);
696
+ }
697
+ const customMcp = await ctx.firstMatch(["mcp-server/**/*.{ts,js}", "mcp/**/*.{ts,js}", "scripts/mcp-server.{ts,js,mjs}"]);
698
+ if (customMcp) mcpHints.push(`custom MCP server: ${customMcp}`);
699
+ if (mcpHints.length === 0 && !registry) {
700
+ return {
701
+ score: 0,
702
+ rationale: "No MCP server, registry endpoint, or MCP-related dependency detected.",
703
+ evidence: [{ kind: "note", detail: "checked package.json deps, registry.json, Storybook MCP addon, mcp-server/" }],
704
+ suggestion: "Lowest-friction options: Storybook 10.3+ MCP addon, `npx shadcn registry:mcp`, or a custom MCP wrapping your component manifest."
705
+ };
706
+ }
707
+ if (registry && mcpHints.length > 0) {
708
+ return {
709
+ score: 3,
710
+ rationale: `Registry endpoint and MCP indicators both present.`,
711
+ evidence: [{ kind: "file", path: registry }, ...mcpHints.map((h) => ({ kind: "note", detail: h }))],
712
+ suggestion: "Confirm the MCP is hosted, authenticated, versioned, and returns curated (not raw) responses."
713
+ };
714
+ }
715
+ if (registry) {
716
+ return {
717
+ score: 2,
718
+ rationale: `shadcn-style registry endpoint detected: ${registry}.`,
719
+ evidence: [{ kind: "file", path: registry }],
720
+ suggestion: "Expose the registry via `npx shadcn registry:mcp` so MCP-aware agents can register it."
721
+ };
722
+ }
723
+ return {
724
+ score: 1,
725
+ rationale: `MCP-related artifacts detected but no published endpoint confirmed: ${mcpHints.join("; ")}.`,
726
+ evidence: mcpHints.map((h) => ({ kind: "note", detail: h })),
727
+ suggestion: "Host the MCP (Chromatic's Storybook MCP auto-publish is one-click) and add its URL to AGENTS.md."
728
+ };
729
+ },
730
+ "5.3": async (ctx) => {
731
+ const presentRules = [];
732
+ for (const r of RULES_FILES) {
733
+ if (await ctx.exists(r)) presentRules.push(r);
734
+ }
735
+ if (presentRules.length === 0) {
736
+ return {
737
+ score: 0,
738
+ rationale: "No agent-rules file (.cursorrules, .windsurfrules, etc.) and no AGENTS.md detected.",
739
+ evidence: [{ kind: "glob", path: RULES_FILES.join(", "), detail: "no matches" }],
740
+ suggestion: "Per Brad Frost's progressive disclosure: put foundations (spacing/color/type) in always-on rules, not behind MCP."
741
+ };
742
+ }
743
+ return {
744
+ score: null,
745
+ status: "pending",
746
+ pending: "requires-agent",
747
+ rationale: `Rules file(s) detected (${presentRules.join(", ")}); agent should verify they include token/foundation rules, not just generic instructions.`,
748
+ evidence: presentRules.map((f) => ({ kind: "file", path: f }))
749
+ };
750
+ },
751
+ "5.4": async (ctx) => {
752
+ const pkg = await ctx.pkgJson();
753
+ if (!pkg) {
754
+ return {
755
+ score: 0,
756
+ rationale: "No package.json at root \u2014 cannot determine canonical import path.",
757
+ evidence: [{ kind: "note", detail: "package.json missing" }]
758
+ };
759
+ }
760
+ const exports = pkg.exports;
761
+ if (exports && typeof exports === "object") {
762
+ return {
763
+ score: 2,
764
+ rationale: 'package.json declares an "exports" map; subpath imports are likely stable.',
765
+ evidence: [{ kind: "file", path: "package.json", detail: "exports field declared" }],
766
+ suggestion: "Document the canonical import path in AGENTS.md so agents do not guess."
767
+ };
768
+ }
769
+ return {
770
+ score: 1,
771
+ rationale: 'package.json has no "exports" map; subpath imports are ad hoc.',
772
+ evidence: [{ kind: "file", path: "package.json", detail: "no exports field" }],
773
+ suggestion: 'Add an "exports" map and document canonical paths in AGENTS.md.'
774
+ };
775
+ }
776
+ };
777
+
778
+ // src/checks/category-6-distribution.ts
779
+ var SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.+-]+)?$/;
780
+ var checks6 = {
781
+ "6.1": async (ctx) => {
782
+ const pkg = await ctx.pkgJson();
783
+ const registry = await ctx.firstMatch(["registry.json", "**/registry.json"]);
784
+ if (!pkg && !registry) {
785
+ return {
786
+ score: 0,
787
+ rationale: "No root package.json and no registry.json \u2014 installation path unclear.",
788
+ evidence: [{ kind: "note", detail: "neither package.json nor registry.json found" }],
789
+ suggestion: "Publish as an npm package or adopt the shadcn registry pattern."
790
+ };
791
+ }
792
+ const hints = [];
793
+ if (pkg?.name && pkg.private !== true) hints.push(`npm package: ${String(pkg.name)}`);
794
+ if (registry) hints.push(`registry: ${registry}`);
795
+ if (pkg?.workspaces) hints.push("monorepo workspaces");
796
+ const score = hints.length >= 2 ? 3 : 2;
797
+ return {
798
+ score,
799
+ rationale: hints.join("; "),
800
+ evidence: [
801
+ ...pkg ? [{ kind: "file", path: "package.json" }] : [],
802
+ ...registry ? [{ kind: "file", path: registry }] : []
803
+ ],
804
+ suggestion: score < 3 ? "Combine a versioned npm package with a registry endpoint for AI tooling." : void 0
805
+ };
806
+ },
807
+ "6.2": async () => ({
808
+ score: null,
809
+ status: "pending",
810
+ pending: "requires-agent",
811
+ rationale: "Detecting structured deprecation (manifest `deprecated: true`) requires reading the manifest contents; agent should verify."
812
+ }),
813
+ "6.3": async (ctx) => {
814
+ const pkg = await ctx.pkgJson();
815
+ const hasSemver = !!pkg?.version && SEMVER_RE.test(String(pkg.version));
816
+ const agentsContent = await ctx.readFile("AGENTS.md") ?? await ctx.readFile("CLAUDE.md") ?? "";
817
+ const mentionsVersion = /version|`v\d+\.\d+`|@\w+\/.+@/i.test(agentsContent);
818
+ if (!hasSemver && !mentionsVersion) {
819
+ return {
820
+ score: 0,
821
+ rationale: "No semver version in package.json and no version reference in AGENTS.md/CLAUDE.md.",
822
+ evidence: [{ kind: "note", detail: "version contract not communicated" }],
823
+ suggestion: "Pin a semver version in package.json and document the target version in AGENTS.md."
824
+ };
825
+ }
826
+ if (hasSemver && mentionsVersion) {
827
+ return {
828
+ score: 2,
829
+ rationale: "Semver version present and AGENTS.md mentions a version target.",
830
+ evidence: [
831
+ { kind: "file", path: "package.json", detail: `version ${String(pkg?.version)}` }
832
+ ]
833
+ };
834
+ }
835
+ return {
836
+ score: 1,
837
+ rationale: hasSemver ? "Semver version present but AGENTS.md does not state which version agents should target." : "AGENTS.md mentions versioning but package.json lacks a semver value.",
838
+ evidence: [
839
+ ...hasSemver ? [{ kind: "file", path: "package.json", detail: `version ${String(pkg?.version)}` }] : []
840
+ ],
841
+ suggestion: hasSemver ? "Add a 'system-version' note to AGENTS.md telling agents to check package.json." : "Pin a semver version (1.0.0+) in package.json."
842
+ };
843
+ }
844
+ };
845
+
846
+ // src/checks/category-7-quality.ts
847
+ var RAW_HTML_TAGS = ["button", "input", "select", "textarea"];
848
+ var checks7 = {
849
+ "7.1": async (ctx) => {
850
+ const stories = await ctx.glob(["**/*.stories.{ts,tsx,js,jsx,mdx}"]);
851
+ const tests = await ctx.glob(["**/*.{test,spec}.{ts,tsx,js,jsx}"]);
852
+ const storybookConfig = await ctx.firstMatch([".storybook/main.{ts,tsx,js,cjs,mjs}"]);
853
+ if (stories.length === 0 && tests.length === 0) {
854
+ return {
855
+ score: 0,
856
+ rationale: "No stories or test files found.",
857
+ evidence: [
858
+ { kind: "glob", path: "**/*.stories.*", detail: "no matches" },
859
+ { kind: "glob", path: "**/*.test.*", detail: "no matches" }
860
+ ],
861
+ suggestion: "Add Storybook stories or Vitest/Jest tests covering each component's variants."
862
+ };
863
+ }
864
+ const score = stories.length > 0 && tests.length > 0 ? 3 : storybookConfig ? 2 : 1;
865
+ return {
866
+ score,
867
+ rationale: `Found ${stories.length} stories and ${tests.length} test files${storybookConfig ? " with Storybook configured" : ""}.`,
868
+ evidence: [
869
+ ...storybookConfig ? [{ kind: "file", path: storybookConfig }] : [],
870
+ ...stories.slice(0, 3).map((f) => ({ kind: "file", path: f }))
871
+ ],
872
+ suggestion: score < 3 ? "Combine stories with interaction tests (play functions) to reach Level 3." : void 0
873
+ };
874
+ },
875
+ "7.2": async (ctx) => {
876
+ const pkg = await ctx.pkgJson();
877
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
878
+ const vrTools = ["@chromatic-com/storybook", "chromatic", "@percy/cli", "loki", "lost-pixel", "@playwright/test"];
879
+ const found = vrTools.filter((t) => Object.prototype.hasOwnProperty.call(deps ?? {}, t));
880
+ const chromaticConfig = await ctx.firstMatch(["chromatic.config.json", ".chromatic.json"]);
881
+ if (found.length === 0 && !chromaticConfig) {
882
+ return {
883
+ score: 0,
884
+ rationale: "No visual-regression tooling (Chromatic, Percy, Loki, Lost Pixel, Playwright) detected.",
885
+ evidence: [{ kind: "note", detail: "no matching deps or config" }],
886
+ suggestion: "Add Chromatic (one-click for Storybook) or Lost Pixel for OSS."
887
+ };
888
+ }
889
+ return {
890
+ score: 2,
891
+ rationale: `Visual-regression tooling detected: ${found.join(", ") || chromaticConfig}.`,
892
+ evidence: [
893
+ ...chromaticConfig ? [{ kind: "file", path: chromaticConfig }] : [],
894
+ { kind: "file", path: "package.json", detail: found.join(", ") || "" }
895
+ ],
896
+ suggestion: "Wire visual regression into the agent codegen loop (auto-screenshot, diff against baseline)."
897
+ };
898
+ },
899
+ "7.3": async (ctx) => {
900
+ const pkg = await ctx.pkgJson();
901
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
902
+ const a11yTools = ["axe-core", "@axe-core/react", "@axe-core/playwright", "jest-axe", "@storybook/addon-a11y", "vitest-axe"];
903
+ const found = a11yTools.filter((t) => Object.prototype.hasOwnProperty.call(deps ?? {}, t));
904
+ if (found.length === 0) {
905
+ return {
906
+ score: 0,
907
+ rationale: "No automated accessibility tooling detected.",
908
+ evidence: [{ kind: "note", detail: "axe / lighthouse / a11y addon not found" }],
909
+ suggestion: "Add @storybook/addon-a11y and run axe on stories in CI."
910
+ };
911
+ }
912
+ return {
913
+ score: 2,
914
+ rationale: `A11y tooling detected: ${found.join(", ")}.`,
915
+ evidence: [{ kind: "file", path: "package.json", detail: found.join(", ") }],
916
+ suggestion: "Gate CI on a11y failures and expose the check via MCP for agent-generated UI."
917
+ };
918
+ },
919
+ "7.4": async (ctx) => {
920
+ const compDirs = await ctx.componentDirs();
921
+ if (compDirs.length === 0) {
922
+ return {
923
+ score: null,
924
+ status: "pending",
925
+ pending: "requires-agent",
926
+ rationale: "No components/ directory detected to scan for raw HTML or hex literals."
927
+ };
928
+ }
929
+ const sources = await ctx.glob(compDirs.map((d) => `${d}/**/*.{ts,tsx,jsx,vue,svelte}`));
930
+ let rawHtmlHits = 0;
931
+ let hexInCss = 0;
932
+ const sampleRawHtml = [];
933
+ const sampleHex = [];
934
+ const tagRe = new RegExp(`<(${RAW_HTML_TAGS.join("|")})(\\s|>|/)`);
935
+ for (const f of sources.slice(0, 100)) {
936
+ const content = await ctx.readFile(f);
937
+ if (content && tagRe.test(content)) {
938
+ rawHtmlHits++;
939
+ if (sampleRawHtml.length < 3) sampleRawHtml.push(f);
940
+ }
941
+ }
942
+ const cssFiles = await ctx.glob(compDirs.map((d) => `${d}/**/*.{css,scss}`));
943
+ for (const f of cssFiles.slice(0, 50)) {
944
+ const content = await ctx.readFile(f);
945
+ if (content && /#[0-9a-fA-F]{3,8}\b/.test(content)) {
946
+ hexInCss++;
947
+ if (sampleHex.length < 3) sampleHex.push(f);
948
+ }
949
+ }
950
+ if (rawHtmlHits === 0 && hexInCss === 0) {
951
+ return {
952
+ score: 3,
953
+ rationale: "No raw <button>/<input> or hex literals detected in scanned component sources.",
954
+ evidence: [{ kind: "note", detail: `scanned ${sources.length} component files across ${compDirs.length} dir(s)` }]
955
+ };
956
+ }
957
+ const score = rawHtmlHits + hexInCss > 10 ? 0 : rawHtmlHits + hexInCss > 3 ? 1 : 2;
958
+ return {
959
+ score,
960
+ rationale: `${rawHtmlHits} component file(s) use raw <button>/<input>/etc.; ${hexInCss} component CSS file(s) contain hex literals.`,
961
+ evidence: [
962
+ ...sampleRawHtml.map((p) => ({ kind: "file", path: p, detail: "raw HTML tag" })),
963
+ ...sampleHex.map((p) => ({ kind: "file", path: p, detail: "hex literal" }))
964
+ ],
965
+ suggestion: "Replace raw HTML with library primitives and move colors to tokens."
966
+ };
967
+ }
968
+ };
969
+
970
+ // src/checks/category-8-evaluation.ts
971
+ var checks8 = {
972
+ "8.1": async (ctx) => {
973
+ const candidates = ["evals", "evaluation", "eval", "agent-evals", "tests/evals"];
974
+ const found = await ctx.existsAny(candidates);
975
+ if (!found) {
976
+ return {
977
+ score: 0,
978
+ rationale: "No evals/ or evaluation/ directory detected.",
979
+ evidence: [{ kind: "glob", path: candidates.join(", "), detail: "no matches" }],
980
+ suggestion: "Build an Indeed-style eval harness: a fixed prompt set scored on real-component imports + token compliance + LLM-as-judge."
981
+ };
982
+ }
983
+ return {
984
+ score: null,
985
+ status: "pending",
986
+ pending: "requires-agent",
987
+ rationale: `An evals directory exists (${found}); agent should verify it actually scores agent output against the design system.`,
988
+ evidence: [{ kind: "file", path: found }]
989
+ };
990
+ },
991
+ "8.2": async () => ({
992
+ score: null,
993
+ status: "pending",
994
+ pending: "requires-agent",
995
+ rationale: "Trust levels for agent actions live in AGENTS.md and CI config; agent should verify the policy is encoded."
996
+ }),
997
+ "8.3": async () => ({
998
+ score: null,
999
+ status: "pending",
1000
+ pending: "requires-agent",
1001
+ rationale: "Drift detection between docs/tokens/code is a CI check; agent should verify it exists."
1002
+ }),
1003
+ "8.4": async () => ({
1004
+ score: null,
1005
+ status: "pending",
1006
+ pending: "requires-agent",
1007
+ rationale: "Agent-PR labeling and review routing is a process question; agent should ask or inspect CODEOWNERS / labeler.yml."
1008
+ })
1009
+ };
1010
+
1011
+ // src/checks/index.ts
1012
+ var allChecks = {
1013
+ ...checks,
1014
+ ...checks2,
1015
+ ...checks3,
1016
+ ...checks4,
1017
+ ...checks5,
1018
+ ...checks6,
1019
+ ...checks7,
1020
+ ...checks8
1021
+ };
1022
+
1023
+ // src/runner.ts
1024
+ import { readFile as readFile2 } from "fs/promises";
1025
+ async function runAssessment(opts) {
1026
+ const ctx = new CheckContext(opts.target);
1027
+ let existing = null;
1028
+ if (opts.merge) {
1029
+ try {
1030
+ existing = JSON.parse(await readFile2(opts.merge, "utf8"));
1031
+ } catch {
1032
+ existing = null;
1033
+ }
1034
+ }
1035
+ const criteria = [];
1036
+ for (const entry of RUBRIC) {
1037
+ const existingHit = existing?.criteria.find((c) => c.id === entry.id);
1038
+ if (existingHit && existingHit.status === "scored") {
1039
+ criteria.push(existingHit);
1040
+ continue;
1041
+ }
1042
+ const check = allChecks[entry.id];
1043
+ if (!check) {
1044
+ criteria.push({
1045
+ id: entry.id,
1046
+ category: entry.category,
1047
+ title: entry.title,
1048
+ score: null,
1049
+ status: "pending",
1050
+ pending: "requires-agent",
1051
+ rationale: "No deterministic check implemented; requires an agent.",
1052
+ evidence: []
1053
+ });
1054
+ continue;
1055
+ }
1056
+ try {
1057
+ const result = await check(ctx);
1058
+ const criterion = {
1059
+ id: entry.id,
1060
+ category: entry.category,
1061
+ title: entry.title,
1062
+ score: result.score,
1063
+ status: result.status ?? (result.score === null ? "pending" : "scored"),
1064
+ evidence: result.evidence ?? [],
1065
+ ...result.pending ? { pending: result.pending } : {},
1066
+ ...result.rationale ? { rationale: result.rationale } : {},
1067
+ ...result.suggestion ? { suggestion: result.suggestion } : {}
1068
+ };
1069
+ criteria.push(criterion);
1070
+ } catch (err) {
1071
+ criteria.push({
1072
+ id: entry.id,
1073
+ category: entry.category,
1074
+ title: entry.title,
1075
+ score: null,
1076
+ status: "error",
1077
+ rationale: `Check threw: ${err.message}`,
1078
+ evidence: []
1079
+ });
1080
+ }
1081
+ }
1082
+ const rollup = computeRollup(criteria);
1083
+ const backlog = generateBacklog(criteria);
1084
+ return {
1085
+ schemaVersion: "0.1.0",
1086
+ rubricVersion: "0.1.0",
1087
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1088
+ target: { path: ctx.root, name: opts.name ?? existing?.target?.name },
1089
+ producer: {
1090
+ kind: existing ? "merged" : "cli",
1091
+ name: "agentready-design-cli",
1092
+ version: "0.1.0"
1093
+ },
1094
+ criteria,
1095
+ rollup,
1096
+ backlog
1097
+ };
1098
+ }
1099
+
1100
+ // src/render.ts
1101
+ var CATEGORY_NAMES = {
1102
+ 1: "Tokens & Foundations",
1103
+ 2: "Component API Contract",
1104
+ 3: "Documentation Format",
1105
+ 4: "Composition & Layout",
1106
+ 5: "Retrieval & Discoverability",
1107
+ 6: "Distribution & Versioning",
1108
+ 7: "Quality Signals",
1109
+ 8: "Evaluation & Governance"
1110
+ };
1111
+ function fmt(s) {
1112
+ if (s === null || s === void 0) return "\u2013";
1113
+ if (Number.isInteger(s)) return String(s);
1114
+ return s.toFixed(1);
1115
+ }
1116
+ function cell(c) {
1117
+ if (!c) return "\u2013";
1118
+ if (c.status === "scored" && c.score !== null) return String(c.score);
1119
+ if (c.status === "pending") return "?";
1120
+ if (c.status === "error") return "!";
1121
+ if (c.status === "skipped") return "\u2014";
1122
+ return "?";
1123
+ }
1124
+ function renderMarkdown(report) {
1125
+ const lines = [];
1126
+ lines.push("# Agent-Ready Design \u2014 Assessment");
1127
+ lines.push("");
1128
+ lines.push(`**Target:** \`${report.target.path}\`${report.target.name ? ` _(${report.target.name})_` : ""}`);
1129
+ lines.push(`**Producer:** ${report.producer.name ?? report.producer.kind} (${report.producer.kind})`);
1130
+ lines.push(`**Generated:** ${report.generatedAt}`);
1131
+ lines.push(`**Rubric version:** ${report.rubricVersion}`);
1132
+ lines.push("");
1133
+ const { rollup } = report;
1134
+ lines.push("## Roll-up");
1135
+ lines.push("");
1136
+ lines.push(`- **Foundational floor (Categories 1\u20134): ${rollup.foundationalFloor} / 4**`);
1137
+ lines.push(`- **Retrieval level (Category 5 max): ${rollup.retrievalLevel} / 4**`);
1138
+ lines.push(`- **Overall median: ${fmt(rollup.overallMedian)} / 4**`);
1139
+ lines.push(`- **Tier: ${rollup.tier} \u2014 ${TIER_LABELS[rollup.tier] ?? "?"}**`);
1140
+ lines.push("");
1141
+ lines.push("## Heatmap");
1142
+ lines.push("");
1143
+ lines.push("Legend: `0`\u2013`4` score, `?` pending agent judgment, `!` error, `\u2014` not applicable.");
1144
+ lines.push("");
1145
+ lines.push("| Cat | 1 | 2 | 3 | 4 | 5 | Floor | Median |");
1146
+ lines.push("|---|---|---|---|---|---|---|---|");
1147
+ for (const cat of [1, 2, 3, 4, 5, 6, 7, 8]) {
1148
+ const inCat = report.criteria.filter((c) => c.category === cat);
1149
+ const row = [1, 2, 3, 4, 5].map((i) => cell(inCat.find((c) => c.id === `${cat}.${i}`)));
1150
+ const floor = rollup.categoryFloors[`${cat}`];
1151
+ const med = rollup.categoryMedians[`${cat}`];
1152
+ lines.push(
1153
+ `| **${cat}. ${CATEGORY_NAMES[cat]}** | ${row.join(" | ")} | ${fmt(floor)} | ${fmt(med)} |`
1154
+ );
1155
+ }
1156
+ lines.push("");
1157
+ lines.push("## Per-criterion");
1158
+ for (const cat of [1, 2, 3, 4, 5, 6, 7, 8]) {
1159
+ lines.push("");
1160
+ lines.push(`### Category ${cat} \u2014 ${CATEGORY_NAMES[cat]}`);
1161
+ lines.push("");
1162
+ for (const c of report.criteria.filter((x) => x.category === cat)) {
1163
+ const scoreLabel = c.status === "scored" && c.score !== null ? `${c.score}/4` : c.status === "pending" ? "pending (requires agent)" : c.status === "error" ? "error" : "\u2014";
1164
+ lines.push(`- **${c.id} ${c.title} \u2014 ${scoreLabel}**`);
1165
+ if (c.rationale) lines.push(` - _Rationale:_ ${c.rationale}`);
1166
+ if (c.evidence.length > 0) {
1167
+ const ev = c.evidence.slice(0, 4).map((e) => {
1168
+ if (e.kind === "file") return `\`${e.path}${e.line ? ":" + e.line : ""}\`${e.detail ? " \u2014 " + e.detail : ""}`;
1169
+ if (e.kind === "glob") return `glob \`${e.path}\`${e.detail ? " \u2014 " + e.detail : ""}`;
1170
+ if (e.kind === "note") return e.detail ?? "(note)";
1171
+ if (e.kind === "url") return `<${e.path}>`;
1172
+ if (e.kind === "command") return `\`$ ${e.path}\``;
1173
+ return e.detail ?? "";
1174
+ }).filter(Boolean).join("; ");
1175
+ if (ev) lines.push(` - _Evidence:_ ${ev}`);
1176
+ }
1177
+ if (c.suggestion) lines.push(` - _Suggestion:_ ${c.suggestion}`);
1178
+ }
1179
+ }
1180
+ lines.push("");
1181
+ if (report.backlog.length > 0) {
1182
+ lines.push("## Prioritized backlog");
1183
+ lines.push("");
1184
+ for (const item of report.backlog) {
1185
+ const effort = item.estimatedEffort ? ` _Effort: ${item.estimatedEffort}._` : "";
1186
+ lines.push(
1187
+ `${item.priority}. **${item.title}** \u2014 Criteria ${item.criteria.join(", ")}.${item.rationale ? " " + item.rationale : ""}${effort}`
1188
+ );
1189
+ }
1190
+ lines.push("");
1191
+ }
1192
+ lines.push("");
1193
+ lines.push("---");
1194
+ lines.push("");
1195
+ lines.push("_Generated by [agentready-design-cli](https://github.com/hgillispie/Agent-Ready-Design). Pending rows can be completed by pasting [`prompts/self-assessment.md`](https://github.com/hgillispie/Agent-Ready-Design/blob/main/prompts/self-assessment.md) into your agent with this `agent-ready-report.json` at the repo root._");
1196
+ return lines.join("\n");
1197
+ }
1198
+
1199
+ export {
1200
+ RUBRIC,
1201
+ TOTAL_CRITERIA,
1202
+ computeRollup,
1203
+ TIER_LABELS,
1204
+ generateBacklog,
1205
+ runAssessment,
1206
+ renderMarkdown
1207
+ };