@designtools/manifest 0.1.0 → 0.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @designtools/manifest
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3da728f: From the first end-to-end run:
8
+
9
+ - A comment after a declaration on the same line (`--size-xl: 3rem; /* 48px controls */`) now describes that declaration. It used to become the next token's description, so every description in a scale written that way landed one token late.
10
+ - A token re-declared in a later stylesheet records it in `overriddenIn`.
11
+ - Each component records `import`, the specifier an app imports it by through the tsconfig path alias that reaches the system folder (`@ds/button/button`), so the import line in the docs and the agent markdown resolves.
12
+ - Brand assets keep the order `assets.json` gives them, so the default logo leads.
13
+ - `prose-check` reads the places listed in `manifest.prose` (a folder contributes its markdown and `*content.ts(x)` files), treats a TypeScript template literal as prose rather than code, and honours `prose-check-ignore`, `prose-check-ignore-next-line` and `prose-check-ignore-start` … `-end` for pages that name wrong forms on purpose.
14
+ - A prop's JSDoc no longer leaks into other components. react-docgen-typescript caches a prop by the file of its first declaration, so Alert's documented `children` (first declared by React) gave its description, type and declarations to every component with `children`. Each component now reads its props afresh.
15
+ - `agents` writes to `AGENTS.md` when `CLAUDE.md` imports it (`@AGENTS.md`, as `next dev` writes it), so every agent reads the section; `manifest.agents` in `designtools.json` names another file. The section no longer forbids every ramp step: ramps are for charts, illustration and a decorative colour, never text, surfaces or actions.
16
+ - `manifest.usage` names the file `@designtools/tokens --usage` writes; every colour that fails somewhere as text or a shape becomes a rule in rules.json (`usage-<colour>`), marked with the new status `measured`: a fact from the tokens, not a client decision. Status fills are left to the default status-text rule.
17
+
3
18
  ## 0.1.0
4
19
 
5
20
  ### Minor Changes
package/README.md CHANGED
@@ -28,6 +28,8 @@ Record the system folder and the token stylesheets once, in `designtools.json` a
28
28
 
29
29
  The manifest is written inside the system folder (`src/ds/manifest/`), so it moves with the system if the system moves to its own package. Flags override the file: `--system`, `--tokens` (repeat it), `--tsconfig`, `--out`, `--root`.
30
30
 
