@platforma-sdk/model 1.80.2 → 1.80.8

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 (34) hide show
  1. package/dist/columns/column.cjs.map +1 -1
  2. package/dist/columns/column.js.map +1 -1
  3. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs +1 -1
  4. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs.map +1 -1
  5. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js +1 -1
  6. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js.map +1 -1
  7. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs +4 -4
  8. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs.map +1 -1
  9. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts +1 -1
  10. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts.map +1 -1
  11. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js +5 -5
  12. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js.map +1 -1
  13. package/dist/labels/derive_distinct_labels.cjs +80 -57
  14. package/dist/labels/derive_distinct_labels.cjs.map +1 -1
  15. package/dist/labels/derive_distinct_labels.d.ts +16 -9
  16. package/dist/labels/derive_distinct_labels.d.ts.map +1 -1
  17. package/dist/labels/derive_distinct_labels.js +80 -57
  18. package/dist/labels/derive_distinct_labels.js.map +1 -1
  19. package/dist/labels/linked_column_postfix.cjs +189 -0
  20. package/dist/labels/linked_column_postfix.cjs.map +1 -0
  21. package/dist/labels/linked_column_postfix.d.ts +27 -0
  22. package/dist/labels/linked_column_postfix.d.ts.map +1 -0
  23. package/dist/labels/linked_column_postfix.js +188 -0
  24. package/dist/labels/linked_column_postfix.js.map +1 -0
  25. package/dist/package.cjs +1 -1
  26. package/dist/package.js +1 -1
  27. package/package.json +10 -10
  28. package/src/columns/column.ts +6 -6
  29. package/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts +110 -2
  30. package/src/components/PlDataTable/createPlDataTable/discoverColumns.ts +14 -18
  31. package/src/labels/derive_distinct_labels.test.ts +117 -72
  32. package/src/labels/derive_distinct_labels.ts +141 -112
  33. package/src/labels/linked_column_postfix.test.ts +112 -0
  34. package/src/labels/linked_column_postfix.ts +312 -0
@@ -1,3 +1,4 @@
1
+ import { derivePostfixes } from "./linked_column_postfix.js";
1
2
  import { Annotation, parseJson, readAnnotation } from "@milaboratories/pl-model-common";
2
3
  import { throwError } from "@milaboratories/helpers";
3
4
  import { isFunction, isNil as isNil$1 } from "es-toolkit";
@@ -5,27 +6,42 @@ import { isFunction, isNil as isNil$1 } from "es-toolkit";
5
6
  const DISTANCE_PENALTY = .001;
6
7
  const LABEL_TYPE = "__LABEL__";
7
8
  const LABEL_TYPE_FULL = "__LABEL__@1";
8
- const LINKER_TYPE = "__LINKER__";
9
- const LINKER_TYPE_FULL = "__LINKER__@1";
10
9
  const HIT_QUAL_TYPE = "__HIT_QUAL__";
11
10
  const ANCHOR_QUAL_TYPE_PREFIX = "__ANCHOR_QUAL__:";
12
11
  function isAnchorQualType(t) {
13
12
  return t.startsWith(ANCHOR_QUAL_TYPE_PREFIX);
14
13
  }
15
14
  function isSyntheticType(t) {
16
- return t === LINKER_TYPE || t === HIT_QUAL_TYPE || isAnchorQualType(t);
15
+ return t === HIT_QUAL_TYPE || isAnchorQualType(t);
17
16
  }
