synthesisui 0.16.54 → 0.16.56

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.
@@ -317,7 +317,31 @@ export async function takeCensus(root) {
317
317
  },
318
318
  propFiles: {},
319
319
  }));
320
- const merged = [...inventory, ...fromTypes];
320
+ /**
321
+ * A composed component that is ALSO defined keeps its usage and gains the
322
+ * axes its type declares - the declaration wins per axis, because it is the
323
+ * closed set the author designed.
324
+ *
325
+ * Without this the usage silently truncated the contract: `MetricCard` is
326
+ * composed once with `size="xsm"`, so its axis had ONE option and was dropped
327
+ * by the two-option rule, while its type declares four sizes and three
328
+ * variants (dono, 31/07). The component would have reached the platform with
329
+ * no contract at all, which is the exact thing reading definitions was for.
330
+ *
331
+ * Carried BESIDE the usage rather than written into it. The first attempt
332
+ * merged the declaration into `props` and broke the two readings that need
333
+ * `props` to mean "what the code passed": the composition list started
334
+ * showing declared options as composed, and the dead-option check went quiet
335
+ * because every declared option now looked used.
336
+ */
337
+ const declaredBy = new Map(defined.map((d) => [d.name, d]));
338
+ const enriched = inventory.map((c) => {
339
+ const d = declaredBy.get(c.name);
340
+ if (!d || Object.keys(d.axes).length === 0)
341
+ return c;
342
+ return { ...c, declaredAxes: d.axes };
343
+ });
344
+ const merged = [...enriched, ...fromTypes];
321
345
  // The verdict travels WITH the payload: the platform reads one reading rather
322
346
  // than computing a second opinion from the same numbers, which is how two
323
347
  // implementations of the same judgement start disagreeing.
@@ -238,6 +238,28 @@ export const TWIN_OVERLAP = 0.5;
238
238
  * the colour work learned twice: a ratio over a tiny set says nothing.
239
239
  */
240
240
  export const TWIN_MIN_SHARED = 2;
241
+ /**
242
+ * Axes that measure rather than mean.
243
+ *
244
+ * Every component has a size, and `sm|md|lg` is the same three words in all of
245
+ * them - so a shared size scale is a shared SCALE, never a shared purpose. It
246
+ * paired a metric card with a text input at 50% (dono, 31/07), which is the
247
+ * kind of finding that teaches a person to stop reading the list.
248
+ *
249
+ * `intent`, `tone`, `status` and their kin are what say what a thing IS, and a
250
+ * pair needs at least one of those in common before its sizes count for
251
+ * anything.
252
+ */
253
+ const DIMENSIONAL = new Set(["size", "density"]);
254
+ /**
255
+ * Values that are the absence of a choice.
256
+ *
257
+ * `default` and `neutral` appear on almost every axis and mean "nothing in
258
+ * particular", so sharing them is not agreement. With sizes excluded, a metric
259
+ * card and a text input still paired because both offer a `default` variant
260
+ * (dono, 31/07) - the same false positive one level down.
261
+ */
262
+ const UNCHOSEN = new Set(["neutral", "default", "none", "normal", "base"]);
241
263
  function sharedCount(a, b) {
242
264
  const va = new Set();
243
265
  const vb = new Set();
@@ -259,6 +281,29 @@ function sharedCount(a, b) {
259
281
  n += 1;
260
282
  return n;
261
283
  }
284
+ /** Shared values on an axis that MEANS something, not one that measures. */
285
+ function sharedMeaning(a, b) {
286
+ const pick = (x) => {
287
+ const out = new Set();
288
+ for (const [p, values] of Object.entries(x)) {
289
+ const axis = AXIS_SYNONYM[p.toLowerCase()];
290
+ if (!axis || DIMENSIONAL.has(axis))
291
+ continue;
292
+ for (const v of canonicalValues(values)) {
293
+ if (!UNCHOSEN.has(v))
294
+ out.add(v);
295
+ }
296
+ }
297
+ return out;
298
+ };
299
+ const va = pick(a);
300
+ const vb = pick(b);
301
+ let n = 0;
302
+ for (const v of va)
303
+ if (vb.has(v))
304
+ n += 1;
305
+ return n;
306
+ }
262
307
  export function crosswalk(components) {
263
308
  const mine = components.filter((c) => !c.from);
264
309
  // Two of THEIR components reading as the same catalogue entry is the
@@ -294,7 +339,8 @@ export function crosswalk(components) {
294
339
  overlap: axisOverlap(component.props, o.props),
295
340
  }))
296
341
  .filter((t) => t.overlap >= TWIN_OVERLAP &&
297
- sharedCount(component.props, mine.find((m) => m.name === t.name)?.props ?? {}) >= TWIN_MIN_SHARED)
342
+ sharedCount(component.props, mine.find((m) => m.name === t.name)?.props ?? {}) >= TWIN_MIN_SHARED &&
343
+ sharedMeaning(component.props, mine.find((m) => m.name === t.name)?.props ?? {}) >= 1)
298
344
  .sort((a, b) => b.overlap - a.overlap);
299
345
  if (canonical && CATALOGUE[canonical]) {
300
346
  const alsoClaiming = (claims.get(canonical) ?? []).filter((n) => n !== component.name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.54",
3
+ "version": "0.16.56",
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": {