@yschimke/compose-design-map 1.44.0 → 1.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/design-map.mjs +89 -9
  2. package/package.json +1 -1
package/design-map.mjs CHANGED
@@ -408,6 +408,33 @@ export function variantSeeds(preview) {
408
408
  }
409
409
 
410
410
  /** The name a variant render goes by, for a report and for the design-map `state` slot. */
411
+ /**
412
+ * How a folded variant names itself in the reference diagnostics: `<parentId> [<axis>=<value> …]`.
413
+ *
414
+ * Not the bare `componentId` — that is the PARENT's id for a VARIANT role, so reporting a variant's
415
+ * stated absence under it would read as a finding about a parent that may carry a perfectly good
416
+ * reference.
417
+ *
418
+ * Not [variantName] either, though that is what the variant is called elsewhere: it narrows to the
419
+ * `state` alone whenever there is one, so two variants of a parent sharing a state and differing
420
+ * only in `props` produce the SAME name. These labels are map keys, so a collision silently drops
421
+ * one of the two stated absences — losing exactly the record this diagnostic exists to keep. The
422
+ * full seed vector is what distinguishes them, so the label is built from that.
423
+ */
424
+ export function variantAbsenceId(preview) {
425
+ const parent = preview.catalog?.componentId ?? "(unnamed)";
426
+ const axes = variantSeeds(preview)
427
+ .map((seed) => `${seed.key}=${seed.raw}`)
428
+ .join(" ");
429
+ // `state` and `props` are both optional, so a variant CAN declare `noReference` and name no axis
430
+ // at all. Falling back to the bare parent id then publishes "Button — <reason>" for a parent that
431
+ // may hold a perfectly good reference: it reads as a finding about the parent, and says nothing
432
+ // about which variant the reason belongs to. The function name is what distinguishes such a
433
+ // variant, so it stands in for the axes it did not give.
434
+ const label = axes || preview.functionName || captureIdentity(preview).subject;
435
+ return label ? `${parent} [${label}]` : parent;
436
+ }
437
+
411
438
  function variantName(preview, seeds) {
412
439
  const catalog = preview.catalog;
413
440
  const cell = preview.overrides?.name;
@@ -529,29 +556,80 @@ export function projectDesignMap(previews, opts = {}) {
529
556
  * however many captures it publishes.
530
557
  */
531
558
  const unmappedIds = new Map();
559
+ /**
560
+ * Stated absences, keyed by a COLLISION-SAFE identity and carrying the display label separately.
561
+ *
562
+ * A component keys on its own id and a variant on its capture subject, each behind its own
563
+ * prefix. Both are free-form strings from different namespaces — a `@CatalogComponent(id = …)`
564
+ * may legally be spelled like a capture subject — so sharing one map without tagging the domain
565
+ * is the same collision one namespace over. Never keyed on the rendered label. The label is built by joining `key=value` pairs, and
566
+ * discovery splits an annotation prop at its FIRST `=` only, so a value may legally contain both
567
+ * a space and an `=`: `props = ["a=b c=d"]` is one prop, and renders identically to the two props
568
+ * `a=b` and `c=d`. Keying on that string would silently drop one of two distinct absences — the
569
+ * same data loss this diagnostic exists to prevent, one level subtler.
570
+ */
532
571
  const statedAbsentIds = new Map();
572
+ /** Capture subjects whose absence is stated — a variant is named by subject, not by component. */
573
+ const referencelessSubjects = new Set();
574
+ /** Component ids whose absence is stated, for the componentId-keyed ambiguity filter below. */
575
+ const statedAbsentComponentIds = new Set();
533
576
  for (const preview of previews) {
534
577
  const catalog = preview.catalog;
535
- if (!catalog || catalog.role !== "COMPONENT" || catalog.reference) continue;
578
+ if (!catalog || catalog.reference) continue;
536
579
  if (isVariantCapture(preview)) continue;
580
+ // A `@CatalogVariant` can now state its own kit correspondence, so its absence is reported
581
+ // like a component's. Scanning components alone meant folding a render under a parent silently
582
+ // dropped its stated absence from this accounting — a catalog could lose an audit signal by
583
+ // restructuring, which is exactly what `statedAbsent` exists to prevent. `--strict` counts a
584
+ // variant's stated absence the same as a component's: someone looked, and wrote down what they
585
+ // found, wherever the render sits.
586
+ //
587
+ // Reported under the variant's own label, not its parent's id (`componentId` is the PARENT for
588
+ // a VARIANT), or a folded variant's reason would be reported against a parent that may have a
589
+ // perfectly good reference of its own. The label is for reading; the KEY is the capture
590
+ // subject, which cannot collide — see the map's own note above.
591
+ if (catalog.role === "VARIANT") {
592
+ if (!catalog.noReference) continue; // silence under a parent is the parent's business
593
+ const subject = captureIdentity(preview).subject;
594
+ statedAbsentIds.set(`subject:${subject}`, {
595
+ label: variantAbsenceId(preview),
596
+ reason: catalog.noReference,
597
+ });
598
+ // The ambiguity filter below matches on componentId, which for a VARIANT is the PARENT's --
599
+ // so a variant's own stated absence could not suppress its own ambiguous-mode record, and a
600
+ // parent carrying a good reference left it unsuppressed. `--strict --allow-stated-absence`
601
+ // then failed on precisely the case that flag exists to accept. A variant render has its own
602
+ // capture subject, so record that instead of trying to name it by component.
603
+ referencelessSubjects.add(subject);
604
+ continue;
605
+ }
606
+ if (catalog.role !== "COMPONENT") continue;
537
607
  const id = catalog.componentId;
538
- if (catalog.noReference) statedAbsentIds.set(id, catalog.noReference);
539
- else if (!statedAbsentIds.has(id)) unmappedIds.set(id, true);
608
+ if (catalog.noReference) {
609
+ statedAbsentIds.set(`component:${id}`, { label: id, reason: catalog.noReference });
610
+ statedAbsentComponentIds.add(id);
611
+ } else if (!statedAbsentIds.has(id)) unmappedIds.set(id, true);
540
612
  }
541
613
  /** Components carrying neither a reference nor a stated reason for its absence. */
542
- const unmapped = [...unmappedIds.keys()].filter((id) => !statedAbsentIds.has(id));
614
+ const unmapped = [...unmappedIds.keys()].filter((id) => !statedAbsentComponentIds.has(id));
543
615
  /**
544
616
  * Components whose reference is absent for a STATED reason. Reported apart from `unmapped`
545
617
  * because they are the opposite situation: someone looked, and what they found is that the kit
546
618
  * has nothing live to point at. Rolling the two together is what made a retired pattern read as
547
619
  * neglect.
548
620
  */
549
- const statedAbsent = [...statedAbsentIds].map(([componentId, reason]) => ({
550
- componentId,
621
+ const statedAbsent = [...statedAbsentIds.values()].map(({ label, reason }) => ({
622
+ componentId: label,
551
623
  reason,
552
624
  }));
553
- /** Every component that reaches no reference, however its absence was spelled. */
554
- const referencelessIds = new Set([...unmapped, ...statedAbsentIds.keys()]);
625
+ /**
626
+ * Every COMPONENT that reaches no reference, however its absence was spelled — the set the
627
+ * ambiguity filter tests `componentIds` against. A variant's key is a capture subject rather than
628
+ * a component id, so it is deliberately absent here and suppressed through
629
+ * [referencelessSubjects] instead; putting subjects in this set would only add entries no
630
+ * componentId can ever equal.
631
+ */
632
+ const referencelessIds = new Set([...unmapped, ...statedAbsentComponentIds]);
555
633
 
556
634
  for (const preview of previews) {
557
635
  const catalog = preview.catalog;
@@ -614,7 +692,9 @@ export function projectDesignMap(previews, opts = {}) {
614
692
  // absence already reported above, and under --strict it would be a second, unfixable failure
615
693
  // for the same component.
616
694
  ambiguousMode: selection.ambiguous.filter(
617
- (a) => !a.componentIds.length || a.componentIds.some((id) => !referencelessIds.has(id)),
695
+ (a) =>
696
+ !referencelessSubjects.has(a.subject) &&
697
+ (!a.componentIds.length || a.componentIds.some((id) => !referencelessIds.has(id))),
618
698
  ),
619
699
  variantRenders: declarations.reduce((n, d) => n + d.renders.length, 0),
620
700
  withSet: components.filter((c) => c.refSet).length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yschimke/compose-design-map",
3
- "version": "1.44.0",
3
+ "version": "1.45.0",
4
4
  "description": "Project a compose-preview discovery manifest into design-parity's design-map.json, plus a sidecar of unresolved variant declarations. Dependency-free.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",