31
+ List every stylesheet that declares a token, in the order the app imports them: the generated colour tiers, the authored scale, and wherever the font families live (`base.css` on the suite's default stack). A token re-declared in a later file takes that file's value and records it in `overriddenIn`. `prose` adds places for `prose-check` to read (see [For agents](#for-agents)).
32
+
31
33
  ## What it reads
32
34
 
33
35
  **Components.** Every exported component in a `.tsx` file under the system folder. Fixtures, tests, stories and `.d.ts` files are skipped. Props come from [react-docgen-typescript](https://github.com/styleguidist/react-docgen-typescript), run once over a single TypeScript program, so types resolve across files and through the project's path aliases. A component's own props are kept, and so are those of a Base UI primitive it wraps. Props inherited from the DOM, ARIA or React are left out, and recorded as what it `inherits` (`"span"`, `"@base-ui/react/select:Select.Root"`).
@@ -60,7 +62,7 @@ The manifest is written inside the system folder (`src/ds/manifest/`), so it mov
60
62
 
61
63
  Any other tag is a warning, with a suggestion when it is close to one of these.
62
64
 
63
- **Tokens.** Every custom property in the listed stylesheets: the colour tiers `@designtools/tokens` generates and the scale you author beside them. Each token records its value in every context it is declared in: `default` (`@theme`, `:root`), `light`, `dark`, `p3:` for the wide-gamut layer, and any other selector as written. A `prefers-color-scheme` fallback never overrides an explicit dark block. The comment above a declaration, or above the run of declarations it starts, becomes its description. Tiers follow Tailwind's namespaces (`--text-*`, `--radius-*`, …); semantic colours like `--primary` are recognised by their values.
65
+ **Tokens.** Every custom property in the listed stylesheets: the colour tiers `@designtools/tokens` generates and the scale you author beside them. Each token records its value in every context it is declared in: `default` (`@theme`, `:root`), `light`, `dark`, `p3:` for the wide-gamut layer, and any other selector as written. A `prefers-color-scheme` fallback never overrides an explicit dark block. The comment above a declaration, or above the run of declarations it starts, becomes its description; a comment after a declaration on the same line describes that declaration alone. Tiers follow Tailwind's namespaces (`--text-*`, `--radius-*`, …); semantic colours like `--primary` are recognised by their values.
64
66
 
65
67
  **Rules.** One `<id>.rule.json` per rule in `<system>/rules`, brand and interface in the same format: title, statement, rationale, threshold, what it applies to, exceptions, the tokens it rests on, whether it is `confirmed` or still an `assumption`, and the check that proves it. [`schemas/rule.schema.json`](schemas/rule.schema.json) validates them in an editor.
66
68
 
@@ -68,7 +70,9 @@ Any other tag is a warning, with a suggestion when it is close to one of these.
68
70
 
69
71
  **Taxonomy.** The canonical names come from everything else: components, variant axes and their meaningful options, semantic colours, patterns and rules. `<system>/taxonomy.json` adds the wrong forms (`{ "name": "destructive", "wrong": ["error", "danger"] }`) and product terms the code never names (`{ "name": "sign in", "kind": "product", "wrong": ["log in"] }`).
70
72
 
71
- **Brand assets.** Every image in `<system>/brand`, with an `assets.json` there saying how to use each: kind, usage, minimum size, clear space, the surfaces it may sit on, alt text. SVG and PNG sizes are read from the files. A file the index does not mention is listed anyway, with a warning.
73
+ **Measured usage rules.** With `manifest.usage` naming the file `@designtools/tokens --usage` writes, every colour that fails somewhere, as text (4.5:1 at AA) or as a shape (3:1), gets a rule in rules.json: `usage-primary`, "`--primary` reads as text on … On … it is neither, down to 1.06:1 …". They are marked `measured`, a fact from the tokens that changes when they do, and sit beside the authored rules on the rules page. A brand colour's limits are then a brand decision, written as a confirmed brand rule.
74
+
75
+ **Brand assets.** Every image in `<system>/brand`, with an `assets.json` there saying how to use each: kind, usage, minimum size, clear space, the surfaces it may sit on, alt text. SVG and PNG sizes are read from the files. They keep the order `assets.json` gives them, so the default logo can lead. A file the index does not mention is listed anyway, after them, with a warning.
72
76
 
73
77
  ## The files
74
78
 
@@ -84,6 +88,7 @@ Any other tag is a warning, with a suggestion when it is close to one of these.
84
88
  "name": "Badge",
85
89
  "export": "Badge",
86
90
  "source": "src/ds/badge/badge.tsx",
91
+ "import": "@ds/badge/badge",
87
92
  "description": "A pill: soft tinted background, strong text of the same hue.",
88
93
  "inherits": ["span"],
89
94
  "props": {
@@ -149,9 +154,9 @@ Components are matched by their path inside the system folder, so moving the who
149
154
 
150
155
  ## For agents
151
156
 
152
- `agents` writes a section of `CLAUDE.md` (or `AGENTS.md`) between markers, and leaves the rest of the file alone. It points agents at each manifest file and the docs, and lists nothing: a list of components or tokens written into prose is a second copy that drifts.
157
+ `agents` writes a section of `CLAUDE.md` or `AGENTS.md` between markers, and leaves the rest of the file alone. It picks `AGENTS.md` when `CLAUDE.md` imports it (`@AGENTS.md`, as `next dev` writes it), so every agent reads the section and Claude reaches it through the import; otherwise `CLAUDE.md` if there is one. `manifest.agents` in `designtools.json`, or `--file`, names another. It points agents at each manifest file and the docs, and lists nothing: a list of components or tokens written into prose is a second copy that drifts.
153
158
 
154
- `prose-check` reads `CLAUDE.md`, `AGENTS.md`, `.claude/`, `docs/` and the system folder's markdown, and warns about a component tag the system does not have (`<Modal>`), a wrong form from the taxonomy (`Dropdown` for `Select`, "log in" for "sign in"), and a paragraph or list that restates several components or tokens. The generated section is skipped.
159
+ `prose-check` reads `CLAUDE.md`, `AGENTS.md`, `.claude/`, `docs/` and the system folder's markdown, plus anything listed in `manifest.prose` (a folder there contributes its markdown and its `*content.ts(x)` files, which is where a docs site keeps its editorial copy), and warns about a component tag the system does not have (`<Modal>`), a wrong form from the taxonomy (`Dropdown` for `Select`, "log in" for "sign in"), and a paragraph or list that restates several components or tokens. The generated section is skipped. In TypeScript a backtick opens a template literal, so its words are read as prose, not code. A page that names wrong forms on purpose, such as a voice page saying "never the customer", marks them: `prose-check-ignore` on a line skips it, `prose-check-ignore-next-line` skips the next, and `prose-check-ignore-start` … `prose-check-ignore-end` skip a block, in any comment syntax.
155
160
 
156
161
  `provenance` prints the project's version and the commit it was built at (`VERCEL_GIT_COMMIT_SHA` or `GITHUB_SHA` when set, else git), for the header of a generated docs page. A generated page never carries a review date.
157
162
 
@@ -316,6 +316,15 @@ import { basename, dirname, join, relative, resolve, sep } from "path";
316
316
  import ts4 from "typescript";
317
317
  import { createRequire } from "module";
318
318
  var docgen = createRequire(import.meta.url)("react-docgen-typescript");
319
+ var parserProto = docgen.Parser.prototype;
320
+ if (!parserProto.__designtoolsUncached) {
321
+ const getPropsInfo = parserProto.getPropsInfo;
322
+ parserProto.getPropsInfo = function(...args) {
323
+ this.propertiesOfPropsCache.clear();
324
+ return getPropsInfo.apply(this, args);
325
+ };
326
+ parserProto.__designtoolsUncached = true;
327
+ }
319
328
  var SKIP_FILE = /\.(examples|pattern|test|spec|stories)\.tsx$|\.d\.ts$/;
320
329
  var SKIP_DIR = /* @__PURE__ */ new Set(["node_modules", ".next", "dist", "build", "coverage"]);
321
330
  var BASE_UI = /[\\/]@base-ui[\\/]react[\\/]/;
@@ -394,8 +403,37 @@ function readComponents(options, warn) {
394
403
  attachExamples(root, file, entries, warn);
395
404
  out.push(...entries.map((e) => e.entry));
396
405
  }
406
+ for (const entry of out) {
407
+ const specifier = importSpecifier(root, entry.source, compiler);
408
+ if (specifier) entry.import = specifier;
409
+ }
397
410
  return out.sort((a, b) => compare(a.name, b.name) || compare(a.source, b.source));
398
411
  }
412
+ function importSpecifier(root, source, compiler) {
413
+ const paths = compiler.paths;
414
+ if (!paths) return void 0;
415
+ const base = compiler.pathsBasePath ?? compiler.baseUrl ?? root;
416
+ const file = resolve(root, source).replace(/\.(tsx?|jsx?|mts|cts)$/, "").replace(/\/index$/, "");
417
+ let best;
418
+ for (const [pattern, targets] of Object.entries(paths)) {
419
+ for (const target of targets) {
420
+ const star = target.indexOf("*");
421
+ const absolute = resolve(base, star === -1 ? target : target.slice(0, star));
422
+ if (star === -1) {
423
+ if (absolute.replace(/\.(tsx?|jsx?)$/, "").replace(/\/index$/, "") === file && !pattern.includes("*")) {
424
+ return pattern;
425
+ }
426
+ continue;
427
+ }
428
+ const prefix = target.slice(0, star).endsWith("/") ? `${absolute}/` : absolute;
429
+ const suffix = target.slice(star + 1).replace(/\.(tsx?|jsx?)$/, "");
430
+ if (!file.startsWith(prefix) || !file.endsWith(suffix)) continue;
431
+ const middle = file.slice(prefix.length, file.length - suffix.length);
432
+ if (!best || prefix.length > best.prefix) best = { specifier: pattern.replace("*", middle), prefix: prefix.length };
433
+ }
434
+ }
435
+ return best?.specifier;
436
+ }
399
437
  function compare(a, b) {
400
438
  return a < b ? -1 : a > b ? 1 : 0;
401
439
  }
@@ -585,7 +623,7 @@ function parameterTypeText(sf, decl) {
585
623
  }
586
624
  function configResolver(sf, configs, imported) {
587
625
  const local = new Map(configs.map((c) => [c.name, c]));
588
- const resolve7 = (name) => local.get(name) ?? imported(sf, name);
626
+ const resolve8 = (name) => local.get(name) ?? imported(sf, name);
589
627
  const aliases = /* @__PURE__ */ new Map();
590
628
  for (const stmt of sf.statements) {
591
629
  if (!ts4.isVariableStatement(stmt)) continue;
@@ -595,7 +633,7 @@ function configResolver(sf, configs, imported) {
595
633
  }
596
634
  }
597
635
  }
598
- const find = (name) => resolve7(name) ?? (aliases.has(name) ? resolve7(aliases.get(name)) : void 0);
636
+ const find = (name) => resolve8(name) ?? (aliases.has(name) ? resolve8(aliases.get(name)) : void 0);
599
637
  return (name) => {
600
638
  const config = find(name);
601
639
  return config && withBase(config, find, /* @__PURE__ */ new Set([config.name]));
@@ -628,16 +666,16 @@ function withBase(config, find, seen) {
628
666
  if (parts.length) entry.parts = parts;
629
667
  return { ...config, entry, description: config.description ?? base.description };
630
668
  }
631
- function linkConfig(decl, paramTypes, resolve7) {
669
+ function linkConfig(decl, paramTypes, resolve8) {
632
670
  for (const m of paramTypes.matchAll(/VariantProps\s*<\s*typeof\s+(\w+)\s*>/g)) {
633
- const c = resolve7(m[1]);
671
+ const c = resolve8(m[1]);
634
672
  if (c) return c;
635
673
  }
636
674
  let found;
637
675
  const visit = (n) => {
638
676
  if (found) return;
639
- if (ts4.isCallExpression(n) && ts4.isIdentifier(n.expression)) found = resolve7(n.expression.text);
640
- else if (ts4.isPropertyAccessExpression(n) && ts4.isIdentifier(n.expression)) found = resolve7(n.expression.text);
677
+ if (ts4.isCallExpression(n) && ts4.isIdentifier(n.expression)) found = resolve8(n.expression.text);
678
+ else if (ts4.isPropertyAccessExpression(n) && ts4.isIdentifier(n.expression)) found = resolve8(n.expression.text);
641
679
  if (!found) ts4.forEachChild(n, visit);
642
680
  };
643
681
  visit(decl);
@@ -1122,7 +1160,9 @@ function readAssets(root, brandDir, warn) {
1122
1160
  for (const file of notes.keys()) {
1123
1161
  if (!found.has(file)) warn({ file: rel2(root, indexPath), message: `assets.json lists ${file}, which is not in the brand folder` });
1124
1162
  }
1125
- return out.sort((a, b) => compare(a.file, b.file));
1163
+ const order = [...notes.keys()];
1164
+ const rank2 = (file) => order.includes(file) ? order.indexOf(file) : order.length;
1165
+ return out.sort((a, b) => rank2(a.file) - rank2(b.file) || compare(a.file, b.file));
1126
1166
  }
1127
1167
  function kindFromFolder(folder) {
1128
1168
  const f = folder.toLowerCase();
@@ -1166,7 +1206,14 @@ function readTokens(root, sources, warn) {
1166
1206
  const walk3 = (container, chain) => {
1167
1207
  let comment;
1168
1208
  let fresh = false;
1209
+ let previous;
1169
1210
  for (const node of container.nodes ?? []) {
1211
+ if (node.type === "comment" && previous && !/\n/.test(node.raws.before ?? "")) {
1212
+ previous.description = cleanComment(node.text);
1213
+ previous = void 0;
1214
+ continue;
1215
+ }
1216
+ previous = void 0;
1170
1217
  if (node.type === "comment") {
1171
1218
  comment = cleanComment(node.text);
1172
1219
  fresh = true;
@@ -1188,11 +1235,15 @@ function readTokens(root, sources, warn) {
1188
1235
  const existing = entry.values[key];
1189
1236
  const explicitAlready = existing !== void 0 && !mediaKeys.has(key);
1190
1237
  if (!(media && explicitAlready)) {
1238
+ if (existing !== void 0 && existing !== value && source !== entry.source && !entry.overriddenIn?.includes(source)) {
1239
+ entry.overriddenIn = [...entry.overriddenIn ?? [], source];
1240
+ }
1191
1241
  entry.values[key] = value;
1192
1242
  if (media) mediaKeys.add(key);
1193
1243
  else mediaKeys.delete(key);
1194
1244
  }
1195
1245
  if (comment && !entry.description) entry.description = comment;
1246
+ previous = entry;
1196
1247
  continue;
1197
1248
  }
1198
1249
  fresh = false;
@@ -1284,14 +1335,72 @@ function tierOf(name, values) {
1284
1335
  var MANIFEST_SCHEMA = "designtools.manifest/1";
1285
1336
 
1286
1337
  // src/build.ts
1287
- import { readFileSync as readFileSync5 } from "fs";
1338
+ import { readFileSync as readFileSync6 } from "fs";
1288
1339
  import { dirname as dirname3, join as join3 } from "path";
1289
1340
  import { fileURLToPath } from "url";
1341
+
1342
+ // src/usage.ts
1343
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
1344
+ import { resolve as resolve4 } from "path";
1345
+ var STATUS = /^(success|warning|destructive)$|-subdued-foreground$/;
1346
+ function usageRules(root, file, warn) {
1347
+ const path = resolve4(root, file);
1348
+ if (!existsSync3(path)) {
1349
+ warn({ file, message: "usage file not found: run @designtools/tokens with --usage" });
1350
+ return [];
1351
+ }
1352
+ let usage;
1353
+ try {
1354
+ usage = JSON.parse(readFileSync5(path, "utf8"));
1355
+ } catch (e) {
1356
+ warn({ file, message: `usage file is not valid JSON: ${e.message}` });
1357
+ return [];
1358
+ }
1359
+ if (usage.schema !== "designtools.usage/1") {
1360
+ warn({ file, message: `usage file has schema ${usage.schema}; this manifest reads designtools.usage/1` });
1361
+ return [];
1362
+ }
1363
+ const colours = [.../* @__PURE__ */ new Set([...Object.keys(usage.light), ...Object.keys(usage.dark)])].filter((c) => !STATUS.test(c)).sort(compare);
1364
+ const rules = [];
1365
+ for (const colour of colours) {
1366
+ const by = { text: [], shape: [], none: [] };
1367
+ let worst;
1368
+ for (const mode of ["light", "dark"]) {
1369
+ for (const [ground, { ratio, use }] of Object.entries(usage[mode][colour] ?? {})) {
1370
+ by[use].push(`${ground} (${mode})`);
1371
+ if (use !== "text" && (!worst || ratio < worst.ratio)) worst = { ratio, where: `${ground} in ${mode} mode` };
1372
+ }
1373
+ }
1374
+ if (!by.shape.length && !by.none.length) continue;
1375
+ const list2 = (xs) => xs.length > 1 ? `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}` : xs[0];
1376
+ const parts = [
1377
+ by.text.length ? `\`--${colour}\` reads as text on ${list2(by.text)}.` : `\`--${colour}\` is never text on these backgrounds.`,
1378
+ by.shape.length ? `On ${list2(by.shape)} it is a shape only (an icon, a border, a meter), not text.` : "",
1379
+ by.none.length ? `On ${list2(by.none)} it is neither${worst ? `, down to ${worst.ratio}:1 on ${worst.where}` : ""}: use it there only as a fill with its own label.` : ""
1380
+ ].filter(Boolean);
1381
+ rules.push({
1382
+ id: `usage-${colour}`,
1383
+ kind: "interface",
1384
+ title: `Where ${colour} can go`,
1385
+ statement: parts.join(" "),
1386
+ rationale: "Measured from the generated tokens; it changes when they do. No palette makes every colour work on every background, so a brand colour's limits are a brand decision: confirm them as a brand rule.",
1387
+ threshold: `${usage.floors.text}:1 as text, ${usage.floors.shape}:1 as a shape (WCAG 2.2 1.4.3 and 1.4.11)`,
1388
+ appliesTo: [colour],
1389
+ tokens: [`--${colour}`],
1390
+ status: "measured",
1391
+ check: { kind: "test", ref: "mxa-tests", description: "axe measures contrast on every rendered page; this says where to look first." },
1392
+ source: file
1393
+ });
1394
+ }
1395
+ return rules;
1396
+ }
1397
+
1398
+ // src/build.ts
1290
1399
  var GENERATOR_NAME = "@designtools/manifest";
1291
1400
  var VERSION = (() => {
1292
1401
  try {
1293
1402
  const here = dirname3(fileURLToPath(import.meta.url));
1294
- return JSON.parse(readFileSync5(join3(here, "../package.json"), "utf8")).version;
1403
+ return JSON.parse(readFileSync6(join3(here, "../package.json"), "utf8")).version;
1295
1404
  } catch {
1296
1405
  return "0.0.0";
1297
1406
  }
@@ -1312,7 +1421,10 @@ function buildManifest(config) {
1312
1421
  components: readComponents({ root: config.root, system: config.system, tsconfig: config.tsconfig, out }, warn)
1313
1422
  };
1314
1423
  const tokens = { ...head, sources: config.tokens, tokens: readTokens(config.root, config.tokens, warn) };
1315
- const rules = { ...head, rules: readRules(config.root, config.rules ?? `${system}/rules`, warn) };
1424
+ const rules = {
1425
+ ...head,
1426
+ rules: [...readRules(config.root, config.rules ?? `${system}/rules`, warn), ...config.usage ? usageRules(config.root, config.usage, warn) : []]
1427
+ };
1316
1428
  const patterns = {
1317
1429
  ...head,
1318
1430
  patterns: readPatterns(config.root, system, components.components, compilerOptions(config.root, config.tsconfig), warn, out)
@@ -1363,14 +1475,17 @@ function sortKeys(value) {
1363
1475
 
1364
1476
  // src/agents.ts
1365
1477
  import { execFileSync } from "child_process";
1366
- import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
1367
- import { resolve as resolve4 } from "path";
1478
+ import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
1479
+ import { resolve as resolve5 } from "path";
1368
1480
  var AGENTS_START = "<!-- designtools:start \xB7 generated by designtools-manifest agents; edit outside these markers -->";
1369
1481
  var AGENTS_END = "<!-- designtools:end -->";
1370
- function agentsFile(root, given) {
1482
+ function agentsFile(root, given, configured) {
1371
1483
  if (given) return given;
1372
- if (existsSync3(resolve4(root, "CLAUDE.md"))) return "CLAUDE.md";
1373
- return "AGENTS.md";
1484
+ if (configured) return configured;
1485
+ const claude = readIfExists(resolve5(root, "CLAUDE.md"));
1486
+ if (claude === void 0) return "AGENTS.md";
1487
+ if (/^\s*@AGENTS\.md\s*$/m.test(claude)) return "AGENTS.md";
1488
+ return "CLAUDE.md";
1374
1489
  }
1375
1490
  function agentsSection(config) {
1376
1491
  const out = outDir(config);
@@ -1394,7 +1509,7 @@ function agentsSection(config) {
1394
1509
  "",
1395
1510
  "- Start from a component's canonical example, and follow its do and don't examples.",
1396
1511
  "- Change how a component looks with its variants, not a `className`.",
1397
- "- Use semantic tokens. Never a ramp step, a hex value or an arbitrary value.",
1512
+ "- Use semantic tokens for colour in UI. Never a hex value or an arbitrary value. A ramp step (`--color-<name>-500`) is for charts, illustration and a decorative colour that has no semantic slot, never for text, surfaces or actions.",
1398
1513
  "- Use the taxonomy's names, never its wrong forms, in code and in copy.",
1399
1514
  "- Carry status when you quote: say when a component is emerging or deprecated, or a rule is still an assumption.",
1400
1515
  AGENTS_END
@@ -1412,12 +1527,12 @@ function withAgentsSection(existing, section) {
1412
1527
  return existing.replace(/\s*$/, "\n\n") + section;
1413
1528
  }
1414
1529
  function readIfExists(path) {
1415
- return existsSync3(path) ? readFileSync6(path, "utf8") : void 0;
1530
+ return existsSync4(path) ? readFileSync7(path, "utf8") : void 0;
1416
1531
  }
1417
1532
  function provenance(root) {
1418
1533
  let version = "unversioned";
1419
1534
  try {
1420
- const pkg = JSON.parse(readFileSync6(resolve4(root, "package.json"), "utf8"));
1535
+ const pkg = JSON.parse(readFileSync7(resolve5(root, "package.json"), "utf8"));
1421
1536
  version = `${pkg.name ?? "project"}@${pkg.version ?? "0.0.0"}`;
1422
1537
  } catch {
1423
1538
  }
@@ -1435,18 +1550,18 @@ function provenance(root) {
1435
1550
  }
1436
1551
 
1437
1552
  // src/config.ts
1438
- import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
1439
- import { resolve as resolve5 } from "path";
1553
+ import { existsSync as existsSync5, readFileSync as readFileSync8 } from "fs";
1554
+ import { resolve as resolve6 } from "path";
1440
1555
  var CONFIG_FILE = "designtools.json";
1441
- var PATH_FIELDS = ["tsconfig", "out", "rules", "taxonomy", "brand", "docs"];
1556
+ var PATH_FIELDS = ["tsconfig", "out", "rules", "taxonomy", "brand", "docs", "agents", "usage"];
1442
1557
  function loadConfig(flags) {
1443
- const root = resolve5(flags.root ?? ".");
1444
- const path = resolve5(root, CONFIG_FILE);
1558
+ const root = resolve6(flags.root ?? ".");
1559
+ const path = resolve6(root, CONFIG_FILE);
1445
1560
  let file = {};
1446
- if (existsSync4(path)) {
1561
+ if (existsSync5(path)) {
1447
1562
  let parsed;
1448
1563
  try {
1449
- parsed = JSON.parse(readFileSync7(path, "utf8"));
1564
+ parsed = JSON.parse(readFileSync8(path, "utf8"));
1450
1565
  } catch (e) {
1451
1566
  throw new Error(`${CONFIG_FILE} is not valid JSON: ${e.message}`);
1452
1567
  }
@@ -1463,6 +1578,12 @@ function loadConfig(flags) {
1463
1578
  throw new Error(`manifest.tokens in ${CONFIG_FILE} must be a list of stylesheet paths`);
1464
1579
  }
1465
1580
  const config = { root, system, tokens };
1581
+ if (file.prose !== void 0) {
1582
+ if (!Array.isArray(file.prose) || file.prose.some((p) => typeof p !== "string")) {
1583
+ throw new Error(`manifest.prose in ${CONFIG_FILE} must be a list of files or folders`);
1584
+ }
1585
+ config.prose = file.prose;
1586
+ }
1466
1587
  for (const field of PATH_FIELDS) {
1467
1588
  const value = flags[field] ?? file[field];
1468
1589
  if (value !== void 0) config[field] = value;
@@ -1667,25 +1788,33 @@ No changes to components or tokens.
1667
1788
  }
1668
1789
 
1669
1790
  // src/prose.ts
1670
- import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
1671
- import { join as join4, relative as relative3, resolve as resolve6, sep as sep3 } from "path";
1791
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
1792
+ import { join as join4, relative as relative3, resolve as resolve7, sep as sep3 } from "path";
1672
1793
  var NOT_SYSTEM = /* @__PURE__ */ new Set(["Fragment", "Suspense", "StrictMode", "Profiler", "Link", "Image", "Script", "Head", "Html", "Main", "NextScript", "Form"]);
1673
1794
  var LIST_COMPONENTS = 4;
1674
1795
  var LIST_TOKENS = 6;
1675
- function defaultProseFiles(root, system) {
1796
+ function defaultProseFiles(root, system, extra = []) {
1676
1797
  const out = [];
1677
- for (const f of ["CLAUDE.md", "AGENTS.md"]) if (existsSync5(resolve6(root, f))) out.push(f);
1678
- for (const dir of [".claude", "docs", system]) out.push(...walk2(root, resolve6(root, dir)));
1798
+ for (const f of ["CLAUDE.md", "AGENTS.md"]) if (existsSync6(resolve7(root, f))) out.push(f);
1799
+ for (const dir of [".claude", "docs", system]) out.push(...walk2(root, resolve7(root, dir)));
1800
+ for (const p of extra) {
1801
+ const path = resolve7(root, p);
1802
+ if (!existsSync6(path)) continue;
1803
+ if (statSync3(path).isDirectory()) out.push(...walk2(root, path, true));
1804
+ else out.push(relative3(root, path).split(sep3).join("/"));
1805
+ }
1679
1806
  return [...new Set(out)];
1680
1807
  }
1681
- function walk2(root, dir) {
1682
- if (!existsSync5(dir) || !statSync3(dir).isDirectory()) return [];
1808
+ function walk2(root, dir, content = false) {
1809
+ if (!existsSync6(dir) || !statSync3(dir).isDirectory()) return [];
1683
1810
  const out = [];
1684
1811
  for (const name of readdirSync3(dir).sort()) {
1685
1812
  const path = join4(dir, name);
1686
1813
  if (statSync3(path).isDirectory()) {
1687
- if (name !== "node_modules" && name !== "manifest") out.push(...walk2(root, path));
1688
- } else if (/\.mdx?$/.test(name)) out.push(relative3(root, path).split(sep3).join("/"));
1814
+ if (name !== "node_modules" && name !== "manifest" && name !== ".next") out.push(...walk2(root, path, content));
1815
+ } else if (/\.mdx?$/.test(name) || content && /content\.tsx?$/.test(name)) {
1816
+ out.push(relative3(root, path).split(sep3).join("/"));
1817
+ }
1689
1818
  }
1690
1819
  return out;
1691
1820
  }
@@ -1708,10 +1837,13 @@ function checkProse(root, files, inputs) {
1708
1837
  }
1709
1838
  const findings = [];
1710
1839
  for (const file of files) {
1711
- const path = resolve6(root, file);
1712
- if (!existsSync5(path)) continue;
1713
- const lines = stripGenerated(readFileSync8(path, "utf8")).split(/\r?\n/);
1840
+ const path = resolve7(root, file);
1841
+ if (!existsSync6(path)) continue;
1842
+ const lines = stripGenerated(readFileSync9(path, "utf8")).split(/\r?\n/);
1843
+ const script = /\.[cm]?[jt]sx?$/.test(file);
1714
1844
  let inFence = false;
1845
+ let ignoring = false;
1846
+ let skipNext = false;
1715
1847
  let block;
1716
1848
  const closeBlock = () => {
1717
1849
  if (block && block.components.size >= LIST_COMPONENTS) {
@@ -1724,7 +1856,16 @@ function checkProse(root, files, inputs) {
1724
1856
  };
1725
1857
  lines.forEach((line, i) => {
1726
1858
  const n = i + 1;
1727
- if (/^\s*(```|~~~)/.test(line)) inFence = !inFence;
1859
+ if (/prose-check-ignore-start/.test(line)) ignoring = true;
1860
+ if (/prose-check-ignore-end/.test(line)) {
1861
+ ignoring = false;
1862
+ return;
1863
+ }
1864
+ const skip = ignoring || skipNext || /prose-check-ignore(?!-)/.test(line);
1865
+ skipNext = /prose-check-ignore-next-line/.test(line);
1866
+ if (skip) return;
1867
+ if (script && /^\s*(import|export\s+(type|interface))\b/.test(line)) return;
1868
+ if (!script && /^\s*(```|~~~)/.test(line)) inFence = !inFence;
1728
1869
  if (!line.trim()) {
1729
1870
  closeBlock();
1730
1871
  return;
@@ -1740,7 +1881,7 @@ function checkProse(root, files, inputs) {
1740
1881
  message: wrong ? `<${name}>: the system calls this ${wrong.name}` : `<${name}> looks like a component, but the system has none by that name`
1741
1882
  });
1742
1883
  }
1743
- const code = inFence ? [line] : [...line.matchAll(/`([^`]+)`/g)].map((m) => m[1]);
1884
+ const code = inFence ? [line] : script ? [] : [...line.matchAll(/`([^`]+)`/g)].map((m) => m[1]);
1744
1885
  for (const span of code) {
1745
1886
  for (const word of span.match(/[A-Za-z][\w-]*/g) ?? []) {
1746
1887
  if (components.has(word)) block.components.add(word);
@@ -1752,7 +1893,7 @@ function checkProse(root, files, inputs) {
1752
1893
  }
1753
1894
  }
1754
1895
  if (!inFence) {
1755
- for (const word of line.replace(/`[^`]*`/g, " ").match(/\b[A-Z][A-Za-z0-9]+\b/g) ?? []) if (components.has(word)) block.components.add(word);
1896
+ for (const word of (script ? line : line.replace(/`[^`]*`/g, " ")).match(/\b[A-Z][A-Za-z0-9]+\b/g) ?? []) if (components.has(word)) block.components.add(word);
1756
1897
  for (const { re, form, term } of wrongProse) if (re.test(line)) findings.push({ file, line: n, message: `"${form}": the product says "${term.name}"` });
1757
1898
  }
1758
1899
  });
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  provenance,
15
15
  readIfExists,
16
16
  withAgentsSection
17
- } from "./chunk-EFGVJ5ZQ.js";
17
+ } from "./chunk-PQGMDEUF.js";
18
18
 
19
19
  // src/cli.ts
20
20
  import { execFileSync } from "child_process";
@@ -43,7 +43,9 @@ Options
43
43
  --tsconfig <file> tsconfig.json for resolving imports (found from the root when absent)
44
44
  --out <dir> Where the manifest goes (default: <system>/manifest)
45
45
  --root <dir> Project root (default: the current directory)
46
- --file <file> agents: the file to write (default: CLAUDE.md if it exists, else AGENTS.md)
46
+ --file <file> agents: the file to write (default: manifest.agents in designtools.json;
47
+ else AGENTS.md when CLAUDE.md imports it with @AGENTS.md; else
48
+ CLAUDE.md if it exists; else AGENTS.md)
47
49
  --check agents: compare instead of writing
48
50
  --json diff, prose-check, provenance: print JSON
49
51
  --fail-on <class> diff: exit 1 if any change of this class is found (breaking, additive, visual, docs)
@@ -186,7 +188,7 @@ switch (command) {
186
188
  process.exit(ok ? 0 : 1);
187
189
  }
188
190
  case "agents": {
189
- const file = agentsFile(config.root, values.file);
191
+ const file = agentsFile(config.root, values.file, config.agents);
190
192
  const path = resolve(config.root, file);
191
193
  const existing = readIfExists(path);
192
194
  const next = withAgentsSection(existing, agentsSection(config));
@@ -203,7 +205,7 @@ switch (command) {
203
205
  process.exit(0);
204
206
  }
205
207
  case "prose-check": {
206
- const files = rest.length ? rest : defaultProseFiles(config.root, config.system);
208
+ const files = rest.length ? rest : defaultProseFiles(config.root, config.system, config.prose);
207
209
  const findings = checkProse(config.root, files, {
208
210
  components: built.components.components,
209
211
  patterns: built.patterns.patterns,
package/dist/index.d.ts CHANGED
@@ -34,6 +34,12 @@ interface ComponentEntry {
34
34
  export: string;
35
35
  /** The file that exports it. */
36
36
  source: string;
37
+ /**
38
+ * The module specifier an app imports it by, through the tsconfig path alias
39
+ * that reaches the system folder (`@ds/button/button`). Absent when no alias does,
40
+ * in which case `source` is the only honest answer.
41
+ */
42
+ import?: string;
37
43
  /** The component's JSDoc, or its variant config's when the component has none. */
38
44
  description?: string;
39
45
  /** `@category`. Blocks group components without one under "Components". */
@@ -156,6 +162,8 @@ interface TokenEntry {
156
162
  description?: string;
157
163
  /** The stylesheet that first declares it. */
158
164
  source: string;
165
+ /** Later stylesheets whose declaration replaced a value, in the order read: an override layer. */
166
+ overriddenIn?: string[];
159
167
  }
160
168
  /** rules.json: brand and interface rules in one format. */
161
169
  interface RulesManifest extends ManifestFile {
@@ -178,8 +186,11 @@ interface RuleEntry {
178
186
  exceptions?: string[];
179
187
  /** Tokens it rests on, e.g. `--size-2xl`. */
180
188
  tokens?: string[];
181
- /** Confirmed with the client, or an assumption still to confirm. */
182
- status: "confirmed" | "assumption";
189
+ /**
190
+ * Confirmed with the client, an assumption still to confirm, or measured: generated from the
191
+ * tokens (where a colour can be text), a fact that changes when they do, not a decision.
192
+ */
193
+ status: "confirmed" | "assumption" | "measured";
183
194
  /** How it is proved: a test, a lint rule, an audit, or a person. */
184
195
  check?: {
185
196
  kind: "test" | "lint" | "audit" | "manual";
@@ -298,6 +309,19 @@ interface ManifestConfig {
298
309
  brand?: string;
299
310
  /** Where the docs are served, for the agent pointer file, e.g. `/design/system`. */
300
311
  docs?: string;
312
+ /**
313
+ * The usage file `@designtools/tokens --usage` writes: every colour on every background with a
314
+ * verdict. Its failures become measured usage rules in rules.json.
315
+ */
316
+ usage?: string;
317
+ /** The agent file the pointer section goes in, when the default choice is wrong for the project. */
318
+ agents?: string;
319
+ /**
320
+ * More places for prose-check to read, beside the agent files, `.claude/`, `docs/`
321
+ * and the system folder: files of any kind, or folders, whose markdown and
322
+ * `*content.ts(x)` files (the docs site's editorial copy) are read.
323
+ */
324
+ prose?: string[];
301
325
  }
302
326
  interface BuiltManifest {
303
327
  components: ComponentsManifest;
@@ -422,7 +446,8 @@ declare function classifyExamples(source: string, exports: ExampleRef[]): Exampl
422
446
  *
423
447
  * The comment above a declaration, or above the run of declarations it starts,
424
448
  * is that token's description: the scale's comments already say why each value
425
- * is what it is.
449
+ * is what it is. A comment on the same line after a declaration describes that
450
+ * declaration (`--size-xl: 3rem; /* 48px controls *\/`), and does not start a run.
426
451
  */
427
452
 
428
453
  declare function readTokens(root: string, sources: string[], warn: (w: Warning) => void): TokenEntry[];
@@ -458,7 +483,8 @@ declare function buildTaxonomy(root: string, curatedPath: string, inputs: {
458
483
  /**
459
484
  * Brand assets from one folder. `assets.json` in it says how to use each file;
460
485
  * files it does not mention are listed too, with a warning, so nothing in the
461
- * folder goes unseen.
486
+ * folder goes unseen. The order is the author's (the wordmark first, because it
487
+ * is the default), then anything unlisted by file name.
462
488
  */
463
489
  declare function readAssets(root: string, brandDir: string, warn: (w: Warning) => void): AssetEntry[];
464
490
 
@@ -471,8 +497,13 @@ declare function readAssets(root: string, brandDir: string, warn: (w: Warning) =
471
497
 
472
498
  declare const AGENTS_START = "<!-- designtools:start \u00B7 generated by designtools-manifest agents; edit outside these markers -->";
473
499
  declare const AGENTS_END = "<!-- designtools:end -->";
474
- /** CLAUDE.md if the project has one, else AGENTS.md. */
475
- declare function agentsFile(root: string, given?: string): string;
500
+ /**
501
+ * The file the pointer goes in: the flag, then `manifest.agents` in designtools.json,
502
+ * then AGENTS.md when CLAUDE.md imports it (`@AGENTS.md`, as `next dev` writes it), so
503
+ * every agent reads the section and Claude still reaches it through the import; else
504
+ * CLAUDE.md if the project has one, else AGENTS.md.
505
+ */
506
+ declare function agentsFile(root: string, given?: string, configured?: string): string;
476
507
  declare function agentsSection(config: ManifestConfig & {
477
508
  docs?: string;
478
509
  }): string;
@@ -500,6 +531,11 @@ declare function provenance(root: string): Provenance;
500
531
  * a wrong form from the taxonomy (`Modal` for `Dialog`, "log in" for "sign in")
501
532
  * a restated list: a paragraph or list naming several components or tokens,
502
533
  * which is a second copy of the manifest that will drift
534
+ *
535
+ * A page that names wrong forms on purpose (a voice page: "never the customer")
536
+ * says so: `prose-check-ignore` on a line skips it, `prose-check-ignore-next-line`
537
+ * skips the one after, and `prose-check-ignore-start` … `prose-check-ignore-end`
538
+ * skip everything between, in whatever comment syntax the file uses.
503
539
  */
504
540
 
505
541
  interface ProseFinding {
@@ -513,7 +549,7 @@ interface ProseInputs {
513
549
  tokens: TokenEntry[];
514
550
  terms: TermEntry[];
515
551
  }
516
- declare function defaultProseFiles(root: string, system: string): string[];
552
+ declare function defaultProseFiles(root: string, system: string, extra?: string[]): string[];
517
553
  declare function checkProse(root: string, files: string[], inputs: ProseInputs): ProseFinding[];
518
554
 
519
555
  /**
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  serialise,
33
33
  tierOf,
34
34
  withAgentsSection
35
- } from "./chunk-EFGVJ5ZQ.js";
35
+ } from "./chunk-PQGMDEUF.js";
36
36
  export {
37
37
  AGENTS_END,
38
38
  AGENTS_START,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@designtools/manifest",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Reads a React design system (components, variants, data-slots, examples, tokens, rules, patterns, taxonomy and brand assets) into committed JSON that docs, checks and agents read from. Deterministic CLI: build, check, diff, agents, prose-check.",
5
5
  "type": "module",
6
6
  "license": "MIT",