@platforma-sdk/model 1.75.8 → 1.75.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/labels/derive_distinct_labels.cjs +58 -23
- package/dist/labels/derive_distinct_labels.cjs.map +1 -1
- package/dist/labels/derive_distinct_labels.js +58 -23
- package/dist/labels/derive_distinct_labels.js.map +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.js +1 -1
- package/package.json +6 -6
- package/src/labels/derive_distinct_labels.test.ts +22 -0
- package/src/labels/derive_distinct_labels.ts +101 -31
|
@@ -40,18 +40,22 @@ function deriveDistinctLabels(values, options = {}) {
|
|
|
40
40
|
for (let i = 0; i < includedCount; ++i) currentSet.add(mainTypes[i]);
|
|
41
41
|
if (additionalType >= 0) currentSet.add(mainTypes[additionalType]);
|
|
42
42
|
const candidateResult = build(currentSet, false);
|
|
43
|
-
if (candidateResult !== void 0 && countUniqueLabels(candidateResult) === values.length)
|
|
43
|
+
if (candidateResult !== void 0 && countUniqueLabels(candidateResult) === values.length) {
|
|
44
|
+
const minimized = minimizeTypeSet(currentSet, records, stats, forceTraceElements, forcedSet, separator);
|
|
45
|
+
return dropRedundantLinkerSuffix(records, minimized, forceTraceElements, forcedSet, separator, build(minimized, false) ?? (0, _milaboratories_helpers.throwError)("Failed to derive unique labels"));
|
|
46
|
+
}
|
|
44
47
|
additionalType++;
|
|
45
48
|
if (additionalType >= mainTypes.length) {
|
|
46
49
|
includedCount++;
|
|
47
50
|
additionalType = includedCount;
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
|
-
|
|
53
|
+
const minimized = minimizeTypeSet(new Set([
|
|
51
54
|
...forcedSet,
|
|
52
55
|
...mainTypes,
|
|
53
56
|
...secondaryTypes
|
|
54
|
-
]), records, stats, forceTraceElements, forcedSet, separator)
|
|
57
|
+
]), records, stats, forceTraceElements, forcedSet, separator);
|
|
58
|
+
return dropRedundantLinkerSuffix(records, minimized, forceTraceElements, forcedSet, separator, build(minimized, true) ?? (0, _milaboratories_helpers.throwError)("Failed to derive unique labels"));
|
|
55
59
|
}
|
|
56
60
|
function extractEntryParts(entry) {
|
|
57
61
|
if (!("spec" in entry && typeof entry.spec === "object")) return {
|
|
@@ -184,36 +188,67 @@ function classifyTypes(stats, totalRecords) {
|
|
|
184
188
|
secondaryTypes
|
|
185
189
|
};
|
|
186
190
|
}
|
|
191
|
+
function renderRecordLabel(record, includedTypes, forceTraceElements, separator) {
|
|
192
|
+
const traceParts = [];
|
|
193
|
+
const anchorParts = [];
|
|
194
|
+
let linkerLabel;
|
|
195
|
+
let hitLabel;
|
|
196
|
+
for (const ft of record.fullTrace) {
|
|
197
|
+
if (!(includedTypes.has(ft.fullType) || forceTraceElements?.has(ft.type))) continue;
|
|
198
|
+
if (ft.type === LINKER_TYPE) linkerLabel = ft.label;
|
|
199
|
+
else if (ft.type === HIT_QUAL_TYPE) hitLabel = ft.label;
|
|
200
|
+
else if (isAnchorQualType(ft.type)) anchorParts.push(ft.label);
|
|
201
|
+
else traceParts.push(ft.label);
|
|
202
|
+
}
|
|
203
|
+
if (traceParts.length === 0 && anchorParts.length === 0 && linkerLabel === void 0 && hitLabel === void 0) return void 0;
|
|
204
|
+
let label = traceParts.join(separator);
|
|
205
|
+
const append = (part) => {
|
|
206
|
+
label = label.length === 0 ? part : `${label} ${part}`;
|
|
207
|
+
};
|
|
208
|
+
if (linkerLabel !== void 0) append(linkerLabel);
|
|
209
|
+
for (const a of anchorParts) append(a);
|
|
210
|
+
if (hitLabel !== void 0) append(hitLabel);
|
|
211
|
+
return label;
|
|
212
|
+
}
|
|
187
213
|
function buildLabels(records, includedTypes, forceTraceElements, separator, force) {
|
|
188
214
|
const result = [];
|
|
189
215
|
for (const r of records) {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
let linkerLabel;
|
|
193
|
-
let hitLabel;
|
|
194
|
-
for (const ft of r.fullTrace) {
|
|
195
|
-
if (!(includedTypes.has(ft.fullType) || forceTraceElements?.has(ft.type))) continue;
|
|
196
|
-
if (ft.type === LINKER_TYPE) linkerLabel = ft.label;
|
|
197
|
-
else if (ft.type === HIT_QUAL_TYPE) hitLabel = ft.label;
|
|
198
|
-
else if (isAnchorQualType(ft.type)) anchorParts.push(ft.label);
|
|
199
|
-
else traceParts.push(ft.label);
|
|
200
|
-
}
|
|
201
|
-
if (traceParts.length === 0 && anchorParts.length === 0 && linkerLabel === void 0 && hitLabel === void 0) {
|
|
216
|
+
const rendered = renderRecordLabel(r, includedTypes, forceTraceElements, separator);
|
|
217
|
+
if (rendered === void 0) {
|
|
202
218
|
if (!force) return void 0;
|
|
203
219
|
result.push("Unlabeled");
|
|
204
220
|
continue;
|
|
205
221
|
}
|
|
206
|
-
|
|
207
|
-
const append = (part) => {
|
|
208
|
-
label = label.length === 0 ? part : `${label} ${part}`;
|
|
209
|
-
};
|
|
210
|
-
if (linkerLabel !== void 0) append(linkerLabel);
|
|
211
|
-
for (const a of anchorParts) append(a);
|
|
212
|
-
if (hitLabel !== void 0) append(hitLabel);
|
|
213
|
-
result.push(label);
|
|
222
|
+
result.push(rendered);
|
|
214
223
|
}
|
|
215
224
|
return result;
|
|
216
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Drop the "via …" linker suffix from records whose label is already unique without it.
|
|
228
|
+
*
|
|
229
|
+
* Global minimization may include `LINKER_TYPE_FULL` solely to resolve a collision between a
|
|
230
|
+
* subset of records — but `buildLabels` then renders the suffix on every record that carries a
|
|
231
|
+
* linker trace entry, including ones whose stem is already unique. We strip the suffix where it
|
|
232
|
+
* isn't load-bearing while keeping the symmetric rendering required by `linkerForced` /
|
|
233
|
+
* `forceTraceElements`.
|
|
234
|
+
*
|
|
235
|
+
* Rule: a record's linker suffix is redundant iff its stem (label rendered without LINKER) does
|
|
236
|
+
* not appear anywhere else in the set.
|
|
237
|
+
*/
|
|
238
|
+
function dropRedundantLinkerSuffix(records, globalTypeSet, forceTraceElements, forcedSet, separator, labels) {
|
|
239
|
+
if (!globalTypeSet.has(LINKER_TYPE_FULL)) return labels;
|
|
240
|
+
if (forcedSet.has(LINKER_TYPE_FULL) || forceTraceElements?.has(LINKER_TYPE)) return labels;
|
|
241
|
+
const setWithoutLinker = new Set(globalTypeSet);
|
|
242
|
+
setWithoutLinker.delete(LINKER_TYPE_FULL);
|
|
243
|
+
const stems = records.map((r) => renderRecordLabel(r, setWithoutLinker, forceTraceElements, separator));
|
|
244
|
+
const stemOccurrences = /* @__PURE__ */ new Map();
|
|
245
|
+
for (const s of stems) if (s !== void 0) stemOccurrences.set(s, (stemOccurrences.get(s) ?? 0) + 1);
|
|
246
|
+
return labels.map((label, i) => {
|
|
247
|
+
const stem = stems[i];
|
|
248
|
+
if (stem === void 0) return label;
|
|
249
|
+
return stemOccurrences.get(stem) === 1 ? stem : label;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
217
252
|
function countUniqueLabels(result) {
|
|
218
253
|
if (result === void 0) return 0;
|
|
219
254
|
return new Set(result).size;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"derive_distinct_labels.cjs","names":["Annotation"],"sources":["../../src/labels/derive_distinct_labels.ts"],"sourcesContent":["import {\n Annotation,\n parseJson,\n readAnnotation,\n type AxisQualification,\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 type { MatchQualifications } from \"../columns/column_collection_builder\";\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 return build(minimized, false) ?? throwError(\"Failed to derive unique labels\");\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 return build(minimized, true) ?? throwError(\"Failed to derive unique labels\");\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 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 traceParts: string[] = [];\n const anchorParts: string[] = [];\n let linkerLabel: string | undefined;\n let hitLabel: string | undefined;\n\n for (const ft of r.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) {\n if (!force) return undefined;\n result.push(\"Unlabeled\");\n continue;\n }\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 result.push(label);\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"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"derive_distinct_labels.cjs","names":["Annotation"],"sources":["../../src/labels/derive_distinct_labels.ts"],"sourcesContent":["import {\n Annotation,\n parseJson,\n readAnnotation,\n type AxisQualification,\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 type { MatchQualifications } from \"../columns/column_collection_builder\";\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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"}
|
|
@@ -39,18 +39,22 @@ function deriveDistinctLabels(values, options = {}) {
|
|
|
39
39
|
for (let i = 0; i < includedCount; ++i) currentSet.add(mainTypes[i]);
|
|
40
40
|
if (additionalType >= 0) currentSet.add(mainTypes[additionalType]);
|
|
41
41
|
const candidateResult = build(currentSet, false);
|
|
42
|
-
if (candidateResult !== void 0 && countUniqueLabels(candidateResult) === values.length)
|
|
42
|
+
if (candidateResult !== void 0 && countUniqueLabels(candidateResult) === values.length) {
|
|
43
|
+
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"));
|
|
45
|
+
}
|
|
43
46
|
additionalType++;
|
|
44
47
|
if (additionalType >= mainTypes.length) {
|
|
45
48
|
includedCount++;
|
|
46
49
|
additionalType = includedCount;
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
|
-
|
|
52
|
+
const minimized = minimizeTypeSet(new Set([
|
|
50
53
|
...forcedSet,
|
|
51
54
|
...mainTypes,
|
|
52
55
|
...secondaryTypes
|
|
53
|
-
]), records, stats, forceTraceElements, forcedSet, separator)
|
|
56
|
+
]), records, stats, forceTraceElements, forcedSet, separator);
|
|
57
|
+
return dropRedundantLinkerSuffix(records, minimized, forceTraceElements, forcedSet, separator, build(minimized, true) ?? throwError("Failed to derive unique labels"));
|
|
54
58
|
}
|
|
55
59
|
function extractEntryParts(entry) {
|
|
56
60
|
if (!("spec" in entry && typeof entry.spec === "object")) return {
|
|
@@ -183,36 +187,67 @@ function classifyTypes(stats, totalRecords) {
|
|
|
183
187
|
secondaryTypes
|
|
184
188
|
};
|
|
185
189
|
}
|
|
190
|
+
function renderRecordLabel(record, includedTypes, forceTraceElements, separator) {
|
|
191
|
+
const traceParts = [];
|
|
192
|
+
const anchorParts = [];
|
|
193
|
+
let linkerLabel;
|
|
194
|
+
let hitLabel;
|
|
195
|
+
for (const ft of record.fullTrace) {
|
|
196
|
+
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;
|
|
199
|
+
else if (isAnchorQualType(ft.type)) anchorParts.push(ft.label);
|
|
200
|
+
else traceParts.push(ft.label);
|
|
201
|
+
}
|
|
202
|
+
if (traceParts.length === 0 && anchorParts.length === 0 && linkerLabel === void 0 && hitLabel === void 0) return void 0;
|
|
203
|
+
let label = traceParts.join(separator);
|
|
204
|
+
const append = (part) => {
|
|
205
|
+
label = label.length === 0 ? part : `${label} ${part}`;
|
|
206
|
+
};
|
|
207
|
+
if (linkerLabel !== void 0) append(linkerLabel);
|
|
208
|
+
for (const a of anchorParts) append(a);
|
|
209
|
+
if (hitLabel !== void 0) append(hitLabel);
|
|
210
|
+
return label;
|
|
211
|
+
}
|
|
186
212
|
function buildLabels(records, includedTypes, forceTraceElements, separator, force) {
|
|
187
213
|
const result = [];
|
|
188
214
|
for (const r of records) {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
let linkerLabel;
|
|
192
|
-
let hitLabel;
|
|
193
|
-
for (const ft of r.fullTrace) {
|
|
194
|
-
if (!(includedTypes.has(ft.fullType) || forceTraceElements?.has(ft.type))) continue;
|
|
195
|
-
if (ft.type === LINKER_TYPE) linkerLabel = ft.label;
|
|
196
|
-
else if (ft.type === HIT_QUAL_TYPE) hitLabel = ft.label;
|
|
197
|
-
else if (isAnchorQualType(ft.type)) anchorParts.push(ft.label);
|
|
198
|
-
else traceParts.push(ft.label);
|
|
199
|
-
}
|
|
200
|
-
if (traceParts.length === 0 && anchorParts.length === 0 && linkerLabel === void 0 && hitLabel === void 0) {
|
|
215
|
+
const rendered = renderRecordLabel(r, includedTypes, forceTraceElements, separator);
|
|
216
|
+
if (rendered === void 0) {
|
|
201
217
|
if (!force) return void 0;
|
|
202
218
|
result.push("Unlabeled");
|
|
203
219
|
continue;
|
|
204
220
|
}
|
|
205
|
-
|
|
206
|
-
const append = (part) => {
|
|
207
|
-
label = label.length === 0 ? part : `${label} ${part}`;
|
|
208
|
-
};
|
|
209
|
-
if (linkerLabel !== void 0) append(linkerLabel);
|
|
210
|
-
for (const a of anchorParts) append(a);
|
|
211
|
-
if (hitLabel !== void 0) append(hitLabel);
|
|
212
|
-
result.push(label);
|
|
221
|
+
result.push(rendered);
|
|
213
222
|
}
|
|
214
223
|
return result;
|
|
215
224
|
}
|
|
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
|
+
}
|
|
216
251
|
function countUniqueLabels(result) {
|
|
217
252
|
if (result === void 0) return 0;
|
|
218
253
|
return new Set(result).size;
|
|
@@ -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 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 type { MatchQualifications } from \"../columns/column_collection_builder\";\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 return build(minimized, false) ?? throwError(\"Failed to derive unique labels\");\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 return build(minimized, true) ?? throwError(\"Failed to derive unique labels\");\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 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 traceParts: string[] = [];\n const anchorParts: string[] = [];\n let linkerLabel: string | undefined;\n let hitLabel: string | undefined;\n\n for (const ft of r.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) {\n if (!force) return undefined;\n result.push(\"Unlabeled\");\n continue;\n }\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 result.push(label);\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"],"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 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 type { MatchQualifications } from \"../columns/column_collection_builder\";\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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"}
|
package/dist/package.cjs
CHANGED
package/dist/package.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platforma-sdk/model",
|
|
3
|
-
"version": "1.75.
|
|
3
|
+
"version": "1.75.10",
|
|
4
4
|
"description": "Platforma.bio SDK / Block Model",
|
|
5
5
|
"files": [
|
|
6
6
|
"./dist/**/*",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"utility-types": "^3.11.0",
|
|
32
32
|
"zod": "~3.25.76",
|
|
33
33
|
"@milaboratories/helpers": "1.14.2",
|
|
34
|
-
"@milaboratories/pl-model-common": "1.41.2",
|
|
35
|
-
"@milaboratories/pl-model-middle-layer": "1.19.3",
|
|
36
34
|
"@milaboratories/pl-error-like": "1.12.10",
|
|
35
|
+
"@milaboratories/pl-model-middle-layer": "1.19.3",
|
|
36
|
+
"@milaboratories/pl-model-common": "1.41.2",
|
|
37
37
|
"@milaboratories/ptabler-expression-js": "1.2.24"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"fast-json-patch": "^3.1.1",
|
|
42
42
|
"typescript": "~5.9.3",
|
|
43
43
|
"vitest": "^4.1.3",
|
|
44
|
+
"@milaboratories/ts-builder": "1.4.0",
|
|
45
|
+
"@milaboratories/build-configs": "2.0.0",
|
|
44
46
|
"@milaboratories/pf-spec-driver": "1.3.15",
|
|
45
47
|
"@milaboratories/ts-configs": "1.2.3",
|
|
46
|
-
"@milaboratories/
|
|
47
|
-
"@milaboratories/pf-driver": "1.4.10",
|
|
48
|
-
"@milaboratories/ts-builder": "1.4.0"
|
|
48
|
+
"@milaboratories/pf-driver": "1.4.10"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "ts-builder build --target node",
|
|
@@ -706,6 +706,28 @@ describe("deriveDistinctLabels v2 — linker path & qualifications", () => {
|
|
|
706
706
|
expect(deriveDistinctLabels(entries)).toEqual(["Read counts", "Read counts via Sample mapper"]);
|
|
707
707
|
});
|
|
708
708
|
|
|
709
|
+
test("linker suffix is NOT added to records whose native label is already unique, even when other records collide", () => {
|
|
710
|
+
// Repro for "Cluster Id via Clone to cluster link" bug:
|
|
711
|
+
// - "Representative Sequence" exists both as a direct column and via a linker → collision
|
|
712
|
+
// forces algorithm to include LINKER_TYPE in the type set.
|
|
713
|
+
// - As a side effect, every linked entry gets the "via …" suffix appended — including
|
|
714
|
+
// ones whose native label ("Cluster Id") is unique and needs no disambiguation.
|
|
715
|
+
const linker = linkerSpec("Clone to cluster link");
|
|
716
|
+
const entries: Entry[] = [
|
|
717
|
+
{ spec: labeledSpec("Representative Sequence", "rep_direct") },
|
|
718
|
+
{
|
|
719
|
+
spec: labeledSpec("Representative Sequence", "rep_linked"),
|
|
720
|
+
linkerPath: [{ spec: linker }],
|
|
721
|
+
},
|
|
722
|
+
{ spec: labeledSpec("Cluster Id", "cluster_id"), linkerPath: [{ spec: linker }] },
|
|
723
|
+
];
|
|
724
|
+
expect(deriveDistinctLabels(entries)).toEqual([
|
|
725
|
+
"Representative Sequence",
|
|
726
|
+
"Representative Sequence via Clone to cluster link",
|
|
727
|
+
"Cluster Id",
|
|
728
|
+
]);
|
|
729
|
+
});
|
|
730
|
+
|
|
709
731
|
test("two linker paths → both get distinguishing via-suffix", () => {
|
|
710
732
|
const s = labeledSpec("Counts");
|
|
711
733
|
const entries: Entry[] = [
|
|
@@ -152,7 +152,16 @@ export function deriveDistinctLabels(values: Entry[], options: DeriveLabelsOptio
|
|
|
152
152
|
forcedSet,
|
|
153
153
|
separator,
|
|
154
154
|
);
|
|
155
|
-
|
|
155
|
+
const minimizedLabels =
|
|
156
|
+
build(minimized, false) ?? throwError("Failed to derive unique labels");
|
|
157
|
+
return dropRedundantLinkerSuffix(
|
|
158
|
+
records,
|
|
159
|
+
minimized,
|
|
160
|
+
forceTraceElements,
|
|
161
|
+
forcedSet,
|
|
162
|
+
separator,
|
|
163
|
+
minimizedLabels,
|
|
164
|
+
);
|
|
156
165
|
}
|
|
157
166
|
|
|
158
167
|
additionalType++;
|
|
@@ -171,7 +180,15 @@ export function deriveDistinctLabels(values: Entry[], options: DeriveLabelsOptio
|
|
|
171
180
|
forcedSet,
|
|
172
181
|
separator,
|
|
173
182
|
);
|
|
174
|
-
|
|
183
|
+
const minimizedLabels = build(minimized, true) ?? throwError("Failed to derive unique labels");
|
|
184
|
+
return dropRedundantLinkerSuffix(
|
|
185
|
+
records,
|
|
186
|
+
minimized,
|
|
187
|
+
forceTraceElements,
|
|
188
|
+
forcedSet,
|
|
189
|
+
separator,
|
|
190
|
+
minimizedLabels,
|
|
191
|
+
);
|
|
175
192
|
}
|
|
176
193
|
|
|
177
194
|
// --- Pure helpers ---
|
|
@@ -359,6 +376,44 @@ function classifyTypes(
|
|
|
359
376
|
return { mainTypes, secondaryTypes };
|
|
360
377
|
}
|
|
361
378
|
|
|
379
|
+
function renderRecordLabel(
|
|
380
|
+
record: EnrichedRecord,
|
|
381
|
+
includedTypes: Set<string>,
|
|
382
|
+
forceTraceElements: Set<string> | undefined,
|
|
383
|
+
separator: string,
|
|
384
|
+
): string | undefined {
|
|
385
|
+
const traceParts: string[] = [];
|
|
386
|
+
const anchorParts: string[] = [];
|
|
387
|
+
let linkerLabel: string | undefined;
|
|
388
|
+
let hitLabel: string | undefined;
|
|
389
|
+
|
|
390
|
+
for (const ft of record.fullTrace) {
|
|
391
|
+
if (!(includedTypes.has(ft.fullType) || forceTraceElements?.has(ft.type))) continue;
|
|
392
|
+
if (ft.type === LINKER_TYPE) linkerLabel = ft.label;
|
|
393
|
+
else if (ft.type === HIT_QUAL_TYPE) hitLabel = ft.label;
|
|
394
|
+
else if (isAnchorQualType(ft.type)) anchorParts.push(ft.label);
|
|
395
|
+
else traceParts.push(ft.label);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const isEmpty =
|
|
399
|
+
traceParts.length === 0 &&
|
|
400
|
+
anchorParts.length === 0 &&
|
|
401
|
+
linkerLabel === undefined &&
|
|
402
|
+
hitLabel === undefined;
|
|
403
|
+
|
|
404
|
+
if (isEmpty) return undefined;
|
|
405
|
+
|
|
406
|
+
let label = traceParts.join(separator);
|
|
407
|
+
const append = (part: string) => {
|
|
408
|
+
label = label.length === 0 ? part : `${label} ${part}`;
|
|
409
|
+
};
|
|
410
|
+
if (linkerLabel !== undefined) append(linkerLabel);
|
|
411
|
+
for (const a of anchorParts) append(a);
|
|
412
|
+
if (hitLabel !== undefined) append(hitLabel);
|
|
413
|
+
|
|
414
|
+
return label;
|
|
415
|
+
}
|
|
416
|
+
|
|
362
417
|
function buildLabels(
|
|
363
418
|
records: EnrichedRecord[],
|
|
364
419
|
includedTypes: Set<string>,
|
|
@@ -369,43 +424,58 @@ function buildLabels(
|
|
|
369
424
|
const result: string[] = [];
|
|
370
425
|
|
|
371
426
|
for (const r of records) {
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
let linkerLabel: string | undefined;
|
|
375
|
-
let hitLabel: string | undefined;
|
|
376
|
-
|
|
377
|
-
for (const ft of r.fullTrace) {
|
|
378
|
-
if (!(includedTypes.has(ft.fullType) || forceTraceElements?.has(ft.type))) continue;
|
|
379
|
-
if (ft.type === LINKER_TYPE) linkerLabel = ft.label;
|
|
380
|
-
else if (ft.type === HIT_QUAL_TYPE) hitLabel = ft.label;
|
|
381
|
-
else if (isAnchorQualType(ft.type)) anchorParts.push(ft.label);
|
|
382
|
-
else traceParts.push(ft.label);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const isEmpty =
|
|
386
|
-
traceParts.length === 0 &&
|
|
387
|
-
anchorParts.length === 0 &&
|
|
388
|
-
linkerLabel === undefined &&
|
|
389
|
-
hitLabel === undefined;
|
|
390
|
-
|
|
391
|
-
if (isEmpty) {
|
|
427
|
+
const rendered = renderRecordLabel(r, includedTypes, forceTraceElements, separator);
|
|
428
|
+
if (rendered === undefined) {
|
|
392
429
|
if (!force) return undefined;
|
|
393
430
|
result.push("Unlabeled");
|
|
394
431
|
continue;
|
|
395
432
|
}
|
|
433
|
+
result.push(rendered);
|
|
434
|
+
}
|
|
396
435
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Drop the "via …" linker suffix from records whose label is already unique without it.
|
|
441
|
+
*
|
|
442
|
+
* Global minimization may include `LINKER_TYPE_FULL` solely to resolve a collision between a
|
|
443
|
+
* subset of records — but `buildLabels` then renders the suffix on every record that carries a
|
|
444
|
+
* linker trace entry, including ones whose stem is already unique. We strip the suffix where it
|
|
445
|
+
* isn't load-bearing while keeping the symmetric rendering required by `linkerForced` /
|
|
446
|
+
* `forceTraceElements`.
|
|
447
|
+
*
|
|
448
|
+
* Rule: a record's linker suffix is redundant iff its stem (label rendered without LINKER) does
|
|
449
|
+
* not appear anywhere else in the set.
|
|
450
|
+
*/
|
|
451
|
+
function dropRedundantLinkerSuffix(
|
|
452
|
+
records: EnrichedRecord[],
|
|
453
|
+
globalTypeSet: Set<string>,
|
|
454
|
+
forceTraceElements: Set<string> | undefined,
|
|
455
|
+
forcedSet: Set<string>,
|
|
456
|
+
separator: string,
|
|
457
|
+
labels: string[],
|
|
458
|
+
): string[] {
|
|
459
|
+
if (!globalTypeSet.has(LINKER_TYPE_FULL)) return labels;
|
|
460
|
+
if (forcedSet.has(LINKER_TYPE_FULL) || forceTraceElements?.has(LINKER_TYPE)) return labels;
|
|
461
|
+
|
|
462
|
+
const setWithoutLinker = new Set(globalTypeSet);
|
|
463
|
+
setWithoutLinker.delete(LINKER_TYPE_FULL);
|
|
404
464
|
|
|
405
|
-
|
|
465
|
+
const stems = records.map((r) =>
|
|
466
|
+
renderRecordLabel(r, setWithoutLinker, forceTraceElements, separator),
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
const stemOccurrences = new Map<string, number>();
|
|
470
|
+
for (const s of stems) {
|
|
471
|
+
if (s !== undefined) stemOccurrences.set(s, (stemOccurrences.get(s) ?? 0) + 1);
|
|
406
472
|
}
|
|
407
473
|
|
|
408
|
-
return
|
|
474
|
+
return labels.map((label, i) => {
|
|
475
|
+
const stem = stems[i];
|
|
476
|
+
if (stem === undefined) return label;
|
|
477
|
+
return stemOccurrences.get(stem) === 1 ? stem : label;
|
|
478
|
+
});
|
|
409
479
|
}
|
|
410
480
|
|
|
411
481
|
function countUniqueLabels(result: string[] | undefined): number {
|