17
+ /**
18
+ * Distinct labels for a set of columns. Two phases:
19
+ * 1. {@link deriveStems} — treats each column as a single entity (native label + trace + hit/anchor
20
+ * qualifications) and produces the minimal distinguishing "stem".
21
+ * 2. {@link derivePostfixes} — for columns still colliding on their stem, appends a "via …" postfix
22
+ * describing the difference between their linker sources (root axis, then linker chain).
23
+ */
18
24
  function deriveDistinctLabels(values, options = {}) {
25
+ const stems = deriveStems(values, options);
26
+ return derivePostfixes(values.map((v, i) => {
27
+ const { spec, linkerPath } = extractEntryParts(v);
28
+ return {
29
+ stem: stems[i],
30
+ hit: spec,
31
+ linkers: (linkerPath ?? []).map((s) => s.spec)
32
+ };
33
+ }), options.formatters?.linker);
34
+ }
35
+ /** Phase 1: minimal per-column stem — native label + trace + hit/anchor qualifications, no linkers. */
36
+ function deriveStems(values, options) {
19
37
  const forceTraceElements = options.forceTraceElements !== void 0 && options.forceTraceElements.length > 0 ? new Set(options.forceTraceElements) : void 0;
20
38
  const separator = options.separator ?? " / ";
21
39
  const records = values.map((v, i) => enrichRecord(v, i, options));
22
40
  const stats = collectTypeStats(records);
23
41
  const hasAnySynthetic = records.some((r) => r.fullTrace.some((ft) => isSyntheticType(ft.type)));
24
42
  const labelForced = (options.includeNativeLabel === true || hasAnySynthetic) && stats.countByType.has(LABEL_TYPE_FULL);
25
- const linkerForced = stats.countByType.get(LINKER_TYPE_FULL) === values.length;
26
43
  const forcedSet = /* @__PURE__ */ new Set();
27
44
  if (labelForced) forcedSet.add(LABEL_TYPE_FULL);
28
- if (linkerForced) forcedSet.add(LINKER_TYPE_FULL);
29
45
  const { mainTypes, secondaryTypes } = classifyTypes(stats, values.length);
30
46
  const build = (typeSet, force) => buildLabels(records, typeSet, forceTraceElements, separator, force);
31
47
  if (mainTypes.length === 0) {
@@ -41,7 +57,7 @@ function deriveDistinctLabels(values, options = {}) {
41
57
  const candidateResult = build(currentSet, false);
42
58
  if (candidateResult !== void 0 && countUniqueLabels(candidateResult) === values.length) {
43
59
  const minimized = minimizeTypeSet(currentSet, records, stats, forceTraceElements, forcedSet, separator);
44
- return dropRedundantLinkerSuffix(records, minimized, forceTraceElements, forcedSet, separator, build(minimized, false) ?? throwError("Failed to derive unique labels"));
60
+ return repairBareByPresence(build(minimized, false) ?? throwError("Failed to derive unique labels"), minimized, records, stats, forcedSet, forceTraceElements, separator);
45
61
  }
46
62
  additionalType++;
47
63
  if (additionalType >= mainTypes.length) {
@@ -54,7 +70,7 @@ function deriveDistinctLabels(values, options = {}) {
54
70
  ...mainTypes,
55
71
  ...secondaryTypes
56
72
  ]), records, stats, forceTraceElements, forcedSet, separator);
57
- return dropRedundantLinkerSuffix(records, minimized, forceTraceElements, forcedSet, separator, build(minimized, true) ?? throwError("Failed to derive unique labels"));
73
+ return repairBareByPresence(build(minimized, true) ?? throwError("Failed to derive unique labels"), minimized, records, stats, forcedSet, forceTraceElements, separator);
58
74
  }
59
75
  function extractEntryParts(entry) {
60
76
  if (!("spec" in entry && typeof entry.spec === "object")) return {
@@ -80,13 +96,6 @@ function formatQualification(q) {
80
96
  function formatQualifications(qs) {
81
97
  return qs.map(formatQualification).join("; ");
82
98
  }
83
- function computeStepLabel(step, stepIndex, formatters) {
84
- const base = (readAnnotation(step.spec, Annotation.LinkLabel) ?? readAnnotation(step.spec, Annotation.Label))?.trim();
85
- if (isNil$1(base) || base.length === 0) return void 0;
86
- if (step.qualifications === void 0 || step.qualifications.length === 0) return base;
87
- const qualText = isFunction(formatters?.linkerStepQualification) ? formatters.linkerStepQualification(step.qualifications, stepIndex, step.spec) : `[${formatQualifications(step.qualifications)}]`;
88
- return isNil$1(qualText) ? base : `${base} ${qualText}`;
89
- }
90
99
  function buildFullTrace(trace) {
91
100
  const result = [];
92
101
  const occurrences = /* @__PURE__ */ new Map();
@@ -104,7 +113,7 @@ function buildFullTrace(trace) {
104
113
  return result;
105
114
  }
106
115
  function enrichRecord(value, index, options) {
107
- const { spec, extraTrace, linkerPath, qualifications } = extractEntryParts(value);
116
+ const { spec, extraTrace, qualifications } = extractEntryParts(value);
108
117
  const formatters = options.formatters;
109
118
  const rawLabel = readAnnotation(spec, Annotation.Label);
110
119
  const traceStr = readAnnotation(spec, Annotation.Trace);
@@ -128,17 +137,6 @@ function enrichRecord(value, index, options) {
128
137
  else trace.splice(0, 0, labelEntry);
129
138
  }
130
139
  }
131
- if (linkerPath !== void 0 && linkerPath.length > 0) {
132
- const stepLabels = linkerPath.map((step, i) => computeStepLabel(step, i, formatters)).filter((s) => !isNil$1(s));
133
- if (stepLabels.length > 0) {
134
- const linkerText = isFunction(formatters?.linker) ? formatters.linker(stepLabels, spec, index) : `via ${stepLabels.join(" > ")}`;
135
- if (!isNil$1(linkerText)) trace.push({
136
- type: LINKER_TYPE,
137
- label: linkerText,
138
- importance: -10
139
- });
140
- }
141
- }
142
140
  if (qualifications !== void 0 && qualifications.forQueries !== void 0) {
143
141
  for (const [anchorId, qs] of Object.entries(qualifications.forQueries)) {
144
142
  if (qs.length === 0) continue;
@@ -190,21 +188,18 @@ function classifyTypes(stats, totalRecords) {
190
188
  function renderRecordLabel(record, includedTypes, forceTraceElements, separator) {
191
189
  const traceParts = [];
192
190
  const anchorParts = [];
193
- let linkerLabel;
194
191
  let hitLabel;
195
192
  for (const ft of record.fullTrace) {
196
193
  if (!(includedTypes.has(ft.fullType) || forceTraceElements?.has(ft.type))) continue;
197
- if (ft.type === LINKER_TYPE) linkerLabel = ft.label;
198
- else if (ft.type === HIT_QUAL_TYPE) hitLabel = ft.label;
194
+ if (ft.type === HIT_QUAL_TYPE) hitLabel = ft.label;
199
195
  else if (isAnchorQualType(ft.type)) anchorParts.push(ft.label);
200
196
  else traceParts.push(ft.label);
201
197
  }
202
- if (traceParts.length === 0 && anchorParts.length === 0 && linkerLabel === void 0 && hitLabel === void 0) return void 0;
198
+ if (traceParts.length === 0 && anchorParts.length === 0 && hitLabel === void 0) return void 0;
203
199
  let label = traceParts.join(separator);
204
200
  const append = (part) => {
205
201
  label = label.length === 0 ? part : `${label} ${part}`;
206
202
  };
207
- if (linkerLabel !== void 0) append(linkerLabel);
208
203
  for (const a of anchorParts) append(a);
209
204
  if (hitLabel !== void 0) append(hitLabel);
210
205
  return label;
@@ -222,32 +217,6 @@ function buildLabels(records, includedTypes, forceTraceElements, separator, forc
222
217
  }
223
218
  return result;
224
219
  }
225
- /**
226
- * Drop the "via …" linker suffix from records whose label is already unique without it.
227
- *
228
- * Global minimization may include `LINKER_TYPE_FULL` solely to resolve a collision between a
229
- * subset of records — but `buildLabels` then renders the suffix on every record that carries a
230
- * linker trace entry, including ones whose stem is already unique. We strip the suffix where it
231
- * isn't load-bearing while keeping the symmetric rendering required by `linkerForced` /
232
- * `forceTraceElements`.
233
- *
234
- * Rule: a record's linker suffix is redundant iff its stem (label rendered without LINKER) does
235
- * not appear anywhere else in the set.
236
- */
237
- function dropRedundantLinkerSuffix(records, globalTypeSet, forceTraceElements, forcedSet, separator, labels) {
238
- if (!globalTypeSet.has(LINKER_TYPE_FULL)) return labels;
239
- if (forcedSet.has(LINKER_TYPE_FULL) || forceTraceElements?.has(LINKER_TYPE)) return labels;
240
- const setWithoutLinker = new Set(globalTypeSet);
241
- setWithoutLinker.delete(LINKER_TYPE_FULL);
242
- const stems = records.map((r) => renderRecordLabel(r, setWithoutLinker, forceTraceElements, separator));
243
- const stemOccurrences = /* @__PURE__ */ new Map();
244
- for (const s of stems) if (s !== void 0) stemOccurrences.set(s, (stemOccurrences.get(s) ?? 0) + 1);
245
- return labels.map((label, i) => {
246
- const stem = stems[i];
247
- if (stem === void 0) return label;
248
- return stemOccurrences.get(stem) === 1 ? stem : label;
249
- });
250
- }
251
220
  function countUniqueLabels(result) {
252
221
  if (result === void 0) return 0;
253
222
  return new Set(result).size;
@@ -266,6 +235,60 @@ function minimizeTypeSet(typeSet, records, stats, forceTraceElements, forcedSet,
266
235
  }
267
236
  return result;
268
237
  }
238
+ const ABSENT_VALUE = "\0absent";
239
+ /** Whether `fullType`'s rendered value differs across the group (absence counts as a value). */
240
+ function typeDistinguishes(records, group, fullType) {
241
+ const values = /* @__PURE__ */ new Set();
242
+ for (const i of group) {
243
+ const ft = records[i].fullTrace.find((e) => e.fullType === fullType);
244
+ values.add(ft?.label ?? ABSENT_VALUE);
245
+ if (values.size > 1) return true;
246
+ }
247
+ return false;
248
+ }
249
+ /** The highest-importance trace type this row HAS (outside `typeSet`) that sets it apart from its
250
+ * group peers. `undefined` when the row carries nothing distinguishing of its own. */
251
+ function bestDistinguishingType(records, group, row, typeSet, forcedSet, forceTraceElements, stats) {
252
+ return records[row].fullTrace.reduce((best, ft) => {
253
+ if (typeSet.has(ft.fullType) || forcedSet.has(ft.fullType) || forceTraceElements?.has(ft.type)) return best;
254
+ if (!typeDistinguishes(records, group, ft.fullType)) return best;
255
+ const imp = stats.importances.get(ft.fullType) ?? 0;
256
+ return best === void 0 || imp > best.imp ? {
257
+ type: ft.fullType,
258
+ imp
259
+ } : best;
260
+ }, void 0)?.type;
261
+ }
262
+ /**
263
+ * Un-bare columns distinguished only "by absence". After minimization a column can end up with a
264
+ * bare trace zone (only the forced native label) while a peer sharing that same base renders extra
265
+ * tokens — the column reads as unique purely because it LACKS what the peer has. For each such bare
266
+ * column this re-renders JUST that column's label with the highest-importance trace type it actually
267
+ * carries that tells it apart from its peers, so every colliding column is distinguished by a token
268
+ * it HAS rather than by omission.
269
+ *
270
+ * Patches individual labels rather than the shared type set on purpose: the set is global, so adding
271
+ * a type there would also decorate unrelated columns in other groups that happen to carry it. Only
272
+ * bare columns' labels grow (a superset of their previous value), so uniqueness is preserved. Groups
273
+ * are keyed by the base = the label rendered from the forced types alone.
274
+ */
275
+ function repairBareByPresence(labels, minimized, records, stats, forcedSet, forceTraceElements, separator) {
276
+ const base = records.map((r) => renderRecordLabel(r, forcedSet, forceTraceElements, separator));
277
+ const isBare = records.map((r, i) => renderRecordLabel(r, minimized, forceTraceElements, separator) === base[i]);
278
+ const groups = records.reduce((acc, _, i) => acc.set(base[i] ?? "", [...acc.get(base[i] ?? "") ?? [], i]), /* @__PURE__ */ new Map());
279
+ const patched = [...labels];
280
+ for (const group of groups.values()) {
281
+ if (!group.some((i) => !isBare[i])) continue;
282
+ for (const i of group) {
283
+ if (!isBare[i]) continue;
284
+ const chosen = bestDistinguishingType(records, group, i, minimized, forcedSet, forceTraceElements, stats);
285
+ if (chosen === void 0) continue;
286
+ const withToken = /* @__PURE__ */ new Set([...minimized, chosen]);
287
+ patched[i] = renderRecordLabel(records[i], withToken, forceTraceElements, separator) ?? patched[i];
288
+ }
289
+ }
290
+ return patched;
291
+ }
269
292
  //#endregion
270
293
  export { deriveDistinctLabels };
271
294
 
@@ -1 +1 @@
1
- {"version":3,"file":"derive_distinct_labels.js","names":["isNil"],"sources":["../../src/labels/derive_distinct_labels.ts"],"sourcesContent":["import {\n Annotation,\n parseJson,\n readAnnotation,\n type AxisQualification,\n type MatchQualifications,\n type PObjectId,\n type PObjectSpec,\n type StringifiedJson,\n type Trace,\n} from \"@milaboratories/pl-model-common\";\nimport { throwError } from \"@milaboratories/helpers\";\nimport { isFunction, isNil } from \"es-toolkit\";\n\nexport type { Trace, TraceEntry } from \"@milaboratories/pl-model-common\";\n\nconst DISTANCE_PENALTY = 0.001;\nconst LABEL_TYPE = \"__LABEL__\";\nconst LABEL_TYPE_FULL = \"__LABEL__@1\";\nconst LINKER_TYPE = \"__LINKER__\";\nconst LINKER_TYPE_FULL = \"__LINKER__@1\";\nconst HIT_QUAL_TYPE = \"__HIT_QUAL__\";\nconst ANCHOR_QUAL_TYPE_PREFIX = \"__ANCHOR_QUAL__:\";\n\nfunction isAnchorQualType(t: string): boolean {\n return t.startsWith(ANCHOR_QUAL_TYPE_PREFIX);\n}\n\nfunction isSyntheticType(t: string): boolean {\n return t === LINKER_TYPE || t === HIT_QUAL_TYPE || isAnchorQualType(t);\n}\n\n/** SDK-internal trace shape — adds fields used by this algorithm only, not part of the on-disk contract. */\ntype ExtendedTraceEntry = Trace[number] & {\n importance?: number;\n position?: \"prefix\" | \"suffix\";\n};\n\nexport type LinkerStep = {\n spec: PObjectSpec;\n qualifications?: AxisQualification[];\n};\n\nexport type Entry =\n | PObjectSpec\n | {\n spec: PObjectSpec;\n /** Extra trace entries merged with the base trace from annotations. */\n extraTrace?: ExtendedTraceEntry[];\n /** Linker steps traversed to discover this column; rendered as \"via …\" only when needed for uniqueness. */\n linkerPath?: LinkerStep[];\n /** Axis qualifications applied to the hit column / already-bound anchors; rendered as \"[…]\" suffixes. */\n qualifications?: MatchQualifications;\n };\n\n/**\n * Per-zone formatters. Each one receives raw inputs and returns the rendered text for that zone,\n * or `undefined` to suppress the zone entirely (no synthetic injection → no minimization, no render).\n */\nexport type DeriveLabelsFormatters = {\n /** Native column label. Default: identity. `undefined` → label entry not added (treated as if spec had no label). */\n native?: (label: string, spec: PObjectSpec, index: number) => string | undefined;\n /** Linker zone (whole \"via …\" piece). Receives step labels with step-quals already inlined.\n * Default: `via ${steps.join(\" > \")}`. */\n linker?: (linkerLabels: string[], spec: PObjectSpec, index: number) => string | undefined;\n /** Per-step linker qualifications inlined into the step base label.\n * Default: `[${formatQualifications(qs)}]`. `undefined` → step rendered without quals. */\n linkerStepQualification?: (\n qualifications: AxisQualification[],\n stepIndex: number,\n stepSpec: PObjectSpec,\n ) => string | undefined;\n /** Hit-axis qualifications block. Default: `[${formatQualifications(qs)}]`. */\n hitQualification?: (\n qualifications: AxisQualification[],\n spec: PObjectSpec,\n index: number,\n ) => string | undefined;\n /** Per-anchor qualifications block. Default: `[${anchorId}: ${formatQualifications(qs)}]`. */\n anchorQualification?: (\n anchorId: PObjectId,\n qualifications: AxisQualification[],\n spec: PObjectSpec,\n index: number,\n ) => string | undefined;\n};\n\nexport type DeriveLabelsOptions = {\n /** Separator to use between label parts (\" / \" by default). */\n separator?: string;\n /** If true, native label is appended at the end of the trace zone. By default it is prepended (label is the most important name). */\n addLabelAsSuffix?: boolean;\n /** Force inclusion of native column label even when not needed for uniqueness. */\n includeNativeLabel?: boolean;\n /** Trace types that must be included in the label. */\n forceTraceElements?: string[];\n /** Per-zone custom formatters. Returning `undefined` from any formatter suppresses the corresponding zone. */\n formatters?: DeriveLabelsFormatters;\n};\n\nexport function deriveDistinctLabels(values: Entry[], options: DeriveLabelsOptions = {}): string[] {\n const forceTraceElements =\n options.forceTraceElements !== undefined && options.forceTraceElements.length > 0\n ? new Set(options.forceTraceElements)\n : undefined;\n const separator = options.separator ?? \" / \";\n\n const records = values.map((v, i) => enrichRecord(v, i, options));\n const stats = collectTypeStats(records);\n\n const hasAnySynthetic = records.some((r) => r.fullTrace.some((ft) => isSyntheticType(ft.type)));\n const labelForced =\n (options.includeNativeLabel === true || hasAnySynthetic) &&\n stats.countByType.has(LABEL_TYPE_FULL);\n // Tied to labeled-step presence, not path presence: entries with a non-empty linkerPath\n // but no labeled steps contribute no LINKER_TYPE trace entry, so they do not count here.\n const linkerForced = stats.countByType.get(LINKER_TYPE_FULL) === values.length;\n\n const forcedSet = new Set<string>();\n if (labelForced) forcedSet.add(LABEL_TYPE_FULL);\n if (linkerForced) forcedSet.add(LINKER_TYPE_FULL);\n\n const { mainTypes, secondaryTypes } = classifyTypes(stats, values.length);\n\n const build = (typeSet: Set<string>, force: boolean) =>\n buildLabels(records, typeSet, forceTraceElements, separator, force);\n\n if (mainTypes.length === 0) {\n if (secondaryTypes.length !== 0)\n throw new Error(\"Non-empty secondary types list while main types list is empty.\");\n\n return (\n build(new Set([LABEL_TYPE_FULL]), true) ??\n throwError(\"Failed to derive labels using native column labels\")\n );\n }\n\n let includedCount = 0;\n let additionalType = -1;\n while (includedCount < mainTypes.length) {\n const currentSet = new Set<string>(forcedSet);\n for (let i = 0; i < includedCount; ++i) currentSet.add(mainTypes[i]);\n if (additionalType >= 0) currentSet.add(mainTypes[additionalType]);\n\n const candidateResult = build(currentSet, false);\n if (candidateResult !== undefined && countUniqueLabels(candidateResult) === values.length) {\n const minimized = minimizeTypeSet(\n currentSet,\n records,\n stats,\n forceTraceElements,\n forcedSet,\n separator,\n );\n const minimizedLabels =\n build(minimized, false) ?? throwError(\"Failed to derive unique labels\");\n return dropRedundantLinkerSuffix(\n records,\n minimized,\n forceTraceElements,\n forcedSet,\n separator,\n minimizedLabels,\n );\n }\n\n additionalType++;\n if (additionalType >= mainTypes.length) {\n includedCount++;\n additionalType = includedCount;\n }\n }\n\n const fallbackSet = new Set([...forcedSet, ...mainTypes, ...secondaryTypes]);\n const minimized = minimizeTypeSet(\n fallbackSet,\n records,\n stats,\n forceTraceElements,\n forcedSet,\n separator,\n );\n const minimizedLabels = build(minimized, true) ?? throwError(\"Failed to derive unique labels\");\n return dropRedundantLinkerSuffix(\n records,\n minimized,\n forceTraceElements,\n forcedSet,\n separator,\n minimizedLabels,\n );\n}\n\n// --- Pure helpers ---\ntype FullTraceEntry = ExtendedTraceEntry & { fullType: string; occurrenceIndex: number };\n\ntype EnrichedRecord = {\n fullTrace: FullTraceEntry[];\n};\n\nfunction extractEntryParts(entry: Entry): {\n spec: PObjectSpec;\n extraTrace: ExtendedTraceEntry[] | undefined;\n linkerPath: LinkerStep[] | undefined;\n qualifications: MatchQualifications | undefined;\n} {\n const isEnriched = \"spec\" in entry && typeof entry.spec === \"object\";\n if (!isEnriched) {\n return {\n spec: entry as PObjectSpec,\n extraTrace: undefined,\n linkerPath: undefined,\n qualifications: undefined,\n };\n }\n return {\n spec: entry.spec,\n extraTrace: entry.extraTrace,\n linkerPath: entry.linkerPath,\n qualifications: entry.qualifications,\n };\n}\n\nfunction formatQualification(q: AxisQualification): string {\n const ctx = q.contextDomain ?? {};\n const keys = Object.keys(ctx);\n if (keys.length === 0) return q.axis.name;\n const pairs = keys.map((k) => `${k}=${ctx[k]}`).join(\", \");\n return Object.prototype.hasOwnProperty.call(ctx, q.axis.name) ? pairs : `${q.axis.name} ${pairs}`;\n}\n\nfunction formatQualifications(qs: AxisQualification[]): string {\n return qs.map(formatQualification).join(\"; \");\n}\n\nfunction computeStepLabel(\n step: LinkerStep,\n stepIndex: number,\n formatters: DeriveLabelsFormatters | undefined,\n): string | undefined {\n const base = (\n readAnnotation(step.spec, Annotation.LinkLabel) ?? readAnnotation(step.spec, Annotation.Label)\n )?.trim();\n if (isNil(base) || base.length === 0) return undefined;\n if (step.qualifications === undefined || step.qualifications.length === 0) return base;\n const qualText = isFunction(formatters?.linkerStepQualification)\n ? formatters.linkerStepQualification(step.qualifications, stepIndex, step.spec)\n : `[${formatQualifications(step.qualifications)}]`;\n return isNil(qualText) ? base : `${base} ${qualText}`;\n}\n\nfunction buildFullTrace(trace: ExtendedTraceEntry[]): FullTraceEntry[] {\n const result: FullTraceEntry[] = [];\n const occurrences = new Map<string, number>();\n\n for (let i = trace.length - 1; i >= 0; --i) {\n const entry = trace[i];\n const occurrenceIndex = (occurrences.get(entry.type) ?? 0) + 1;\n occurrences.set(entry.type, occurrenceIndex);\n result.push({\n ...entry,\n fullType: `${entry.type}@${occurrenceIndex}`,\n occurrenceIndex,\n });\n }\n\n result.reverse();\n return result;\n}\n\nfunction enrichRecord(value: Entry, index: number, options: DeriveLabelsOptions): EnrichedRecord {\n const { spec, extraTrace, linkerPath, qualifications } = extractEntryParts(value);\n const formatters = options.formatters;\n\n const rawLabel = readAnnotation(spec, Annotation.Label);\n const traceStr = readAnnotation(spec, Annotation.Trace);\n const baseTrace = traceStr\n ? (parseJson(traceStr as StringifiedJson<ExtendedTraceEntry[]>) ?? [])\n : [];\n const prefixExtra = extraTrace?.filter((e) => e.position === \"prefix\") ?? [];\n const suffixExtra = extraTrace?.filter((e) => e.position !== \"prefix\") ?? [];\n const trace: ExtendedTraceEntry[] = [...prefixExtra, ...baseTrace, ...suffixExtra];\n\n if (!isNil(rawLabel)) {\n const label = isFunction(formatters?.native)\n ? formatters.native(rawLabel, spec, index)\n : rawLabel;\n if (!isNil(label)) {\n const labelEntry = { label, type: LABEL_TYPE, importance: -2 };\n if (options.addLabelAsSuffix === true) trace.push(labelEntry);\n else trace.splice(0, 0, labelEntry);\n }\n }\n\n if (linkerPath !== undefined && linkerPath.length > 0) {\n const stepLabels = linkerPath\n .map((step, i) => computeStepLabel(step, i, formatters))\n .filter((s): s is string => !isNil(s));\n if (stepLabels.length > 0) {\n const linkerText = isFunction(formatters?.linker)\n ? formatters.linker(stepLabels, spec, index)\n : `via ${stepLabels.join(\" > \")}`;\n if (!isNil(linkerText)) {\n trace.push({ type: LINKER_TYPE, label: linkerText, importance: -10 });\n }\n }\n }\n\n if (qualifications !== undefined && qualifications.forQueries !== undefined) {\n for (const [anchorId, qs] of Object.entries(qualifications.forQueries)) {\n if (qs.length === 0) continue;\n const anchorText = isFunction(formatters?.anchorQualification)\n ? formatters.anchorQualification(anchorId as PObjectId, qs, spec, index)\n : `[${anchorId}: ${formatQualifications(qs)}]`;\n if (isNil(anchorText)) continue;\n trace.push({\n type: `${ANCHOR_QUAL_TYPE_PREFIX}${anchorId}`,\n label: anchorText,\n importance: -11,\n });\n }\n if (qualifications.forHit !== undefined && qualifications.forHit.length > 0) {\n const hitText = isFunction(formatters?.hitQualification)\n ? formatters.hitQualification(qualifications.forHit, spec, index)\n : `[${formatQualifications(qualifications.forHit)}]`;\n if (!isNil(hitText)) {\n trace.push({ type: HIT_QUAL_TYPE, label: hitText, importance: -12 });\n }\n }\n }\n\n return { fullTrace: buildFullTrace(trace) };\n}\n\ntype TypeStats = {\n importances: Map<string, number>;\n countByType: Map<string, number>;\n};\n\nfunction collectTypeStats(records: EnrichedRecord[]): TypeStats {\n const importances = new Map<string, number>();\n const countByType = new Map<string, number>();\n\n for (const record of records) {\n for (let i = 0; i < record.fullTrace.length; i++) {\n const { fullType, importance: rawImportance } = record.fullTrace[i];\n const importance = rawImportance ?? 0;\n const distance = (record.fullTrace.length - i) * DISTANCE_PENALTY;\n\n countByType.set(fullType, (countByType.get(fullType) ?? 0) + 1);\n importances.set(\n fullType,\n Math.max(importances.get(fullType) ?? Number.NEGATIVE_INFINITY, importance - distance),\n );\n }\n }\n\n return { importances, countByType };\n}\n\nfunction classifyTypes(\n stats: TypeStats,\n totalRecords: number,\n): { mainTypes: string[]; secondaryTypes: string[] } {\n const sorted = [...stats.importances].sort(([, i1], [, i2]) => i2 - i1);\n\n const mainTypes: string[] = [];\n const secondaryTypes: string[] = [];\n\n for (const [typeName] of sorted) {\n if (typeName.endsWith(\"@1\") || stats.countByType.get(typeName) === totalRecords)\n mainTypes.push(typeName);\n else secondaryTypes.push(typeName);\n }\n\n return { mainTypes, secondaryTypes };\n}\n\nfunction renderRecordLabel(\n record: EnrichedRecord,\n includedTypes: Set<string>,\n forceTraceElements: Set<string> | undefined,\n separator: string,\n): string | undefined {\n const traceParts: string[] = [];\n const anchorParts: string[] = [];\n let linkerLabel: string | undefined;\n let hitLabel: string | undefined;\n\n for (const ft of record.fullTrace) {\n if (!(includedTypes.has(ft.fullType) || forceTraceElements?.has(ft.type))) continue;\n if (ft.type === LINKER_TYPE) linkerLabel = ft.label;\n else if (ft.type === HIT_QUAL_TYPE) hitLabel = ft.label;\n else if (isAnchorQualType(ft.type)) anchorParts.push(ft.label);\n else traceParts.push(ft.label);\n }\n\n const isEmpty =\n traceParts.length === 0 &&\n anchorParts.length === 0 &&\n linkerLabel === undefined &&\n hitLabel === undefined;\n\n if (isEmpty) return undefined;\n\n let label = traceParts.join(separator);\n const append = (part: string) => {\n label = label.length === 0 ? part : `${label} ${part}`;\n };\n if (linkerLabel !== undefined) append(linkerLabel);\n for (const a of anchorParts) append(a);\n if (hitLabel !== undefined) append(hitLabel);\n\n return label;\n}\n\nfunction buildLabels(\n records: EnrichedRecord[],\n includedTypes: Set<string>,\n forceTraceElements: Set<string> | undefined,\n separator: string,\n force: boolean,\n): string[] | undefined {\n const result: string[] = [];\n\n for (const r of records) {\n const rendered = renderRecordLabel(r, includedTypes, forceTraceElements, separator);\n if (rendered === undefined) {\n if (!force) return undefined;\n result.push(\"Unlabeled\");\n continue;\n }\n result.push(rendered);\n }\n\n return result;\n}\n\n/**\n * Drop the \"via …\" linker suffix from records whose label is already unique without it.\n *\n * Global minimization may include `LINKER_TYPE_FULL` solely to resolve a collision between a\n * subset of records — but `buildLabels` then renders the suffix on every record that carries a\n * linker trace entry, including ones whose stem is already unique. We strip the suffix where it\n * isn't load-bearing while keeping the symmetric rendering required by `linkerForced` /\n * `forceTraceElements`.\n *\n * Rule: a record's linker suffix is redundant iff its stem (label rendered without LINKER) does\n * not appear anywhere else in the set.\n */\nfunction dropRedundantLinkerSuffix(\n records: EnrichedRecord[],\n globalTypeSet: Set<string>,\n forceTraceElements: Set<string> | undefined,\n forcedSet: Set<string>,\n separator: string,\n labels: string[],\n): string[] {\n if (!globalTypeSet.has(LINKER_TYPE_FULL)) return labels;\n if (forcedSet.has(LINKER_TYPE_FULL) || forceTraceElements?.has(LINKER_TYPE)) return labels;\n\n const setWithoutLinker = new Set(globalTypeSet);\n setWithoutLinker.delete(LINKER_TYPE_FULL);\n\n const stems = records.map((r) =>\n renderRecordLabel(r, setWithoutLinker, forceTraceElements, separator),\n );\n\n const stemOccurrences = new Map<string, number>();\n for (const s of stems) {\n if (s !== undefined) stemOccurrences.set(s, (stemOccurrences.get(s) ?? 0) + 1);\n }\n\n return labels.map((label, i) => {\n const stem = stems[i];\n if (stem === undefined) return label;\n return stemOccurrences.get(stem) === 1 ? stem : label;\n });\n}\n\nfunction countUniqueLabels(result: string[] | undefined): number {\n if (result === undefined) return 0;\n return new Set(result).size;\n}\n\nfunction minimizeTypeSet(\n typeSet: Set<string>,\n records: EnrichedRecord[],\n stats: TypeStats,\n forceTraceElements: Set<string> | undefined,\n forcedSet: Set<string>,\n separator: string,\n): Set<string> {\n const initialResult = buildLabels(records, typeSet, forceTraceElements, separator, false);\n if (initialResult === undefined) return typeSet;\n\n const targetCardinality = countUniqueLabels(initialResult);\n const result = new Set(typeSet);\n\n const removable = [...result]\n .filter((t) => !forceTraceElements?.has(t.split(\"@\")[0]) && !forcedSet.has(t))\n .sort((a, b) => (stats.importances.get(a) ?? 0) - (stats.importances.get(b) ?? 0));\n\n for (const typeToRemove of removable) {\n const candidate = new Set(result);\n candidate.delete(typeToRemove);\n const candidateResult = buildLabels(records, candidate, forceTraceElements, separator, false);\n if (candidateResult !== undefined && countUniqueLabels(candidateResult) >= targetCardinality) {\n result.delete(typeToRemove);\n }\n }\n\n return result;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"derive_distinct_labels.js","names":["isNil"],"sources":["../../src/labels/derive_distinct_labels.ts"],"sourcesContent":["import {\n Annotation,\n parseJson,\n readAnnotation,\n type AxisQualification,\n type MatchQualifications,\n type PColumnSpec,\n type PObjectId,\n type PObjectSpec,\n type StringifiedJson,\n type Trace,\n} from \"@milaboratories/pl-model-common\";\nimport { throwError } from \"@milaboratories/helpers\";\nimport { isFunction, isNil } from \"es-toolkit\";\nimport { derivePostfixes, type LinkerFormatter } from \"./linked_column_postfix\";\n\nexport type { Trace, TraceEntry } from \"@milaboratories/pl-model-common\";\n\nconst DISTANCE_PENALTY = 0.001;\nconst LABEL_TYPE = \"__LABEL__\";\nconst LABEL_TYPE_FULL = \"__LABEL__@1\";\nconst HIT_QUAL_TYPE = \"__HIT_QUAL__\";\nconst ANCHOR_QUAL_TYPE_PREFIX = \"__ANCHOR_QUAL__:\";\n\nfunction isAnchorQualType(t: string): boolean {\n return t.startsWith(ANCHOR_QUAL_TYPE_PREFIX);\n}\n\nfunction isSyntheticType(t: string): boolean {\n return t === HIT_QUAL_TYPE || isAnchorQualType(t);\n}\n\n/** SDK-internal trace shape — adds fields used by this algorithm only, not part of the on-disk contract. */\ntype ExtendedTraceEntry = Trace[number] & {\n importance?: number;\n position?: \"prefix\" | \"suffix\";\n};\n\nexport type LinkerStep = {\n /** Linker column spec — its `axesSpec` yields the source axis (root); its `LinkLabel`/`Label` names it. */\n spec: PColumnSpec;\n /** Axis qualifications applied on this hop. Not yet consumed by the postfix (deferred). */\n qualifications?: AxisQualification[];\n};\n\nexport type Entry =\n | PObjectSpec\n | {\n spec: PObjectSpec;\n /** Extra trace entries merged with the base trace from annotations. */\n extraTrace?: ExtendedTraceEntry[];\n /** Linker steps (`[0]` source-most) traversed to reach this column; rendered as a \"via …\"\n * postfix only when needed for uniqueness — see {@link derivePostfixes}. */\n linkerPath?: LinkerStep[];\n /** Axis qualifications applied to the hit column / already-bound anchors; rendered as \"[…]\" suffixes. */\n qualifications?: MatchQualifications;\n };\n\n/**\n * Per-zone formatters. Each one receives raw inputs and returns the rendered text for that zone,\n * or `undefined` to suppress the zone entirely (no synthetic injection → no minimization, no render).\n */\nexport type DeriveLabelsFormatters = {\n /** Native column label. Default: identity. `undefined` → label entry not added (treated as if spec had no label). */\n native?: (label: string, spec: PObjectSpec, index: number) => string | undefined;\n /** Linker/source postfix zone (phase 2). Receives the distinguishing tokens (`{ root, linkers }`)\n * and returns the \"via …\" text, or `undefined` to suppress. */\n linker?: LinkerFormatter;\n /** Hit-axis qualifications block. Default: `[${formatQualifications(qs)}]`. */\n hitQualification?: (\n qualifications: AxisQualification[],\n spec: PObjectSpec,\n index: number,\n ) => string | undefined;\n /** Per-anchor qualifications block. Default: `[${anchorId}: ${formatQualifications(qs)}]`. */\n anchorQualification?: (\n anchorId: PObjectId,\n qualifications: AxisQualification[],\n spec: PObjectSpec,\n index: number,\n ) => string | undefined;\n};\n\nexport type DeriveLabelsOptions = {\n /** Separator to use between label parts (\" / \" by default). */\n separator?: string;\n /** If true, native label is appended at the end of the trace zone. By default it is prepended (label is the most important name). */\n addLabelAsSuffix?: boolean;\n /** Force inclusion of native column label even when not needed for uniqueness. */\n includeNativeLabel?: boolean;\n /** Trace types that must be included in the label. */\n forceTraceElements?: string[];\n /** Per-zone custom formatters. Returning `undefined` from any formatter suppresses the corresponding zone. */\n formatters?: DeriveLabelsFormatters;\n};\n\n/**\n * Distinct labels for a set of columns. Two phases:\n * 1. {@link deriveStems} — treats each column as a single entity (native label + trace + hit/anchor\n * qualifications) and produces the minimal distinguishing \"stem\".\n * 2. {@link derivePostfixes} — for columns still colliding on their stem, appends a \"via …\" postfix\n * describing the difference between their linker sources (root axis, then linker chain).\n */\nexport function deriveDistinctLabels(values: Entry[], options: DeriveLabelsOptions = {}): string[] {\n const stems = deriveStems(values, options);\n return derivePostfixes(\n values.map((v, i) => {\n const { spec, linkerPath } = extractEntryParts(v);\n return {\n stem: stems[i],\n // Hit columns are PColumns; the postfix reads their axesSpec to orient linkers.\n hit: spec as PColumnSpec,\n linkers: (linkerPath ?? []).map((s) => s.spec),\n };\n }),\n options.formatters?.linker,\n );\n}\n\n/** Phase 1: minimal per-column stem — native label + trace + hit/anchor qualifications, no linkers. */\nfunction deriveStems(values: Entry[], options: DeriveLabelsOptions): string[] {\n const forceTraceElements =\n options.forceTraceElements !== undefined && options.forceTraceElements.length > 0\n ? new Set(options.forceTraceElements)\n : undefined;\n const separator = options.separator ?? \" / \";\n\n const records = values.map((v, i) => enrichRecord(v, i, options));\n const stats = collectTypeStats(records);\n\n const hasAnySynthetic = records.some((r) => r.fullTrace.some((ft) => isSyntheticType(ft.type)));\n const labelForced =\n (options.includeNativeLabel === true || hasAnySynthetic) &&\n stats.countByType.has(LABEL_TYPE_FULL);\n\n const forcedSet = new Set<string>();\n if (labelForced) forcedSet.add(LABEL_TYPE_FULL);\n\n const { mainTypes, secondaryTypes } = classifyTypes(stats, values.length);\n\n const build = (typeSet: Set<string>, force: boolean) =>\n buildLabels(records, typeSet, forceTraceElements, separator, force);\n\n if (mainTypes.length === 0) {\n if (secondaryTypes.length !== 0)\n throw new Error(\"Non-empty secondary types list while main types list is empty.\");\n\n return (\n build(new Set([LABEL_TYPE_FULL]), true) ??\n throwError(\"Failed to derive labels using native column labels\")\n );\n }\n\n let includedCount = 0;\n let additionalType = -1;\n while (includedCount < mainTypes.length) {\n const currentSet = new Set<string>(forcedSet);\n for (let i = 0; i < includedCount; ++i) currentSet.add(mainTypes[i]);\n if (additionalType >= 0) currentSet.add(mainTypes[additionalType]);\n\n const candidateResult = build(currentSet, false);\n if (candidateResult !== undefined && countUniqueLabels(candidateResult) === values.length) {\n const minimized = minimizeTypeSet(\n currentSet,\n records,\n stats,\n forceTraceElements,\n forcedSet,\n separator,\n );\n const rendered = build(minimized, false) ?? throwError(\"Failed to derive unique labels\");\n return repairBareByPresence(\n rendered,\n minimized,\n records,\n stats,\n forcedSet,\n forceTraceElements,\n separator,\n );\n }\n\n additionalType++;\n if (additionalType >= mainTypes.length) {\n includedCount++;\n additionalType = includedCount;\n }\n }\n\n const fallbackSet = new Set([...forcedSet, ...mainTypes, ...secondaryTypes]);\n const minimized = minimizeTypeSet(\n fallbackSet,\n records,\n stats,\n forceTraceElements,\n forcedSet,\n separator,\n );\n const rendered = build(minimized, true) ?? throwError(\"Failed to derive unique labels\");\n return repairBareByPresence(\n rendered,\n minimized,\n records,\n stats,\n forcedSet,\n forceTraceElements,\n separator,\n );\n}\n\n// --- Pure helpers ---\ntype FullTraceEntry = ExtendedTraceEntry & { fullType: string; occurrenceIndex: number };\n\ntype EnrichedRecord = {\n fullTrace: FullTraceEntry[];\n};\n\nfunction extractEntryParts(entry: Entry): {\n spec: PObjectSpec;\n extraTrace: ExtendedTraceEntry[] | undefined;\n linkerPath: LinkerStep[] | undefined;\n qualifications: MatchQualifications | undefined;\n} {\n const isEnriched = \"spec\" in entry && typeof entry.spec === \"object\";\n if (!isEnriched) {\n return {\n spec: entry as PObjectSpec,\n extraTrace: undefined,\n linkerPath: undefined,\n qualifications: undefined,\n };\n }\n return {\n spec: entry.spec,\n extraTrace: entry.extraTrace,\n linkerPath: entry.linkerPath,\n qualifications: entry.qualifications,\n };\n}\n\nfunction formatQualification(q: AxisQualification): string {\n const ctx = q.contextDomain ?? {};\n const keys = Object.keys(ctx);\n if (keys.length === 0) return q.axis.name;\n const pairs = keys.map((k) => `${k}=${ctx[k]}`).join(\", \");\n return Object.prototype.hasOwnProperty.call(ctx, q.axis.name) ? pairs : `${q.axis.name} ${pairs}`;\n}\n\nfunction formatQualifications(qs: AxisQualification[]): string {\n return qs.map(formatQualification).join(\"; \");\n}\n\nfunction buildFullTrace(trace: ExtendedTraceEntry[]): FullTraceEntry[] {\n const result: FullTraceEntry[] = [];\n const occurrences = new Map<string, number>();\n\n for (let i = trace.length - 1; i >= 0; --i) {\n const entry = trace[i];\n const occurrenceIndex = (occurrences.get(entry.type) ?? 0) + 1;\n occurrences.set(entry.type, occurrenceIndex);\n result.push({\n ...entry,\n fullType: `${entry.type}@${occurrenceIndex}`,\n occurrenceIndex,\n });\n }\n\n result.reverse();\n return result;\n}\n\nfunction enrichRecord(value: Entry, index: number, options: DeriveLabelsOptions): EnrichedRecord {\n const { spec, extraTrace, qualifications } = extractEntryParts(value);\n const formatters = options.formatters;\n\n const rawLabel = readAnnotation(spec, Annotation.Label);\n const traceStr = readAnnotation(spec, Annotation.Trace);\n const baseTrace = traceStr\n ? (parseJson(traceStr as StringifiedJson<ExtendedTraceEntry[]>) ?? [])\n : [];\n const prefixExtra = extraTrace?.filter((e) => e.position === \"prefix\") ?? [];\n const suffixExtra = extraTrace?.filter((e) => e.position !== \"prefix\") ?? [];\n const trace: ExtendedTraceEntry[] = [...prefixExtra, ...baseTrace, ...suffixExtra];\n\n if (!isNil(rawLabel)) {\n const label = isFunction(formatters?.native)\n ? formatters.native(rawLabel, spec, index)\n : rawLabel;\n if (!isNil(label)) {\n const labelEntry = { label, type: LABEL_TYPE, importance: -2 };\n if (options.addLabelAsSuffix === true) trace.push(labelEntry);\n else trace.splice(0, 0, labelEntry);\n }\n }\n\n if (qualifications !== undefined && qualifications.forQueries !== undefined) {\n for (const [anchorId, qs] of Object.entries(qualifications.forQueries)) {\n if (qs.length === 0) continue;\n const anchorText = isFunction(formatters?.anchorQualification)\n ? formatters.anchorQualification(anchorId as PObjectId, qs, spec, index)\n : `[${anchorId}: ${formatQualifications(qs)}]`;\n if (isNil(anchorText)) continue;\n trace.push({\n type: `${ANCHOR_QUAL_TYPE_PREFIX}${anchorId}`,\n label: anchorText,\n importance: -11,\n });\n }\n if (qualifications.forHit !== undefined && qualifications.forHit.length > 0) {\n const hitText = isFunction(formatters?.hitQualification)\n ? formatters.hitQualification(qualifications.forHit, spec, index)\n : `[${formatQualifications(qualifications.forHit)}]`;\n if (!isNil(hitText)) {\n trace.push({ type: HIT_QUAL_TYPE, label: hitText, importance: -12 });\n }\n }\n }\n\n return { fullTrace: buildFullTrace(trace) };\n}\n\ntype TypeStats = {\n importances: Map<string, number>;\n countByType: Map<string, number>;\n};\n\nfunction collectTypeStats(records: EnrichedRecord[]): TypeStats {\n const importances = new Map<string, number>();\n const countByType = new Map<string, number>();\n\n for (const record of records) {\n for (let i = 0; i < record.fullTrace.length; i++) {\n const { fullType, importance: rawImportance } = record.fullTrace[i];\n const importance = rawImportance ?? 0;\n const distance = (record.fullTrace.length - i) * DISTANCE_PENALTY;\n\n countByType.set(fullType, (countByType.get(fullType) ?? 0) + 1);\n importances.set(\n fullType,\n Math.max(importances.get(fullType) ?? Number.NEGATIVE_INFINITY, importance - distance),\n );\n }\n }\n\n return { importances, countByType };\n}\n\nfunction classifyTypes(\n stats: TypeStats,\n totalRecords: number,\n): { mainTypes: string[]; secondaryTypes: string[] } {\n const sorted = [...stats.importances].sort(([, i1], [, i2]) => i2 - i1);\n\n const mainTypes: string[] = [];\n const secondaryTypes: string[] = [];\n\n for (const [typeName] of sorted) {\n if (typeName.endsWith(\"@1\") || stats.countByType.get(typeName) === totalRecords)\n mainTypes.push(typeName);\n else secondaryTypes.push(typeName);\n }\n\n return { mainTypes, secondaryTypes };\n}\n\nfunction renderRecordLabel(\n record: EnrichedRecord,\n includedTypes: Set<string>,\n forceTraceElements: Set<string> | undefined,\n separator: string,\n): string | undefined {\n const traceParts: string[] = [];\n const anchorParts: string[] = [];\n let hitLabel: string | undefined;\n\n for (const ft of record.fullTrace) {\n if (!(includedTypes.has(ft.fullType) || forceTraceElements?.has(ft.type))) continue;\n if (ft.type === HIT_QUAL_TYPE) hitLabel = ft.label;\n else if (isAnchorQualType(ft.type)) anchorParts.push(ft.label);\n else traceParts.push(ft.label);\n }\n\n const isEmpty = traceParts.length === 0 && anchorParts.length === 0 && hitLabel === undefined;\n\n if (isEmpty) return undefined;\n\n let label = traceParts.join(separator);\n const append = (part: string) => {\n label = label.length === 0 ? part : `${label} ${part}`;\n };\n for (const a of anchorParts) append(a);\n if (hitLabel !== undefined) append(hitLabel);\n\n return label;\n}\n\nfunction buildLabels(\n records: EnrichedRecord[],\n includedTypes: Set<string>,\n forceTraceElements: Set<string> | undefined,\n separator: string,\n force: boolean,\n): string[] | undefined {\n const result: string[] = [];\n\n for (const r of records) {\n const rendered = renderRecordLabel(r, includedTypes, forceTraceElements, separator);\n if (rendered === undefined) {\n if (!force) return undefined;\n result.push(\"Unlabeled\");\n continue;\n }\n result.push(rendered);\n }\n\n return result;\n}\n\nfunction countUniqueLabels(result: string[] | undefined): number {\n if (result === undefined) return 0;\n return new Set(result).size;\n}\n\nfunction minimizeTypeSet(\n typeSet: Set<string>,\n records: EnrichedRecord[],\n stats: TypeStats,\n forceTraceElements: Set<string> | undefined,\n forcedSet: Set<string>,\n separator: string,\n): Set<string> {\n const initialResult = buildLabels(records, typeSet, forceTraceElements, separator, false);\n if (initialResult === undefined) return typeSet;\n\n const targetCardinality = countUniqueLabels(initialResult);\n const result = new Set(typeSet);\n\n const removable = [...result]\n .filter((t) => !forceTraceElements?.has(t.split(\"@\")[0]) && !forcedSet.has(t))\n .sort((a, b) => (stats.importances.get(a) ?? 0) - (stats.importances.get(b) ?? 0));\n\n for (const typeToRemove of removable) {\n const candidate = new Set(result);\n candidate.delete(typeToRemove);\n const candidateResult = buildLabels(records, candidate, forceTraceElements, separator, false);\n if (candidateResult !== undefined && countUniqueLabels(candidateResult) >= targetCardinality) {\n result.delete(typeToRemove);\n }\n }\n\n return result;\n}\n\nconst ABSENT_VALUE = \"\u0000absent\"; // sentinel value for \"row has no entry of this type\"\n\n/** Whether `fullType`'s rendered value differs across the group (absence counts as a value). */\nfunction typeDistinguishes(records: EnrichedRecord[], group: number[], fullType: string): boolean {\n const values = new Set<string>();\n for (const i of group) {\n const ft = records[i].fullTrace.find((e) => e.fullType === fullType);\n values.add(ft?.label ?? ABSENT_VALUE);\n if (values.size > 1) return true;\n }\n return false;\n}\n\n/** The highest-importance trace type this row HAS (outside `typeSet`) that sets it apart from its\n * group peers. `undefined` when the row carries nothing distinguishing of its own. */\nfunction bestDistinguishingType(\n records: EnrichedRecord[],\n group: number[],\n row: number,\n typeSet: Set<string>,\n forcedSet: Set<string>,\n forceTraceElements: Set<string> | undefined,\n stats: TypeStats,\n): string | undefined {\n return records[row].fullTrace.reduce<{ type: string; imp: number } | undefined>((best, ft) => {\n if (typeSet.has(ft.fullType) || forcedSet.has(ft.fullType) || forceTraceElements?.has(ft.type))\n return best;\n if (!typeDistinguishes(records, group, ft.fullType)) return best;\n const imp = stats.importances.get(ft.fullType) ?? 0;\n return best === undefined || imp > best.imp ? { type: ft.fullType, imp } : best;\n }, undefined)?.type;\n}\n\n/**\n * Un-bare columns distinguished only \"by absence\". After minimization a column can end up with a\n * bare trace zone (only the forced native label) while a peer sharing that same base renders extra\n * tokens — the column reads as unique purely because it LACKS what the peer has. For each such bare\n * column this re-renders JUST that column's label with the highest-importance trace type it actually\n * carries that tells it apart from its peers, so every colliding column is distinguished by a token\n * it HAS rather than by omission.\n *\n * Patches individual labels rather than the shared type set on purpose: the set is global, so adding\n * a type there would also decorate unrelated columns in other groups that happen to carry it. Only\n * bare columns' labels grow (a superset of their previous value), so uniqueness is preserved. Groups\n * are keyed by the base = the label rendered from the forced types alone.\n */\nfunction repairBareByPresence(\n labels: string[],\n minimized: Set<string>,\n records: EnrichedRecord[],\n stats: TypeStats,\n forcedSet: Set<string>,\n forceTraceElements: Set<string> | undefined,\n separator: string,\n): string[] {\n const base = records.map((r) => renderRecordLabel(r, forcedSet, forceTraceElements, separator));\n const isBare = records.map(\n (r, i) => renderRecordLabel(r, minimized, forceTraceElements, separator) === base[i],\n );\n\n const groups = records.reduce<Map<string, number[]>>(\n (acc, _, i) => acc.set(base[i] ?? \"\", [...(acc.get(base[i] ?? \"\") ?? []), i]),\n new Map(),\n );\n\n const patched = [...labels];\n for (const group of groups.values()) {\n // Only asymmetric groups need repair: a bare column beside a richer peer. When every column is\n // equally bare there is nothing to un-hide (they carry no distinguishing token of their own).\n if (!group.some((i) => !isBare[i])) continue;\n for (const i of group) {\n if (!isBare[i]) continue;\n const chosen = bestDistinguishingType(\n records,\n group,\n i,\n minimized,\n forcedSet,\n forceTraceElements,\n stats,\n );\n if (chosen === undefined) continue;\n const withToken = new Set([...minimized, chosen]);\n patched[i] =\n renderRecordLabel(records[i], withToken, forceTraceElements, separator) ?? patched[i];\n }\n }\n\n return patched;\n}\n"],"mappings":"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"}
@@ -0,0 +1,189 @@
1
+ let _milaboratories_pl_model_common = require("@milaboratories/pl-model-common");
2
+ let es_toolkit = require("es-toolkit");
3
+ //#region src/labels/linked_column_postfix.ts
4
+ /**
5
+ * Structural postfix derivation for linked columns (phase 2 of `deriveDistinctLabels`).
6
+ *
7
+ * A linked column is
8
+ * reached by a CHAIN of linkers, and we distinguish two such columns by the same ideology the main
9
+ * algorithm uses — "find the minimal difference, escalate until unique" — over two dimensions:
10
+ *
11
+ * - root: the single source axis of the chain, derived from `linkers[0]` (one of its axesSpec).
12
+ * This is "the source" — it's what we prefer to show ("difference of sources").
13
+ * - linkers: the linker chain itself (by `LinkLabel`), a per-step fallback used only where roots
14
+ * coincide.
15
+ *
16
+ * Nothing is stored redundantly: the caller passes the linker path (`linkers`) and the hit spec;
17
+ * root and every token are computed on the fly. The caller also supplies the `stem` (label+trace+
18
+ * quals from the existing single-entity machinery); this module only adds the path postfix, and
19
+ * only where stems collide.
20
+ */
21
+ function defaultLinkerFormatter(parts) {
22
+ const chain = parts.linkers.map((l) => l.text).join(" > ");
23
+ const pieces = [parts.root?.text, chain || void 0].filter((p) => !(0, es_toolkit.isNil)(p));
24
+ return pieces.length === 0 ? "" : `via ${pieces.join(" ")}`;
25
+ }
26
+ function extractAxisIdentity(axis) {
27
+ const id = (0, _milaboratories_pl_model_common.getAxisId)(axis);
28
+ return {
29
+ label: (0, _milaboratories_pl_model_common.readAnnotation)(axis, _milaboratories_pl_model_common.Annotation.Label)?.trim() || void 0,
30
+ name: id.name,
31
+ domain: id.domain,
32
+ contextDomain: id.contextDomain
33
+ };
34
+ }
35
+ /**
36
+ * A linker is named ONLY by its human label (`LinkLabel`/`Label`) — never by its technical column
37
+ * name. Returns `undefined` for an unlabeled linker, which callers treat as "not renderable": such a
38
+ * step is skipped, and disambiguation falls to another step or the root.
39
+ */
40
+ function extractLinkerIdentity(linker) {
41
+ const label = ((0, _milaboratories_pl_model_common.readAnnotation)(linker, _milaboratories_pl_model_common.Annotation.LinkLabel) ?? (0, _milaboratories_pl_model_common.readAnnotation)(linker, _milaboratories_pl_model_common.Annotation.Label))?.trim();
42
+ return label ? {
43
+ label,
44
+ name: label
45
+ } : void 0;
46
+ }
47
+ /** Domain keys where `mine` differs from at least one competitor. */
48
+ function differingKeys(mine, others) {
49
+ const my = mine ?? {};
50
+ const diffKeys = /* @__PURE__ */ new Set();
51
+ for (const o of others) {
52
+ const od = o ?? {};
53
+ for (const k of Object.keys(my)) if (my[k] !== od[k]) diffKeys.add(k);
54
+ for (const k of Object.keys(od)) if (my[k] !== od[k]) diffKeys.add(k);
55
+ }
56
+ return [...diffKeys];
57
+ }
58
+ function renderWithDomain(base, keys, domain) {
59
+ if (keys.length === 0) return base;
60
+ const d = domain ?? {};
61
+ return `${base}[${keys.map((k) => `${k}=${d[k] ?? "∅"}`).join(", ")}]`;
62
+ }
63
+ /**
64
+ * Minimal token distinguishing `mine` from `others`: label → name → only the differing domain keys.
65
+ * Falls back to the bare label/name when indistinguishable at this dimension (another one covers it).
66
+ */
67
+ function deriveToken(mine, others) {
68
+ const base = mine.label ?? mine.name;
69
+ if (mine.label !== void 0 && others.every((o) => o.label !== mine.label)) return mine.label;
70
+ if (others.every((o) => o.name !== mine.name)) return base;
71
+ const dk = differingKeys(mine.domain, others.map((o) => o.domain));
72
+ if (dk.length > 0) return renderWithDomain(base, dk, mine.domain);
73
+ const ck = differingKeys(mine.contextDomain, others.map((o) => o.contextDomain));
74
+ if (ck.length > 0) return renderWithDomain(base, ck, mine.contextDomain);
75
+ return base;
76
+ }
77
+ /**
78
+ * The chain's single root: the source-side axis of `linkers[0]`. A linker bridges two axis groups;
79
+ * the side facing the next hop (`linkers[1]`, or the hit for a single-linker chain) is the target,
80
+ * the other side is the source → its axis is the root. `undefined` if `linkers[0]` is not a
81
+ * well-formed two-group linker.
82
+ */
83
+ function extractRoot(hit, linkers) {
84
+ const first = linkers[0];
85
+ if (first === void 0) return void 0;
86
+ const axes = first.axesSpec;
87
+ if (axes.length !== 2) return void 0;
88
+ const second = linkers[1] ?? hit;
89
+ return axes.find((a) => !second.axesSpec.some((b) => b.name === a.name));
90
+ }
91
+ const ABSENT = "__ABSENT__";
92
+ function deriveSlotKey(group, slot, row) {
93
+ if (slot.kind === "root") {
94
+ const r = group.roots[row];
95
+ return r === void 0 ? ABSENT : (0, _milaboratories_pl_model_common.canonicalizeJson)((0, _milaboratories_pl_model_common.getAxisId)(r));
96
+ } else {
97
+ const l = group.entries[row].linkers?.[slot.i];
98
+ const id = l && extractLinkerIdentity(l);
99
+ return (0, es_toolkit.isNil)(id) ? ABSENT : (0, _milaboratories_pl_model_common.canonicalizeJson)(id);
100
+ }
101
+ }
102
+ function deriveSlotToken(group, slot, row) {
103
+ if (slot.kind === "root") {
104
+ const r = group.roots[row];
105
+ if (r === void 0) return void 0;
106
+ const comp = group.roots.filter((o, j) => j !== row && !(0, es_toolkit.isNil)(o)).map((o) => extractAxisIdentity(o));
107
+ return deriveToken(extractAxisIdentity(r), comp);
108
+ }
109
+ const l = group.entries[row].linkers?.[slot.i];
110
+ const id = l && extractLinkerIdentity(l);
111
+ if ((0, es_toolkit.isNil)(id)) return void 0;
112
+ return deriveToken(id, group.entries.filter((_, j) => j !== row).map((e) => e.linkers?.[slot.i]).map((x) => x ? extractLinkerIdentity(x) : void 0).filter((x) => !(0, es_toolkit.isNil)(x)));
113
+ }
114
+ function getDiscriminates(group, slot) {
115
+ return new Set(group.entries.map((_, r) => deriveSlotKey(group, slot, r))).size > 1;
116
+ }
117
+ /** Render one row against a chosen slot set: the distinguishing root + linker pieces, formatted. */
118
+ function renderRow(group, slots, row) {
119
+ const rootSpec = group.roots[row];
120
+ const rootText = slots.some((s) => s.kind === "root") ? deriveSlotToken(group, { kind: "root" }, row) : void 0;
121
+ const root = rootSpec !== void 0 && rootText !== void 0 ? {
122
+ spec: rootSpec,
123
+ text: rootText
124
+ } : void 0;
125
+ const linkers = slots.filter((s) => s.kind === "linker").sort((a, b) => a.i - b.i).map((s) => {
126
+ const spec = group.entries[row].linkers?.[s.i];
127
+ const text = deriveSlotToken(group, s, row);
128
+ return spec !== void 0 && text !== void 0 ? {
129
+ spec,
130
+ text
131
+ } : void 0;
132
+ }).filter((l) => !(0, es_toolkit.isNil)(l));
133
+ if (root === void 0 && linkers.length === 0) return "";
134
+ return group.format({
135
+ root,
136
+ linkers
137
+ }, group.entries[row].hit, group.indices[row]) || "";
138
+ }
139
+ function renderAll(group, slots) {
140
+ return group.entries.map((_, r) => renderRow(group, slots, r));
141
+ }
142
+ function allUnique(rendered) {
143
+ return new Set(rendered).size === rendered.length;
144
+ }
145
+ /**
146
+ * Minimal slot set that makes the group unique. Escalate by priority (root, then linkers by step),
147
+ * then drop any redundant slot; render every row symmetrically against the result.
148
+ *
149
+ * KNOWN LIMITATION (review point): symmetric render can over-decorate a row in a mixed group (e.g.
150
+ * `via Sample MapperA` where `via Sample` alone is already unique for that row). Per-row trimming is
151
+ * a generalized `dropRedundantLinkerSuffix`; naive greedy trimming is unstable, so it's deferred.
152
+ */
153
+ function resolveGroup(entries, indices, format) {
154
+ const group = {
155
+ entries,
156
+ roots: entries.map((e) => e.hit && e.linkers?.length ? extractRoot(e.hit, e.linkers) : void 0),
157
+ indices,
158
+ format
159
+ };
160
+ const maxLen = Math.max(0, ...entries.map((e) => e.linkers?.length ?? 0));
161
+ const escalated = [{ kind: "root" }, ...Array.from({ length: maxLen }, (_, i) => ({
162
+ kind: "linker",
163
+ i
164
+ }))].reduce((acc, slot) => allUnique(renderAll(group, acc)) || !getDiscriminates(group, slot) ? acc : (acc.push(slot), acc), []);
165
+ return renderAll(group, escalated.reduce((acc, slot) => {
166
+ const trial = acc.filter((s) => s !== slot);
167
+ return allUnique(renderAll(group, trial)) ? trial : acc;
168
+ }, escalated));
169
+ }
170
+ /**
171
+ * Full label per entry: `stem` plus, where stems collide, a minimal postfix distinguishing the
172
+ * linked columns by the difference between their sources.
173
+ */
174
+ function derivePostfixes(entries, format = defaultLinkerFormatter) {
175
+ const postfix = [...entries.reduce((acc, e, idx) => acc.set(e.stem, [...acc.get(e.stem) ?? [], idx]), /* @__PURE__ */ new Map()).values()].reduce((acc, idxs) => {
176
+ if (idxs.length < 2) return acc;
177
+ const resolved = resolveGroup(idxs.map((i) => entries[i]), idxs, format);
178
+ return idxs.reduce((m, i, k) => m.set(i, resolved[k]), acc);
179
+ }, /* @__PURE__ */ new Map());
180
+ return entries.map((e, i) => {
181
+ const p = postfix.get(i);
182
+ return p ? `${e.stem} ${p}` : e.stem;
183
+ });
184
+ }
185
+ //#endregion
186
+ exports.derivePostfixes = derivePostfixes;
187
+ exports.extractRoot = extractRoot;
188
+
189
+ //# sourceMappingURL=linked_column_postfix.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linked_column_postfix.cjs","names":["Annotation"],"sources":["../../src/labels/linked_column_postfix.ts"],"sourcesContent":["/**\n * Structural postfix derivation for linked columns (phase 2 of `deriveDistinctLabels`).\n *\n * A linked column is\n * reached by a CHAIN of linkers, and we distinguish two such columns by the same ideology the main\n * algorithm uses — \"find the minimal difference, escalate until unique\" — over two dimensions:\n *\n * - root: the single source axis of the chain, derived from `linkers[0]` (one of its axesSpec).\n * This is \"the source\" — it's what we prefer to show (\"difference of sources\").\n * - linkers: the linker chain itself (by `LinkLabel`), a per-step fallback used only where roots\n * coincide.\n *\n * Nothing is stored redundantly: the caller passes the linker path (`linkers`) and the hit spec;\n * root and every token are computed on the fly. The caller also supplies the `stem` (label+trace+\n * quals from the existing single-entity machinery); this module only adds the path postfix, and\n * only where stems collide.\n */\nimport {\n Annotation,\n canonicalizeJson,\n getAxisId,\n readAnnotation,\n type AxisSpec,\n type PColumnSpec,\n} from \"@milaboratories/pl-model-common\";\nimport { isNil } from \"es-toolkit\";\n\n/**\n * One entry to label: its stem plus the linker path that reached it (`linkers[0]` source-most) and\n * the hit spec used to orient the chain. Plain columns pass neither.\n */\nexport type PostfixEntry = { stem: string; hit?: PColumnSpec; linkers?: PColumnSpec[] };\n\n/**\n * One distinguishing piece of the postfix: the full source spec (axis or linker column — no info\n * lost) plus `text`, the minimal text that sets it apart from the siblings (a label, or only the\n * differing domain keys). Custom formatters may use `spec` freely; `text` is the default rendering.\n */\nexport type LinkerPart<Spec> = { spec: Spec; text: string };\n\n/** The distinguishing source of one column: its root axis (when it differs) + the linker chain. */\nexport type LinkerParts = {\n root?: LinkerPart<AxisSpec>;\n linkers: LinkerPart<PColumnSpec>[];\n};\n\n/**\n * Renders the postfix zone from the distinguishing sources. Default:\n * `via ${[root, linkers.join(\" > \")].filter(Boolean).join(\" \")}` (using each piece's `text`).\n * Returning `undefined` — or the empty string `\"\"` — suppresses the postfix entirely (the column\n * keeps just its stem); the two are treated identically.\n */\nexport type LinkerFormatter = (\n parts: LinkerParts,\n hit: PColumnSpec | undefined,\n index: number,\n) => string | undefined;\n\nfunction defaultLinkerFormatter(parts: LinkerParts): string {\n const chain = parts.linkers.map((l) => l.text).join(\" > \");\n const pieces = [parts.root?.text, chain || undefined].filter((p): p is string => !isNil(p));\n return pieces.length === 0 ? \"\" : `via ${pieces.join(\" \")}`;\n}\n\ntype AxisIdentity = { label?: string } & Pick<AxisSpec, \"name\" | \"domain\" | \"contextDomain\">;\n\nfunction extractAxisIdentity(axis: AxisSpec): AxisIdentity {\n const id = getAxisId(axis);\n return {\n label: readAnnotation(axis, Annotation.Label)?.trim() || undefined,\n name: id.name,\n domain: id.domain,\n contextDomain: id.contextDomain,\n };\n}\n\n/**\n * A linker is named ONLY by its human label (`LinkLabel`/`Label`) — never by its technical column\n * name. Returns `undefined` for an unlabeled linker, which callers treat as \"not renderable\": such a\n * step is skipped, and disambiguation falls to another step or the root.\n */\nfunction extractLinkerIdentity(linker: PColumnSpec): AxisIdentity | undefined {\n const label = (\n readAnnotation(linker, Annotation.LinkLabel) ?? readAnnotation(linker, Annotation.Label)\n )?.trim();\n return label ? { label, name: label } : undefined;\n}\n\n/** Domain keys where `mine` differs from at least one competitor. */\nfunction differingKeys(\n mine: Record<string, string> | undefined,\n others: (Record<string, string> | undefined)[],\n): string[] {\n const my = mine ?? {};\n const diffKeys = new Set<string>();\n for (const o of others) {\n const od = o ?? {};\n for (const k of Object.keys(my)) {\n if (my[k] !== od[k]) {\n diffKeys.add(k);\n }\n }\n for (const k of Object.keys(od)) {\n if (my[k] !== od[k]) {\n diffKeys.add(k);\n }\n }\n }\n return [...diffKeys];\n}\n\nfunction renderWithDomain(\n base: string,\n keys: string[],\n domain: Record<string, string> | undefined,\n): string {\n if (keys.length === 0) return base;\n const d = domain ?? {};\n const pairs = keys.map((k) => `${k}=${d[k] ?? \"∅\"}`).join(\", \");\n return `${base}[${pairs}]`;\n}\n\n/**\n * Minimal token distinguishing `mine` from `others`: label → name → only the differing domain keys.\n * Falls back to the bare label/name when indistinguishable at this dimension (another one covers it).\n */\nfunction deriveToken(mine: AxisIdentity, others: AxisIdentity[]): string {\n const base = mine.label ?? mine.name;\n if (mine.label !== undefined && others.every((o) => o.label !== mine.label)) return mine.label;\n if (others.every((o) => o.name !== mine.name)) return base;\n const dk = differingKeys(\n mine.domain,\n others.map((o) => o.domain),\n );\n if (dk.length > 0) return renderWithDomain(base, dk, mine.domain);\n const ck = differingKeys(\n mine.contextDomain,\n others.map((o) => o.contextDomain),\n );\n if (ck.length > 0) return renderWithDomain(base, ck, mine.contextDomain);\n return base;\n}\n\n/**\n * The chain's single root: the source-side axis of `linkers[0]`. A linker bridges two axis groups;\n * the side facing the next hop (`linkers[1]`, or the hit for a single-linker chain) is the target,\n * the other side is the source → its axis is the root. `undefined` if `linkers[0]` is not a\n * well-formed two-group linker.\n */\nexport function extractRoot(hit: PColumnSpec, linkers: PColumnSpec[]): AxisSpec | undefined {\n const first = linkers[0];\n if (first === undefined) return undefined;\n const axes = first.axesSpec;\n if (axes.length !== 2) return undefined;\n const second = linkers[1] ?? hit;\n return axes.find((a) => !second.axesSpec.some((b) => b.name === a.name));\n}\n\ntype Slot = { kind: \"root\" } | { kind: \"linker\"; i: number };\n\nconst ABSENT = \"__ABSENT__\"; // sentinel for \"this row has no value at this slot\"\n\n/** A colliding group: entries + their derived roots + original indices + the postfix formatter. */\ntype Group = {\n entries: PostfixEntry[];\n roots: (AxisSpec | undefined)[];\n indices: number[];\n format: LinkerFormatter;\n};\n\nfunction deriveSlotKey(group: Group, slot: Slot, row: number): string {\n if (slot.kind === \"root\") {\n const r = group.roots[row];\n return r === undefined ? ABSENT : canonicalizeJson(getAxisId(r));\n } else {\n const l = group.entries[row].linkers?.[slot.i];\n const id = l && extractLinkerIdentity(l);\n return isNil(id) ? ABSENT : canonicalizeJson(id);\n }\n}\n\nfunction deriveSlotToken(group: Group, slot: Slot, row: number): string | undefined {\n if (slot.kind === \"root\") {\n const r = group.roots[row];\n if (r === undefined) return undefined;\n const comp = group.roots\n .filter((o, j) => j !== row && !isNil(o))\n .map((o) => extractAxisIdentity(o!));\n return deriveToken(extractAxisIdentity(r), comp);\n }\n const l = group.entries[row].linkers?.[slot.i];\n const id = l && extractLinkerIdentity(l);\n if (isNil(id)) return undefined; // unlabeled / absent linker → not renderable, skip\n const comp = group.entries\n .filter((_, j) => j !== row)\n .map((e) => e.linkers?.[slot.i])\n .map((x) => (x ? extractLinkerIdentity(x) : undefined))\n .filter((x): x is AxisIdentity => !isNil(x));\n return deriveToken(id, comp);\n}\n\nfunction getDiscriminates(group: Group, slot: Slot): boolean {\n return new Set(group.entries.map((_, r) => deriveSlotKey(group, slot, r))).size > 1;\n}\n\n/** Render one row against a chosen slot set: the distinguishing root + linker pieces, formatted. */\nfunction renderRow(group: Group, slots: Slot[], row: number): string {\n const rootSpec = group.roots[row];\n const rootText = slots.some((s) => s.kind === \"root\")\n ? deriveSlotToken(group, { kind: \"root\" }, row)\n : undefined;\n const root: LinkerPart<AxisSpec> | undefined =\n rootSpec !== undefined && rootText !== undefined\n ? { spec: rootSpec, text: rootText }\n : undefined;\n\n const linkers = slots\n .filter((s): s is { kind: \"linker\"; i: number } => s.kind === \"linker\")\n .sort((a, b) => a.i - b.i)\n .map((s) => {\n const spec = group.entries[row].linkers?.[s.i];\n const text = deriveSlotToken(group, s, row);\n return spec !== undefined && text !== undefined ? { spec, text } : undefined;\n })\n .filter((l): l is LinkerPart<PColumnSpec> => !isNil(l));\n\n // No distinguishing pieces for this row → no postfix (don't invoke the formatter with empties).\n if (root === undefined && linkers.length === 0) return \"\";\n // A formatter may suppress the postfix by returning `undefined` or `\"\"`; `|| \"\"` collapses both\n // to the same empty value so neither counts as a distinguishing token during slot escalation.\n return group.format({ root, linkers }, group.entries[row].hit, group.indices[row]) || \"\";\n}\n\nfunction renderAll(group: Group, slots: Slot[]): string[] {\n return group.entries.map((_, r) => renderRow(group, slots, r));\n}\n\nfunction allUnique(rendered: string[]): boolean {\n return new Set(rendered).size === rendered.length;\n}\n\n/**\n * Minimal slot set that makes the group unique. Escalate by priority (root, then linkers by step),\n * then drop any redundant slot; render every row symmetrically against the result.\n *\n * KNOWN LIMITATION (review point): symmetric render can over-decorate a row in a mixed group (e.g.\n * `via Sample MapperA` where `via Sample` alone is already unique for that row). Per-row trimming is\n * a generalized `dropRedundantLinkerSuffix`; naive greedy trimming is unstable, so it's deferred.\n */\nfunction resolveGroup(\n entries: PostfixEntry[],\n indices: number[],\n format: LinkerFormatter,\n): string[] {\n const group: Group = {\n entries,\n roots: entries.map((e) =>\n e.hit && e.linkers?.length ? extractRoot(e.hit, e.linkers) : undefined,\n ),\n indices,\n format,\n };\n const maxLen = Math.max(0, ...entries.map((e) => e.linkers?.length ?? 0));\n\n const slots: Slot[] = [\n { kind: \"root\" },\n ...Array.from({ length: maxLen }, (_, i): Slot => ({ kind: \"linker\", i })),\n ];\n\n const escalated = slots.reduce<Slot[]>(\n (acc, slot) =>\n allUnique(renderAll(group, acc)) || !getDiscriminates(group, slot)\n ? acc\n : (acc.push(slot), acc),\n [],\n );\n const chosen = escalated.reduce<Slot[]>((acc, slot) => {\n const trial = acc.filter((s) => s !== slot);\n return allUnique(renderAll(group, trial)) ? trial : acc;\n }, escalated);\n\n return renderAll(group, chosen);\n}\n\n/**\n * Full label per entry: `stem` plus, where stems collide, a minimal postfix distinguishing the\n * linked columns by the difference between their sources.\n */\nexport function derivePostfixes(\n entries: PostfixEntry[],\n format: LinkerFormatter = defaultLinkerFormatter,\n): string[] {\n const groups = entries.reduce<Map<string, number[]>>(\n (acc, e, idx) => acc.set(e.stem, [...(acc.get(e.stem) ?? []), idx]),\n new Map(),\n );\n\n const postfix = [...groups.values()].reduce<Map<number, string>>((acc, idxs) => {\n if (idxs.length < 2) return acc; // stem already unique — no postfix\n const resolved = resolveGroup(\n idxs.map((i) => entries[i]),\n idxs,\n format,\n );\n return idxs.reduce((m, i, k) => m.set(i, resolved[k]), acc);\n }, new Map());\n\n return entries.map((e, i) => {\n const p = postfix.get(i);\n return p ? `${e.stem} ${p}` : e.stem;\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA0DA,SAAS,uBAAuB,OAA4B;CAC1D,MAAM,QAAQ,MAAM,QAAQ,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK;CACzD,MAAM,SAAS,CAAC,MAAM,MAAM,MAAM,SAAS,KAAA,CAAS,CAAC,CAAC,QAAQ,MAAmB,EAAA,GAAA,WAAA,MAAA,CAAO,CAAC,CAAC;CAC1F,OAAO,OAAO,WAAW,IAAI,KAAK,OAAO,OAAO,KAAK,GAAG;AAC1D;AAIA,SAAS,oBAAoB,MAA8B;CACzD,MAAM,MAAA,GAAA,gCAAA,UAAA,CAAe,IAAI;CACzB,OAAO;EACL,QAAA,GAAA,gCAAA,eAAA,CAAsB,MAAMA,gCAAAA,WAAW,KAAK,CAAC,EAAE,KAAK,KAAK,KAAA;EACzD,MAAM,GAAG;EACT,QAAQ,GAAG;EACX,eAAe,GAAG;CACpB;AACF;;;;;;AAOA,SAAS,sBAAsB,QAA+C;CAC5E,MAAM,UAAA,GAAA,gCAAA,eAAA,CACW,QAAQA,gCAAAA,WAAW,SAAS,MAAA,GAAA,gCAAA,eAAA,CAAoB,QAAQA,gCAAAA,WAAW,KAAK,EAAA,EACtF,KAAK;CACR,OAAO,QAAQ;EAAE;EAAO,MAAM;CAAM,IAAI,KAAA;AAC1C;;AAGA,SAAS,cACP,MACA,QACU;CACV,MAAM,KAAK,QAAQ,CAAC;CACpB,MAAM,2BAAW,IAAI,IAAY;CACjC,KAAK,MAAM,KAAK,QAAQ;EACtB,MAAM,KAAK,KAAK,CAAC;EACjB,KAAK,MAAM,KAAK,OAAO,KAAK,EAAE,GAC5B,IAAI,GAAG,OAAO,GAAG,IACf,SAAS,IAAI,CAAC;EAGlB,KAAK,MAAM,KAAK,OAAO,KAAK,EAAE,GAC5B,IAAI,GAAG,OAAO,GAAG,IACf,SAAS,IAAI,CAAC;CAGpB;CACA,OAAO,CAAC,GAAG,QAAQ;AACrB;AAEA,SAAS,iBACP,MACA,MACA,QACQ;CACR,IAAI,KAAK,WAAW,GAAG,OAAO;CAC9B,MAAM,IAAI,UAAU,CAAC;CAErB,OAAO,GAAG,KAAK,GADD,KAAK,KAAK,MAAM,GAAG,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC,CAAC,KAAK,IACpC,EAAE;AAC1B;;;;;AAMA,SAAS,YAAY,MAAoB,QAAgC;CACvE,MAAM,OAAO,KAAK,SAAS,KAAK;CAChC,IAAI,KAAK,UAAU,KAAA,KAAa,OAAO,OAAO,MAAM,EAAE,UAAU,KAAK,KAAK,GAAG,OAAO,KAAK;CACzF,IAAI,OAAO,OAAO,MAAM,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO;CACtD,MAAM,KAAK,cACT,KAAK,QACL,OAAO,KAAK,MAAM,EAAE,MAAM,CAC5B;CACA,IAAI,GAAG,SAAS,GAAG,OAAO,iBAAiB,MAAM,IAAI,KAAK,MAAM;CAChE,MAAM,KAAK,cACT,KAAK,eACL,OAAO,KAAK,MAAM,EAAE,aAAa,CACnC;CACA,IAAI,GAAG,SAAS,GAAG,OAAO,iBAAiB,MAAM,IAAI,KAAK,aAAa;CACvE,OAAO;AACT;;;;;;;AAQA,SAAgB,YAAY,KAAkB,SAA8C;CAC1F,MAAM,QAAQ,QAAQ;CACtB,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;CAChC,MAAM,OAAO,MAAM;CACnB,IAAI,KAAK,WAAW,GAAG,OAAO,KAAA;CAC9B,MAAM,SAAS,QAAQ,MAAM;CAC7B,OAAO,KAAK,MAAM,MAAM,CAAC,OAAO,SAAS,MAAM,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC;AACzE;AAIA,MAAM,SAAS;AAUf,SAAS,cAAc,OAAc,MAAY,KAAqB;CACpE,IAAI,KAAK,SAAS,QAAQ;EACxB,MAAM,IAAI,MAAM,MAAM;EACtB,OAAO,MAAM,KAAA,IAAY,UAAA,GAAA,gCAAA,iBAAA,EAAA,GAAA,gCAAA,UAAA,CAAoC,CAAC,CAAC;CACjE,OAAO;EACL,MAAM,IAAI,MAAM,QAAQ,IAAI,CAAC,UAAU,KAAK;EAC5C,MAAM,KAAK,KAAK,sBAAsB,CAAC;EACvC,QAAA,GAAA,WAAA,MAAA,CAAa,EAAE,IAAI,UAAA,GAAA,gCAAA,iBAAA,CAA0B,EAAE;CACjD;AACF;AAEA,SAAS,gBAAgB,OAAc,MAAY,KAAiC;CAClF,IAAI,KAAK,SAAS,QAAQ;EACxB,MAAM,IAAI,MAAM,MAAM;EACtB,IAAI,MAAM,KAAA,GAAW,OAAO,KAAA;EAC5B,MAAM,OAAO,MAAM,MAChB,QAAQ,GAAG,MAAM,MAAM,OAAO,EAAA,GAAA,WAAA,MAAA,CAAO,CAAC,CAAC,CAAC,CACxC,KAAK,MAAM,oBAAoB,CAAE,CAAC;EACrC,OAAO,YAAY,oBAAoB,CAAC,GAAG,IAAI;CACjD;CACA,MAAM,IAAI,MAAM,QAAQ,IAAI,CAAC,UAAU,KAAK;CAC5C,MAAM,KAAK,KAAK,sBAAsB,CAAC;CACvC,KAAA,GAAA,WAAA,MAAA,CAAU,EAAE,GAAG,OAAO,KAAA;CAMtB,OAAO,YAAY,IALN,MAAM,QAChB,QAAQ,GAAG,MAAM,MAAM,GAAG,CAAC,CAC3B,KAAK,MAAM,EAAE,UAAU,KAAK,EAAE,CAAC,CAC/B,KAAK,MAAO,IAAI,sBAAsB,CAAC,IAAI,KAAA,CAAU,CAAC,CACtD,QAAQ,MAAyB,EAAA,GAAA,WAAA,MAAA,CAAO,CAAC,CAClB,CAAC;AAC7B;AAEA,SAAS,iBAAiB,OAAc,MAAqB;CAC3D,OAAO,IAAI,IAAI,MAAM,QAAQ,KAAK,GAAG,MAAM,cAAc,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO;AACpF;;AAGA,SAAS,UAAU,OAAc,OAAe,KAAqB;CACnE,MAAM,WAAW,MAAM,MAAM;CAC7B,MAAM,WAAW,MAAM,MAAM,MAAM,EAAE,SAAS,MAAM,IAChD,gBAAgB,OAAO,EAAE,MAAM,OAAO,GAAG,GAAG,IAC5C,KAAA;CACJ,MAAM,OACJ,aAAa,KAAA,KAAa,aAAa,KAAA,IACnC;EAAE,MAAM;EAAU,MAAM;CAAS,IACjC,KAAA;CAEN,MAAM,UAAU,MACb,QAAQ,MAA0C,EAAE,SAAS,QAAQ,CAAC,CACtE,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CACzB,KAAK,MAAM;EACV,MAAM,OAAO,MAAM,QAAQ,IAAI,CAAC,UAAU,EAAE;EAC5C,MAAM,OAAO,gBAAgB,OAAO,GAAG,GAAG;EAC1C,OAAO,SAAS,KAAA,KAAa,SAAS,KAAA,IAAY;GAAE;GAAM;EAAK,IAAI,KAAA;CACrE,CAAC,CAAC,CACD,QAAQ,MAAoC,EAAA,GAAA,WAAA,MAAA,CAAO,CAAC,CAAC;CAGxD,IAAI,SAAS,KAAA,KAAa,QAAQ,WAAW,GAAG,OAAO;CAGvD,OAAO,MAAM,OAAO;EAAE;EAAM;CAAQ,GAAG,MAAM,QAAQ,IAAI,CAAC,KAAK,MAAM,QAAQ,IAAI,KAAK;AACxF;AAEA,SAAS,UAAU,OAAc,OAAyB;CACxD,OAAO,MAAM,QAAQ,KAAK,GAAG,MAAM,UAAU,OAAO,OAAO,CAAC,CAAC;AAC/D;AAEA,SAAS,UAAU,UAA6B;CAC9C,OAAO,IAAI,IAAI,QAAQ,CAAC,CAAC,SAAS,SAAS;AAC7C;;;;;;;;;AAUA,SAAS,aACP,SACA,SACA,QACU;CACV,MAAM,QAAe;EACnB;EACA,OAAO,QAAQ,KAAK,MAClB,EAAE,OAAO,EAAE,SAAS,SAAS,YAAY,EAAE,KAAK,EAAE,OAAO,IAAI,KAAA,CAC/D;EACA;EACA;CACF;CACA,MAAM,SAAS,KAAK,IAAI,GAAG,GAAG,QAAQ,KAAK,MAAM,EAAE,SAAS,UAAU,CAAC,CAAC;CAOxE,MAAM,YAAY,CAJhB,EAAE,MAAM,OAAO,GACf,GAAG,MAAM,KAAK,EAAE,QAAQ,OAAO,IAAI,GAAG,OAAa;EAAE,MAAM;EAAU;CAAE,EAAE,CAGrD,CAAC,CAAC,QACrB,KAAK,SACJ,UAAU,UAAU,OAAO,GAAG,CAAC,KAAK,CAAC,iBAAiB,OAAO,IAAI,IAC7D,OACC,IAAI,KAAK,IAAI,GAAG,MACvB,CAAC,CACH;CAMA,OAAO,UAAU,OALF,UAAU,QAAgB,KAAK,SAAS;EACrD,MAAM,QAAQ,IAAI,QAAQ,MAAM,MAAM,IAAI;EAC1C,OAAO,UAAU,UAAU,OAAO,KAAK,CAAC,IAAI,QAAQ;CACtD,GAAG,SAE0B,CAAC;AAChC;;;;;AAMA,SAAgB,gBACd,SACA,SAA0B,wBAChB;CAMV,MAAM,UAAU,CAAC,GALF,QAAQ,QACpB,KAAK,GAAG,QAAQ,IAAI,IAAI,EAAE,MAAM,CAAC,GAAI,IAAI,IAAI,EAAE,IAAI,KAAK,CAAC,GAAI,GAAG,CAAC,mBAClE,IAAI,IAAI,CAGe,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAA6B,KAAK,SAAS;EAC9E,IAAI,KAAK,SAAS,GAAG,OAAO;EAC5B,MAAM,WAAW,aACf,KAAK,KAAK,MAAM,QAAQ,EAAE,GAC1B,MACA,MACF;EACA,OAAO,KAAK,QAAQ,GAAG,GAAG,MAAM,EAAE,IAAI,GAAG,SAAS,EAAE,GAAG,GAAG;CAC5D,mBAAG,IAAI,IAAI,CAAC;CAEZ,OAAO,QAAQ,KAAK,GAAG,MAAM;EAC3B,MAAM,IAAI,QAAQ,IAAI,CAAC;EACvB,OAAO,IAAI,GAAG,EAAE,KAAK,GAAG,MAAM,EAAE;CAClC,CAAC;AACH"}
@@ -0,0 +1,27 @@
1
+ import { AxisSpec, PColumnSpec } from "@milaboratories/pl-model-common";
2
+
3
+ //#region src/labels/linked_column_postfix.d.ts
4
+ /**
5
+ * One distinguishing piece of the postfix: the full source spec (axis or linker column — no info
6
+ * lost) plus `text`, the minimal text that sets it apart from the siblings (a label, or only the
7
+ * differing domain keys). Custom formatters may use `spec` freely; `text` is the default rendering.
8
+ */
9
+ type LinkerPart<Spec> = {
10
+ spec: Spec;
11
+ text: string;
12
+ };
13
+ /** The distinguishing source of one column: its root axis (when it differs) + the linker chain. */
14
+ type LinkerParts = {
15
+ root?: LinkerPart<AxisSpec>;
16
+ linkers: LinkerPart<PColumnSpec>[];
17
+ };
18
+ /**
19
+ * Renders the postfix zone from the distinguishing sources. Default:
20
+ * `via ${[root, linkers.join(" > ")].filter(Boolean).join(" ")}` (using each piece's `text`).
21
+ * Returning `undefined` — or the empty string `""` — suppresses the postfix entirely (the column
22
+ * keeps just its stem); the two are treated identically.
23
+ */
24
+ type LinkerFormatter = (parts: LinkerParts, hit: PColumnSpec | undefined, index: number) => string | undefined;
25
+ //#endregion
26
+ export { LinkerFormatter, LinkerPart, LinkerParts };
27
+ //# sourceMappingURL=linked_column_postfix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linked_column_postfix.d.ts","names":[],"sources":["../../src/labels/linked_column_postfix.ts"],"mappings":";;;;;AA2CiC;AASjC;;KAdY,UAAA;EAAqB,IAAA,EAAM,IAAI;EAAE,IAAA;AAAA;;KAGjC,WAAA;EACV,IAAA,GAAO,UAAA,CAAW,QAAA;EAClB,OAAA,EAAS,UAAA,CAAW,WAAA;AAAA;;;;;;;KASV,eAAA,IACV,KAAA,EAAO,WAAA,EACP,GAAA,EAAK,WAAW,cAChB,KAAA"}