synthesisui 0.9.0 → 0.9.1

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.
@@ -2,7 +2,7 @@ import { readdir, readFile } from "node:fs/promises";
2
2
  import { join, relative, resolve } from "node:path";
3
3
  import { bindingsFromDocument, countComponents, findOverrides, } from "../doctor/overrides.js";
4
4
  import { diagnose, scanSource, } from "../doctor/scan.js";
5
- import { findSelfConflicts } from "../doctor/self-conflict.js";
5
+ import { findSelfConflicts, forbiddenProps, isReset, propMatchesLabel, } from "../doctor/self-conflict.js";
6
6
  import { buildTable, EMPTY_TABLE, nearestToken, } from "../doctor/tokens.js";
7
7
  import { body, section, snippet } from "../output.js";
8
8
  /**
@@ -313,10 +313,64 @@ export async function doctor(opts) {
313
313
  console.log(body(`+${files.length - shownFiles.length} more files. Run with --all to see everything.`));
314
314
  }
315
315
  }
316
+ // Every other section asks whether the code obeys the system. This one asks
317
+ // whether the system obeys itself, and it is only here because the tool once
318
+ // reported a consumer for overruling a recipe while they were obeying the
319
+ // law written on that same component.
320
+ const conflicts = documents.flatMap((doc) => findSelfConflicts(doc));
321
+ const conflictsInUse = conflicts.filter((c) => used.has(c.component));
322
+ if (conflictsInUse.length > 0) {
323
+ console.log(section("Your system contradicts itself"));
324
+ console.log(body(conflictsInUse.length === 1
325
+ ? "One component says one thing in prose and another in its recipe."
326
+ : `${conflictsInUse.length} components say one thing in prose and another in their recipe.`));
327
+ console.log("");
328
+ for (const c of conflictsInUse) {
329
+ console.log(body(`ds-${c.component}`));
330
+ console.log(` "${c.law}"`);
331
+ console.log(` but ${c.where} binds ${c.value}`);
332
+ console.log("");
333
+ }
334
+ console.log(body("Whoever wrote the law and whoever wrote the recipe disagree."));
335
+ console.log(body("Until they do not, no code here can be correct."));
336
+ }
316
337
  if (overrides.length > 0) {
338
+ // An override on a property the component's own law FORBIDS, written as a
339
+ // reset, is not drift - it is the author keeping a promise the recipe
340
+ // broke. Counting it against them was the report being unfair to the only
341
+ // person in the loop who read the law (investidorez, 25/07).
342
+ const forbidden = new Map();
343
+ for (const doc of documents) {
344
+ for (const [comp, props] of forbiddenProps(doc)) {
345
+ const merged = forbidden.get(comp) ?? new Map();
346
+ for (const [label, law] of props)
347
+ merged.set(label, law);
348
+ forbidden.set(comp, merged);
349
+ }
350
+ }
351
+ const lawKept = (o) => {
352
+ if (!isReset(o.wrote))
353
+ return null;
354
+ const props = forbidden.get(o.component);
355
+ if (!props)
356
+ return null;
357
+ for (const [label, law] of props) {
358
+ if (propMatchesLabel(o.prop, label))
359
+ return law;
360
+ }
361
+ return null;
362
+ };
363
+ const drifting = overrides.filter((o) => lawKept(o) === null);
364
+ const correcting = overrides.length - drifting.length;
317
365
  console.log(section("Overruled"));
318
- console.log(body(`${overrides.length} place${overrides.length === 1 ? "" : "s"} where the code takes a component`));
366
+ console.log(body(`${drifting.length} place${drifting.length === 1 ? "" : "s"} where the code takes a component`));
319
367
  console.log(body("the system defines, and then overrules it locally."));
368
+ if (correcting > 0) {
369
+ console.log("");
370
+ console.log(body(correcting === 1
371
+ ? "One more overrules it to KEEP a law the recipe breaks - marked below."
372
+ : `${correcting} more overrule it to KEEP a law the recipe breaks - marked below.`));
373
+ }
320
374
  console.log("");
321
375
  const byFile = new Map();
322
376
  for (const o of overrides) {
@@ -338,6 +392,11 @@ export async function doctor(opts) {
338
392
  console.log(` the recipe binds ${o.recipe}`);
339
393
  else if (o.where)
340
394
  console.log(` the recipe binds it ${o.where}`);
395
+ const kept = lawKept(o);
396
+ if (kept) {
397
+ console.log(` ✓ but the law says: "${kept}"`);
398
+ console.log(" this override keeps it - the recipe does not");
399
+ }
341
400
  }
342
401
  if (!opts.all && list.length > 3) {
343
402
  console.log(` +${list.length - 3} more`);
@@ -351,27 +410,6 @@ export async function doctor(opts) {
351
410
  // The system's own words, for the components this project actually uses.
352
411
  // Prose, so nothing verifies it - the value is putting it in front of
353
412
  // whoever is touching the component, which no other tool is positioned to
354
- // Every other section asks whether the code obeys the system. This one asks
355
- // whether the system obeys itself, and it is only here because the tool once
356
- // reported a consumer for overruling a recipe while they were obeying the
357
- // law written on that same component.
358
- const conflicts = documents.flatMap((doc) => findSelfConflicts(doc));
359
- const conflictsInUse = conflicts.filter((c) => used.has(c.component));
360
- if (conflictsInUse.length > 0) {
361
- console.log(section("Your system contradicts itself"));
362
- console.log(body(conflictsInUse.length === 1
363
- ? "One component says one thing in prose and another in its recipe."
364
- : `${conflictsInUse.length} components say one thing in prose and another in their recipe.`));
365
- console.log("");
366
- for (const c of conflictsInUse) {
367
- console.log(body(`ds-${c.component}`));
368
- console.log(` "${c.law}"`);
369
- console.log(` but ${c.where} binds ${c.value}`);
370
- console.log("");
371
- }
372
- console.log(body("Whoever wrote the law and whoever wrote the recipe disagree."));
373
- console.log(body("Until they do not, no code here can be correct."));
374
- }
375
413
  // do because no other tool knows these laws exist.
376
414
  const inUse = [...used.entries()]
377
415
  .filter(([name]) => (recipes.get(name)?.usage.length ?? 0) > 0)
@@ -94,6 +94,43 @@ function* declarations(node, where) {
94
94
  }
95
95
  }
96
96
  }
97
+ /**
98
+ * Per component, the properties its own laws forbid and the law that does it.
99
+ *
100
+ * The report needs this separately from the conflicts: an override on a
101
+ * forbidden property is not drift, it is the author keeping a promise the
102
+ * recipe broke. Listing them together was accusing the person who was right.
103
+ */
104
+ export function forbiddenProps(document) {
105
+ const out = new Map();
106
+ const doc = (document ?? {});
107
+ const components = (doc.components ?? {});
108
+ for (const [name, raw] of Object.entries(components)) {
109
+ const recipe = (raw ?? {});
110
+ const laws = Array.isArray(recipe.usage)
111
+ ? recipe.usage.filter((x) => typeof x === "string")
112
+ : [];
113
+ for (const law of laws) {
114
+ for (const rule of FORBIDDABLE) {
115
+ if (!forbids(law, rule.words))
116
+ continue;
117
+ const byProp = out.get(name) ?? new Map();
118
+ byProp.set(rule.label, law);
119
+ out.set(name, byProp);
120
+ }
121
+ }
122
+ }
123
+ return out;
124
+ }
125
+ /** Is this property one the law names, given how the rule matches? */
126
+ export function propMatchesLabel(prop, label) {
127
+ const rule = FORBIDDABLE.find((r) => r.label === label);
128
+ return rule ? rule.prop.test(prop) : false;
129
+ }
130
+ /** A value that removes rather than sets - the law being kept. */
131
+ export function isReset(value) {
132
+ return !isSet(value);
133
+ }
97
134
  /**
98
135
  * Every place a component's recipe does what its own law forbids.
99
136
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {