@bitcobblers/wod-wiki-lang 0.50.2186 → 0.50.2188

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/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { wallClockNow, formatDurationSmart } from './chunk-PFQDVQQO.mjs';
2
2
  export { createBrowserRuntimeNow, frozenNow, getRuntimeNowMs, wallClockNow } from './chunk-PFQDVQQO.mjs';
3
- import { MetricType, MetricContainer, BlockKey, REFERENCE_METADATA_KEY, ParsedCodeStatement, TAG_REFERENCE_METADATA_KEY, getReferenceMetadata, OutputStatement, valueKindOf, normalizeFieldPath, getTagReference } from '@bitcobblers/wod-wiki-core';
3
+ import { MetricType, MetricContainer, BlockKey, REFERENCE_METADATA_KEY, ParsedCodeStatement, getReferenceMetadata, TAG_REFERENCE_METADATA_KEY, OutputStatement, valueKindOf, normalizeFieldPath, getTagReference } from '@bitcobblers/wod-wiki-core';
4
4
  import { LRParser } from '@lezer/lr';
5
5
  import { LRLanguage, LanguageSupport, syntaxTree } from '@codemirror/language';
6
6
  import { NodeProp } from '@lezer/common';
@@ -511,6 +511,9 @@ var EffortMetric = class {
511
511
  this.effort = effort;
512
512
  this.value = options?.resolvedSlug ?? effort;
513
513
  this.image = effort;
514
+ if (options?.canonicalLabel) {
515
+ this.name = options.canonicalLabel;
516
+ }
514
517
  if (options?.references) {
515
518
  this.metadata = { [REFERENCE_METADATA_KEY]: options.references };
516
519
  }
@@ -518,6 +521,7 @@ var EffortMetric = class {
518
521
  effort;
519
522
  value;
520
523
  image;
524
+ name;
521
525
  origin = "parser";
522
526
  metadata;
523
527
  type = MetricType.Effort;
@@ -1531,11 +1535,12 @@ var EquipmentMetric = class {
1531
1535
  /** Canonical slugs, ordered and deduplicated. */
1532
1536
  value;
1533
1537
  image;
1534
- name = "equipment";
1538
+ name;
1535
1539
  metadata;
1536
1540
  constructor(slugs, options) {
1537
1541
  this.value = [...new Set(slugs)];
1538
1542
  this.image = options?.source ?? this.value.join(" ");
1543
+ this.name = options?.refs?.length ? options.refs.map((r) => r.label).join(", ") : "equipment";
1539
1544
  this.origin = options?.origin ?? "parser";
1540
1545
  const reference = {
1541
1546
  matched: options?.matched ?? "whole",
@@ -1679,9 +1684,14 @@ function classifyStatements(facts, references) {
1679
1684
  metricPairs.push(slot.pair);
1680
1685
  }
1681
1686
  const mergedPairs = mergeFragments(metricPairs);
1682
- const finalPairs = references ? applyReferenceResolution(mergedPairs, fact, references) : mergedPairs;
1687
+ const finalPairs = references?.index ? applyReferenceResolution(mergedPairs, fact, references) : mergedPairs;
1683
1688
  statement.metrics = finalPairs.map((pair) => pair.metrics);
1684
1689
  statement.metricMeta = new Map(finalPairs.map((pair) => [pair.metrics, pair.meta]));
1690
+ const effortMetric = finalPairs.find((p) => p.metrics.type === MetricType.Effort)?.metrics;
1691
+ const effortRef = getReferenceMetadata(effortMetric?.metadata)?.refs?.find((r) => r.kind === "effort");
1692
+ if (effortRef?.slug) {
1693
+ statement.exerciseId = effortRef.slug;
1694
+ }
1685
1695
  statement.id = fact.id;
1686
1696
  statement.line = fact.line;
1687
1697
  statement.text = fact.meta.raw;
@@ -1709,33 +1719,34 @@ function equipmentMeta(resolution, authored, meta2) {
1709
1719
  };
1710
1720
  }
1711
1721
  function applyReferenceResolution(pairs, fact, references) {
1712
- const { index, defaults } = references;
1722
+ const index = references.index;
1723
+ const { defaults } = references;
1713
1724
  const result = [];
1714
1725
  const emitResolution = (resolution, authored, meta2) => {
1715
1726
  if (resolution.effortRef || resolution.matched !== "none" || resolution.diagnostics.length > 0) {
1716
1727
  result.push({
1717
1728
  metrics: new EffortMetric(authored, {
1718
1729
  resolvedSlug: resolution.effortRef?.slug,
1730
+ canonicalLabel: resolution.effortRef?.label,
1719
1731
  references: referenceMetadata(resolution)
1720
1732
  }),
1721
1733
  meta: meta2
1722
1734
  });
1723
1735
  }
1724
1736
  if (resolution.equipment.length > 0) {
1737
+ const eqMeta = equipmentMeta(resolution, authored, meta2);
1738
+ const eqSource = resolution.matched === "split" ? eqMeta.raw : resolution.matched === "explicit" ? resolution.equipment.map((ref) => ref.label).join(", ") : authored;
1725
1739
  result.push({
1726
1740
  metrics: new EquipmentMetric(
1727
1741
  resolution.equipment.map((ref) => ref.slug),
1728
1742
  {
1729
1743
  refs: resolution.equipment,
1730
- source: authored,
1744
+ source: eqSource,
1731
1745
  matched: resolution.matched,
1732
1746
  diagnostics: resolution.diagnostics
1733
1747
  }
1734
1748
  ),
1735
- // The equipment is the prefix of the authored label; the effort is
1736
- // what follows. Marking the whole phrase for both would let one mark
1737
- // hide the other.
1738
- meta: equipmentMeta(resolution, authored, meta2)
1749
+ meta: eqMeta
1739
1750
  });
1740
1751
  }
1741
1752
  };
@@ -1810,6 +1821,64 @@ function applyReferenceResolution(pairs, fact, references) {
1810
1821
  result.push(...malformedHints);
1811
1822
  return result;
1812
1823
  }
1824
+ function resolveFusedEffortReferences(statements, references) {
1825
+ if (!references?.index) return;
1826
+ const { index, defaults } = references;
1827
+ for (const statement of statements) {
1828
+ const current = statement.metrics.toArray();
1829
+ if (!current.some((m) => m.type === MetricType.Effort && !m.metadata)) continue;
1830
+ const nextMetrics = [];
1831
+ const nextMeta = new Map(statement.metricMeta);
1832
+ for (const metric of current) {
1833
+ if (metric.type !== MetricType.Effort || metric.metadata) {
1834
+ nextMetrics.push(metric);
1835
+ continue;
1836
+ }
1837
+ const authored = metric instanceof EffortMetric ? metric.effort : String(metric.value ?? "");
1838
+ const normalized = authored.trim().toLowerCase();
1839
+ if (STRUCTURAL_EFFORT_WORDS.has(normalized) || STANDARD_UNITS2.has(normalized)) {
1840
+ nextMetrics.push(metric);
1841
+ continue;
1842
+ }
1843
+ const meta2 = statement.metricMeta?.get(metric);
1844
+ const resolution = index.resolve(authored, defaults);
1845
+ if (resolution.matched === "none" && resolution.diagnostics.length === 0) {
1846
+ nextMetrics.push(metric);
1847
+ continue;
1848
+ }
1849
+ nextMeta.delete(metric);
1850
+ if (resolution.effortRef || resolution.matched !== "none" || resolution.diagnostics.length > 0) {
1851
+ const resolvedEffort = new EffortMetric(authored, {
1852
+ resolvedSlug: resolution.effortRef?.slug,
1853
+ canonicalLabel: resolution.effortRef?.label,
1854
+ references: referenceMetadata(resolution)
1855
+ });
1856
+ nextMetrics.push(resolvedEffort);
1857
+ if (meta2) nextMeta.set(resolvedEffort, meta2);
1858
+ if (resolution.effortRef?.slug) {
1859
+ statement.exerciseId = resolution.effortRef.slug;
1860
+ }
1861
+ }
1862
+ if (resolution.equipment.length > 0) {
1863
+ const eqMeta = meta2 ? equipmentMeta(resolution, authored, meta2) : void 0;
1864
+ const eqSource = resolution.matched === "split" && eqMeta ? eqMeta.raw : authored;
1865
+ const eqMetric = new EquipmentMetric(
1866
+ resolution.equipment.map((ref) => ref.slug),
1867
+ {
1868
+ refs: resolution.equipment,
1869
+ source: eqSource,
1870
+ matched: resolution.matched,
1871
+ diagnostics: resolution.diagnostics
1872
+ }
1873
+ );
1874
+ nextMetrics.push(eqMetric);
1875
+ if (eqMeta) nextMeta.set(eqMetric, eqMeta);
1876
+ }
1877
+ }
1878
+ statement.metrics.clear().add(...nextMetrics);
1879
+ statement.metricMeta = nextMeta;
1880
+ }
1881
+ }
1813
1882
  function classifyPrimitive(primitive) {
1814
1883
  switch (primitive.kind) {
1815
1884
  case "lap":
@@ -2427,6 +2496,7 @@ function extractStatements(sourceOrTree, sportOrTree, sportArg, references) {
2427
2496
  const facts = extractSyntaxFacts(resolved.source, resolved.tree);
2428
2497
  const statements = classifyStatements(facts, references);
2429
2498
  dialectStack.processAll(statements, sport);
2499
+ resolveFusedEffortReferences(statements, references);
2430
2500
  return normalizeInlineRounds(statements);
2431
2501
  }
2432
2502
  function isInlineRoundsPrescription(stmt) {
@@ -2548,557 +2618,87 @@ var WhiteboardScript = class {
2548
2618
  }
2549
2619
  };
2550
2620
 
2551
- // src/effort-registry/types.ts
2552
- var EFFORT_REGISTRY_ORIGINS = ["bundled", "user"];
2553
- var SEED_PRECEDENCE_RANK = {
2554
- core: 0,
2555
- community: 1,
2556
- imported: 2,
2557
- user: 3
2558
- };
2559
- function resolveSourceTier(sourceIdOrMetaKey, explicitTier) {
2560
- if (explicitTier) return explicitTier;
2561
- if (!sourceIdOrMetaKey) return "core";
2562
- if (sourceIdOrMetaKey === "user") return "user";
2563
- const id = sourceIdOrMetaKey.startsWith("seed:") ? sourceIdOrMetaKey.slice(5) : sourceIdOrMetaKey;
2564
- if (id === "core" || id === "efforts" || id === "equipment") return "core";
2565
- if (id === "community" || id.startsWith("community-") || id === "common") return "community";
2566
- return "imported";
2567
- }
2568
- function resolveSourceRank(sourceIdOrMetaKey, explicitTier) {
2569
- return SEED_PRECEDENCE_RANK[resolveSourceTier(sourceIdOrMetaKey, explicitTier)];
2570
- }
2571
- function resolveEntityTier(entity) {
2572
- if (entity.registrySource === "user") return "user";
2573
- return resolveSourceTier(entity.sourceId);
2574
- }
2575
- function resolveEntityRank(entity) {
2576
- return SEED_PRECEDENCE_RANK[resolveEntityTier(entity)];
2577
- }
2578
- function dedupeEntitiesByPrecedence(entities) {
2579
- const bySlug = /* @__PURE__ */ new Map();
2580
- for (const entity of entities) {
2581
- const key = entity.slug.toLowerCase();
2582
- const prior = bySlug.get(key);
2583
- if (!prior || resolveEntityRank(entity) >= resolveEntityRank(prior)) {
2584
- bySlug.set(key, entity);
2585
- }
2621
+ // src/parser/parseScript.ts
2622
+ function parseScript(inputText, options = {}) {
2623
+ if (!inputText || !inputText.trim()) {
2624
+ return new WhiteboardScript(inputText, [], []);
2625
+ }
2626
+ try {
2627
+ const doc = inputText.endsWith("\n") ? inputText : inputText + "\n";
2628
+ const tree = parser.parse(doc);
2629
+ const sport = options.sport ?? options.dialect;
2630
+ const references = options.references || options.tags ? {
2631
+ ...options.references ? { index: options.references } : {},
2632
+ defaults: options.defaults,
2633
+ tags: options.tags
2634
+ } : void 0;
2635
+ const statements = options.withoutDialects ? extractStatementsRaw(inputText, tree, references) : extractStatements(inputText, tree, sport, references);
2636
+ return new WhiteboardScript(inputText, statements, []);
2637
+ } catch (error) {
2638
+ const msg = error instanceof Error ? error.message : "Unknown parse error";
2639
+ return new WhiteboardScript(inputText, [], [{ message: msg }]);
2586
2640
  }
2587
- return Array.from(bySlug.values());
2588
2641
  }
2589
- var INTENSITY_TIERS = ["low", "moderate", "high"];
2590
- var DEFAULT_RESOLVER_OPTIONS = {
2591
- defaultMet: 5,
2592
- defaultThreshold: 2
2593
- };
2594
2642
 
2595
- // src/effort-registry/fuzzyMatch.ts
2596
- function levenshteinDistance(a, b) {
2597
- if (a === b) return 0;
2598
- if (a.length === 0) return b.length;
2599
- if (b.length === 0) return a.length;
2600
- if (a.length > b.length) {
2601
- const tmp = a;
2602
- a = b;
2603
- b = tmp;
2643
+ // src/parser/md-timer.ts
2644
+ var MdTimerRuntime = class {
2645
+ config;
2646
+ constructor(config) {
2647
+ this.config = config;
2604
2648
  }
2605
- const m = a.length;
2606
- const n = b.length;
2607
- let prev = new Uint32Array(m + 1);
2608
- let curr = new Uint32Array(m + 1);
2609
- for (let i = 0; i <= m; i++) {
2610
- prev[i] = i;
2649
+ /**
2650
+ * Parse WhiteboardScript source into a {@link WhiteboardScript}, running the
2651
+ * Dialect Stack on every statement.
2652
+ *
2653
+ * @param sport - The block's `:sport` fence suffix (` ```log:climbing `).
2654
+ */
2655
+ read(inputText, sport) {
2656
+ return parseScript(inputText, { sport, ...this.config });
2611
2657
  }
2612
- for (let j = 1; j <= n; j++) {
2613
- curr[0] = j;
2614
- const bj = b.charCodeAt(j - 1);
2615
- for (let i = 1; i <= m; i++) {
2616
- const cost = a.charCodeAt(i - 1) === bj ? 0 : 1;
2617
- curr[i] = Math.min(
2618
- prev[i] + 1,
2619
- // deletion
2620
- curr[i - 1] + 1,
2621
- // insertion
2622
- prev[i - 1] + cost
2623
- // substitution
2624
- );
2625
- }
2626
- const swap = prev;
2627
- prev = curr;
2628
- curr = swap;
2658
+ /**
2659
+ * Parse without running the Dialect Stack. Used by the parser test harness
2660
+ * which applies its own Dialect set. Production consumers should use {@link read}.
2661
+ */
2662
+ readWithoutDialects(inputText) {
2663
+ return parseScript(inputText, { withoutDialects: true, ...this.config });
2629
2664
  }
2630
- return prev[m];
2631
- }
2632
- function normalizeForFuzzy(input) {
2633
- return input.toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, " ").replace(/[-_]+/g, " ").trim().replace(/\s+/g, " ");
2634
- }
2635
- function isWithinThreshold(a, b, threshold) {
2636
- const na = normalizeForFuzzy(a);
2637
- const nb = normalizeForFuzzy(b);
2638
- if (na === nb) return true;
2639
- if (Math.abs(na.length - nb.length) > threshold) return false;
2640
- return levenshteinDistance(na, nb) <= threshold;
2665
+ };
2666
+
2667
+ // src/parser/parserInstance.ts
2668
+ function createParser(config) {
2669
+ return new MdTimerRuntime(config);
2641
2670
  }
2642
- function findBestFuzzyMatch(query, candidates, threshold) {
2643
- const nq = normalizeForFuzzy(query);
2644
- let best = null;
2645
- for (const candidate of candidates) {
2646
- const nc = normalizeForFuzzy(candidate);
2647
- if (Math.abs(nq.length - nc.length) > threshold) continue;
2648
- const distance = levenshteinDistance(nq, nc);
2649
- if (distance > threshold) continue;
2650
- if (!best || distance < best.distance) {
2651
- best = { target: candidate, distance };
2652
- if (distance === 0) break;
2653
- }
2671
+ var nextTagID = 0;
2672
+ var Tag = class _Tag {
2673
+ /**
2674
+ @internal
2675
+ */
2676
+ constructor(name2, set, base, modified) {
2677
+ this.name = name2;
2678
+ this.set = set;
2679
+ this.base = base;
2680
+ this.modified = modified;
2681
+ this.id = nextTagID++;
2654
2682
  }
2655
- return best;
2656
- }
2657
-
2658
- // src/effort-registry/reference-resolution.ts
2659
- function buildExactIndex(entities) {
2660
- const deduped = dedupeEntitiesByPrecedence(entities);
2661
- const byKey = /* @__PURE__ */ new Map();
2662
- const add = (key, entity) => {
2663
- if (!key) return;
2664
- const existing = byKey.get(key);
2665
- if (existing) {
2666
- if (!existing.includes(entity)) existing.push(entity);
2667
- } else {
2668
- byKey.set(key, [entity]);
2669
- }
2670
- };
2671
- for (const entity of deduped) {
2672
- add(entity.slug.toLowerCase(), entity);
2673
- add(normalizeForFuzzy(entity.label), entity);
2674
- for (const alias of entity.aliases) add(normalizeForFuzzy(alias), entity);
2683
+ toString() {
2684
+ let { name: name2 } = this;
2685
+ for (let mod of this.modified)
2686
+ if (mod.name)
2687
+ name2 = `${mod.name}(${name2})`;
2688
+ return name2;
2675
2689
  }
2676
- return {
2677
- byKey,
2678
- lookup(key) {
2679
- const direct = byKey.get(key) ?? byKey.get(normalizeForFuzzy(key));
2680
- if (!direct || direct.length === 0) return { entity: null, ambiguous: [] };
2681
- if (direct.length === 1) return { entity: direct[0], ambiguous: [] };
2682
- let maxRank = -1;
2683
- for (const candidate of direct) {
2684
- const rank = resolveEntityRank(candidate);
2685
- if (rank > maxRank) maxRank = rank;
2686
- }
2687
- const topTier = direct.filter((candidate) => resolveEntityRank(candidate) === maxRank);
2688
- if (topTier.length === 1) return { entity: topTier[0], ambiguous: [] };
2689
- return { entity: null, ambiguous: topTier };
2690
- }
2691
- };
2692
- }
2693
- function equipmentRef(entity) {
2694
- return { kind: "equipment", slug: entity.slug, label: entity.label };
2695
- }
2696
- function dedupeRefs(refs) {
2697
- const seen = /* @__PURE__ */ new Set();
2698
- const result = [];
2699
- for (const ref of refs) {
2700
- const key = `${ref.kind}:${ref.slug}`;
2701
- if (seen.has(key)) continue;
2702
- seen.add(key);
2703
- result.push(ref);
2704
- }
2705
- return result;
2706
- }
2707
- function createReferenceIndex(catalog, materialize) {
2708
- const dedupedEquipment = dedupeEntitiesByPrecedence(catalog.equipment ?? []);
2709
- const efforts = buildExactIndex(catalog.efforts ?? []);
2710
- const equipment = buildExactIndex(dedupedEquipment);
2711
- const prefixCandidates = dedupedEquipment.flatMap((entity) => [entity.label, ...entity.aliases].filter((name2) => !!name2 && !!name2.trim()).map((name2) => ({ lower: name2.trim().toLowerCase(), entity }))).sort((a, b) => b.lower.length - a.lower.length);
2712
- const materializer = materialize ?? ((effort) => ({
2713
- effort,
2714
- definition: effort,
2715
- slug: effort.slug,
2716
- label: effort.label,
2717
- met: effort.baseAttributes.met,
2718
- baseAttributes: effort.baseAttributes,
2719
- discipline: effort.baseAttributes.discipline,
2720
- disciplineFactor: effort.baseAttributes.disciplineFactor ?? 1,
2721
- intensityTier: effort.baseAttributes.intensityTier,
2722
- modifiers: {},
2723
- registrySource: effort.registrySource,
2724
- resolvedFrom: effort.registrySource === "user" ? "user" : effort.registrySource === "bundled" ? "bundled" : "default",
2725
- isEstimated: false
2726
- }));
2727
- const resolveEffortExact = (key) => {
2728
- const found = efforts.lookup(key.trim());
2729
- if (found.entity) return { entity: found.entity };
2730
- if (found.ambiguous.length > 0) {
2731
- return {
2732
- entity: null,
2733
- diagnostic: {
2734
- reason: "ambiguous-effort",
2735
- message: `"${key}" matches multiple efforts.`,
2736
- token: key,
2737
- candidates: found.ambiguous.map((e) => e.slug)
2738
- }
2739
- };
2740
- }
2741
- return {
2742
- entity: null,
2743
- diagnostic: { reason: "unknown-effort", message: `Unknown effort "${key}".`, token: key }
2744
- };
2745
- };
2746
- const resolveEquipmentValues = (values) => {
2747
- const refs = [];
2748
- const diagnostics = [];
2749
- for (const value of values) {
2750
- if (typeof value !== "string" || !value.trim()) {
2751
- diagnostics.push({
2752
- reason: "invalid-explicit",
2753
- message: `Equipment reference must be a slug string, got ${JSON.stringify(value) ?? "null"}.`,
2754
- token: String(value)
2755
- });
2756
- continue;
2757
- }
2758
- const found = equipment.lookup(value.trim());
2759
- if (found.entity) {
2760
- refs.push(equipmentRef(found.entity));
2761
- } else if (found.ambiguous.length > 0) {
2762
- diagnostics.push({
2763
- reason: "ambiguous-equipment",
2764
- message: `"${value}" matches multiple equipment entries.`,
2765
- token: value,
2766
- candidates: found.ambiguous.map((e) => e.slug)
2767
- });
2768
- } else {
2769
- diagnostics.push({
2770
- reason: "invalid-explicit",
2771
- message: `Unknown equipment "${value}".`,
2772
- token: value
2773
- });
2774
- }
2775
- }
2776
- return { refs: dedupeRefs(refs), diagnostics };
2777
- };
2778
- const index = {
2779
- resolveEffort(slugOrAlias) {
2780
- return efforts.lookup(slugOrAlias).entity;
2781
- },
2782
- resolveEquipment(slugOrAlias) {
2783
- return equipment.lookup(slugOrAlias).entity;
2784
- },
2785
- materialize(effort) {
2786
- return materializer(effort);
2787
- },
2788
- resolve(label, defaults) {
2789
- const source = (label ?? "").trim();
2790
- const diagnostics = [];
2791
- const push = (diagnostic) => {
2792
- if (diagnostic) diagnostics.push(diagnostic);
2793
- };
2794
- let matched = "none";
2795
- let effortEntity = null;
2796
- let remaining = "";
2797
- let equipmentRefs = [];
2798
- let equipmentAuthored = false;
2799
- const fillEquipment = () => {
2800
- if (equipmentRefs.length > 0 || equipmentAuthored) return;
2801
- if (effortEntity === null) return;
2802
- if (defaults?.equipment?.length) {
2803
- const resolvedDefaults = resolveEquipmentValues(defaults.equipment);
2804
- equipmentRefs = resolvedDefaults.refs;
2805
- for (const diagnostic of resolvedDefaults.diagnostics) {
2806
- push({ ...diagnostic, reason: "unknown-equipment" });
2807
- }
2808
- if (equipmentRefs.length > 0) return;
2809
- }
2810
- if (effortEntity.equipment?.length) {
2811
- const resolvedDefEquipment = resolveEquipmentValues(effortEntity.equipment);
2812
- equipmentRefs = resolvedDefEquipment.refs;
2813
- for (const diagnostic of resolvedDefEquipment.diagnostics) {
2814
- push({ ...diagnostic, reason: "unknown-equipment" });
2815
- }
2816
- }
2817
- };
2818
- if (!source) {
2819
- if (defaults?.effort) {
2820
- const found = resolveEffortExact(defaults.effort);
2821
- if (found.entity) {
2822
- effortEntity = found.entity;
2823
- matched = "default";
2824
- } else {
2825
- push({
2826
- reason: found.diagnostic?.reason === "ambiguous-effort" ? "ambiguous-effort" : "unknown-effort",
2827
- message: `Default effort "${defaults.effort}" is not in the catalog.`,
2828
- token: defaults.effort,
2829
- candidates: found.diagnostic?.candidates
2830
- });
2831
- }
2832
- }
2833
- fillEquipment();
2834
- return {
2835
- matched,
2836
- label: source,
2837
- effort: effortEntity ? materializer(effortEntity) : null,
2838
- effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
2839
- equipment: equipmentRefs,
2840
- remaining: "",
2841
- diagnostics
2842
- };
2843
- }
2844
- if (source.startsWith("{")) {
2845
- let parsed;
2846
- try {
2847
- parsed = JSON.parse(source);
2848
- } catch {
2849
- return {
2850
- matched: "none",
2851
- label: source,
2852
- effort: null,
2853
- effortRef: null,
2854
- equipment: [],
2855
- remaining: "",
2856
- diagnostics: [{
2857
- reason: "malformed-json",
2858
- message: "Reference object is not valid JSON.",
2859
- token: source
2860
- }]
2861
- };
2862
- }
2863
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && ("effort" in parsed || "equipment" in parsed)) {
2864
- matched = "explicit";
2865
- const obj = parsed;
2866
- if (obj.effort !== void 0) {
2867
- if (typeof obj.effort === "string" && obj.effort.trim()) {
2868
- const found = resolveEffortExact(obj.effort);
2869
- effortEntity = found.entity;
2870
- if (found.diagnostic) {
2871
- push(found.diagnostic.reason === "ambiguous-effort" ? found.diagnostic : { ...found.diagnostic, reason: "invalid-explicit", message: `Explicit effort "${obj.effort}" is not in the catalog.` });
2872
- }
2873
- } else {
2874
- push({
2875
- reason: "invalid-explicit",
2876
- message: 'Explicit "effort" must be a slug string.',
2877
- token: String(obj.effort)
2878
- });
2879
- }
2880
- }
2881
- if (obj.equipment !== void 0) {
2882
- equipmentAuthored = true;
2883
- const values = Array.isArray(obj.equipment) ? obj.equipment : [obj.equipment];
2884
- const resolvedEquipment = resolveEquipmentValues(values);
2885
- equipmentRefs = resolvedEquipment.refs;
2886
- diagnostics.push(...resolvedEquipment.diagnostics);
2887
- if (effortEntity?.equipment?.length) {
2888
- const definitionSlugs = new Set(effortEntity.equipment);
2889
- const authoredSlugs = equipmentRefs.map((ref) => ref.slug);
2890
- if (authoredSlugs.length > 0 && authoredSlugs.some((slug) => !definitionSlugs.has(slug))) {
2891
- push({
2892
- reason: "conflict",
2893
- message: `Explicit equipment [${authoredSlugs.join(", ")}] contradicts effort "${effortEntity.slug}" definition equipment [${effortEntity.equipment.join(", ")}].`,
2894
- token: source
2895
- });
2896
- }
2897
- }
2898
- }
2899
- fillEquipment();
2900
- return {
2901
- matched,
2902
- label: source,
2903
- effort: effortEntity ? materializer(effortEntity) : null,
2904
- effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
2905
- equipment: equipmentRefs,
2906
- remaining: "",
2907
- diagnostics
2908
- };
2909
- }
2910
- }
2911
- const lowerSource = source.toLowerCase();
2912
- for (const candidate of prefixCandidates) {
2913
- if (!lowerSource.startsWith(candidate.lower)) continue;
2914
- const after = source.slice(candidate.lower.length);
2915
- if (after && !/^\s/.test(after)) continue;
2916
- const candidateRemaining = after.trim();
2917
- if (!candidateRemaining) continue;
2918
- const equipmentFound = equipment.lookup(candidate.lower);
2919
- if (equipmentFound.ambiguous.length > 0) {
2920
- push({
2921
- reason: "ambiguous-equipment",
2922
- message: `"${candidate.lower}" matches multiple equipment entries.`,
2923
- token: source,
2924
- candidates: equipmentFound.ambiguous.map((e) => e.slug)
2925
- });
2926
- return {
2927
- matched: "none",
2928
- label: source,
2929
- effort: null,
2930
- effortRef: null,
2931
- equipment: [],
2932
- remaining: "",
2933
- diagnostics
2934
- };
2935
- }
2936
- if (!equipmentFound.entity) continue;
2937
- const effortFound = resolveEffortExact(candidateRemaining);
2938
- if (effortFound.entity) {
2939
- matched = "split";
2940
- effortEntity = effortFound.entity;
2941
- equipmentRefs = [equipmentRef(equipmentFound.entity)];
2942
- equipmentAuthored = true;
2943
- remaining = candidateRemaining;
2944
- break;
2945
- }
2946
- if (effortFound.diagnostic?.reason === "ambiguous-effort") {
2947
- push(effortFound.diagnostic);
2948
- return {
2949
- matched: "none",
2950
- label: source,
2951
- effort: null,
2952
- effortRef: null,
2953
- equipment: [],
2954
- remaining: "",
2955
- diagnostics
2956
- };
2957
- }
2958
- }
2959
- if (matched === "none") {
2960
- const found = resolveEffortExact(source);
2961
- if (found.entity) {
2962
- effortEntity = found.entity;
2963
- matched = "whole";
2964
- } else if (found.diagnostic?.reason === "ambiguous-effort") {
2965
- push(found.diagnostic);
2966
- return {
2967
- matched: "none",
2968
- label: source,
2969
- effort: null,
2970
- effortRef: null,
2971
- equipment: [],
2972
- remaining: "",
2973
- diagnostics
2974
- };
2975
- } else if (found.diagnostic) {
2976
- push(found.diagnostic);
2977
- }
2978
- }
2979
- fillEquipment();
2980
- return {
2981
- matched,
2982
- label: source,
2983
- effort: effortEntity ? materializer(effortEntity) : null,
2984
- effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
2985
- equipment: equipmentRefs,
2986
- remaining,
2987
- diagnostics
2988
- };
2989
- }
2990
- };
2991
- return index;
2992
- }
2993
- function extractStatementRefs(statements) {
2994
- const efforts = [];
2995
- const equipment = [];
2996
- const seenEffort = /* @__PURE__ */ new Set();
2997
- const seenEquipment = /* @__PURE__ */ new Set();
2998
- const pushUnique = (list, seen, value) => {
2999
- if (!value) return;
3000
- if (seen.has(value)) return;
3001
- seen.add(value);
3002
- list.push(value);
3003
- };
3004
- for (const statement of statements ?? []) {
3005
- const metrics = Array.isArray(statement.metrics) ? statement.metrics : statement.metrics?.toArray?.() ?? [];
3006
- for (const metric of metrics) {
3007
- const refs = getReferenceMetadata(metric.metadata)?.refs;
3008
- if (!refs) continue;
3009
- for (const ref of refs) {
3010
- if (ref.kind === "effort") pushUnique(efforts, seenEffort, ref.slug);
3011
- if (ref.kind === "equipment") pushUnique(equipment, seenEquipment, ref.slug);
3012
- }
3013
- }
3014
- }
3015
- return { efforts, equipment };
3016
- }
3017
- function slugifyRefLabel(label) {
3018
- return normalizeForFuzzy(label).replace(/\s+/g, "-");
3019
- }
3020
-
3021
- // src/parser/parseScript.ts
3022
- function parseScript(inputText, options = {}) {
3023
- if (!inputText || !inputText.trim()) {
3024
- return new WhiteboardScript(inputText, [], []);
3025
- }
3026
- try {
3027
- const doc = inputText.endsWith("\n") ? inputText : inputText + "\n";
3028
- const tree = parser.parse(doc);
3029
- const sport = options.sport ?? options.dialect;
3030
- const references = options.references || options.tags ? {
3031
- index: options.references ?? createReferenceIndex({ efforts: [], equipment: [] }),
3032
- defaults: options.defaults,
3033
- tags: options.tags
3034
- } : void 0;
3035
- const statements = options.withoutDialects ? extractStatementsRaw(inputText, tree, references) : extractStatements(inputText, tree, sport, references);
3036
- return new WhiteboardScript(inputText, statements, []);
3037
- } catch (error) {
3038
- const msg = error instanceof Error ? error.message : "Unknown parse error";
3039
- return new WhiteboardScript(inputText, [], [{ message: msg }]);
3040
- }
3041
- }
3042
-
3043
- // src/parser/md-timer.ts
3044
- var MdTimerRuntime = class {
3045
- config;
3046
- constructor(config) {
3047
- this.config = config;
3048
- }
3049
- /**
3050
- * Parse WhiteboardScript source into a {@link WhiteboardScript}, running the
3051
- * Dialect Stack on every statement.
3052
- *
3053
- * @param sport - The block's `:sport` fence suffix (` ```log:climbing `).
3054
- */
3055
- read(inputText, sport) {
3056
- return parseScript(inputText, { sport, ...this.config });
3057
- }
3058
- /**
3059
- * Parse without running the Dialect Stack. Used by the parser test harness
3060
- * which applies its own Dialect set. Production consumers should use {@link read}.
3061
- */
3062
- readWithoutDialects(inputText) {
3063
- return parseScript(inputText, { withoutDialects: true, ...this.config });
3064
- }
3065
- };
3066
-
3067
- // src/parser/parserInstance.ts
3068
- function createParser(config) {
3069
- return new MdTimerRuntime(config);
3070
- }
3071
- var nextTagID = 0;
3072
- var Tag = class _Tag {
3073
- /**
3074
- @internal
3075
- */
3076
- constructor(name2, set, base, modified) {
3077
- this.name = name2;
3078
- this.set = set;
3079
- this.base = base;
3080
- this.modified = modified;
3081
- this.id = nextTagID++;
3082
- }
3083
- toString() {
3084
- let { name: name2 } = this;
3085
- for (let mod of this.modified)
3086
- if (mod.name)
3087
- name2 = `${mod.name}(${name2})`;
3088
- return name2;
3089
- }
3090
- static define(nameOrParent, parent) {
3091
- let name2 = typeof nameOrParent == "string" ? nameOrParent : "?";
3092
- if (nameOrParent instanceof _Tag)
3093
- parent = nameOrParent;
3094
- if (parent === null || parent === void 0 ? void 0 : parent.base)
3095
- throw new Error("Can not derive from a modified tag");
3096
- let tag = new _Tag(name2, [], null, []);
3097
- tag.set.push(tag);
3098
- if (parent)
3099
- for (let t2 of parent.set)
3100
- tag.set.push(t2);
3101
- return tag;
2690
+ static define(nameOrParent, parent) {
2691
+ let name2 = typeof nameOrParent == "string" ? nameOrParent : "?";
2692
+ if (nameOrParent instanceof _Tag)
2693
+ parent = nameOrParent;
2694
+ if (parent === null || parent === void 0 ? void 0 : parent.base)
2695
+ throw new Error("Can not derive from a modified tag");
2696
+ let tag = new _Tag(name2, [], null, []);
2697
+ tag.set.push(tag);
2698
+ if (parent)
2699
+ for (let t2 of parent.set)
2700
+ tag.set.push(t2);
2701
+ return tag;
3102
2702
  }
3103
2703
  /**
3104
2704
  Define a tag _modifier_, which is a function that, given a tag,
@@ -3811,17 +3411,16 @@ function structureKeywords() {
3811
3411
  }));
3812
3412
  }
3813
3413
  function toCompletions(values, detail, quoteValues = false) {
3814
- return values.map((value) => ({
3815
- label: value.label ?? value.value,
3816
- // `Property` values only lex as String | Number | Identifier, and
3817
- // Identifier is `[a-zA-Z][a-zA-Z0-9]*` — quote slugs it cannot represent
3818
- // (hyphens, spaces, leading digits) so they insert parse-valid text;
3819
- // `parsePropertyValue` unwraps the quotes when the property is consumed.
3820
- apply: quoteValues && !/^[A-Za-z][A-Za-z0-9]*$/.test(value.value) ? JSON.stringify(value.value) : value.value,
3821
- detail: value.detail ?? detail,
3822
- type: "text",
3823
- info: value.info
3824
- }));
3414
+ return values.map((value) => {
3415
+ const insertText = quoteValues ? !/^[A-Za-z][A-Za-z0-9]*$/.test(value.value) ? JSON.stringify(value.value) : value.value : value.label ?? value.value;
3416
+ return {
3417
+ label: value.label ?? value.value,
3418
+ apply: insertText,
3419
+ detail: value.detail ?? (value.label && value.label !== value.value ? value.value : detail),
3420
+ type: "text",
3421
+ info: value.info
3422
+ };
3423
+ });
3825
3424
  }
3826
3425
  function dialectWords(sport) {
3827
3426
  if (!sport) return [];
@@ -3904,7 +3503,13 @@ function whiteboardScriptCompletionSource(options = {}) {
3904
3503
  return null;
3905
3504
  }
3906
3505
  const efforts = options.efforts ? toCompletions(await options.efforts(), "effort") : [];
3907
- const effortOptions = [...keywords, ...efforts, ...sportWords];
3506
+ const equipmentPrefixes = options.equipment ? (await options.equipment()).map((eq) => ({
3507
+ label: eq.label ?? eq.value,
3508
+ apply: `${eq.label ?? eq.value} `,
3509
+ detail: eq.detail ?? "equipment",
3510
+ type: "text"
3511
+ })) : [];
3512
+ const effortOptions = [...keywords, ...efforts, ...equipmentPrefixes, ...sportWords];
3908
3513
  if (/^[ \t]*[A-Za-z][A-Za-z0-9]*$/.test(before)) {
3909
3514
  const supplied = suppliedPropertyKeys(context);
3910
3515
  return {
@@ -3947,7 +3552,7 @@ function whiteboardScriptCompletionSource(options = {}) {
3947
3552
  const typed = context.state.doc.sliceString(first.from, pos).toLowerCase();
3948
3553
  const compatible = effortOptions.filter((option) => {
3949
3554
  const inserted = typeof option.apply === "string" ? option.apply : option.label;
3950
- return inserted.toLowerCase().startsWith(typed);
3555
+ return inserted.toLowerCase().startsWith(typed) || option.label.toLowerCase().startsWith(typed);
3951
3556
  });
3952
3557
  return { from: first.from, options: compatible, validFor: /^[A-Za-z ]*$/ };
3953
3558
  }
@@ -3996,6 +3601,50 @@ function whiteboardScript(options) {
3996
3601
  return new LanguageSupport(whiteboardScriptLanguage, whiteboardScriptCompletion(options ?? {}));
3997
3602
  }
3998
3603
 
3604
+ // src/effort-registry/types.ts
3605
+ var EFFORT_REGISTRY_ORIGINS = ["bundled", "user"];
3606
+ var SEED_PRECEDENCE_RANK = {
3607
+ core: 0,
3608
+ community: 1,
3609
+ imported: 2,
3610
+ user: 3
3611
+ };
3612
+ function resolveSourceTier(sourceIdOrMetaKey, explicitTier) {
3613
+ if (explicitTier) return explicitTier;
3614
+ if (!sourceIdOrMetaKey) return "core";
3615
+ if (sourceIdOrMetaKey === "user") return "user";
3616
+ const id = sourceIdOrMetaKey.startsWith("seed:") ? sourceIdOrMetaKey.slice(5) : sourceIdOrMetaKey;
3617
+ if (id === "core" || id === "efforts" || id === "equipment") return "core";
3618
+ if (id === "community" || id.startsWith("community-") || id === "common") return "community";
3619
+ return "imported";
3620
+ }
3621
+ function resolveSourceRank(sourceIdOrMetaKey, explicitTier) {
3622
+ return SEED_PRECEDENCE_RANK[resolveSourceTier(sourceIdOrMetaKey, explicitTier)];
3623
+ }
3624
+ function resolveEntityTier(entity) {
3625
+ if (entity.registrySource === "user") return "user";
3626
+ return resolveSourceTier(entity.sourceId);
3627
+ }
3628
+ function resolveEntityRank(entity) {
3629
+ return SEED_PRECEDENCE_RANK[resolveEntityTier(entity)];
3630
+ }
3631
+ function dedupeEntitiesByPrecedence(entities) {
3632
+ const bySlug = /* @__PURE__ */ new Map();
3633
+ for (const entity of entities) {
3634
+ const key = entity.slug.toLowerCase();
3635
+ const prior = bySlug.get(key);
3636
+ if (!prior || resolveEntityRank(entity) >= resolveEntityRank(prior)) {
3637
+ bySlug.set(key, entity);
3638
+ }
3639
+ }
3640
+ return Array.from(bySlug.values());
3641
+ }
3642
+ var INTENSITY_TIERS = ["low", "moderate", "high"];
3643
+ var DEFAULT_RESOLVER_OPTIONS = {
3644
+ defaultMet: 5,
3645
+ defaultThreshold: 2
3646
+ };
3647
+
3999
3648
  // src/effort-registry/disciplines.ts
4000
3649
  var EFFORT_DISCIPLINES = [
4001
3650
  "bodyweight",
@@ -4601,6 +4250,71 @@ function tagCategoryToDocument(category) {
4601
4250
  }
4602
4251
  return lines.join("\n");
4603
4252
  }
4253
+
4254
+ // src/effort-registry/fuzzyMatch.ts
4255
+ function levenshteinDistance(a, b) {
4256
+ if (a === b) return 0;
4257
+ if (a.length === 0) return b.length;
4258
+ if (b.length === 0) return a.length;
4259
+ if (a.length > b.length) {
4260
+ const tmp = a;
4261
+ a = b;
4262
+ b = tmp;
4263
+ }
4264
+ const m = a.length;
4265
+ const n = b.length;
4266
+ let prev = new Uint32Array(m + 1);
4267
+ let curr = new Uint32Array(m + 1);
4268
+ for (let i = 0; i <= m; i++) {
4269
+ prev[i] = i;
4270
+ }
4271
+ for (let j = 1; j <= n; j++) {
4272
+ curr[0] = j;
4273
+ const bj = b.charCodeAt(j - 1);
4274
+ for (let i = 1; i <= m; i++) {
4275
+ const cost = a.charCodeAt(i - 1) === bj ? 0 : 1;
4276
+ curr[i] = Math.min(
4277
+ prev[i] + 1,
4278
+ // deletion
4279
+ curr[i - 1] + 1,
4280
+ // insertion
4281
+ prev[i - 1] + cost
4282
+ // substitution
4283
+ );
4284
+ }
4285
+ const swap = prev;
4286
+ prev = curr;
4287
+ curr = swap;
4288
+ }
4289
+ return prev[m];
4290
+ }
4291
+ function normalizeForFuzzy(input) {
4292
+ return input.toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, " ").replace(/[-_]+/g, " ").trim().replace(/\s+/g, " ");
4293
+ }
4294
+ function isWithinThreshold(a, b, threshold) {
4295
+ const na = normalizeForFuzzy(a);
4296
+ const nb = normalizeForFuzzy(b);
4297
+ if (na === nb) return true;
4298
+ if (Math.abs(na.length - nb.length) > threshold) return false;
4299
+ return levenshteinDistance(na, nb) <= threshold;
4300
+ }
4301
+ function findBestFuzzyMatch(query, candidates, threshold) {
4302
+ const nq = normalizeForFuzzy(query);
4303
+ let best = null;
4304
+ for (const candidate of candidates) {
4305
+ const nc = normalizeForFuzzy(candidate);
4306
+ if (Math.abs(nq.length - nc.length) > threshold) continue;
4307
+ const distance = levenshteinDistance(nq, nc);
4308
+ if (distance > threshold) continue;
4309
+ if (!best || distance < best.distance) {
4310
+ best = { target: candidate, distance };
4311
+ if (distance === 0) break;
4312
+ }
4313
+ }
4314
+ return best;
4315
+ }
4316
+
4317
+ // src/effort-registry/EffortResolver.ts
4604
4318
  function normalizeSlug(label) {
4605
4319
  return normalizeForFuzzy(label).replace(/\s+/g, "-");
4606
4320
  }
@@ -4962,148 +4676,519 @@ var CompositePropertyRegistry = class {
4962
4676
  for (const item of this.userItems.values()) merged.set(item.slug, item);
4963
4677
  return [...merged.values()];
4964
4678
  }
4965
- listByOrigin(origin) {
4966
- return origin === "user" ? [...this.userItems.values()] : [...this.bundledItems.values()];
4679
+ listByOrigin(origin) {
4680
+ return origin === "user" ? [...this.userItems.values()] : [...this.bundledItems.values()];
4681
+ }
4682
+ async upsert(item) {
4683
+ if (item.registrySource !== "user") {
4684
+ throw new Error(`Cannot upsert property with registrySource '${item.registrySource}'; clone it as 'user' first`);
4685
+ }
4686
+ this.userItems.set(item.slug, item);
4687
+ if (this.storage) await this.storage.save(item);
4688
+ }
4689
+ async delete(slug) {
4690
+ const item = this.userItems.get(slug);
4691
+ if (!item) throw new Error(`Cannot delete '${slug}': not a user-owned record`);
4692
+ this.userItems.delete(slug);
4693
+ if (this.storage) await this.storage.delete(slug);
4694
+ }
4695
+ };
4696
+ var CompositeEquipmentRegistry = class extends CompositePropertyRegistry {
4697
+ constructor(options) {
4698
+ super({ storage: options?.storage });
4699
+ }
4700
+ };
4701
+
4702
+ // src/effort-registry/CompositeEffortRegistry.ts
4703
+ var CompositeEffortRegistry = class extends CompositePropertyRegistry {
4704
+ constructor(options) {
4705
+ super({
4706
+ bundled: options?.bundled,
4707
+ storage: options?.storage
4708
+ });
4709
+ }
4710
+ async loadBundled(customBundled) {
4711
+ this.bundledItems.clear();
4712
+ const source = customBundled ?? this.initialBundled ?? bundledEfforts;
4713
+ for (const effort of source) {
4714
+ const prior = this.bundledItems.get(effort.slug);
4715
+ if (!prior || resolveEntityRank(effort) >= resolveEntityRank(prior)) {
4716
+ this.bundledItems.set(effort.slug, effort);
4717
+ }
4718
+ }
4719
+ this.userItems.clear();
4720
+ if (this.storage) {
4721
+ try {
4722
+ const userEfforts = await this.storage.load();
4723
+ for (const effort of userEfforts) {
4724
+ if (effort.registrySource === "user") {
4725
+ this.userItems.set(effort.slug, effort);
4726
+ }
4727
+ }
4728
+ } catch (err) {
4729
+ console.warn("[CompositeEffortRegistry] Failed to load user efforts from storage adapter:", err);
4730
+ }
4731
+ }
4732
+ this.initialized = true;
4733
+ }
4734
+ /** True after loadBundled() has resolved */
4735
+ isInitialized() {
4736
+ return this.initialized;
4737
+ }
4738
+ listByOrigin(origin) {
4739
+ if (origin === "user") {
4740
+ return Array.from(this.userItems.values());
4741
+ }
4742
+ if (origin === "bundled") {
4743
+ return Array.from(this.bundledItems.values());
4744
+ }
4745
+ return this.list().filter((e) => e.registrySource === origin);
4746
+ }
4747
+ async upsert(effort) {
4748
+ if (effort.registrySource !== "user") {
4749
+ throw new Error(
4750
+ `CompositeEffortRegistry.upsert: only user efforts can be written. Received registrySource="${effort.registrySource}"`
4751
+ );
4752
+ }
4753
+ this.userItems.set(effort.slug, effort);
4754
+ if (this.storage) {
4755
+ try {
4756
+ await this.storage.save(effort);
4757
+ } catch (err) {
4758
+ console.warn("[CompositeEffortRegistry] Failed to persist effort to storage adapter:", err);
4759
+ }
4760
+ }
4761
+ }
4762
+ async delete(slug) {
4763
+ const existing = this.resolve(slug);
4764
+ if (existing && existing.registrySource !== "user") {
4765
+ throw new Error(
4766
+ `CompositeEffortRegistry.delete: cannot delete non-user effort "${slug}"`
4767
+ );
4768
+ }
4769
+ this.userItems.delete(slug);
4770
+ if (this.storage) {
4771
+ try {
4772
+ await this.storage.delete(slug);
4773
+ } catch (err) {
4774
+ console.warn("[CompositeEffortRegistry] Failed to delete effort from storage adapter:", err);
4775
+ }
4776
+ }
4777
+ }
4778
+ };
4779
+
4780
+ // src/effort-registry/aliasManagement.ts
4781
+ async function addAliasToEffort(registry, targetSlug, newAlias) {
4782
+ const trimmed = newAlias.trim();
4783
+ if (!trimmed) {
4784
+ throw new Error("Alias cannot be empty");
4785
+ }
4786
+ const existing = registry.resolve(targetSlug);
4787
+ if (!existing) {
4788
+ throw new Error(`Effort "${targetSlug}" not found in registry`);
4967
4789
  }
4968
- async upsert(item) {
4969
- if (item.registrySource !== "user") {
4970
- throw new Error(`Cannot upsert property with registrySource '${item.registrySource}'; clone it as 'user' first`);
4971
- }
4972
- this.userItems.set(item.slug, item);
4973
- if (this.storage) await this.storage.save(item);
4790
+ const normalizedNew = normalizeForFuzzy(trimmed);
4791
+ const alreadyHas = normalizeForFuzzy(existing.label) === normalizedNew || existing.aliases.some((a) => normalizeForFuzzy(a) === normalizedNew);
4792
+ if (alreadyHas) {
4793
+ return existing;
4974
4794
  }
4975
- async delete(slug) {
4976
- const item = this.userItems.get(slug);
4977
- if (!item) throw new Error(`Cannot delete '${slug}': not a user-owned record`);
4978
- this.userItems.delete(slug);
4979
- if (this.storage) await this.storage.delete(slug);
4795
+ const updatedAliases = [...existing.aliases, trimmed];
4796
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4797
+ if (existing.registrySource === "bundled") {
4798
+ const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? `effort-user-${crypto.randomUUID()}` : `effort-user-${Date.now()}`;
4799
+ const userEffort = {
4800
+ ...existing,
4801
+ id,
4802
+ registrySource: "user",
4803
+ aliases: updatedAliases,
4804
+ updatedAt: now,
4805
+ derivation: existing.derivation ?? {
4806
+ parentSlug: existing.slug,
4807
+ coefficients: {},
4808
+ hardOverrides: {}
4809
+ }
4810
+ };
4811
+ await registry.upsert(userEffort);
4812
+ return userEffort;
4980
4813
  }
4981
- };
4982
- var CompositeEquipmentRegistry = class extends CompositePropertyRegistry {
4983
- constructor(options) {
4984
- super({ storage: options?.storage });
4814
+ const updatedUserEffort = {
4815
+ ...existing,
4816
+ aliases: updatedAliases,
4817
+ updatedAt: now
4818
+ };
4819
+ await registry.upsert(updatedUserEffort);
4820
+ return updatedUserEffort;
4821
+ }
4822
+ function buildExactIndex(entities) {
4823
+ const deduped = dedupeEntitiesByPrecedence(entities);
4824
+ const byKey = /* @__PURE__ */ new Map();
4825
+ const add = (key, entity) => {
4826
+ if (!key) return;
4827
+ const existing = byKey.get(key);
4828
+ if (existing) {
4829
+ if (!existing.includes(entity)) existing.push(entity);
4830
+ } else {
4831
+ byKey.set(key, [entity]);
4832
+ }
4833
+ };
4834
+ for (const entity of deduped) {
4835
+ add(entity.slug.toLowerCase(), entity);
4836
+ add(normalizeForFuzzy(entity.label), entity);
4837
+ for (const alias of entity.aliases) add(normalizeForFuzzy(alias), entity);
4985
4838
  }
4986
- };
4987
-
4988
- // src/effort-registry/CompositeEffortRegistry.ts
4989
- var CompositeEffortRegistry = class extends CompositePropertyRegistry {
4990
- constructor(options) {
4991
- super({
4992
- bundled: options?.bundled,
4993
- storage: options?.storage
4994
- });
4839
+ return {
4840
+ byKey,
4841
+ lookup(key) {
4842
+ const direct = byKey.get(key) ?? byKey.get(normalizeForFuzzy(key));
4843
+ if (!direct || direct.length === 0) return { entity: null, ambiguous: [] };
4844
+ if (direct.length === 1) return { entity: direct[0], ambiguous: [] };
4845
+ let maxRank = -1;
4846
+ for (const candidate of direct) {
4847
+ const rank = resolveEntityRank(candidate);
4848
+ if (rank > maxRank) maxRank = rank;
4849
+ }
4850
+ const topTier = direct.filter((candidate) => resolveEntityRank(candidate) === maxRank);
4851
+ if (topTier.length === 1) return { entity: topTier[0], ambiguous: [] };
4852
+ return { entity: null, ambiguous: topTier };
4853
+ }
4854
+ };
4855
+ }
4856
+ function equipmentRef(entity) {
4857
+ return { kind: "equipment", slug: entity.slug, label: entity.label };
4858
+ }
4859
+ function dedupeRefs(refs) {
4860
+ const seen = /* @__PURE__ */ new Set();
4861
+ const result = [];
4862
+ for (const ref of refs) {
4863
+ const key = `${ref.kind}:${ref.slug}`;
4864
+ if (seen.has(key)) continue;
4865
+ seen.add(key);
4866
+ result.push(ref);
4995
4867
  }
4996
- async loadBundled(customBundled) {
4997
- this.bundledItems.clear();
4998
- const source = customBundled ?? this.initialBundled ?? bundledEfforts;
4999
- for (const effort of source) {
5000
- const prior = this.bundledItems.get(effort.slug);
5001
- if (!prior || resolveEntityRank(effort) >= resolveEntityRank(prior)) {
5002
- this.bundledItems.set(effort.slug, effort);
4868
+ return result;
4869
+ }
4870
+ function createReferenceIndex(catalog, materialize) {
4871
+ const dedupedEquipment = dedupeEntitiesByPrecedence(catalog.equipment ?? []);
4872
+ const efforts = buildExactIndex(catalog.efforts ?? []);
4873
+ const equipment = buildExactIndex(dedupedEquipment);
4874
+ const prefixCandidates = dedupedEquipment.flatMap((entity) => [entity.label, ...entity.aliases].filter((name2) => !!name2 && !!name2.trim()).map((name2) => ({ lower: name2.trim().toLowerCase(), entity }))).sort((a, b) => b.lower.length - a.lower.length);
4875
+ const materializer = materialize ?? ((effort) => ({
4876
+ effort,
4877
+ definition: effort,
4878
+ slug: effort.slug,
4879
+ label: effort.label,
4880
+ met: effort.baseAttributes.met,
4881
+ baseAttributes: effort.baseAttributes,
4882
+ discipline: effort.baseAttributes.discipline,
4883
+ disciplineFactor: effort.baseAttributes.disciplineFactor ?? 1,
4884
+ intensityTier: effort.baseAttributes.intensityTier,
4885
+ modifiers: {},
4886
+ registrySource: effort.registrySource,
4887
+ resolvedFrom: effort.registrySource === "user" ? "user" : effort.registrySource === "bundled" ? "bundled" : "default",
4888
+ isEstimated: false
4889
+ }));
4890
+ const resolveEffortExact = (key) => {
4891
+ const found = efforts.lookup(key.trim());
4892
+ if (found.entity) return { entity: found.entity };
4893
+ if (found.ambiguous.length > 0) {
4894
+ return {
4895
+ entity: null,
4896
+ diagnostic: {
4897
+ reason: "ambiguous-effort",
4898
+ message: `"${key}" matches multiple efforts.`,
4899
+ token: key,
4900
+ candidates: found.ambiguous.map((e) => e.slug)
4901
+ }
4902
+ };
4903
+ }
4904
+ return {
4905
+ entity: null,
4906
+ diagnostic: { reason: "unknown-effort", message: `Unknown effort "${key}".`, token: key }
4907
+ };
4908
+ };
4909
+ const resolveEquipmentValues = (values) => {
4910
+ const refs = [];
4911
+ const diagnostics = [];
4912
+ for (const value of values) {
4913
+ if (typeof value !== "string" || !value.trim()) {
4914
+ diagnostics.push({
4915
+ reason: "invalid-explicit",
4916
+ message: `Equipment reference must be a slug string, got ${JSON.stringify(value) ?? "null"}.`,
4917
+ token: String(value)
4918
+ });
4919
+ continue;
4920
+ }
4921
+ const found = equipment.lookup(value.trim());
4922
+ if (found.entity) {
4923
+ refs.push(equipmentRef(found.entity));
4924
+ } else if (found.ambiguous.length > 0) {
4925
+ diagnostics.push({
4926
+ reason: "ambiguous-equipment",
4927
+ message: `"${value}" matches multiple equipment entries.`,
4928
+ token: value,
4929
+ candidates: found.ambiguous.map((e) => e.slug)
4930
+ });
4931
+ } else {
4932
+ diagnostics.push({
4933
+ reason: "invalid-explicit",
4934
+ message: `Unknown equipment "${value}".`,
4935
+ token: value
4936
+ });
5003
4937
  }
5004
4938
  }
5005
- this.userItems.clear();
5006
- if (this.storage) {
5007
- try {
5008
- const userEfforts = await this.storage.load();
5009
- for (const effort of userEfforts) {
5010
- if (effort.registrySource === "user") {
5011
- this.userItems.set(effort.slug, effort);
4939
+ return { refs: dedupeRefs(refs), diagnostics };
4940
+ };
4941
+ const index = {
4942
+ resolveEffort(slugOrAlias) {
4943
+ return efforts.lookup(slugOrAlias).entity;
4944
+ },
4945
+ resolveEquipment(slugOrAlias) {
4946
+ return equipment.lookup(slugOrAlias).entity;
4947
+ },
4948
+ materialize(effort) {
4949
+ return materializer(effort);
4950
+ },
4951
+ resolve(label, defaults) {
4952
+ const source = (label ?? "").trim();
4953
+ const diagnostics = [];
4954
+ const push = (diagnostic) => {
4955
+ if (diagnostic) diagnostics.push(diagnostic);
4956
+ };
4957
+ let matched = "none";
4958
+ let effortEntity = null;
4959
+ let remaining = "";
4960
+ let equipmentRefs = [];
4961
+ let equipmentAuthored = false;
4962
+ const fillEquipment = () => {
4963
+ if (equipmentRefs.length > 0 || equipmentAuthored) return;
4964
+ if (effortEntity === null) return;
4965
+ if (defaults?.equipment?.length) {
4966
+ const resolvedDefaults = resolveEquipmentValues(defaults.equipment);
4967
+ equipmentRefs = resolvedDefaults.refs;
4968
+ for (const diagnostic of resolvedDefaults.diagnostics) {
4969
+ push({ ...diagnostic, reason: "unknown-equipment" });
4970
+ }
4971
+ if (equipmentRefs.length > 0) return;
4972
+ }
4973
+ if (effortEntity.equipment?.length) {
4974
+ const resolvedDefEquipment = resolveEquipmentValues(effortEntity.equipment);
4975
+ equipmentRefs = resolvedDefEquipment.refs;
4976
+ for (const diagnostic of resolvedDefEquipment.diagnostics) {
4977
+ push({ ...diagnostic, reason: "unknown-equipment" });
4978
+ }
4979
+ }
4980
+ };
4981
+ if (!source) {
4982
+ if (defaults?.effort) {
4983
+ const found = resolveEffortExact(defaults.effort);
4984
+ if (found.entity) {
4985
+ effortEntity = found.entity;
4986
+ matched = "default";
4987
+ } else {
4988
+ push({
4989
+ reason: found.diagnostic?.reason === "ambiguous-effort" ? "ambiguous-effort" : "unknown-effort",
4990
+ message: `Default effort "${defaults.effort}" is not in the catalog.`,
4991
+ token: defaults.effort,
4992
+ candidates: found.diagnostic?.candidates
4993
+ });
4994
+ }
4995
+ }
4996
+ fillEquipment();
4997
+ return {
4998
+ matched,
4999
+ label: source,
5000
+ effort: effortEntity ? materializer(effortEntity) : null,
5001
+ effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
5002
+ equipment: equipmentRefs,
5003
+ remaining: "",
5004
+ diagnostics
5005
+ };
5006
+ }
5007
+ if (source.startsWith("{")) {
5008
+ let parsed;
5009
+ try {
5010
+ parsed = JSON.parse(source);
5011
+ } catch {
5012
+ return {
5013
+ matched: "none",
5014
+ label: source,
5015
+ effort: null,
5016
+ effortRef: null,
5017
+ equipment: [],
5018
+ remaining: "",
5019
+ diagnostics: [{
5020
+ reason: "malformed-json",
5021
+ message: "Reference object is not valid JSON.",
5022
+ token: source
5023
+ }]
5024
+ };
5025
+ }
5026
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && ("effort" in parsed || "equipment" in parsed)) {
5027
+ matched = "explicit";
5028
+ const obj = parsed;
5029
+ if (obj.effort !== void 0) {
5030
+ if (typeof obj.effort === "string" && obj.effort.trim()) {
5031
+ const found = resolveEffortExact(obj.effort);
5032
+ effortEntity = found.entity;
5033
+ if (found.diagnostic) {
5034
+ push(found.diagnostic.reason === "ambiguous-effort" ? found.diagnostic : { ...found.diagnostic, reason: "invalid-explicit", message: `Explicit effort "${obj.effort}" is not in the catalog.` });
5035
+ }
5036
+ } else {
5037
+ push({
5038
+ reason: "invalid-explicit",
5039
+ message: 'Explicit "effort" must be a slug string.',
5040
+ token: String(obj.effort)
5041
+ });
5042
+ }
5043
+ }
5044
+ if (obj.equipment !== void 0) {
5045
+ equipmentAuthored = true;
5046
+ const values = Array.isArray(obj.equipment) ? obj.equipment : [obj.equipment];
5047
+ const resolvedEquipment = resolveEquipmentValues(values);
5048
+ equipmentRefs = resolvedEquipment.refs;
5049
+ diagnostics.push(...resolvedEquipment.diagnostics);
5050
+ if (effortEntity?.equipment?.length) {
5051
+ const definitionSlugs = new Set(effortEntity.equipment);
5052
+ const authoredSlugs = equipmentRefs.map((ref) => ref.slug);
5053
+ if (authoredSlugs.length > 0 && authoredSlugs.some((slug) => !definitionSlugs.has(slug))) {
5054
+ push({
5055
+ reason: "conflict",
5056
+ message: `Explicit equipment [${authoredSlugs.join(", ")}] contradicts effort "${effortEntity.slug}" definition equipment [${effortEntity.equipment.join(", ")}].`,
5057
+ token: source
5058
+ });
5059
+ }
5060
+ }
5012
5061
  }
5062
+ fillEquipment();
5063
+ return {
5064
+ matched,
5065
+ label: source,
5066
+ effort: effortEntity ? materializer(effortEntity) : null,
5067
+ effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
5068
+ equipment: equipmentRefs,
5069
+ remaining: "",
5070
+ diagnostics
5071
+ };
5013
5072
  }
5014
- } catch (err) {
5015
- console.warn("[CompositeEffortRegistry] Failed to load user efforts from storage adapter:", err);
5016
5073
  }
5017
- }
5018
- this.initialized = true;
5019
- }
5020
- /** True after loadBundled() has resolved */
5021
- isInitialized() {
5022
- return this.initialized;
5023
- }
5024
- listByOrigin(origin) {
5025
- if (origin === "user") {
5026
- return Array.from(this.userItems.values());
5027
- }
5028
- if (origin === "bundled") {
5029
- return Array.from(this.bundledItems.values());
5030
- }
5031
- return this.list().filter((e) => e.registrySource === origin);
5032
- }
5033
- async upsert(effort) {
5034
- if (effort.registrySource !== "user") {
5035
- throw new Error(
5036
- `CompositeEffortRegistry.upsert: only user efforts can be written. Received registrySource="${effort.registrySource}"`
5037
- );
5038
- }
5039
- this.userItems.set(effort.slug, effort);
5040
- if (this.storage) {
5041
- try {
5042
- await this.storage.save(effort);
5043
- } catch (err) {
5044
- console.warn("[CompositeEffortRegistry] Failed to persist effort to storage adapter:", err);
5074
+ const lowerSource = source.toLowerCase();
5075
+ for (const candidate of prefixCandidates) {
5076
+ if (!lowerSource.startsWith(candidate.lower)) continue;
5077
+ const after = source.slice(candidate.lower.length);
5078
+ if (after && !/^\s/.test(after)) continue;
5079
+ const candidateRemaining = after.trim();
5080
+ if (!candidateRemaining) continue;
5081
+ const equipmentFound = equipment.lookup(candidate.lower);
5082
+ if (equipmentFound.ambiguous.length > 0) {
5083
+ push({
5084
+ reason: "ambiguous-equipment",
5085
+ message: `"${candidate.lower}" matches multiple equipment entries.`,
5086
+ token: source,
5087
+ candidates: equipmentFound.ambiguous.map((e) => e.slug)
5088
+ });
5089
+ return {
5090
+ matched: "none",
5091
+ label: source,
5092
+ effort: null,
5093
+ effortRef: null,
5094
+ equipment: [],
5095
+ remaining: "",
5096
+ diagnostics
5097
+ };
5098
+ }
5099
+ if (!equipmentFound.entity) continue;
5100
+ const effortFound = resolveEffortExact(candidateRemaining);
5101
+ if (effortFound.entity) {
5102
+ matched = "split";
5103
+ effortEntity = effortFound.entity;
5104
+ const rawPrefix = source.slice(0, candidate.lower.length).trim();
5105
+ equipmentRefs = [{
5106
+ ...equipmentRef(equipmentFound.entity),
5107
+ ...rawPrefix.toLowerCase() !== equipmentFound.entity.label.toLowerCase() ? { matchedAlias: rawPrefix } : {}
5108
+ }];
5109
+ equipmentAuthored = true;
5110
+ remaining = candidateRemaining;
5111
+ break;
5112
+ }
5113
+ if (effortFound.diagnostic?.reason === "ambiguous-effort") {
5114
+ push(effortFound.diagnostic);
5115
+ return {
5116
+ matched: "none",
5117
+ label: source,
5118
+ effort: null,
5119
+ effortRef: null,
5120
+ equipment: [],
5121
+ remaining: "",
5122
+ diagnostics
5123
+ };
5124
+ }
5045
5125
  }
5046
- }
5047
- }
5048
- async delete(slug) {
5049
- const existing = this.resolve(slug);
5050
- if (existing && existing.registrySource !== "user") {
5051
- throw new Error(
5052
- `CompositeEffortRegistry.delete: cannot delete non-user effort "${slug}"`
5053
- );
5054
- }
5055
- this.userItems.delete(slug);
5056
- if (this.storage) {
5057
- try {
5058
- await this.storage.delete(slug);
5059
- } catch (err) {
5060
- console.warn("[CompositeEffortRegistry] Failed to delete effort from storage adapter:", err);
5126
+ if (matched === "none") {
5127
+ const found = resolveEffortExact(source);
5128
+ if (found.entity) {
5129
+ effortEntity = found.entity;
5130
+ matched = "whole";
5131
+ } else if (found.diagnostic?.reason === "ambiguous-effort") {
5132
+ push(found.diagnostic);
5133
+ return {
5134
+ matched: "none",
5135
+ label: source,
5136
+ effort: null,
5137
+ effortRef: null,
5138
+ equipment: [],
5139
+ remaining: "",
5140
+ diagnostics
5141
+ };
5142
+ } else if (found.diagnostic) {
5143
+ push(found.diagnostic);
5144
+ }
5061
5145
  }
5146
+ fillEquipment();
5147
+ const effortMatchText = matched === "split" ? remaining : source;
5148
+ return {
5149
+ matched,
5150
+ label: source,
5151
+ effort: effortEntity ? materializer(effortEntity) : null,
5152
+ effortRef: effortEntity ? {
5153
+ kind: "effort",
5154
+ slug: effortEntity.slug,
5155
+ label: effortEntity.label,
5156
+ ...effortMatchText && effortMatchText.toLowerCase() !== effortEntity.label.toLowerCase() ? { matchedAlias: effortMatchText } : {}
5157
+ } : null,
5158
+ equipment: equipmentRefs,
5159
+ remaining,
5160
+ diagnostics
5161
+ };
5062
5162
  }
5063
- }
5064
- };
5065
-
5066
- // src/effort-registry/aliasManagement.ts
5067
- async function addAliasToEffort(registry, targetSlug, newAlias) {
5068
- const trimmed = newAlias.trim();
5069
- if (!trimmed) {
5070
- throw new Error("Alias cannot be empty");
5071
- }
5072
- const existing = registry.resolve(targetSlug);
5073
- if (!existing) {
5074
- throw new Error(`Effort "${targetSlug}" not found in registry`);
5075
- }
5076
- const normalizedNew = normalizeForFuzzy(trimmed);
5077
- const alreadyHas = normalizeForFuzzy(existing.label) === normalizedNew || existing.aliases.some((a) => normalizeForFuzzy(a) === normalizedNew);
5078
- if (alreadyHas) {
5079
- return existing;
5080
- }
5081
- const updatedAliases = [...existing.aliases, trimmed];
5082
- const now = (/* @__PURE__ */ new Date()).toISOString();
5083
- if (existing.registrySource === "bundled") {
5084
- const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? `effort-user-${crypto.randomUUID()}` : `effort-user-${Date.now()}`;
5085
- const userEffort = {
5086
- ...existing,
5087
- id,
5088
- registrySource: "user",
5089
- aliases: updatedAliases,
5090
- updatedAt: now,
5091
- derivation: existing.derivation ?? {
5092
- parentSlug: existing.slug,
5093
- coefficients: {},
5094
- hardOverrides: {}
5163
+ };
5164
+ return index;
5165
+ }
5166
+ function extractStatementRefs(statements) {
5167
+ const efforts = [];
5168
+ const equipment = [];
5169
+ const seenEffort = /* @__PURE__ */ new Set();
5170
+ const seenEquipment = /* @__PURE__ */ new Set();
5171
+ const pushUnique = (list, seen, value) => {
5172
+ if (!value) return;
5173
+ if (seen.has(value)) return;
5174
+ seen.add(value);
5175
+ list.push(value);
5176
+ };
5177
+ for (const statement of statements ?? []) {
5178
+ const metrics = Array.isArray(statement.metrics) ? statement.metrics : statement.metrics?.toArray?.() ?? [];
5179
+ for (const metric of metrics) {
5180
+ const refs = getReferenceMetadata(metric.metadata)?.refs;
5181
+ if (!refs) continue;
5182
+ for (const ref of refs) {
5183
+ if (ref.kind === "effort") pushUnique(efforts, seenEffort, ref.slug);
5184
+ if (ref.kind === "equipment") pushUnique(equipment, seenEquipment, ref.slug);
5095
5185
  }
5096
- };
5097
- await registry.upsert(userEffort);
5098
- return userEffort;
5186
+ }
5099
5187
  }
5100
- const updatedUserEffort = {
5101
- ...existing,
5102
- aliases: updatedAliases,
5103
- updatedAt: now
5104
- };
5105
- await registry.upsert(updatedUserEffort);
5106
- return updatedUserEffort;
5188
+ return { efforts, equipment };
5189
+ }
5190
+ function slugifyRefLabel(label) {
5191
+ return normalizeForFuzzy(label).replace(/\s+/g, "-");
5107
5192
  }
5108
5193
 
5109
5194
  // src/runtime/contracts/IRuntimeOptions.ts
@@ -10105,8 +10190,11 @@ function tryEnrichBlock(block, resolver, references, overwrite, debug, propertyS
10105
10190
  if (normalizedBlockType !== "effort" && normalizedBlockType !== "exercise") {
10106
10191
  return;
10107
10192
  }
10108
- const label = block.label;
10109
- if (!label) {
10193
+ const statementEffortMetric = block.getAllMemory().flatMap((loc) => loc.metrics.toArray()).find((m) => m.type === MetricType.Effort);
10194
+ const preResolvedMeta = getReferenceMetadata(statementEffortMetric?.metadata);
10195
+ const preResolvedEffortRef = preResolvedMeta?.refs?.find((r) => r.kind === "effort");
10196
+ const label = block.label || preResolvedMeta?.source || (typeof statementEffortMetric?.image === "string" ? statementEffortMetric.image : "");
10197
+ if (!label && !preResolvedEffortRef && !block.context?.exerciseId) {
10110
10198
  if (debug) logTrace(`[EffortEnrichment] Skipping block with no label: ${block.key}`);
10111
10199
  return;
10112
10200
  }
@@ -10123,13 +10211,23 @@ function tryEnrichBlock(block, resolver, references, overwrite, debug, propertyS
10123
10211
  let equipmentRefs = [];
10124
10212
  let diagnostics = [];
10125
10213
  if (references) {
10126
- const resolution = references.resolve(label);
10127
- resolved = resolution.effort ?? void 0;
10128
- matchedKind = resolution.matched;
10129
- equipmentRefs = resolution.equipment;
10130
- diagnostics = resolution.diagnostics;
10214
+ const directSlug = preResolvedEffortRef?.slug || block.context?.exerciseId;
10215
+ const directEntity = directSlug ? references.resolveEffort(directSlug) : null;
10216
+ if (directEntity && preResolvedMeta) {
10217
+ resolved = references.materialize(directEntity);
10218
+ matchedKind = preResolvedMeta.matched;
10219
+ equipmentRefs = preResolvedMeta.refs?.filter((r) => r.kind === "equipment") ?? [];
10220
+ diagnostics = [...preResolvedMeta.diagnostics ?? []];
10221
+ } else {
10222
+ const resolution = references.resolve(label);
10223
+ resolved = resolution.effort ?? void 0;
10224
+ matchedKind = resolution.matched;
10225
+ equipmentRefs = resolution.equipment;
10226
+ diagnostics = resolution.diagnostics;
10227
+ }
10131
10228
  } else {
10132
- resolved = resolver.resolveEffort(label);
10229
+ const lookupKey = preResolvedEffortRef?.slug || block.context?.exerciseId || label;
10230
+ resolved = resolver.resolveEffort(lookupKey) ?? (lookupKey !== label ? resolver.resolveEffort(label) : void 0);
10133
10231
  }
10134
10232
  if (!resolved) {
10135
10233
  if (diagnostics.length > 0) {
@@ -15865,6 +15963,6 @@ function compareStatement(expectations, actual, mode) {
15865
15963
  return diffs;
15866
15964
  }
15867
15965
 
15868
- export { ABSENT, AGGREGATE_BUILTINS, AUTHORITATIVE_CASTS, AbortSessionAction, AmrapLogicStrategy, AnalyticsEngine, AnalyticsTransformer, BUILTIN_CALCS, BUNDLED_EFFORT_COUNT, BehaviorContext, BlockBuilder, BlockContext, ButtonBehavior, COMPLETION_PROPERTY_KEYS, CONSUMED_HINTS, CONSUMED_HINT_KEYS, CONTEXT_ATOMS, CalcEngine, CalcEvalError, CalcParseError, CalcRegistrationError, CalcUnitError, CalculationRegistry, CardioDialect, ChildSelectionBehavior, ChildrenStrategy, ChoiceGroupMetric, ClearChildrenAction, ClimbDialect, ClimbMetricType, CompileAndPushBlockAction, CompletionTimestampBehavior, CompositeEffortRegistry, CompositeEquipmentRegistry, CompositePropertyRegistry, CountdownTimerBehavior, CountupTimerBehavior, CrossFitDialect, DAY, DEFAULT_RESOLVER_OPTIONS, DEFAULT_RUNTIME_OPTIONS, DEFAULT_SOUND_PROFILE, DEFAULT_UNRESOLVED_EFFORT_MET, DIM_COUNT, DIM_ENERGY, DIM_LENGTH, DIM_MASS, DIM_TIME, DIM_ZERO, DISCIPLINE_FACTORS, DialectStack, EFFORT_DATA_METRIC_TYPE, EFFORT_DISCIPLINES, EFFORT_REGISTRY_ORIGINS, EffortBlock, EffortFallbackStrategy, EffortResolver, EmitEventAction, EmitSystemOutputAction, ErrorAction, EventBus, ExecutionContext, ExitBehavior, FixtureSyntaxError, GenericGroupStrategy, GenericLoopStrategy, GenericTimerStrategy, HabitsDialect, INTENSITY_TIERS, InMemoryEffortRegistry, IntervalLogicStrategy, JitCompiler, LabelingBehavior, LocalEventProvider, LocalRuntimeSubscription, LookupRegistry, MAX_TAG_CONTRIBUTION_DEPTH, MdTimerRuntime, MemoryLocation, MemoryTypeEnum, MetricPromotionBehavior, NAMED_COMPOUNDS, NextAction, NextEvent, NextEventHandler, OutputEmitter, PRODUCTION_STRATEGIES, PROPERTY_KEY_TO_METRIC_TYPE, PopBlockAction, PushBlockAction, PushRestBlockAction, REFERENCE_PAIR_KEYS, REST_EXCLUSION, RPE_LABELS, RUNTIME_LOG_SUBJECT_WIDTH, RegisterEventHandlerAction, Registry, ReportOutputBehavior, ReportOutputStrategy, RestBlock, RoundsMetric, RuntimeAdapter, RuntimeBlock, RuntimeBuilder, RuntimeClock, RuntimeFactory, RuntimeLogger, RuntimeMemory, RuntimeObservers, RuntimeStack, SEED_PRECEDENCE_RANK, SOUND_PLAY_EVENT, SOUND_PROFILE_METRIC, STANDARD_UNITS, STORE_CALCS, STREAM_ATOMS, STRUCTURAL_EFFORT_WORDS, ScriptRuntime, SessionRootBlock, SessionRootStrategy, SnapshotClock, SoundProfileBehavior, SoundStrategy, SpanTrackingBehavior, StandardAnalyticsProfile, StartSessionAction, StartWorkoutAction, SubscriptionManager, TAG_MARKER_PREFIX, ThrowError, ThrowErrorAction, TimeSpan, TwoPassEffortResolutionProcess, TypedMemoryReference, UNITS, UnitRegistry, UnitSet, UnitsDialect, UnregisterEventHandlerAction, UpdateNextPreviewAction, VISIBILITY_ICONS, VISIBILITY_LABELS, WaitingToStartBlock, WaitingToStartInjectorBehavior, WhiteboardScript, WodDialect, YogaDialect, addAliasToEffort, addDim, applyTemplateDefaults, buildStaticLookups, buildTooltip, bundledEfforts, calculateElapsed, canonicalizeType, checkCalcLine, classifyStatements, closeCurrentSpan, collapseUnresolvedChoices, commonFixtureSet, compareStatement, compileLineForm, compilePropertySummaryCalcs, compilePropertySummaryCalcsWithDiagnostics, composeDivUnit, composeMulUnit, compoundName, computeColumnLabel, computeLabel, computeWorkloadRollups, convertScalar, createAnalyticsEngineForBlock, createCalcEngine, createCalcStack, createCompiler, createDialectStack, createDisciplinesTable, createEffortTable, createLineLocator, createPolicy as createMetricPresentationPolicy, createMockClock, createParser, createProfileTable, createReferenceIndex, createRpeLabelsTable, dailySessionLoads, dayBucket, dedupeEntitiesByPrecedence, dialectRegistry, dialectStack, diffStatement, dimEquals, disciplineFactorFor, documentToEffort, documentToEquipment, documentToTag, effortToDocument, equipmentToDocument, evaluate, extractEffortData, extractMetrics, extractPropertyRulesFromMeta, extractStatementRefs, extractStatements, extractStatementsRaw, extractSyntaxFacts, extractTagPropertyRules, filterTagsByVisibility, findBestFuzzyMatch, findTagContributionCycles, findUnresolvedChoices, fixtureBackSquat, fixtureBurpee, fixturePlank, fixtureRowing, fixtureRunning, fixtureUserCustom, fixtureUserOverride, formatDim, formatDuration, formatRuntimeLogLine, formatRuntimeLogRule, formatRuntimeTimestamp, formatSoundTrigger, metricsToLabel as fragmentsToLabel, fullFixtureSet, fuseUnits, fuseUnitsInMetrics, getAnalyticsFromLogs, getAnalyticsFromRuntime, getHints, getMetricVisibility, getUnit, hasHint, hintMetric, hintsToContainer, inferDim, isCastSubscription, isChoiceResolved, isDimensionless, isEffortDiscipline, isFragmentTag, isTimeLikeMetric, isWithinThreshold, levenshteinDistance, metricPresentation, mutateTimerSpans, normalizeForFuzzy, num, openSpan, outputNodeId, outputNodeIds, padRuntimeLogEvent, padRuntimeLogSubject, parseCalcLine, parseEffortFile, parseEquipmentFile, parseExpression, parseMetricLine, parseScript, parseSoundTrigger, printCalculation, readNestedSection, readTimer, realtimeProcessorRegistry, renderMetric, resolveDominantOrigin, resolveEntityRank, resolveEntityTier, resolveProfileRules, resolveSoundProfile, resolveSourceRank, resolveSourceTier, resolveTagContributions, runMetricShakeOutQueue, slugifyRefLabel, snippetCompletions, startSpan, str, strategyRegistry, subDim, summaryProcessorRegistry, tagCategoryToDocument, tagContributionMetrics, tagMarkerSlug, tagMarkerSlugs, tagMarkerType, tagToDocument, toStoredOutputStatement, truthy, validateOutputUnit, parser as whiteboardParser, whiteboardScript, whiteboardScriptCompletion, whiteboardScriptCompletionSource, whiteboardScriptLanguage, whiteboardScriptSnippets, writeChoiceSelection };
15966
+ export { ABSENT, AGGREGATE_BUILTINS, AUTHORITATIVE_CASTS, AbortSessionAction, AmrapLogicStrategy, AnalyticsEngine, AnalyticsTransformer, BUILTIN_CALCS, BUNDLED_EFFORT_COUNT, BehaviorContext, BlockBuilder, BlockContext, ButtonBehavior, COMPLETION_PROPERTY_KEYS, CONSUMED_HINTS, CONSUMED_HINT_KEYS, CONTEXT_ATOMS, CalcEngine, CalcEvalError, CalcParseError, CalcRegistrationError, CalcUnitError, CalculationRegistry, CardioDialect, ChildSelectionBehavior, ChildrenStrategy, ChoiceGroupMetric, ClearChildrenAction, ClimbDialect, ClimbMetricType, CompileAndPushBlockAction, CompletionTimestampBehavior, CompositeEffortRegistry, CompositeEquipmentRegistry, CompositePropertyRegistry, CountdownTimerBehavior, CountupTimerBehavior, CrossFitDialect, DAY, DEFAULT_RESOLVER_OPTIONS, DEFAULT_RUNTIME_OPTIONS, DEFAULT_SOUND_PROFILE, DEFAULT_UNRESOLVED_EFFORT_MET, DIM_COUNT, DIM_ENERGY, DIM_LENGTH, DIM_MASS, DIM_TIME, DIM_ZERO, DISCIPLINE_FACTORS, DialectStack, EFFORT_DATA_METRIC_TYPE, EFFORT_DISCIPLINES, EFFORT_REGISTRY_ORIGINS, EffortBlock, EffortFallbackStrategy, EffortResolver, EmitEventAction, EmitSystemOutputAction, ErrorAction, EventBus, ExecutionContext, ExitBehavior, FixtureSyntaxError, GenericGroupStrategy, GenericLoopStrategy, GenericTimerStrategy, HabitsDialect, INTENSITY_TIERS, InMemoryEffortRegistry, IntervalLogicStrategy, JitCompiler, LabelingBehavior, LocalEventProvider, LocalRuntimeSubscription, LookupRegistry, MAX_TAG_CONTRIBUTION_DEPTH, MdTimerRuntime, MemoryLocation, MemoryTypeEnum, MetricPromotionBehavior, NAMED_COMPOUNDS, NextAction, NextEvent, NextEventHandler, OutputEmitter, PRODUCTION_STRATEGIES, PROPERTY_KEY_TO_METRIC_TYPE, PopBlockAction, PushBlockAction, PushRestBlockAction, REFERENCE_PAIR_KEYS, REST_EXCLUSION, RPE_LABELS, RUNTIME_LOG_SUBJECT_WIDTH, RegisterEventHandlerAction, Registry, ReportOutputBehavior, ReportOutputStrategy, RestBlock, RoundsMetric, RuntimeAdapter, RuntimeBlock, RuntimeBuilder, RuntimeClock, RuntimeFactory, RuntimeLogger, RuntimeMemory, RuntimeObservers, RuntimeStack, SEED_PRECEDENCE_RANK, SOUND_PLAY_EVENT, SOUND_PROFILE_METRIC, STANDARD_UNITS, STORE_CALCS, STREAM_ATOMS, STRUCTURAL_EFFORT_WORDS, ScriptRuntime, SessionRootBlock, SessionRootStrategy, SnapshotClock, SoundProfileBehavior, SoundStrategy, SpanTrackingBehavior, StandardAnalyticsProfile, StartSessionAction, StartWorkoutAction, SubscriptionManager, TAG_MARKER_PREFIX, ThrowError, ThrowErrorAction, TimeSpan, TwoPassEffortResolutionProcess, TypedMemoryReference, UNITS, UnitRegistry, UnitSet, UnitsDialect, UnregisterEventHandlerAction, UpdateNextPreviewAction, VISIBILITY_ICONS, VISIBILITY_LABELS, WaitingToStartBlock, WaitingToStartInjectorBehavior, WhiteboardScript, WodDialect, YogaDialect, addAliasToEffort, addDim, applyTemplateDefaults, buildStaticLookups, buildTooltip, bundledEfforts, calculateElapsed, canonicalizeType, checkCalcLine, classifyStatements, closeCurrentSpan, collapseUnresolvedChoices, commonFixtureSet, compareStatement, compileLineForm, compilePropertySummaryCalcs, compilePropertySummaryCalcsWithDiagnostics, composeDivUnit, composeMulUnit, compoundName, computeColumnLabel, computeLabel, computeWorkloadRollups, convertScalar, createAnalyticsEngineForBlock, createCalcEngine, createCalcStack, createCompiler, createDialectStack, createDisciplinesTable, createEffortTable, createLineLocator, createPolicy as createMetricPresentationPolicy, createMockClock, createParser, createProfileTable, createReferenceIndex, createRpeLabelsTable, dailySessionLoads, dayBucket, dedupeEntitiesByPrecedence, dialectRegistry, dialectStack, diffStatement, dimEquals, disciplineFactorFor, documentToEffort, documentToEquipment, documentToTag, effortToDocument, equipmentToDocument, evaluate, extractEffortData, extractMetrics, extractPropertyRulesFromMeta, extractStatementRefs, extractStatements, extractStatementsRaw, extractSyntaxFacts, extractTagPropertyRules, filterTagsByVisibility, findBestFuzzyMatch, findTagContributionCycles, findUnresolvedChoices, fixtureBackSquat, fixtureBurpee, fixturePlank, fixtureRowing, fixtureRunning, fixtureUserCustom, fixtureUserOverride, formatDim, formatDuration, formatRuntimeLogLine, formatRuntimeLogRule, formatRuntimeTimestamp, formatSoundTrigger, metricsToLabel as fragmentsToLabel, fullFixtureSet, fuseUnits, fuseUnitsInMetrics, getAnalyticsFromLogs, getAnalyticsFromRuntime, getHints, getMetricVisibility, getUnit, hasHint, hintMetric, hintsToContainer, inferDim, isCastSubscription, isChoiceResolved, isDimensionless, isEffortDiscipline, isFragmentTag, isTimeLikeMetric, isWithinThreshold, levenshteinDistance, metricPresentation, mutateTimerSpans, normalizeForFuzzy, num, openSpan, outputNodeId, outputNodeIds, padRuntimeLogEvent, padRuntimeLogSubject, parseCalcLine, parseEffortFile, parseEquipmentFile, parseExpression, parseMetricLine, parseScript, parseSoundTrigger, printCalculation, readNestedSection, readTimer, realtimeProcessorRegistry, renderMetric, resolveDominantOrigin, resolveEntityRank, resolveEntityTier, resolveFusedEffortReferences, resolveProfileRules, resolveSoundProfile, resolveSourceRank, resolveSourceTier, resolveTagContributions, runMetricShakeOutQueue, slugifyRefLabel, snippetCompletions, startSpan, str, strategyRegistry, subDim, summaryProcessorRegistry, tagCategoryToDocument, tagContributionMetrics, tagMarkerSlug, tagMarkerSlugs, tagMarkerType, tagToDocument, toStoredOutputStatement, truthy, validateOutputUnit, parser as whiteboardParser, whiteboardScript, whiteboardScriptCompletion, whiteboardScriptCompletionSource, whiteboardScriptLanguage, whiteboardScriptSnippets, writeChoiceSelection };
15869
15967
  //# sourceMappingURL=index.mjs.map
15870
15968
  //# sourceMappingURL=index.mjs.map