@bitcobblers/wod-wiki-lang 0.43.2152 → 0.44.2155

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.cjs CHANGED
@@ -305,6 +305,125 @@ function computeColumnLabel(type) {
305
305
  const normalized = key.toLowerCase();
306
306
  return COLUMN_LABEL_MAP[normalized] ?? humanizeMetricKey(key);
307
307
  }
308
+ function tagReferenceFor(tagIndex, slug) {
309
+ const resolved = resolveTagContributions(tagIndex, slug);
310
+ const sound = resolved.metrics.find(
311
+ (contribution) => contribution.type === wodWikiCore.MetricType.Sound && typeof contribution.value === "string"
312
+ );
313
+ return {
314
+ slug,
315
+ label: slug,
316
+ ...resolved.functions.length > 0 ? { functions: [...resolved.functions] } : {},
317
+ ...sound ? { soundProfileId: sound.value } : {}
318
+ };
319
+ }
320
+ function collectTagReferences(metrics, into = []) {
321
+ for (const metric of metrics) {
322
+ const reference = wodWikiCore.getTagReference(metric.metadata);
323
+ if (!reference) continue;
324
+ if (into.some((existing) => existing.slug === reference.slug)) continue;
325
+ into.push(reference);
326
+ }
327
+ return into;
328
+ }
329
+ var TAG_MARKER_PREFIX = "tag.";
330
+ var MAX_TAG_CONTRIBUTION_DEPTH = 8;
331
+ var EMPTY = { metrics: [], functions: [], tags: [] };
332
+ function resolveTagContributions(tagIndex, slug) {
333
+ if (!tagIndex) return EMPTY;
334
+ const metrics = [];
335
+ const functions = [];
336
+ const tags2 = [];
337
+ const seenFunctions = /* @__PURE__ */ new Set();
338
+ const visited = /* @__PURE__ */ new Set();
339
+ const walk = (currentSlug, depth) => {
340
+ if (depth > MAX_TAG_CONTRIBUTION_DEPTH) return;
341
+ if (visited.has(currentSlug)) return;
342
+ const definition = tagIndex.resolve(currentSlug);
343
+ if (!definition) return;
344
+ visited.add(currentSlug);
345
+ tags2.push(currentSlug);
346
+ for (const contribution of definition.contributions) {
347
+ if (contribution.kind === "metric") metrics.push(contribution);
348
+ else if (contribution.kind === "function") {
349
+ if (!seenFunctions.has(contribution.name)) {
350
+ seenFunctions.add(contribution.name);
351
+ functions.push(contribution.name);
352
+ }
353
+ }
354
+ }
355
+ for (const contribution of definition.contributions) {
356
+ if (contribution.kind === "tag") walk(contribution.slug, depth + 1);
357
+ }
358
+ };
359
+ walk(slug, 0);
360
+ return { metrics, functions, tags: tags2 };
361
+ }
362
+ function findTagContributionCycles(tagIndex) {
363
+ if (!tagIndex) return [];
364
+ const cycles = [];
365
+ const seen = /* @__PURE__ */ new Set();
366
+ const walk = (slug, path, onPath) => {
367
+ if (onPath.has(slug)) {
368
+ const start = path.indexOf(slug);
369
+ if (start >= 0) {
370
+ const cycle = [...path.slice(start), slug];
371
+ const key = cycleKey(cycle);
372
+ if (!seen.has(key)) {
373
+ seen.add(key);
374
+ cycles.push(cycle);
375
+ }
376
+ }
377
+ return;
378
+ }
379
+ const definition = tagIndex.resolve(slug);
380
+ if (!definition) return;
381
+ const nextPath = [...path, slug];
382
+ const nextOnPath = new Set(onPath);
383
+ nextOnPath.add(slug);
384
+ for (const contribution of definition.contributions) {
385
+ if (contribution.kind === "tag") walk(contribution.slug, nextPath, nextOnPath);
386
+ }
387
+ };
388
+ for (const definition of tagIndex.list()) {
389
+ walk(definition.slug, [], /* @__PURE__ */ new Set());
390
+ }
391
+ return cycles;
392
+ }
393
+ function cycleKey(cycle) {
394
+ const nodes = cycle.slice(0, -1);
395
+ if (nodes.length === 0) return "";
396
+ let smallest = 0;
397
+ for (let index = 1; index < nodes.length; index++) {
398
+ if (nodes[index] < nodes[smallest]) smallest = index;
399
+ }
400
+ return [...nodes.slice(smallest), ...nodes.slice(0, smallest)].join(">");
401
+ }
402
+ function tagMarkerType(slug) {
403
+ return `${TAG_MARKER_PREFIX}${slug}`;
404
+ }
405
+ function tagMarkerSlug(type) {
406
+ if (!type.startsWith(TAG_MARKER_PREFIX)) return void 0;
407
+ const slug = type.slice(TAG_MARKER_PREFIX.length);
408
+ return slug.length > 0 ? slug : void 0;
409
+ }
410
+ function tagMarkerSlugs(metrics) {
411
+ const slugs = [];
412
+ for (const metric of metrics) {
413
+ const slug = tagMarkerSlug(metric.type);
414
+ if (slug !== void 0 && !slugs.includes(slug)) slugs.push(slug);
415
+ }
416
+ return slugs;
417
+ }
418
+ function tagContributionMetrics(contributions, origin = "parser") {
419
+ return contributions.map((contribution) => ({
420
+ type: contribution.type,
421
+ value: contribution.value,
422
+ origin,
423
+ image: String(contribution.value),
424
+ ...contribution.unit ? { unit: contribution.unit } : {}
425
+ }));
426
+ }
308
427
 
309
428
  // src/metrics/presentation/MetricPresentationPolicy.ts
310
429
  function isRestMetric(metric) {
@@ -368,6 +487,7 @@ function createPolicy() {
368
487
  }
369
488
  const t2 = metric.type;
370
489
  if ((t2 === wodWikiCore.MetricType.Sound || t2 === "sound") && surface !== "debug") return true;
490
+ if (t2.startsWith(TAG_MARKER_PREFIX) && surface !== "debug") return true;
371
491
  if (surface === "review-grid-column") {
372
492
  if (t2 === wodWikiCore.MetricType.Elapsed || t2 === "elapsed" || t2 === wodWikiCore.MetricType.Total || t2 === "total") return true;
373
493
  if (t2 === wodWikiCore.MetricType.System || t2 === "system") return true;
@@ -1505,15 +1625,16 @@ var TextMetric = class {
1505
1625
  origin = "parser";
1506
1626
  type = wodWikiCore.MetricType.Text;
1507
1627
  };
1508
-
1509
- // src/parser/semantic-classifier.ts
1510
1628
  var PROPERTY_KEY_TO_METRIC_TYPE = {
1511
1629
  rpe: wodWikiCore.MetricType.SessionRPE,
1512
1630
  rir: wodWikiCore.MetricType.RIR,
1513
1631
  intensity: wodWikiCore.MetricType.Intensity,
1514
1632
  load: wodWikiCore.MetricType.Load,
1515
1633
  volume: wodWikiCore.MetricType.Volume,
1516
- work: wodWikiCore.MetricType.Work
1634
+ work: wodWikiCore.MetricType.Work,
1635
+ // `sound: classic` names a sound profile, the same way a `#sound-classic`
1636
+ // tag reference does.
1637
+ sound: wodWikiCore.MetricType.Sound
1517
1638
  };
1518
1639
  var STRUCTURAL_EFFORT_WORDS = /* @__PURE__ */ new Set(["rest", "amrap", "emom", "tabata", "for time"]);
1519
1640
  var STANDARD_UNITS2 = UnitRegistry.standard();
@@ -1530,9 +1651,25 @@ function referenceMetadata(resolution) {
1530
1651
  function classifyStatements(facts, references) {
1531
1652
  return facts.statements.map((fact) => {
1532
1653
  const statement = new wodWikiCore.ParsedCodeStatement();
1533
- let metricPairs = [];
1654
+ const slots = [];
1534
1655
  for (const primitive of fact.primitives) {
1535
- metricPairs.push(...classifyPrimitive(primitive));
1656
+ slots.push(...classifySlots(primitive, references));
1657
+ }
1658
+ const explicitTypes = /* @__PURE__ */ new Set();
1659
+ for (const slot of slots) {
1660
+ if (slot.kind === "explicit") explicitTypes.add(slot.pair.metrics.type);
1661
+ }
1662
+ const contributedTypes = /* @__PURE__ */ new Set();
1663
+ const metricPairs = [];
1664
+ for (const slot of slots) {
1665
+ if (slot.kind !== "contributed") {
1666
+ metricPairs.push(slot.pair);
1667
+ continue;
1668
+ }
1669
+ const type = slot.pair.metrics.type;
1670
+ if (explicitTypes.has(type) || contributedTypes.has(type)) continue;
1671
+ contributedTypes.add(type);
1672
+ metricPairs.push(slot.pair);
1536
1673
  }
1537
1674
  const mergedPairs = mergeFragments(metricPairs);
1538
1675
  const finalPairs = references ? applyReferenceResolution(mergedPairs, fact, references) : mergedPairs;
@@ -1549,6 +1686,21 @@ function classifyStatements(facts, references) {
1549
1686
  return statement;
1550
1687
  });
1551
1688
  }
1689
+ function equipmentMeta(resolution, authored, meta2) {
1690
+ if (resolution.matched !== "split" || !resolution.remaining) return meta2;
1691
+ const prefixEnd = authored.trimEnd().length - resolution.remaining.length;
1692
+ if (prefixEnd <= 0) return meta2;
1693
+ const raw = meta2.raw.slice(0, prefixEnd).trimEnd();
1694
+ const length = raw.length;
1695
+ if (length <= 0) return meta2;
1696
+ return {
1697
+ ...meta2,
1698
+ endOffset: meta2.startOffset + length,
1699
+ length,
1700
+ columnEnd: meta2.columnStart + length,
1701
+ raw
1702
+ };
1703
+ }
1552
1704
  function applyReferenceResolution(pairs, fact, references) {
1553
1705
  const { index, defaults } = references;
1554
1706
  const result = [];
@@ -1573,7 +1725,10 @@ function applyReferenceResolution(pairs, fact, references) {
1573
1725
  diagnostics: resolution.diagnostics
1574
1726
  }
1575
1727
  ),
1576
- meta: meta2
1728
+ // The equipment is the prefix of the authored label; the effort is
1729
+ // what follows. Marking the whole phrase for both would let one mark
1730
+ // hide the other.
1731
+ meta: equipmentMeta(resolution, authored, meta2)
1577
1732
  });
1578
1733
  }
1579
1734
  };
@@ -1703,14 +1858,7 @@ function classifyPrimitive(primitive) {
1703
1858
  case "effort":
1704
1859
  return [{ metrics: new EffortMetric(primitive.raw), meta: primitive.meta }];
1705
1860
  case "property": {
1706
- const metricType = PROPERTY_KEY_TO_METRIC_TYPE[primitive.key.toLowerCase()] ?? wodWikiCore.MetricType.Custom;
1707
- return [{
1708
- metrics: new PropertyMetric(primitive.key, primitive.value, {
1709
- type: metricType,
1710
- image: primitive.raw
1711
- }),
1712
- meta: primitive.meta
1713
- }];
1861
+ return [propertyPair(primitive)];
1714
1862
  }
1715
1863
  case "metric_object": {
1716
1864
  const pairs = primitive.pairs;
@@ -1730,6 +1878,85 @@ function classifyPrimitive(primitive) {
1730
1878
  return [];
1731
1879
  }
1732
1880
  }
1881
+ function classifySlots(primitive, references) {
1882
+ if (primitive.kind === "property" && primitive.tagRefs !== void 0) {
1883
+ return propertyTagSlots(primitive, references);
1884
+ }
1885
+ if (primitive.kind === "tag_ref") {
1886
+ return fragmentTagSlots(primitive, references);
1887
+ }
1888
+ return classifyPrimitive(primitive).map((pair) => ({ kind: "explicit", pair }));
1889
+ }
1890
+ function propertyTagSlots(primitive, references) {
1891
+ const { slots, resolved } = referenceSlots(primitive.tagRefs ?? [], primitive.meta, references);
1892
+ if (resolved) return slots;
1893
+ return [{ kind: "explicit", pair: propertyPair(primitive) }];
1894
+ }
1895
+ function fragmentTagSlots(primitive, references) {
1896
+ const { slots, resolved } = referenceSlots([primitive.slug], primitive.meta, references);
1897
+ if (resolved) return slots;
1898
+ return [{
1899
+ kind: "explicit",
1900
+ pair: {
1901
+ metrics: {
1902
+ type: wodWikiCore.MetricType.Custom,
1903
+ value: primitive.raw,
1904
+ origin: "parser",
1905
+ image: primitive.raw
1906
+ },
1907
+ meta: primitive.meta
1908
+ }
1909
+ }];
1910
+ }
1911
+ function referenceSlots(slugs, meta2, references) {
1912
+ const slots = [];
1913
+ let resolved = false;
1914
+ for (const slug of slugs) {
1915
+ const contributions = resolveTagContributions(references?.tags, slug);
1916
+ const reference = tagReferenceFor(references?.tags, slug);
1917
+ const contributed = tagContributionMetrics(contributions.metrics);
1918
+ if (contributed.length > 0) resolved = true;
1919
+ for (const metric of contributed) {
1920
+ slots.push({
1921
+ kind: "contributed",
1922
+ pair: { metrics: withTagReference(metric, reference), meta: meta2 }
1923
+ });
1924
+ }
1925
+ for (const tag of contributions.tags) {
1926
+ resolved = true;
1927
+ const tagReference = tag === slug ? reference : tagReferenceFor(references?.tags, tag);
1928
+ slots.push({
1929
+ kind: "marker",
1930
+ pair: {
1931
+ metrics: withTagReference({
1932
+ type: tagMarkerType(tag),
1933
+ value: tag,
1934
+ origin: "parser",
1935
+ image: tag
1936
+ }, tagReference),
1937
+ meta: meta2
1938
+ }
1939
+ });
1940
+ }
1941
+ }
1942
+ return { slots, resolved };
1943
+ }
1944
+ function withTagReference(metric, reference) {
1945
+ return {
1946
+ ...metric,
1947
+ metadata: { ...metric.metadata ?? {}, [wodWikiCore.TAG_REFERENCE_METADATA_KEY]: reference }
1948
+ };
1949
+ }
1950
+ function propertyPair(primitive) {
1951
+ const metricType = PROPERTY_KEY_TO_METRIC_TYPE[primitive.key.toLowerCase()] ?? wodWikiCore.MetricType.Custom;
1952
+ return {
1953
+ metrics: new PropertyMetric(primitive.key, primitive.value, {
1954
+ type: metricType,
1955
+ image: primitive.raw
1956
+ }),
1957
+ meta: primitive.meta
1958
+ };
1959
+ }
1733
1960
  function mergeFragments(pairs) {
1734
1961
  if (pairs.length < 2) return pairs;
1735
1962
  const result = [];
@@ -1809,33 +2036,34 @@ function mergeFragments(pairs) {
1809
2036
  }
1810
2037
  var parser = lr.LRParser.deserialize({
1811
2038
  version: 14,
1812
- states: "(WQVQPOOO!jQPO'#C}O#}QPO'#CcOOQO'#Cc'#CcOOQO'#Cf'#CfOOQO'#Cg'#CgOOQO'#Ce'#CeO%_QPO'#CeO%gQPO'#CiOOQO'#Ck'#CkOOQO'#C}'#C}OOQO'#Ct'#CtO%oQPO'#ClOOQO'#Cm'#CmOOQO'#Cn'#CnOOQO'#Cp'#CpOOQO'#Cq'#CqOOQO'#Co'#CoO%tQPO'#CoOOQO'#Cr'#CrOOQO'#Cs'#CsO&kQPO'#CuOOQO'#Cd'#CdOOQO'#Cz'#CzO(PQPO'#CbO&rQPO'#CbOOQO'#Cy'#CyQVQPOOO(WQPO,58xOOQO,59P,59PO(cQPO'#CjOOQO'#C{'#C{O(qQPO,59TOOQO-E6{-E6{O(|QPO,59WOOQO,59Z,59ZOOQO'#DO'#DOO)XQPO,59aOOQO,59a,59aOOQO-E6x-E6xOOQO,58|,58|O)`QPO,58|OOQO-E6w-E6wOOQO1G.d1G.dO)gQPO'#C|O)lQPO,59UOOQO-E6y-E6yOOQO1G.o1G.oO)zQPO1G.rOOQO-E6|-E6|OOQO1G.{1G.{OOQO1G.h1G.hOOQO,59h,59hOOQO-E6z-E6zOOQO7+$^7+$^",
1813
- stateData: "*u~OuOS~ORPOTaO[UOwQOxROySOzVO{TO|WO!OXO!PXO!Q[O!RYO!T]O!U^O!V_O!W`O!XcO!YdO!ZeO!^jO~OvlORqXTqX[qXwqXyqXzqX{qX|qX!OqX!PqX!QqX!RqX!TqX!UqX!VqX!WqX!XqX!YqX!ZqX!^qX~ORVXTVX[VXwVXyVXzVX{VX|VX!OVX!PVX!QVX!RVX!TVX!UVX!VVX!WVX!XVX!YVX!ZVX!^qX~O[mO{TO~ORoOTnO~OvrO~OTsO!W`O~ORtOStOTtOjtOktOltOvtO!RtO![tO~O!]vO~P%|ORYOTaO[UOwYOySOzVO{TO|WO!OXO!PXO!Q[O!RYO!T]O!U^O!V_O!W`O!XcO!YdO!ZeO~O!^xO~P&rOR{OS{OT{O~Ow|OR^XT^X}^X~ORoOTnO}!PO~ORYOwYO!RYO~O!]!SO~P%|O!^!TO~P&rOT!UO~Ow|OR^aT^a}^a~ORYOwYO!RYO!S!WO~O!T!U!O!P{[!Vyzv!X!Y!R!Z!]![jklRwxT!^u!R~",
1814
- goto: "$UsPPtPPPtx|!T![!cP!T!m!T!T!T!T!T!q!x!T!T!T!TPPP#S#Y#e#k#q$OTjOkTiOkZgOhikyZfOhikyZVOhikyYUOhikyRmVToWpZbOhikyYaOhikyRsbQkORzkShOkSwhyRyiQpWR!OpQ}nR!V}YZOhikyQq!QR!QrQueR!Ru",
1815
- nodeNames: "\u26A0 Program Property Identifier String Number Block Lap Fragment Duration Trend CollectibleTimer Timer Rounds Sequence RoundScheme Action Text Heading Quantity AtSign Question Slash Pipe Effort MetricObject True False Null",
1816
- maxTerm: 60,
2039
+ states: "(dQVQPOOO!mQPO'#DPO$TQPO'#CdOOQO'#Cd'#CdOOQO'#Cg'#CgOOQO'#Ch'#ChOOQO'#Cf'#CfO%hQPO'#CfO%pQPO'#CjOOQO'#Cl'#ClOOQO'#DP'#DPOOQO'#Cu'#CuO%xQPO'#CmOOQO'#Cn'#CnOOQO'#Co'#CoOOQO'#Cq'#CqOOQO'#Cr'#CrOOQO'#Cp'#CpO%}QPO'#CpOOQO'#Cs'#CsOOQO'#Ct'#CtO&tQPO'#CvOOQO'#Ce'#CeOOQO'#C|'#C|O(]QPO'#CcO&{QPO'#CcOOQO'#Cz'#CzQVQPOOO(dQPO,58xOOQO,59Q,59QO(rQPO'#CkOOQO'#C}'#C}O)QQPO,59UOOQO-E6}-E6}O)]QPO,59XOOQO,59[,59[OOQO'#DQ'#DQO)hQPO,59bOOQO,59b,59bOOQO-E6z-E6zOOQO,58},58}O)oQPO,58}OOQO-E6x-E6xOOQO'#C{'#C{OOQO1G.d1G.dO)vQPO'#DOO){QPO,59VOOQO-E6{-E6{OOQO1G.p1G.pO*ZQPO1G.sOOQO-E7O-E7OOOQO1G.|1G.|OOQO1G.i1G.iPOQO-E6y-E6yOOQO,59j,59jOOQO-E6|-E6|OOQO7+$_7+$_",
2040
+ stateData: "+V~OwOS~ORPOTaOUfO]UOyQOzRO{SO|VO}TO!OWO!QXO!RXO!S[O!TYO!V]O!W^O!X_O!Y`O!ZcO![dO!]eO!`jO~OxlORsXTsXUsX]sXysX{sX|sX}sX!OsX!QsX!RsX!SsX!TsX!VsX!WsX!XsX!YsX!ZsX![sX!]sX!`sX~ORWXTWXUWX]WXyWX{WX|WX}WX!OWX!QWX!RWX!SWX!TWX!VWX!WWX!XWX!YWX!ZWX![WX!]WX!`sX~O]mO}TO~ORoOTnO~OxrO~OTsO!Y`O~ORtOStOTtOktOltOmtOxtO!TtO!^tO~O!_vO~P&VORYOTaOUfO]UOyYO{SO|VO}TO!OWO!QXO!RXO!S[O!TYO!V]O!W^O!X_O!Y`O!ZcO![dO!]eO~O!`xO~P&{OR|OS|OT|OU{O~Oy}OR_XT_X!P_X~ORoOTnO!P!QO~ORYOyYO!TYO~O!_!TO~P&VO!`!UO~P&{OT!WO~Oy}OR_aT_a!P_a~ORYOyYO!TYO!U!YO~O!VU!W!Q!R}]!X{|x!Z![!T!]!_!^klmRyzT!`w!T~",
2041
+ goto: "$ZuPPvPPPPvz!O!V!^!eP!V!o!V!V!V!V!V!s!z!V!V!V!VPPP#U#[#_#j#p#v$TTjOkTiOkZgOhikyZfOhikyZVOhikyYUOhikyRmVToWpZbOhikyYaOhikyRsbQkORzkR|lShOkSwhyRyiQpWR!PpQ!OnR!X!OYZOhikyQq!RR!RrQueR!Su",
2042
+ nodeNames: "\u26A0 Program Property Identifier String Number TagRef Block Lap Fragment Duration Trend CollectibleTimer Timer Rounds Sequence RoundScheme Action Text Heading Quantity AtSign Question Slash Pipe Effort MetricObject True False Null",
2043
+ maxTerm: 62,
1817
2044
  skippedNodes: [0],
1818
- repeatNodeCount: 6,
1819
- tokenData: "6k~RvXY#iYZ#z]^$Ppq#iqr$Vrs$}st'dtu$Vuv$Vvw$Vxy(Oyz(Tz{(Y{|)S|})X}!O*R!O!P*W!P!Q+Z!Q![+}![!].Q!_!`$V!a!b.j!b!c.o!c!}/i!}#O/w#O#P$V#P#Q/|#Q#R0R#T#Y/i#Y#Z0{#Z#b/i#b#c2|#c#h/i#h#i4l#i#o/i#o#p6[#p#q6a#q#r6f~#nSu~XY#iYZ#z]^$Ppq#i~$PO!^~~$SPYZ#z~$[Z!R~qr$Vtu$Vuv$Vvw$Vz{$V|}$V!O!P$V!_!`$V!b!c$V#O#P$V#Q#R$V~%QXOY$}Z]$}^r$}rs%ms#O$}#O#P&_#P;'S$};'S;=`'^<%lO$}~%rXS~OY$}Z]$}^r$}rs%ms#O$}#O#P&_#P;'S$};'S;=`'^<%lO$}~&bRO;'S$};'S;=`&k;=`O$}~&nYOY$}Z]$}^r$}rs%ms#O$}#O#P&_#P;'S$};'S;=`'^;=`<%l$}<%lO$}~'aP;=`<%l$}~'iT!U~OY'dZ]'d^;'S'd;'S;=`'x<%lO'd~'{P;=`<%l'd~(TO|~~(YO}~~(aZz~!R~qr$Vtu$Vuv$Vvw$Vz{$V|}$V!O!P$V!_!`$V!b!c$V#O#P$V#Q#R$V~)XOx~~)`Z!R~![~qr$Vtu$Vuv$Vvw$Vz{$V|}$V!O!P$V!_!`$V!b!c$V#O#P$V#Q#R$V~*WOw~~*][!R~qr$Vtu$Vuv$Vvw$Vz{$V|}$V!O!P$V!Q![+R!_!`$V!b!c$V#O#P$V#Q#R$V~+WPT~!Q![+R~+`P!X~!P!Q+c~+hT!T~OY+cZ]+c^;'S+c;'S;=`+w<%lO+c~+zP;=`<%l+c~,SUT~pq,f!O!P-Z!Q![+}![!]-a!z!{-r#l#m-r~,iRpq,f!z!{,r#l#m,r~,uPpq,x~,{Qpq,x!Q![-R~-WP!P~!Q![-R~-^P!Q![+R~-dP!Q![-g~-lQ[~!Q![-g![!]-a~-uP!Q![-x~-}P!O~!Q![-x~.VQv~!Q![.]!a!b.e~.bP[~!Q![.]~.jO{~~.oO!W~~.vZ!V~!R~qr$Vtu$Vuv$Vvw$Vz{$V|}$V!O!P$V!_!`$V!b!c$V#O#P$V#Q#R$V~/nRR~!Q![/i!c!}/i#T#o/i~/|O!Q~~0RO!S~~0YZy~!R~qr$Vtu$Vuv$Vvw$Vz{$V|}$V!O!P$V!_!`$V!b!c$V#O#P$V#Q#R$V~1QSR~!Q![/i!c!}/i#T#U1^#U#o/i~1cTR~!Q![/i!c!}/i#T#`/i#`#a1r#a#o/i~1wTR~!Q![/i!c!}/i#T#g/i#g#h2W#h#o/i~2]TR~!Q![/i!c!}/i#T#X/i#X#Y2l#Y#o/i~2sRk~R~!Q![/i!c!}/i#T#o/i~3RTR~!Q![/i!c!}/i#T#i/i#i#j3b#j#o/i~3gTR~!Q![/i!c!}/i#T#`/i#`#a3v#a#o/i~3{TR~!Q![/i!c!}/i#T#`/i#`#a4[#a#o/i~4cRl~R~!Q![/i!c!}/i#T#o/i~4qTR~!Q![/i!c!}/i#T#f/i#f#g5Q#g#o/i~5VTR~!Q![/i!c!}/i#T#i/i#i#j5f#j#o/i~5kTR~!Q![/i!c!}/i#T#X/i#X#Y5z#Y#o/i~6RRj~R~!Q![/i!c!}/i#T#o/i~6aO!Z~~6fO!Y~~6kO!]~",
2045
+ repeatNodeCount: 7,
2046
+ tokenData: "7o~RvXY#iYZ#z]^$Ppq#iqr$Vrs$}st'dtu$Vuv$Vvw$Vxy)Pyz)Uz{)Z{|*T|}*Y}!O+S!O!P+X!P!Q,[!Q![-R![!]/U!_!`$V!a!b/n!b!c/s!c!}0m!}#O0{#O#P$V#P#Q1Q#Q#R1V#T#Y0m#Y#Z2P#Z#b0m#b#c4Q#c#h0m#h#i5p#i#o0m#o#p7`#p#q7e#q#r7j~#nSw~XY#iYZ#z]^$Ppq#i~$PO!`~~$SPYZ#z~$[Z!T~qr$Vtu$Vuv$Vvw$Vz{$V|}$V!O!P$V!_!`$V!b!c$V#O#P$V#Q#R$V~%QXOY$}Z]$}^r$}rs%ms#O$}#O#P&_#P;'S$};'S;=`'^<%lO$}~%rXS~OY$}Z]$}^r$}rs%ms#O$}#O#P&_#P;'S$};'S;=`'^<%lO$}~&bRO;'S$};'S;=`&k;=`O$}~&nYOY$}Z]$}^r$}rs%ms#O$}#O#P&_#P;'S$};'S;=`'^;=`<%l$}<%lO$}~'aP;=`<%l$}~'gXO!Q(S!Q![(n![!c(S!c!}(n!}#T(S#T#o(n#o;'S(S;'S;=`(h<%lO(S~(XT!W~OY(SZ](S^;'S(S;'S;=`(h<%lO(S~(kP;=`<%l(S~(sSU~}!O(n!Q![(n!c!}(n#T#o(n~)UO!O~~)ZO!P~~)bZ|~!T~qr$Vtu$Vuv$Vvw$Vz{$V|}$V!O!P$V!_!`$V!b!c$V#O#P$V#Q#R$V~*YOz~~*aZ!T~!^~qr$Vtu$Vuv$Vvw$Vz{$V|}$V!O!P$V!_!`$V!b!c$V#O#P$V#Q#R$V~+XOy~~+^[!T~qr$Vtu$Vuv$Vvw$Vz{$V|}$V!O!P$V!Q![,S!_!`$V!b!c$V#O#P$V#Q#R$V~,XPT~!Q![,S~,aP!Z~!P!Q,d~,iU!V~OY,dZ],d^s,dt;'S,d;'S;=`,{<%lO,d~-OP;=`<%l,d~-WUT~pq-j!O!P._!Q![-R![!].e!z!{.v#l#m.v~-mRpq-j!z!{-v#l#m-v~-yPpq-|~.PQpq-|!Q![.V~.[P!R~!Q![.V~.bP!Q![,S~.hP!Q![.k~.pQ]~!Q![.k![!].e~.yP!Q![.|~/RP!Q~!Q![.|~/ZQx~!Q![/a!a!b/i~/fP]~!Q![/a~/nO}~~/sO!Y~~/zZ!X~!T~qr$Vtu$Vuv$Vvw$Vz{$V|}$V!O!P$V!_!`$V!b!c$V#O#P$V#Q#R$V~0rRR~!Q![0m!c!}0m#T#o0m~1QO!S~~1VO!U~~1^Z{~!T~qr$Vtu$Vuv$Vvw$Vz{$V|}$V!O!P$V!_!`$V!b!c$V#O#P$V#Q#R$V~2USR~!Q![0m!c!}0m#T#U2b#U#o0m~2gTR~!Q![0m!c!}0m#T#`0m#`#a2v#a#o0m~2{TR~!Q![0m!c!}0m#T#g0m#g#h3[#h#o0m~3aTR~!Q![0m!c!}0m#T#X0m#X#Y3p#Y#o0m~3wRl~R~!Q![0m!c!}0m#T#o0m~4VTR~!Q![0m!c!}0m#T#i0m#i#j4f#j#o0m~4kTR~!Q![0m!c!}0m#T#`0m#`#a4z#a#o0m~5PTR~!Q![0m!c!}0m#T#`0m#`#a5`#a#o0m~5gRm~R~!Q![0m!c!}0m#T#o0m~5uTR~!Q![0m!c!}0m#T#f0m#f#g6U#g#o0m~6ZTR~!Q![0m!c!}0m#T#i0m#i#j6j#j#o0m~6oTR~!Q![0m!c!}0m#T#X0m#X#Y7O#Y#o0m~7VRk~R~!Q![0m!c!}0m#T#o0m~7eO!]~~7jO![~~7oO!_~",
1820
2047
  tokenizers: [0],
1821
2048
  topRules: { "Program": [0, 1] },
1822
- tokenPrec: 424
2049
+ tokenPrec: 439
1823
2050
  });
1824
2051
 
1825
2052
  // src/grammar/parser.terms.ts
1826
- var Lap = 7;
1827
- var Fragment = 8;
1828
- var Duration = 9;
1829
- var Rounds = 13;
1830
- var RoundScheme = 15;
1831
- var Action = 16;
1832
- var Text = 17;
1833
- var Heading = 18;
1834
- var Quantity = 19;
1835
- var Slash = 22;
1836
- var Pipe = 23;
1837
- var Effort = 24;
1838
- var MetricObject = 25;
2053
+ var TagRef = 6;
2054
+ var Lap = 8;
2055
+ var Fragment = 9;
2056
+ var Duration = 10;
2057
+ var Rounds = 14;
2058
+ var RoundScheme = 16;
2059
+ var Action = 17;
2060
+ var Text = 18;
2061
+ var Heading = 19;
2062
+ var Quantity = 20;
2063
+ var Slash = 23;
2064
+ var Pipe = 24;
2065
+ var Effort = 25;
2066
+ var MetricObject = 26;
1839
2067
 
1840
2068
  // src/parser/syntax-parser.ts
1841
2069
  function createLineLocator(source) {
@@ -1941,15 +2169,28 @@ function createPropertyPrimitive(locator, source, from, to) {
1941
2169
  const match = raw.match(/^\s*([A-Za-z][A-Za-z0-9]*)\s*:\s*(.*?)\s*$/);
1942
2170
  if (!match) return null;
1943
2171
  const [, key, rawValue] = match;
2172
+ const tagRefs = parseTagRefs(rawValue);
1944
2173
  return {
1945
2174
  kind: "property",
1946
2175
  raw,
1947
2176
  meta: meta2,
1948
2177
  key,
1949
2178
  valueRaw: rawValue,
1950
- value: parsePropertyValue(rawValue)
2179
+ value: tagRefs !== void 0 ? rawValue.trim() : parsePropertyValue(rawValue),
2180
+ ...tagRefs !== void 0 ? { tagRefs } : {}
1951
2181
  };
1952
2182
  }
2183
+ function parseTagRefs(rawValue) {
2184
+ const parts = rawValue.trim().split(/\s+/).filter((part) => part.length > 0);
2185
+ if (parts.length === 0) return void 0;
2186
+ const slugs = [];
2187
+ for (const part of parts) {
2188
+ const match = part.match(/^#([a-zA-Z0-9][a-zA-Z0-9-]*)$/);
2189
+ if (!match) return void 0;
2190
+ slugs.push(match[1]);
2191
+ }
2192
+ return slugs;
2193
+ }
1953
2194
  function parsePropertyValue(rawValue) {
1954
2195
  const trimmed = rawValue.trim();
1955
2196
  if (/^"(?:[^"\\]|\\.)*"$/.test(trimmed)) {
@@ -2049,6 +2290,17 @@ function mapFragmentToPrimitive(locator, source, node) {
2049
2290
  };
2050
2291
  return primitive;
2051
2292
  }
2293
+ case TagRef: {
2294
+ const match = raw.match(/^#([a-zA-Z0-9][a-zA-Z0-9-]*)$/);
2295
+ if (!match) return null;
2296
+ const primitive = {
2297
+ kind: "tag_ref",
2298
+ raw,
2299
+ meta: meta2,
2300
+ slug: match[1]
2301
+ };
2302
+ return primitive;
2303
+ }
2052
2304
  case Slash: {
2053
2305
  const primitive = {
2054
2306
  kind: "effort",
@@ -2289,269 +2541,743 @@ var WhiteboardScript = class {
2289
2541
  }
2290
2542
  };
2291
2543
 
2292
- // src/parser/parseScript.ts
2293
- function parseScript(inputText, options = {}) {
2294
- if (!inputText || !inputText.trim()) {
2295
- return new WhiteboardScript(inputText, [], []);
2296
- }
2297
- try {
2298
- const doc = inputText.endsWith("\n") ? inputText : inputText + "\n";
2299
- const tree = parser.parse(doc);
2300
- const sport = options.sport ?? options.dialect;
2301
- const references = options.references ? { index: options.references, defaults: options.defaults } : void 0;
2302
- const statements = options.withoutDialects ? extractStatementsRaw(inputText, tree, references) : extractStatements(inputText, tree, sport, references);
2303
- return new WhiteboardScript(inputText, statements, []);
2304
- } catch (error) {
2305
- const msg = error instanceof Error ? error.message : "Unknown parse error";
2306
- return new WhiteboardScript(inputText, [], [{ message: msg }]);
2544
+ // src/effort-registry/types.ts
2545
+ var EFFORT_REGISTRY_ORIGINS = ["bundled", "user"];
2546
+ var CATALOG_PRECEDENCE_RANK = {
2547
+ core: 0,
2548
+ community: 1,
2549
+ imported: 2,
2550
+ user: 3
2551
+ };
2552
+ function resolveSourceTier(sourceIdOrMetaKey, explicitTier) {
2553
+ if (explicitTier) return explicitTier;
2554
+ if (!sourceIdOrMetaKey) return "core";
2555
+ if (sourceIdOrMetaKey === "user") return "user";
2556
+ const id = sourceIdOrMetaKey.startsWith("seed:") ? sourceIdOrMetaKey.slice(5) : sourceIdOrMetaKey;
2557
+ if (id === "core" || id === "efforts" || id === "equipment") return "core";
2558
+ if (id === "community" || id.startsWith("community-") || id === "common") return "community";
2559
+ return "imported";
2560
+ }
2561
+ function resolveSourceRank(sourceIdOrMetaKey, explicitTier) {
2562
+ return CATALOG_PRECEDENCE_RANK[resolveSourceTier(sourceIdOrMetaKey, explicitTier)];
2563
+ }
2564
+ function resolveEntityTier(entity) {
2565
+ if (entity.registrySource === "user") return "user";
2566
+ return resolveSourceTier(entity.sourceId);
2567
+ }
2568
+ function resolveEntityRank(entity) {
2569
+ return CATALOG_PRECEDENCE_RANK[resolveEntityTier(entity)];
2570
+ }
2571
+ function dedupeEntitiesByPrecedence(entities) {
2572
+ const bySlug = /* @__PURE__ */ new Map();
2573
+ for (const entity of entities) {
2574
+ const key = entity.slug.toLowerCase();
2575
+ const prior = bySlug.get(key);
2576
+ if (!prior || resolveEntityRank(entity) >= resolveEntityRank(prior)) {
2577
+ bySlug.set(key, entity);
2578
+ }
2307
2579
  }
2580
+ return Array.from(bySlug.values());
2308
2581
  }
2582
+ var INTENSITY_TIERS = ["low", "moderate", "high"];
2583
+ var DEFAULT_RESOLVER_OPTIONS = {
2584
+ defaultMet: 5,
2585
+ defaultThreshold: 2
2586
+ };
2309
2587
 
2310
- // src/parser/md-timer.ts
2311
- var MdTimerRuntime = class {
2312
- config;
2313
- constructor(config) {
2314
- this.config = config;
2588
+ // src/effort-registry/fuzzyMatch.ts
2589
+ function levenshteinDistance(a, b) {
2590
+ if (a === b) return 0;
2591
+ if (a.length === 0) return b.length;
2592
+ if (b.length === 0) return a.length;
2593
+ if (a.length > b.length) {
2594
+ const tmp = a;
2595
+ a = b;
2596
+ b = tmp;
2315
2597
  }
2316
- /**
2317
- * Parse WhiteboardScript source into a {@link WhiteboardScript}, running the
2318
- * Dialect Stack on every statement.
2319
- *
2320
- * @param sport - The block's `:sport` fence suffix (` ```log:climbing `).
2321
- */
2322
- read(inputText, sport) {
2323
- return parseScript(inputText, { sport, ...this.config });
2598
+ const m = a.length;
2599
+ const n = b.length;
2600
+ let prev = new Uint32Array(m + 1);
2601
+ let curr = new Uint32Array(m + 1);
2602
+ for (let i = 0; i <= m; i++) {
2603
+ prev[i] = i;
2324
2604
  }
2325
- /**
2326
- * Parse without running the Dialect Stack. Used by the parser test harness
2327
- * which applies its own Dialect set. Production consumers should use {@link read}.
2328
- */
2329
- readWithoutDialects(inputText) {
2330
- return parseScript(inputText, { withoutDialects: true, ...this.config });
2605
+ for (let j = 1; j <= n; j++) {
2606
+ curr[0] = j;
2607
+ const bj = b.charCodeAt(j - 1);
2608
+ for (let i = 1; i <= m; i++) {
2609
+ const cost = a.charCodeAt(i - 1) === bj ? 0 : 1;
2610
+ curr[i] = Math.min(
2611
+ prev[i] + 1,
2612
+ // deletion
2613
+ curr[i - 1] + 1,
2614
+ // insertion
2615
+ prev[i - 1] + cost
2616
+ // substitution
2617
+ );
2618
+ }
2619
+ const swap = prev;
2620
+ prev = curr;
2621
+ curr = swap;
2331
2622
  }
2332
- };
2333
-
2334
- // src/parser/parserInstance.ts
2335
- function createParser(config) {
2336
- return new MdTimerRuntime(config);
2623
+ return prev[m];
2337
2624
  }
2338
- var nextTagID = 0;
2339
- var Tag = class _Tag {
2340
- /**
2341
- @internal
2342
- */
2343
- constructor(name2, set, base, modified) {
2344
- this.name = name2;
2345
- this.set = set;
2346
- this.base = base;
2347
- this.modified = modified;
2348
- this.id = nextTagID++;
2349
- }
2350
- toString() {
2351
- let { name: name2 } = this;
2352
- for (let mod of this.modified)
2353
- if (mod.name)
2354
- name2 = `${mod.name}(${name2})`;
2355
- return name2;
2625
+ function normalizeForFuzzy(input) {
2626
+ return input.toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, " ").replace(/[-_]+/g, " ").trim().replace(/\s+/g, " ");
2627
+ }
2628
+ function isWithinThreshold(a, b, threshold) {
2629
+ const na = normalizeForFuzzy(a);
2630
+ const nb = normalizeForFuzzy(b);
2631
+ if (na === nb) return true;
2632
+ if (Math.abs(na.length - nb.length) > threshold) return false;
2633
+ return levenshteinDistance(na, nb) <= threshold;
2634
+ }
2635
+ function findBestFuzzyMatch(query, candidates, threshold) {
2636
+ const nq = normalizeForFuzzy(query);
2637
+ let best = null;
2638
+ for (const candidate of candidates) {
2639
+ const nc = normalizeForFuzzy(candidate);
2640
+ if (Math.abs(nq.length - nc.length) > threshold) continue;
2641
+ const distance = levenshteinDistance(nq, nc);
2642
+ if (distance > threshold) continue;
2643
+ if (!best || distance < best.distance) {
2644
+ best = { target: candidate, distance };
2645
+ if (distance === 0) break;
2646
+ }
2356
2647
  }
2357
- static define(nameOrParent, parent) {
2358
- let name2 = typeof nameOrParent == "string" ? nameOrParent : "?";
2359
- if (nameOrParent instanceof _Tag)
2360
- parent = nameOrParent;
2361
- if (parent === null || parent === void 0 ? void 0 : parent.base)
2362
- throw new Error("Can not derive from a modified tag");
2363
- let tag = new _Tag(name2, [], null, []);
2364
- tag.set.push(tag);
2365
- if (parent)
2366
- for (let t2 of parent.set)
2367
- tag.set.push(t2);
2368
- return tag;
2648
+ return best;
2649
+ }
2650
+
2651
+ // src/effort-registry/reference-resolution.ts
2652
+ function buildExactIndex(entities) {
2653
+ const deduped = dedupeEntitiesByPrecedence(entities);
2654
+ const byKey = /* @__PURE__ */ new Map();
2655
+ const add = (key, entity) => {
2656
+ if (!key) return;
2657
+ const existing = byKey.get(key);
2658
+ if (existing) {
2659
+ if (!existing.includes(entity)) existing.push(entity);
2660
+ } else {
2661
+ byKey.set(key, [entity]);
2662
+ }
2663
+ };
2664
+ for (const entity of deduped) {
2665
+ add(entity.slug.toLowerCase(), entity);
2666
+ add(normalizeForFuzzy(entity.label), entity);
2667
+ for (const alias of entity.aliases) add(normalizeForFuzzy(alias), entity);
2369
2668
  }
2370
- /**
2371
- Define a tag _modifier_, which is a function that, given a tag,
2372
- will return a tag that is a subtag of the original. Applying the
2373
- same modifier to a twice tag will return the same value (`m1(t1)
2374
- == m1(t1)`) and applying multiple modifiers will, regardless or
2375
- order, produce the same tag (`m1(m2(t1)) == m2(m1(t1))`).
2376
-
2377
- When multiple modifiers are applied to a given base tag, each
2378
- smaller set of modifiers is registered as a parent, so that for
2379
- example `m1(m2(m3(t1)))` is a subtype of `m1(m2(t1))`,
2380
- `m1(m3(t1)`, and so on.
2381
- */
2382
- static defineModifier(name2) {
2383
- let mod = new Modifier(name2);
2384
- return (tag) => {
2385
- if (tag.modified.indexOf(mod) > -1)
2386
- return tag;
2387
- return Modifier.get(tag.base || tag, tag.modified.concat(mod).sort((a, b) => a.id - b.id));
2388
- };
2389
- }
2390
- };
2391
- var nextModifierID = 0;
2392
- var Modifier = class _Modifier {
2393
- constructor(name2) {
2394
- this.name = name2;
2395
- this.instances = [];
2396
- this.id = nextModifierID++;
2397
- }
2398
- static get(base, mods) {
2399
- if (!mods.length)
2400
- return base;
2401
- let exists = mods[0].instances.find((t2) => t2.base == base && sameArray(mods, t2.modified));
2402
- if (exists)
2403
- return exists;
2404
- let set = [], tag = new Tag(base.name, set, base, mods);
2405
- for (let m of mods)
2406
- m.instances.push(tag);
2407
- let configs = powerSet(mods);
2408
- for (let parent of base.set)
2409
- if (!parent.modified.length)
2410
- for (let config of configs)
2411
- set.push(_Modifier.get(parent, config));
2412
- return tag;
2413
- }
2414
- };
2415
- function sameArray(a, b) {
2416
- return a.length == b.length && a.every((x, i) => x == b[i]);
2417
- }
2418
- function powerSet(array) {
2419
- let sets = [[]];
2420
- for (let i = 0; i < array.length; i++) {
2421
- for (let j = 0, e = sets.length; j < e; j++) {
2422
- sets.push(sets[j].concat(array[i]));
2669
+ return {
2670
+ byKey,
2671
+ lookup(key) {
2672
+ const direct = byKey.get(key) ?? byKey.get(normalizeForFuzzy(key));
2673
+ if (!direct || direct.length === 0) return { entity: null, ambiguous: [] };
2674
+ if (direct.length === 1) return { entity: direct[0], ambiguous: [] };
2675
+ let maxRank = -1;
2676
+ for (const candidate of direct) {
2677
+ const rank = resolveEntityRank(candidate);
2678
+ if (rank > maxRank) maxRank = rank;
2679
+ }
2680
+ const topTier = direct.filter((candidate) => resolveEntityRank(candidate) === maxRank);
2681
+ if (topTier.length === 1) return { entity: topTier[0], ambiguous: [] };
2682
+ return { entity: null, ambiguous: topTier };
2423
2683
  }
2684
+ };
2685
+ }
2686
+ function equipmentRef(entity) {
2687
+ return { kind: "equipment", slug: entity.slug, label: entity.label };
2688
+ }
2689
+ function dedupeRefs(refs) {
2690
+ const seen = /* @__PURE__ */ new Set();
2691
+ const result = [];
2692
+ for (const ref of refs) {
2693
+ const key = `${ref.kind}:${ref.slug}`;
2694
+ if (seen.has(key)) continue;
2695
+ seen.add(key);
2696
+ result.push(ref);
2424
2697
  }
2425
- return sets.sort((a, b) => b.length - a.length);
2698
+ return result;
2426
2699
  }
2427
- function styleTags(spec) {
2428
- let byName = /* @__PURE__ */ Object.create(null);
2429
- for (let prop in spec) {
2430
- let tags2 = spec[prop];
2431
- if (!Array.isArray(tags2))
2432
- tags2 = [tags2];
2433
- for (let part of prop.split(" "))
2434
- if (part) {
2435
- let pieces = [], mode = 2, rest = part;
2436
- for (let pos = 0; ; ) {
2437
- if (rest == "..." && pos > 0 && pos + 3 == part.length) {
2438
- mode = 1;
2439
- break;
2440
- }
2441
- let m = /^"(?:[^"\\]|\\.)*?"|[^\/!]+/.exec(rest);
2442
- if (!m)
2443
- throw new RangeError("Invalid path: " + part);
2444
- pieces.push(m[0] == "*" ? "" : m[0][0] == '"' ? JSON.parse(m[0]) : m[0]);
2445
- pos += m[0].length;
2446
- if (pos == part.length)
2447
- break;
2448
- let next = part[pos++];
2449
- if (pos == part.length && next == "!") {
2450
- mode = 0;
2451
- break;
2452
- }
2453
- if (next != "/")
2454
- throw new RangeError("Invalid path: " + part);
2455
- rest = part.slice(pos);
2700
+ function createReferenceIndex(catalog, materialize) {
2701
+ const dedupedEquipment = dedupeEntitiesByPrecedence(catalog.equipment ?? []);
2702
+ const efforts = buildExactIndex(catalog.efforts ?? []);
2703
+ const equipment = buildExactIndex(dedupedEquipment);
2704
+ 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);
2705
+ const materializer = materialize ?? ((effort) => ({
2706
+ effort,
2707
+ definition: effort,
2708
+ slug: effort.slug,
2709
+ label: effort.label,
2710
+ met: effort.baseAttributes.met,
2711
+ baseAttributes: effort.baseAttributes,
2712
+ discipline: effort.baseAttributes.discipline,
2713
+ disciplineFactor: effort.baseAttributes.disciplineFactor ?? 1,
2714
+ intensityTier: effort.baseAttributes.intensityTier,
2715
+ modifiers: {},
2716
+ registrySource: effort.registrySource,
2717
+ resolvedFrom: effort.registrySource === "user" ? "user" : effort.registrySource === "bundled" ? "bundled" : "default",
2718
+ isEstimated: false
2719
+ }));
2720
+ const resolveEffortExact = (key) => {
2721
+ const found = efforts.lookup(key.trim());
2722
+ if (found.entity) return { entity: found.entity };
2723
+ if (found.ambiguous.length > 0) {
2724
+ return {
2725
+ entity: null,
2726
+ diagnostic: {
2727
+ reason: "ambiguous-effort",
2728
+ message: `"${key}" matches multiple efforts.`,
2729
+ token: key,
2730
+ candidates: found.ambiguous.map((e) => e.slug)
2456
2731
  }
2457
- let last = pieces.length - 1, inner = pieces[last];
2458
- if (!inner)
2459
- throw new RangeError("Invalid path: " + part);
2460
- let rule = new Rule(tags2, mode, last > 0 ? pieces.slice(0, last) : null);
2461
- byName[inner] = rule.sort(byName[inner]);
2732
+ };
2733
+ }
2734
+ return {
2735
+ entity: null,
2736
+ diagnostic: { reason: "unknown-effort", message: `Unknown effort "${key}".`, token: key }
2737
+ };
2738
+ };
2739
+ const resolveEquipmentValues = (values) => {
2740
+ const refs = [];
2741
+ const diagnostics = [];
2742
+ for (const value of values) {
2743
+ if (typeof value !== "string" || !value.trim()) {
2744
+ diagnostics.push({
2745
+ reason: "invalid-explicit",
2746
+ message: `Equipment reference must be a slug string, got ${JSON.stringify(value) ?? "null"}.`,
2747
+ token: String(value)
2748
+ });
2749
+ continue;
2462
2750
  }
2463
- }
2464
- return ruleNodeProp.add(byName);
2465
- }
2466
- var ruleNodeProp = new common.NodeProp({
2467
- combine(a, b) {
2468
- let cur, root, take;
2469
- while (a || b) {
2470
- if (!a || b && a.depth >= b.depth) {
2471
- take = b;
2472
- b = b.next;
2751
+ const found = equipment.lookup(value.trim());
2752
+ if (found.entity) {
2753
+ refs.push(equipmentRef(found.entity));
2754
+ } else if (found.ambiguous.length > 0) {
2755
+ diagnostics.push({
2756
+ reason: "ambiguous-equipment",
2757
+ message: `"${value}" matches multiple equipment entries.`,
2758
+ token: value,
2759
+ candidates: found.ambiguous.map((e) => e.slug)
2760
+ });
2473
2761
  } else {
2474
- take = a;
2475
- a = a.next;
2762
+ diagnostics.push({
2763
+ reason: "invalid-explicit",
2764
+ message: `Unknown equipment "${value}".`,
2765
+ token: value
2766
+ });
2476
2767
  }
2477
- if (cur && cur.mode == take.mode && !take.context && !cur.context)
2478
- continue;
2479
- let copy = new Rule(take.tags, take.mode, take.context);
2480
- if (cur)
2481
- cur.next = copy;
2482
- else
2483
- root = copy;
2484
- cur = copy;
2485
- }
2486
- return root;
2487
- }
2488
- });
2489
- var Rule = class {
2490
- constructor(tags2, mode, context, next) {
2491
- this.tags = tags2;
2492
- this.mode = mode;
2493
- this.context = context;
2494
- this.next = next;
2495
- }
2496
- get opaque() {
2497
- return this.mode == 0;
2498
- }
2499
- get inherit() {
2500
- return this.mode == 1;
2501
- }
2502
- sort(other) {
2503
- if (!other || other.depth < this.depth) {
2504
- this.next = other;
2505
- return this;
2506
2768
  }
2507
- other.next = this.sort(other.next);
2508
- return other;
2509
- }
2510
- get depth() {
2511
- return this.context ? this.context.length : 0;
2512
- }
2513
- };
2514
- Rule.empty = new Rule([], 2, null);
2515
- function tagHighlighter(tags2, options) {
2516
- let map = /* @__PURE__ */ Object.create(null);
2517
- for (let style of tags2) {
2518
- if (!Array.isArray(style.tag))
2519
- map[style.tag.id] = style.class;
2520
- else
2521
- for (let tag of style.tag)
2522
- map[tag.id] = style.class;
2523
- }
2524
- let { scope, all = null } = {};
2525
- return {
2526
- style: (tags3) => {
2527
- let cls = all;
2528
- for (let tag of tags3) {
2529
- for (let sub of tag.set) {
2530
- let tagClass = map[sub.id];
2531
- if (tagClass) {
2532
- cls = cls ? cls + " " + tagClass : tagClass;
2533
- break;
2534
- }
2535
- }
2536
- }
2537
- return cls;
2538
- },
2539
- scope
2769
+ return { refs: dedupeRefs(refs), diagnostics };
2540
2770
  };
2541
- }
2542
- var t = Tag.define;
2543
- var comment = t();
2544
- var name = t();
2545
- var typeName = t(name);
2546
- var propertyName = t(name);
2547
- var literal = t();
2548
- var string = t(literal);
2549
- var number = t(literal);
2550
- var content = t();
2551
- var heading = t(content);
2552
- var keyword = t();
2553
- var operator = t();
2554
- var punctuation = t();
2771
+ const index = {
2772
+ resolveEffort(slugOrAlias) {
2773
+ return efforts.lookup(slugOrAlias).entity;
2774
+ },
2775
+ resolveEquipment(slugOrAlias) {
2776
+ return equipment.lookup(slugOrAlias).entity;
2777
+ },
2778
+ materialize(effort) {
2779
+ return materializer(effort);
2780
+ },
2781
+ resolve(label, defaults) {
2782
+ const source = (label ?? "").trim();
2783
+ const diagnostics = [];
2784
+ const push = (diagnostic) => {
2785
+ if (diagnostic) diagnostics.push(diagnostic);
2786
+ };
2787
+ let matched = "none";
2788
+ let effortEntity = null;
2789
+ let remaining = "";
2790
+ let equipmentRefs = [];
2791
+ let equipmentAuthored = false;
2792
+ const fillEquipment = () => {
2793
+ if (equipmentRefs.length > 0 || equipmentAuthored) return;
2794
+ if (effortEntity === null) return;
2795
+ if (defaults?.equipment?.length) {
2796
+ const resolvedDefaults = resolveEquipmentValues(defaults.equipment);
2797
+ equipmentRefs = resolvedDefaults.refs;
2798
+ for (const diagnostic of resolvedDefaults.diagnostics) {
2799
+ push({ ...diagnostic, reason: "unknown-equipment" });
2800
+ }
2801
+ if (equipmentRefs.length > 0) return;
2802
+ }
2803
+ if (effortEntity.equipment?.length) {
2804
+ const resolvedDefEquipment = resolveEquipmentValues(effortEntity.equipment);
2805
+ equipmentRefs = resolvedDefEquipment.refs;
2806
+ for (const diagnostic of resolvedDefEquipment.diagnostics) {
2807
+ push({ ...diagnostic, reason: "unknown-equipment" });
2808
+ }
2809
+ }
2810
+ };
2811
+ if (!source) {
2812
+ if (defaults?.effort) {
2813
+ const found = resolveEffortExact(defaults.effort);
2814
+ if (found.entity) {
2815
+ effortEntity = found.entity;
2816
+ matched = "default";
2817
+ } else {
2818
+ push({
2819
+ reason: found.diagnostic?.reason === "ambiguous-effort" ? "ambiguous-effort" : "unknown-effort",
2820
+ message: `Default effort "${defaults.effort}" is not in the catalog.`,
2821
+ token: defaults.effort,
2822
+ candidates: found.diagnostic?.candidates
2823
+ });
2824
+ }
2825
+ }
2826
+ fillEquipment();
2827
+ return {
2828
+ matched,
2829
+ label: source,
2830
+ effort: effortEntity ? materializer(effortEntity) : null,
2831
+ effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
2832
+ equipment: equipmentRefs,
2833
+ remaining: "",
2834
+ diagnostics
2835
+ };
2836
+ }
2837
+ if (source.startsWith("{")) {
2838
+ let parsed;
2839
+ try {
2840
+ parsed = JSON.parse(source);
2841
+ } catch {
2842
+ return {
2843
+ matched: "none",
2844
+ label: source,
2845
+ effort: null,
2846
+ effortRef: null,
2847
+ equipment: [],
2848
+ remaining: "",
2849
+ diagnostics: [{
2850
+ reason: "malformed-json",
2851
+ message: "Reference object is not valid JSON.",
2852
+ token: source
2853
+ }]
2854
+ };
2855
+ }
2856
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && ("effort" in parsed || "equipment" in parsed)) {
2857
+ matched = "explicit";
2858
+ const obj = parsed;
2859
+ if (obj.effort !== void 0) {
2860
+ if (typeof obj.effort === "string" && obj.effort.trim()) {
2861
+ const found = resolveEffortExact(obj.effort);
2862
+ effortEntity = found.entity;
2863
+ if (found.diagnostic) {
2864
+ push(found.diagnostic.reason === "ambiguous-effort" ? found.diagnostic : { ...found.diagnostic, reason: "invalid-explicit", message: `Explicit effort "${obj.effort}" is not in the catalog.` });
2865
+ }
2866
+ } else {
2867
+ push({
2868
+ reason: "invalid-explicit",
2869
+ message: 'Explicit "effort" must be a slug string.',
2870
+ token: String(obj.effort)
2871
+ });
2872
+ }
2873
+ }
2874
+ if (obj.equipment !== void 0) {
2875
+ equipmentAuthored = true;
2876
+ const values = Array.isArray(obj.equipment) ? obj.equipment : [obj.equipment];
2877
+ const resolvedEquipment = resolveEquipmentValues(values);
2878
+ equipmentRefs = resolvedEquipment.refs;
2879
+ diagnostics.push(...resolvedEquipment.diagnostics);
2880
+ if (effortEntity?.equipment?.length) {
2881
+ const definitionSlugs = new Set(effortEntity.equipment);
2882
+ const authoredSlugs = equipmentRefs.map((ref) => ref.slug);
2883
+ if (authoredSlugs.length > 0 && authoredSlugs.some((slug) => !definitionSlugs.has(slug))) {
2884
+ push({
2885
+ reason: "conflict",
2886
+ message: `Explicit equipment [${authoredSlugs.join(", ")}] contradicts effort "${effortEntity.slug}" definition equipment [${effortEntity.equipment.join(", ")}].`,
2887
+ token: source
2888
+ });
2889
+ }
2890
+ }
2891
+ }
2892
+ fillEquipment();
2893
+ return {
2894
+ matched,
2895
+ label: source,
2896
+ effort: effortEntity ? materializer(effortEntity) : null,
2897
+ effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
2898
+ equipment: equipmentRefs,
2899
+ remaining: "",
2900
+ diagnostics
2901
+ };
2902
+ }
2903
+ }
2904
+ const lowerSource = source.toLowerCase();
2905
+ for (const candidate of prefixCandidates) {
2906
+ if (!lowerSource.startsWith(candidate.lower)) continue;
2907
+ const after = source.slice(candidate.lower.length);
2908
+ if (after && !/^\s/.test(after)) continue;
2909
+ const candidateRemaining = after.trim();
2910
+ if (!candidateRemaining) continue;
2911
+ const equipmentFound = equipment.lookup(candidate.lower);
2912
+ if (equipmentFound.ambiguous.length > 0) {
2913
+ push({
2914
+ reason: "ambiguous-equipment",
2915
+ message: `"${candidate.lower}" matches multiple equipment entries.`,
2916
+ token: source,
2917
+ candidates: equipmentFound.ambiguous.map((e) => e.slug)
2918
+ });
2919
+ return {
2920
+ matched: "none",
2921
+ label: source,
2922
+ effort: null,
2923
+ effortRef: null,
2924
+ equipment: [],
2925
+ remaining: "",
2926
+ diagnostics
2927
+ };
2928
+ }
2929
+ if (!equipmentFound.entity) continue;
2930
+ const effortFound = resolveEffortExact(candidateRemaining);
2931
+ if (effortFound.entity) {
2932
+ matched = "split";
2933
+ effortEntity = effortFound.entity;
2934
+ equipmentRefs = [equipmentRef(equipmentFound.entity)];
2935
+ equipmentAuthored = true;
2936
+ remaining = candidateRemaining;
2937
+ break;
2938
+ }
2939
+ if (effortFound.diagnostic?.reason === "ambiguous-effort") {
2940
+ push(effortFound.diagnostic);
2941
+ return {
2942
+ matched: "none",
2943
+ label: source,
2944
+ effort: null,
2945
+ effortRef: null,
2946
+ equipment: [],
2947
+ remaining: "",
2948
+ diagnostics
2949
+ };
2950
+ }
2951
+ }
2952
+ if (matched === "none") {
2953
+ const found = resolveEffortExact(source);
2954
+ if (found.entity) {
2955
+ effortEntity = found.entity;
2956
+ matched = "whole";
2957
+ } else if (found.diagnostic?.reason === "ambiguous-effort") {
2958
+ push(found.diagnostic);
2959
+ return {
2960
+ matched: "none",
2961
+ label: source,
2962
+ effort: null,
2963
+ effortRef: null,
2964
+ equipment: [],
2965
+ remaining: "",
2966
+ diagnostics
2967
+ };
2968
+ } else if (found.diagnostic) {
2969
+ push(found.diagnostic);
2970
+ }
2971
+ }
2972
+ fillEquipment();
2973
+ return {
2974
+ matched,
2975
+ label: source,
2976
+ effort: effortEntity ? materializer(effortEntity) : null,
2977
+ effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
2978
+ equipment: equipmentRefs,
2979
+ remaining,
2980
+ diagnostics
2981
+ };
2982
+ }
2983
+ };
2984
+ return index;
2985
+ }
2986
+ function extractStatementRefs(statements) {
2987
+ const efforts = [];
2988
+ const equipment = [];
2989
+ const seenEffort = /* @__PURE__ */ new Set();
2990
+ const seenEquipment = /* @__PURE__ */ new Set();
2991
+ const pushUnique = (list, seen, value) => {
2992
+ if (!value) return;
2993
+ if (seen.has(value)) return;
2994
+ seen.add(value);
2995
+ list.push(value);
2996
+ };
2997
+ for (const statement of statements ?? []) {
2998
+ const metrics = Array.isArray(statement.metrics) ? statement.metrics : statement.metrics?.toArray?.() ?? [];
2999
+ for (const metric of metrics) {
3000
+ const refs = wodWikiCore.getReferenceMetadata(metric.metadata)?.refs;
3001
+ if (!refs) continue;
3002
+ for (const ref of refs) {
3003
+ if (ref.kind === "effort") pushUnique(efforts, seenEffort, ref.slug);
3004
+ if (ref.kind === "equipment") pushUnique(equipment, seenEquipment, ref.slug);
3005
+ }
3006
+ }
3007
+ }
3008
+ return { efforts, equipment };
3009
+ }
3010
+ function slugifyRefLabel(label) {
3011
+ return normalizeForFuzzy(label).replace(/\s+/g, "-");
3012
+ }
3013
+
3014
+ // src/parser/parseScript.ts
3015
+ function parseScript(inputText, options = {}) {
3016
+ if (!inputText || !inputText.trim()) {
3017
+ return new WhiteboardScript(inputText, [], []);
3018
+ }
3019
+ try {
3020
+ const doc = inputText.endsWith("\n") ? inputText : inputText + "\n";
3021
+ const tree = parser.parse(doc);
3022
+ const sport = options.sport ?? options.dialect;
3023
+ const references = options.references || options.tags ? {
3024
+ index: options.references ?? createReferenceIndex({ efforts: [], equipment: [] }),
3025
+ defaults: options.defaults,
3026
+ tags: options.tags
3027
+ } : void 0;
3028
+ const statements = options.withoutDialects ? extractStatementsRaw(inputText, tree, references) : extractStatements(inputText, tree, sport, references);
3029
+ return new WhiteboardScript(inputText, statements, []);
3030
+ } catch (error) {
3031
+ const msg = error instanceof Error ? error.message : "Unknown parse error";
3032
+ return new WhiteboardScript(inputText, [], [{ message: msg }]);
3033
+ }
3034
+ }
3035
+
3036
+ // src/parser/md-timer.ts
3037
+ var MdTimerRuntime = class {
3038
+ config;
3039
+ constructor(config) {
3040
+ this.config = config;
3041
+ }
3042
+ /**
3043
+ * Parse WhiteboardScript source into a {@link WhiteboardScript}, running the
3044
+ * Dialect Stack on every statement.
3045
+ *
3046
+ * @param sport - The block's `:sport` fence suffix (` ```log:climbing `).
3047
+ */
3048
+ read(inputText, sport) {
3049
+ return parseScript(inputText, { sport, ...this.config });
3050
+ }
3051
+ /**
3052
+ * Parse without running the Dialect Stack. Used by the parser test harness
3053
+ * which applies its own Dialect set. Production consumers should use {@link read}.
3054
+ */
3055
+ readWithoutDialects(inputText) {
3056
+ return parseScript(inputText, { withoutDialects: true, ...this.config });
3057
+ }
3058
+ };
3059
+
3060
+ // src/parser/parserInstance.ts
3061
+ function createParser(config) {
3062
+ return new MdTimerRuntime(config);
3063
+ }
3064
+ var nextTagID = 0;
3065
+ var Tag = class _Tag {
3066
+ /**
3067
+ @internal
3068
+ */
3069
+ constructor(name2, set, base, modified) {
3070
+ this.name = name2;
3071
+ this.set = set;
3072
+ this.base = base;
3073
+ this.modified = modified;
3074
+ this.id = nextTagID++;
3075
+ }
3076
+ toString() {
3077
+ let { name: name2 } = this;
3078
+ for (let mod of this.modified)
3079
+ if (mod.name)
3080
+ name2 = `${mod.name}(${name2})`;
3081
+ return name2;
3082
+ }
3083
+ static define(nameOrParent, parent) {
3084
+ let name2 = typeof nameOrParent == "string" ? nameOrParent : "?";
3085
+ if (nameOrParent instanceof _Tag)
3086
+ parent = nameOrParent;
3087
+ if (parent === null || parent === void 0 ? void 0 : parent.base)
3088
+ throw new Error("Can not derive from a modified tag");
3089
+ let tag = new _Tag(name2, [], null, []);
3090
+ tag.set.push(tag);
3091
+ if (parent)
3092
+ for (let t2 of parent.set)
3093
+ tag.set.push(t2);
3094
+ return tag;
3095
+ }
3096
+ /**
3097
+ Define a tag _modifier_, which is a function that, given a tag,
3098
+ will return a tag that is a subtag of the original. Applying the
3099
+ same modifier to a twice tag will return the same value (`m1(t1)
3100
+ == m1(t1)`) and applying multiple modifiers will, regardless or
3101
+ order, produce the same tag (`m1(m2(t1)) == m2(m1(t1))`).
3102
+
3103
+ When multiple modifiers are applied to a given base tag, each
3104
+ smaller set of modifiers is registered as a parent, so that for
3105
+ example `m1(m2(m3(t1)))` is a subtype of `m1(m2(t1))`,
3106
+ `m1(m3(t1)`, and so on.
3107
+ */
3108
+ static defineModifier(name2) {
3109
+ let mod = new Modifier(name2);
3110
+ return (tag) => {
3111
+ if (tag.modified.indexOf(mod) > -1)
3112
+ return tag;
3113
+ return Modifier.get(tag.base || tag, tag.modified.concat(mod).sort((a, b) => a.id - b.id));
3114
+ };
3115
+ }
3116
+ };
3117
+ var nextModifierID = 0;
3118
+ var Modifier = class _Modifier {
3119
+ constructor(name2) {
3120
+ this.name = name2;
3121
+ this.instances = [];
3122
+ this.id = nextModifierID++;
3123
+ }
3124
+ static get(base, mods) {
3125
+ if (!mods.length)
3126
+ return base;
3127
+ let exists = mods[0].instances.find((t2) => t2.base == base && sameArray(mods, t2.modified));
3128
+ if (exists)
3129
+ return exists;
3130
+ let set = [], tag = new Tag(base.name, set, base, mods);
3131
+ for (let m of mods)
3132
+ m.instances.push(tag);
3133
+ let configs = powerSet(mods);
3134
+ for (let parent of base.set)
3135
+ if (!parent.modified.length)
3136
+ for (let config of configs)
3137
+ set.push(_Modifier.get(parent, config));
3138
+ return tag;
3139
+ }
3140
+ };
3141
+ function sameArray(a, b) {
3142
+ return a.length == b.length && a.every((x, i) => x == b[i]);
3143
+ }
3144
+ function powerSet(array) {
3145
+ let sets = [[]];
3146
+ for (let i = 0; i < array.length; i++) {
3147
+ for (let j = 0, e = sets.length; j < e; j++) {
3148
+ sets.push(sets[j].concat(array[i]));
3149
+ }
3150
+ }
3151
+ return sets.sort((a, b) => b.length - a.length);
3152
+ }
3153
+ function styleTags(spec) {
3154
+ let byName = /* @__PURE__ */ Object.create(null);
3155
+ for (let prop in spec) {
3156
+ let tags2 = spec[prop];
3157
+ if (!Array.isArray(tags2))
3158
+ tags2 = [tags2];
3159
+ for (let part of prop.split(" "))
3160
+ if (part) {
3161
+ let pieces = [], mode = 2, rest = part;
3162
+ for (let pos = 0; ; ) {
3163
+ if (rest == "..." && pos > 0 && pos + 3 == part.length) {
3164
+ mode = 1;
3165
+ break;
3166
+ }
3167
+ let m = /^"(?:[^"\\]|\\.)*?"|[^\/!]+/.exec(rest);
3168
+ if (!m)
3169
+ throw new RangeError("Invalid path: " + part);
3170
+ pieces.push(m[0] == "*" ? "" : m[0][0] == '"' ? JSON.parse(m[0]) : m[0]);
3171
+ pos += m[0].length;
3172
+ if (pos == part.length)
3173
+ break;
3174
+ let next = part[pos++];
3175
+ if (pos == part.length && next == "!") {
3176
+ mode = 0;
3177
+ break;
3178
+ }
3179
+ if (next != "/")
3180
+ throw new RangeError("Invalid path: " + part);
3181
+ rest = part.slice(pos);
3182
+ }
3183
+ let last = pieces.length - 1, inner = pieces[last];
3184
+ if (!inner)
3185
+ throw new RangeError("Invalid path: " + part);
3186
+ let rule = new Rule(tags2, mode, last > 0 ? pieces.slice(0, last) : null);
3187
+ byName[inner] = rule.sort(byName[inner]);
3188
+ }
3189
+ }
3190
+ return ruleNodeProp.add(byName);
3191
+ }
3192
+ var ruleNodeProp = new common.NodeProp({
3193
+ combine(a, b) {
3194
+ let cur, root, take;
3195
+ while (a || b) {
3196
+ if (!a || b && a.depth >= b.depth) {
3197
+ take = b;
3198
+ b = b.next;
3199
+ } else {
3200
+ take = a;
3201
+ a = a.next;
3202
+ }
3203
+ if (cur && cur.mode == take.mode && !take.context && !cur.context)
3204
+ continue;
3205
+ let copy = new Rule(take.tags, take.mode, take.context);
3206
+ if (cur)
3207
+ cur.next = copy;
3208
+ else
3209
+ root = copy;
3210
+ cur = copy;
3211
+ }
3212
+ return root;
3213
+ }
3214
+ });
3215
+ var Rule = class {
3216
+ constructor(tags2, mode, context, next) {
3217
+ this.tags = tags2;
3218
+ this.mode = mode;
3219
+ this.context = context;
3220
+ this.next = next;
3221
+ }
3222
+ get opaque() {
3223
+ return this.mode == 0;
3224
+ }
3225
+ get inherit() {
3226
+ return this.mode == 1;
3227
+ }
3228
+ sort(other) {
3229
+ if (!other || other.depth < this.depth) {
3230
+ this.next = other;
3231
+ return this;
3232
+ }
3233
+ other.next = this.sort(other.next);
3234
+ return other;
3235
+ }
3236
+ get depth() {
3237
+ return this.context ? this.context.length : 0;
3238
+ }
3239
+ };
3240
+ Rule.empty = new Rule([], 2, null);
3241
+ function tagHighlighter(tags2, options) {
3242
+ let map = /* @__PURE__ */ Object.create(null);
3243
+ for (let style of tags2) {
3244
+ if (!Array.isArray(style.tag))
3245
+ map[style.tag.id] = style.class;
3246
+ else
3247
+ for (let tag of style.tag)
3248
+ map[tag.id] = style.class;
3249
+ }
3250
+ let { scope, all = null } = {};
3251
+ return {
3252
+ style: (tags3) => {
3253
+ let cls = all;
3254
+ for (let tag of tags3) {
3255
+ for (let sub of tag.set) {
3256
+ let tagClass = map[sub.id];
3257
+ if (tagClass) {
3258
+ cls = cls ? cls + " " + tagClass : tagClass;
3259
+ break;
3260
+ }
3261
+ }
3262
+ }
3263
+ return cls;
3264
+ },
3265
+ scope
3266
+ };
3267
+ }
3268
+ var t = Tag.define;
3269
+ var comment = t();
3270
+ var name = t();
3271
+ var typeName = t(name);
3272
+ var propertyName = t(name);
3273
+ var literal = t();
3274
+ var string = t(literal);
3275
+ var number = t(literal);
3276
+ var content = t();
3277
+ var heading = t(content);
3278
+ var keyword = t();
3279
+ var operator = t();
3280
+ var punctuation = t();
2555
3281
  var bracket = t(punctuation);
2556
3282
  var meta = t();
2557
3283
  var tags = {
@@ -2996,50 +3722,6 @@ function whiteboardScript() {
2996
3722
  return new language.LanguageSupport(whiteboardScriptLanguage);
2997
3723
  }
2998
3724
 
2999
- // src/effort-registry/types.ts
3000
- var EFFORT_REGISTRY_ORIGINS = ["bundled", "user"];
3001
- var CATALOG_PRECEDENCE_RANK = {
3002
- core: 0,
3003
- community: 1,
3004
- imported: 2,
3005
- user: 3
3006
- };
3007
- function resolveSourceTier(sourceIdOrMetaKey, explicitTier) {
3008
- if (explicitTier) return explicitTier;
3009
- if (!sourceIdOrMetaKey) return "core";
3010
- if (sourceIdOrMetaKey === "user") return "user";
3011
- const id = sourceIdOrMetaKey.startsWith("seed:") ? sourceIdOrMetaKey.slice(5) : sourceIdOrMetaKey;
3012
- if (id === "core" || id === "efforts" || id === "equipment") return "core";
3013
- if (id === "community" || id.startsWith("community-") || id === "common") return "community";
3014
- return "imported";
3015
- }
3016
- function resolveSourceRank(sourceIdOrMetaKey, explicitTier) {
3017
- return CATALOG_PRECEDENCE_RANK[resolveSourceTier(sourceIdOrMetaKey, explicitTier)];
3018
- }
3019
- function resolveEntityTier(entity) {
3020
- if (entity.registrySource === "user") return "user";
3021
- return resolveSourceTier(entity.sourceId);
3022
- }
3023
- function resolveEntityRank(entity) {
3024
- return CATALOG_PRECEDENCE_RANK[resolveEntityTier(entity)];
3025
- }
3026
- function dedupeEntitiesByPrecedence(entities) {
3027
- const bySlug = /* @__PURE__ */ new Map();
3028
- for (const entity of entities) {
3029
- const key = entity.slug.toLowerCase();
3030
- const prior = bySlug.get(key);
3031
- if (!prior || resolveEntityRank(entity) >= resolveEntityRank(prior)) {
3032
- bySlug.set(key, entity);
3033
- }
3034
- }
3035
- return Array.from(bySlug.values());
3036
- }
3037
- var INTENSITY_TIERS = ["low", "moderate", "high"];
3038
- var DEFAULT_RESOLVER_OPTIONS = {
3039
- defaultMet: 5,
3040
- defaultThreshold: 2
3041
- };
3042
-
3043
3725
  // src/effort-registry/disciplines.ts
3044
3726
  var EFFORT_DISCIPLINES = [
3045
3727
  "bodyweight",
@@ -3493,71 +4175,6 @@ function equipmentToDocument(equipment) {
3493
4175
  }
3494
4176
  return lines.join("\n");
3495
4177
  }
3496
-
3497
- // src/effort-registry/fuzzyMatch.ts
3498
- function levenshteinDistance(a, b) {
3499
- if (a === b) return 0;
3500
- if (a.length === 0) return b.length;
3501
- if (b.length === 0) return a.length;
3502
- if (a.length > b.length) {
3503
- const tmp = a;
3504
- a = b;
3505
- b = tmp;
3506
- }
3507
- const m = a.length;
3508
- const n = b.length;
3509
- let prev = new Uint32Array(m + 1);
3510
- let curr = new Uint32Array(m + 1);
3511
- for (let i = 0; i <= m; i++) {
3512
- prev[i] = i;
3513
- }
3514
- for (let j = 1; j <= n; j++) {
3515
- curr[0] = j;
3516
- const bj = b.charCodeAt(j - 1);
3517
- for (let i = 1; i <= m; i++) {
3518
- const cost = a.charCodeAt(i - 1) === bj ? 0 : 1;
3519
- curr[i] = Math.min(
3520
- prev[i] + 1,
3521
- // deletion
3522
- curr[i - 1] + 1,
3523
- // insertion
3524
- prev[i - 1] + cost
3525
- // substitution
3526
- );
3527
- }
3528
- const swap = prev;
3529
- prev = curr;
3530
- curr = swap;
3531
- }
3532
- return prev[m];
3533
- }
3534
- function normalizeForFuzzy(input) {
3535
- return input.toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, " ").replace(/[-_]+/g, " ").trim().replace(/\s+/g, " ");
3536
- }
3537
- function isWithinThreshold(a, b, threshold) {
3538
- const na = normalizeForFuzzy(a);
3539
- const nb = normalizeForFuzzy(b);
3540
- if (na === nb) return true;
3541
- if (Math.abs(na.length - nb.length) > threshold) return false;
3542
- return levenshteinDistance(na, nb) <= threshold;
3543
- }
3544
- function findBestFuzzyMatch(query, candidates, threshold) {
3545
- const nq = normalizeForFuzzy(query);
3546
- let best = null;
3547
- for (const candidate of candidates) {
3548
- const nc = normalizeForFuzzy(candidate);
3549
- if (Math.abs(nq.length - nc.length) > threshold) continue;
3550
- const distance = levenshteinDistance(nq, nc);
3551
- if (distance > threshold) continue;
3552
- if (!best || distance < best.distance) {
3553
- best = { target: candidate, distance };
3554
- if (distance === 0) break;
3555
- }
3556
- }
3557
- return best;
3558
- }
3559
-
3560
- // src/effort-registry/EffortResolver.ts
3561
4178
  function normalizeSlug(label) {
3562
4179
  return normalizeForFuzzy(label).replace(/\s+/g, "-");
3563
4180
  }
@@ -3770,162 +4387,64 @@ var fixtureUserOverride = {
3770
4387
  label: "Rowing (Calibrated)",
3771
4388
  aliases: ["row", "rower", "erg"],
3772
4389
  baseAttributes: { met: 8.5, discipline: "rowing", intensityTier: "high" },
3773
- registrySource: "user",
3774
- createdAt: "2024-02-01T08:00:00Z",
3775
- updatedAt: "2024-02-01T08:00:00Z"
3776
- };
3777
- var commonFixtureSet = [
3778
- fixtureRunning,
3779
- fixtureRowing,
3780
- fixtureBackSquat,
3781
- fixtureBurpee,
3782
- fixturePlank
3783
- ];
3784
- var fullFixtureSet = [
3785
- ...commonFixtureSet,
3786
- fixtureUserCustom,
3787
- fixtureUserOverride
3788
- ];
3789
-
3790
- // src/effort-registry/data/bundled-efforts.ts
3791
- var bundledEfforts = commonFixtureSet;
3792
- var BUNDLED_EFFORT_COUNT = bundledEfforts.length;
3793
-
3794
- // src/effort-registry/InMemoryEffortRegistry.ts
3795
- var InMemoryEffortRegistry = class {
3796
- efforts = /* @__PURE__ */ new Map();
3797
- sourceFilter = null;
3798
- initialized = false;
3799
- constructor(sourceFilter) {
3800
- this.sourceFilter = sourceFilter ?? null;
3801
- }
3802
- /** Seed with bundled efforts */
3803
- async loadBundled() {
3804
- for (const effort of bundledEfforts) {
3805
- if (!this.sourceFilter || effort.registrySource === this.sourceFilter) {
3806
- this.efforts.set(effort.slug, effort);
3807
- }
3808
- }
3809
- this.initialized = true;
3810
- }
3811
- isInitialized() {
3812
- return this.initialized;
3813
- }
3814
- /** Seed with arbitrary efforts (useful for tests) */
3815
- seed(efforts) {
3816
- for (const effort of efforts) {
3817
- this.efforts.set(effort.slug, effort);
3818
- }
3819
- }
3820
- /** Clear all efforts */
3821
- clear() {
3822
- this.efforts.clear();
3823
- this.initialized = false;
3824
- }
3825
- resolve(slug) {
3826
- return this.efforts.get(slug) ?? null;
3827
- }
3828
- resolveByAlias(label) {
3829
- const normalized = normalizeForFuzzy(label);
3830
- for (const effort of this.efforts.values()) {
3831
- if (normalizeForFuzzy(effort.label) === normalized) return effort;
3832
- for (const alias of effort.aliases) {
3833
- if (normalizeForFuzzy(alias) === normalized) return effort;
3834
- }
3835
- }
3836
- return null;
3837
- }
3838
- list() {
3839
- return Array.from(this.efforts.values());
3840
- }
3841
- listByOrigin(origin) {
3842
- return this.list().filter((e) => e.registrySource === origin);
3843
- }
3844
- async upsert(effort) {
3845
- if (effort.registrySource !== "user") {
3846
- throw new Error(
3847
- `InMemoryEffortRegistry.upsert: only user efforts can be written. Received registrySource="${effort.registrySource}"`
3848
- );
3849
- }
3850
- this.efforts.set(effort.slug, effort);
3851
- }
3852
- async delete(slug) {
3853
- const existing = this.efforts.get(slug);
3854
- if (existing && existing.registrySource !== "user") {
3855
- throw new Error(
3856
- `InMemoryEffortRegistry.delete: cannot delete non-user effort "${slug}"`
3857
- );
3858
- }
3859
- this.efforts.delete(slug);
3860
- }
3861
- };
3862
-
3863
- // src/effort-registry/CompositeEffortRegistry.ts
3864
- var CompositeEffortRegistry = class {
3865
- userEfforts = /* @__PURE__ */ new Map();
3866
- bundledEfforts = /* @__PURE__ */ new Map();
3867
- initialized = false;
3868
- initialBundled;
3869
- storage;
3870
- constructor(options) {
3871
- if (options) {
3872
- this.initialBundled = options.bundled;
3873
- this.storage = options.storage;
3874
- }
3875
- }
3876
- /**
3877
- * Initialize the registry:
3878
- * 1. Load bundled efforts into memory
3879
- * 2. Load user efforts from storage adapter into memory
3880
- *
3881
- * An explicit `customBundled` REPLACES the bundled tier — passing an empty
3882
- * set clears it — so a seed source removal never leaves stale bundled
3883
- * efforts cached. The no-argument call keeps the constructor/default set.
3884
- * Both tiers rebuild only AFTER a successful adapter load: a failed load
3885
- * propagates (readiness must never report over stale data), and deleted
3886
- * custom rows disappear because the user map is cleared too.
3887
- */
3888
- async loadBundled(customBundled) {
3889
- this.bundledEfforts.clear();
3890
- const source = customBundled ?? this.initialBundled ?? bundledEfforts;
3891
- for (const effort of source) {
3892
- const prior = this.bundledEfforts.get(effort.slug);
3893
- if (!prior || resolveEntityRank(effort) >= resolveEntityRank(prior)) {
3894
- this.bundledEfforts.set(effort.slug, effort);
3895
- }
3896
- }
3897
- this.userEfforts.clear();
3898
- if (this.storage) {
3899
- try {
3900
- const userEfforts = await this.storage.load();
3901
- for (const effort of userEfforts) {
3902
- if (effort.registrySource === "user") {
3903
- this.userEfforts.set(effort.slug, effort);
3904
- }
3905
- }
3906
- } catch (err) {
3907
- console.warn("[CompositeEffortRegistry] Failed to load user efforts from storage adapter:", err);
4390
+ registrySource: "user",
4391
+ createdAt: "2024-02-01T08:00:00Z",
4392
+ updatedAt: "2024-02-01T08:00:00Z"
4393
+ };
4394
+ var commonFixtureSet = [
4395
+ fixtureRunning,
4396
+ fixtureRowing,
4397
+ fixtureBackSquat,
4398
+ fixtureBurpee,
4399
+ fixturePlank
4400
+ ];
4401
+ var fullFixtureSet = [
4402
+ ...commonFixtureSet,
4403
+ fixtureUserCustom,
4404
+ fixtureUserOverride
4405
+ ];
4406
+
4407
+ // src/effort-registry/data/bundled-efforts.ts
4408
+ var bundledEfforts = commonFixtureSet;
4409
+ var BUNDLED_EFFORT_COUNT = bundledEfforts.length;
4410
+
4411
+ // src/effort-registry/InMemoryEffortRegistry.ts
4412
+ var InMemoryEffortRegistry = class {
4413
+ efforts = /* @__PURE__ */ new Map();
4414
+ sourceFilter = null;
4415
+ initialized = false;
4416
+ constructor(sourceFilter) {
4417
+ this.sourceFilter = sourceFilter ?? null;
4418
+ }
4419
+ /** Seed with bundled efforts */
4420
+ async loadBundled() {
4421
+ for (const effort of bundledEfforts) {
4422
+ if (!this.sourceFilter || effort.registrySource === this.sourceFilter) {
4423
+ this.efforts.set(effort.slug, effort);
3908
4424
  }
3909
4425
  }
3910
4426
  this.initialized = true;
3911
4427
  }
3912
- /** True after loadBundled() has resolved */
3913
4428
  isInitialized() {
3914
4429
  return this.initialized;
3915
4430
  }
4431
+ /** Seed with arbitrary efforts (useful for tests) */
4432
+ seed(efforts) {
4433
+ for (const effort of efforts) {
4434
+ this.efforts.set(effort.slug, effort);
4435
+ }
4436
+ }
4437
+ /** Clear all efforts */
4438
+ clear() {
4439
+ this.efforts.clear();
4440
+ this.initialized = false;
4441
+ }
3916
4442
  resolve(slug) {
3917
- return this.userEfforts.get(slug) ?? this.bundledEfforts.get(slug) ?? null;
4443
+ return this.efforts.get(slug) ?? null;
3918
4444
  }
3919
4445
  resolveByAlias(label) {
3920
4446
  const normalized = normalizeForFuzzy(label);
3921
- for (const effort of this.userEfforts.values()) {
3922
- if (normalizeForFuzzy(effort.label) === normalized) return effort;
3923
- for (const alias of effort.aliases) {
3924
- if (normalizeForFuzzy(alias) === normalized) return effort;
3925
- }
3926
- }
3927
- const bundled = Array.from(this.bundledEfforts.values()).filter((effort) => !this.userEfforts.has(effort.slug)).sort((a, b) => resolveEntityRank(b) - resolveEntityRank(a));
3928
- for (const effort of bundled) {
4447
+ for (const effort of this.efforts.values()) {
3929
4448
  if (normalizeForFuzzy(effort.label) === normalized) return effort;
3930
4449
  for (const alias of effort.aliases) {
3931
4450
  if (normalizeForFuzzy(alias) === normalized) return effort;
@@ -3934,456 +4453,193 @@ var CompositeEffortRegistry = class {
3934
4453
  return null;
3935
4454
  }
3936
4455
  list() {
3937
- const merged = new Map(this.bundledEfforts);
3938
- for (const [slug, effort] of this.userEfforts) {
3939
- merged.set(slug, effort);
3940
- }
3941
- return Array.from(merged.values());
4456
+ return Array.from(this.efforts.values());
3942
4457
  }
3943
4458
  listByOrigin(origin) {
3944
- if (origin === "user") {
3945
- return Array.from(this.userEfforts.values());
3946
- }
3947
- if (origin === "bundled") {
3948
- return Array.from(this.bundledEfforts.values());
3949
- }
3950
4459
  return this.list().filter((e) => e.registrySource === origin);
3951
4460
  }
3952
4461
  async upsert(effort) {
3953
4462
  if (effort.registrySource !== "user") {
3954
4463
  throw new Error(
3955
- `CompositeEffortRegistry.upsert: only user efforts can be written. Received registrySource="${effort.registrySource}"`
3956
- );
3957
- }
3958
- this.userEfforts.set(effort.slug, effort);
3959
- if (this.storage) {
3960
- try {
3961
- await this.storage.save(effort);
3962
- } catch (err) {
3963
- console.warn("[CompositeEffortRegistry] Failed to persist effort to storage adapter:", err);
3964
- }
3965
- }
3966
- }
3967
- async delete(slug) {
3968
- const existing = this.resolve(slug);
3969
- if (existing && existing.registrySource !== "user") {
3970
- throw new Error(
3971
- `CompositeEffortRegistry.delete: cannot delete non-user effort "${slug}"`
4464
+ `InMemoryEffortRegistry.upsert: only user efforts can be written. Received registrySource="${effort.registrySource}"`
3972
4465
  );
3973
4466
  }
3974
- this.userEfforts.delete(slug);
3975
- if (this.storage) {
3976
- try {
3977
- await this.storage.delete(slug);
3978
- } catch (err) {
3979
- console.warn("[CompositeEffortRegistry] Failed to delete effort from storage adapter:", err);
3980
- }
3981
- }
3982
- }
3983
- };
3984
-
3985
- // src/effort-registry/aliasManagement.ts
3986
- async function addAliasToEffort(registry, targetSlug, newAlias) {
3987
- const trimmed = newAlias.trim();
3988
- if (!trimmed) {
3989
- throw new Error("Alias cannot be empty");
3990
- }
3991
- const existing = registry.resolve(targetSlug);
3992
- if (!existing) {
3993
- throw new Error(`Effort "${targetSlug}" not found in registry`);
3994
- }
3995
- const normalizedNew = normalizeForFuzzy(trimmed);
3996
- const alreadyHas = normalizeForFuzzy(existing.label) === normalizedNew || existing.aliases.some((a) => normalizeForFuzzy(a) === normalizedNew);
3997
- if (alreadyHas) {
3998
- return existing;
3999
- }
4000
- const updatedAliases = [...existing.aliases, trimmed];
4001
- const now = (/* @__PURE__ */ new Date()).toISOString();
4002
- if (existing.registrySource === "bundled") {
4003
- const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? `effort-user-${crypto.randomUUID()}` : `effort-user-${Date.now()}`;
4004
- const userEffort = {
4005
- ...existing,
4006
- id,
4007
- registrySource: "user",
4008
- aliases: updatedAliases,
4009
- updatedAt: now,
4010
- derivation: existing.derivation ?? {
4011
- parentSlug: existing.slug,
4012
- coefficients: {},
4013
- hardOverrides: {}
4014
- }
4015
- };
4016
- await registry.upsert(userEffort);
4017
- return userEffort;
4018
- }
4019
- const updatedUserEffort = {
4020
- ...existing,
4021
- aliases: updatedAliases,
4022
- updatedAt: now
4023
- };
4024
- await registry.upsert(updatedUserEffort);
4025
- return updatedUserEffort;
4026
- }
4027
- function buildExactIndex(entities) {
4028
- const deduped = dedupeEntitiesByPrecedence(entities);
4029
- const byKey = /* @__PURE__ */ new Map();
4030
- const add = (key, entity) => {
4031
- if (!key) return;
4032
- const existing = byKey.get(key);
4033
- if (existing) {
4034
- if (!existing.includes(entity)) existing.push(entity);
4035
- } else {
4036
- byKey.set(key, [entity]);
4037
- }
4038
- };
4039
- for (const entity of deduped) {
4040
- add(entity.slug.toLowerCase(), entity);
4041
- add(normalizeForFuzzy(entity.label), entity);
4042
- for (const alias of entity.aliases) add(normalizeForFuzzy(alias), entity);
4043
- }
4044
- return {
4045
- byKey,
4046
- lookup(key) {
4047
- const direct = byKey.get(key) ?? byKey.get(normalizeForFuzzy(key));
4048
- if (!direct || direct.length === 0) return { entity: null, ambiguous: [] };
4049
- if (direct.length === 1) return { entity: direct[0], ambiguous: [] };
4050
- let maxRank = -1;
4051
- for (const candidate of direct) {
4052
- const rank = resolveEntityRank(candidate);
4053
- if (rank > maxRank) maxRank = rank;
4054
- }
4055
- const topTier = direct.filter((candidate) => resolveEntityRank(candidate) === maxRank);
4056
- if (topTier.length === 1) return { entity: topTier[0], ambiguous: [] };
4057
- return { entity: null, ambiguous: topTier };
4058
- }
4059
- };
4060
- }
4061
- function equipmentRef(entity) {
4062
- return { kind: "equipment", slug: entity.slug, label: entity.label };
4063
- }
4064
- function dedupeRefs(refs) {
4065
- const seen = /* @__PURE__ */ new Set();
4066
- const result = [];
4067
- for (const ref of refs) {
4068
- const key = `${ref.kind}:${ref.slug}`;
4069
- if (seen.has(key)) continue;
4070
- seen.add(key);
4071
- result.push(ref);
4467
+ this.efforts.set(effort.slug, effort);
4072
4468
  }
4073
- return result;
4074
- }
4075
- function createReferenceIndex(catalog, materialize) {
4076
- const dedupedEquipment = dedupeEntitiesByPrecedence(catalog.equipment ?? []);
4077
- const efforts = buildExactIndex(catalog.efforts ?? []);
4078
- const equipment = buildExactIndex(dedupedEquipment);
4079
- 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);
4080
- const materializer = materialize ?? ((effort) => ({
4081
- effort,
4082
- definition: effort,
4083
- slug: effort.slug,
4084
- label: effort.label,
4085
- met: effort.baseAttributes.met,
4086
- baseAttributes: effort.baseAttributes,
4087
- discipline: effort.baseAttributes.discipline,
4088
- disciplineFactor: effort.baseAttributes.disciplineFactor ?? 1,
4089
- intensityTier: effort.baseAttributes.intensityTier,
4090
- modifiers: {},
4091
- registrySource: effort.registrySource,
4092
- resolvedFrom: effort.registrySource === "user" ? "user" : effort.registrySource === "bundled" ? "bundled" : "default",
4093
- isEstimated: false
4094
- }));
4095
- const resolveEffortExact = (key) => {
4096
- const found = efforts.lookup(key.trim());
4097
- if (found.entity) return { entity: found.entity };
4098
- if (found.ambiguous.length > 0) {
4099
- return {
4100
- entity: null,
4101
- diagnostic: {
4102
- reason: "ambiguous-effort",
4103
- message: `"${key}" matches multiple efforts.`,
4104
- token: key,
4105
- candidates: found.ambiguous.map((e) => e.slug)
4106
- }
4107
- };
4108
- }
4109
- return {
4110
- entity: null,
4111
- diagnostic: { reason: "unknown-effort", message: `Unknown effort "${key}".`, token: key }
4112
- };
4113
- };
4114
- const resolveEquipmentValues = (values) => {
4115
- const refs = [];
4116
- const diagnostics = [];
4117
- for (const value of values) {
4118
- if (typeof value !== "string" || !value.trim()) {
4119
- diagnostics.push({
4120
- reason: "invalid-explicit",
4121
- message: `Equipment reference must be a slug string, got ${JSON.stringify(value) ?? "null"}.`,
4122
- token: String(value)
4123
- });
4124
- continue;
4125
- }
4126
- const found = equipment.lookup(value.trim());
4127
- if (found.entity) {
4128
- refs.push(equipmentRef(found.entity));
4129
- } else if (found.ambiguous.length > 0) {
4130
- diagnostics.push({
4131
- reason: "ambiguous-equipment",
4132
- message: `"${value}" matches multiple equipment entries.`,
4133
- token: value,
4134
- candidates: found.ambiguous.map((e) => e.slug)
4135
- });
4136
- } else {
4137
- diagnostics.push({
4138
- reason: "invalid-explicit",
4139
- message: `Unknown equipment "${value}".`,
4140
- token: value
4141
- });
4142
- }
4469
+ async delete(slug) {
4470
+ const existing = this.efforts.get(slug);
4471
+ if (existing && existing.registrySource !== "user") {
4472
+ throw new Error(
4473
+ `InMemoryEffortRegistry.delete: cannot delete non-user effort "${slug}"`
4474
+ );
4143
4475
  }
4144
- return { refs: dedupeRefs(refs), diagnostics };
4145
- };
4146
- const index = {
4147
- resolveEffort(slugOrAlias) {
4148
- return efforts.lookup(slugOrAlias).entity;
4149
- },
4150
- resolveEquipment(slugOrAlias) {
4151
- return equipment.lookup(slugOrAlias).entity;
4152
- },
4153
- materialize(effort) {
4154
- return materializer(effort);
4155
- },
4156
- resolve(label, defaults) {
4157
- const source = (label ?? "").trim();
4158
- const diagnostics = [];
4159
- const push = (diagnostic) => {
4160
- if (diagnostic) diagnostics.push(diagnostic);
4161
- };
4162
- let matched = "none";
4163
- let effortEntity = null;
4164
- let remaining = "";
4165
- let equipmentRefs = [];
4166
- let equipmentAuthored = false;
4167
- const fillEquipment = () => {
4168
- if (equipmentRefs.length > 0 || equipmentAuthored) return;
4169
- if (effortEntity === null) return;
4170
- if (defaults?.equipment?.length) {
4171
- const resolvedDefaults = resolveEquipmentValues(defaults.equipment);
4172
- equipmentRefs = resolvedDefaults.refs;
4173
- for (const diagnostic of resolvedDefaults.diagnostics) {
4174
- push({ ...diagnostic, reason: "unknown-equipment" });
4175
- }
4176
- if (equipmentRefs.length > 0) return;
4177
- }
4178
- if (effortEntity.equipment?.length) {
4179
- const resolvedDefEquipment = resolveEquipmentValues(effortEntity.equipment);
4180
- equipmentRefs = resolvedDefEquipment.refs;
4181
- for (const diagnostic of resolvedDefEquipment.diagnostics) {
4182
- push({ ...diagnostic, reason: "unknown-equipment" });
4183
- }
4184
- }
4185
- };
4186
- if (!source) {
4187
- if (defaults?.effort) {
4188
- const found = resolveEffortExact(defaults.effort);
4189
- if (found.entity) {
4190
- effortEntity = found.entity;
4191
- matched = "default";
4192
- } else {
4193
- push({
4194
- reason: found.diagnostic?.reason === "ambiguous-effort" ? "ambiguous-effort" : "unknown-effort",
4195
- message: `Default effort "${defaults.effort}" is not in the catalog.`,
4196
- token: defaults.effort,
4197
- candidates: found.diagnostic?.candidates
4198
- });
4199
- }
4200
- }
4201
- fillEquipment();
4202
- return {
4203
- matched,
4204
- label: source,
4205
- effort: effortEntity ? materializer(effortEntity) : null,
4206
- effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
4207
- equipment: equipmentRefs,
4208
- remaining: "",
4209
- diagnostics
4210
- };
4476
+ this.efforts.delete(slug);
4477
+ }
4478
+ };
4479
+
4480
+ // src/effort-registry/CompositeEffortRegistry.ts
4481
+ var CompositeEffortRegistry = class {
4482
+ userEfforts = /* @__PURE__ */ new Map();
4483
+ bundledEfforts = /* @__PURE__ */ new Map();
4484
+ initialized = false;
4485
+ initialBundled;
4486
+ storage;
4487
+ constructor(options) {
4488
+ if (options) {
4489
+ this.initialBundled = options.bundled;
4490
+ this.storage = options.storage;
4491
+ }
4492
+ }
4493
+ /**
4494
+ * Initialize the registry:
4495
+ * 1. Load bundled efforts into memory
4496
+ * 2. Load user efforts from storage adapter into memory
4497
+ *
4498
+ * An explicit `customBundled` REPLACES the bundled tier — passing an empty
4499
+ * set clears it — so a seed source removal never leaves stale bundled
4500
+ * efforts cached. The no-argument call keeps the constructor/default set.
4501
+ * Both tiers rebuild only AFTER a successful adapter load: a failed load
4502
+ * propagates (readiness must never report over stale data), and deleted
4503
+ * custom rows disappear because the user map is cleared too.
4504
+ */
4505
+ async loadBundled(customBundled) {
4506
+ this.bundledEfforts.clear();
4507
+ const source = customBundled ?? this.initialBundled ?? bundledEfforts;
4508
+ for (const effort of source) {
4509
+ const prior = this.bundledEfforts.get(effort.slug);
4510
+ if (!prior || resolveEntityRank(effort) >= resolveEntityRank(prior)) {
4511
+ this.bundledEfforts.set(effort.slug, effort);
4211
4512
  }
4212
- if (source.startsWith("{")) {
4213
- let parsed;
4214
- try {
4215
- parsed = JSON.parse(source);
4216
- } catch {
4217
- return {
4218
- matched: "none",
4219
- label: source,
4220
- effort: null,
4221
- effortRef: null,
4222
- equipment: [],
4223
- remaining: "",
4224
- diagnostics: [{
4225
- reason: "malformed-json",
4226
- message: "Reference object is not valid JSON.",
4227
- token: source
4228
- }]
4229
- };
4230
- }
4231
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && ("effort" in parsed || "equipment" in parsed)) {
4232
- matched = "explicit";
4233
- const obj = parsed;
4234
- if (obj.effort !== void 0) {
4235
- if (typeof obj.effort === "string" && obj.effort.trim()) {
4236
- const found = resolveEffortExact(obj.effort);
4237
- effortEntity = found.entity;
4238
- if (found.diagnostic) {
4239
- push(found.diagnostic.reason === "ambiguous-effort" ? found.diagnostic : { ...found.diagnostic, reason: "invalid-explicit", message: `Explicit effort "${obj.effort}" is not in the catalog.` });
4240
- }
4241
- } else {
4242
- push({
4243
- reason: "invalid-explicit",
4244
- message: 'Explicit "effort" must be a slug string.',
4245
- token: String(obj.effort)
4246
- });
4247
- }
4248
- }
4249
- if (obj.equipment !== void 0) {
4250
- equipmentAuthored = true;
4251
- const values = Array.isArray(obj.equipment) ? obj.equipment : [obj.equipment];
4252
- const resolvedEquipment = resolveEquipmentValues(values);
4253
- equipmentRefs = resolvedEquipment.refs;
4254
- diagnostics.push(...resolvedEquipment.diagnostics);
4255
- if (effortEntity?.equipment?.length) {
4256
- const definitionSlugs = new Set(effortEntity.equipment);
4257
- const authoredSlugs = equipmentRefs.map((ref) => ref.slug);
4258
- if (authoredSlugs.length > 0 && authoredSlugs.some((slug) => !definitionSlugs.has(slug))) {
4259
- push({
4260
- reason: "conflict",
4261
- message: `Explicit equipment [${authoredSlugs.join(", ")}] contradicts effort "${effortEntity.slug}" definition equipment [${effortEntity.equipment.join(", ")}].`,
4262
- token: source
4263
- });
4264
- }
4265
- }
4513
+ }
4514
+ this.userEfforts.clear();
4515
+ if (this.storage) {
4516
+ try {
4517
+ const userEfforts = await this.storage.load();
4518
+ for (const effort of userEfforts) {
4519
+ if (effort.registrySource === "user") {
4520
+ this.userEfforts.set(effort.slug, effort);
4266
4521
  }
4267
- fillEquipment();
4268
- return {
4269
- matched,
4270
- label: source,
4271
- effort: effortEntity ? materializer(effortEntity) : null,
4272
- effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
4273
- equipment: equipmentRefs,
4274
- remaining: "",
4275
- diagnostics
4276
- };
4277
- }
4278
- }
4279
- const lowerSource = source.toLowerCase();
4280
- for (const candidate of prefixCandidates) {
4281
- if (!lowerSource.startsWith(candidate.lower)) continue;
4282
- const after = source.slice(candidate.lower.length);
4283
- if (after && !/^\s/.test(after)) continue;
4284
- const candidateRemaining = after.trim();
4285
- if (!candidateRemaining) continue;
4286
- const equipmentFound = equipment.lookup(candidate.lower);
4287
- if (equipmentFound.ambiguous.length > 0) {
4288
- push({
4289
- reason: "ambiguous-equipment",
4290
- message: `"${candidate.lower}" matches multiple equipment entries.`,
4291
- token: source,
4292
- candidates: equipmentFound.ambiguous.map((e) => e.slug)
4293
- });
4294
- return {
4295
- matched: "none",
4296
- label: source,
4297
- effort: null,
4298
- effortRef: null,
4299
- equipment: [],
4300
- remaining: "",
4301
- diagnostics
4302
- };
4303
- }
4304
- if (!equipmentFound.entity) continue;
4305
- const effortFound = resolveEffortExact(candidateRemaining);
4306
- if (effortFound.entity) {
4307
- matched = "split";
4308
- effortEntity = effortFound.entity;
4309
- equipmentRefs = [equipmentRef(equipmentFound.entity)];
4310
- equipmentAuthored = true;
4311
- remaining = candidateRemaining;
4312
- break;
4313
- }
4314
- if (effortFound.diagnostic?.reason === "ambiguous-effort") {
4315
- push(effortFound.diagnostic);
4316
- return {
4317
- matched: "none",
4318
- label: source,
4319
- effort: null,
4320
- effortRef: null,
4321
- equipment: [],
4322
- remaining: "",
4323
- diagnostics
4324
- };
4325
4522
  }
4523
+ } catch (err) {
4524
+ console.warn("[CompositeEffortRegistry] Failed to load user efforts from storage adapter:", err);
4326
4525
  }
4327
- if (matched === "none") {
4328
- const found = resolveEffortExact(source);
4329
- if (found.entity) {
4330
- effortEntity = found.entity;
4331
- matched = "whole";
4332
- } else if (found.diagnostic?.reason === "ambiguous-effort") {
4333
- push(found.diagnostic);
4334
- return {
4335
- matched: "none",
4336
- label: source,
4337
- effort: null,
4338
- effortRef: null,
4339
- equipment: [],
4340
- remaining: "",
4341
- diagnostics
4342
- };
4343
- } else if (found.diagnostic) {
4344
- push(found.diagnostic);
4345
- }
4526
+ }
4527
+ this.initialized = true;
4528
+ }
4529
+ /** True after loadBundled() has resolved */
4530
+ isInitialized() {
4531
+ return this.initialized;
4532
+ }
4533
+ resolve(slug) {
4534
+ return this.userEfforts.get(slug) ?? this.bundledEfforts.get(slug) ?? null;
4535
+ }
4536
+ resolveByAlias(label) {
4537
+ const normalized = normalizeForFuzzy(label);
4538
+ for (const effort of this.userEfforts.values()) {
4539
+ if (normalizeForFuzzy(effort.label) === normalized) return effort;
4540
+ for (const alias of effort.aliases) {
4541
+ if (normalizeForFuzzy(alias) === normalized) return effort;
4346
4542
  }
4347
- fillEquipment();
4348
- return {
4349
- matched,
4350
- label: source,
4351
- effort: effortEntity ? materializer(effortEntity) : null,
4352
- effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
4353
- equipment: equipmentRefs,
4354
- remaining,
4355
- diagnostics
4356
- };
4357
4543
  }
4358
- };
4359
- return index;
4360
- }
4361
- function extractStatementRefs(statements) {
4362
- const efforts = [];
4363
- const equipment = [];
4364
- const seenEffort = /* @__PURE__ */ new Set();
4365
- const seenEquipment = /* @__PURE__ */ new Set();
4366
- const pushUnique = (list, seen, value) => {
4367
- if (!value) return;
4368
- if (seen.has(value)) return;
4369
- seen.add(value);
4370
- list.push(value);
4371
- };
4372
- for (const statement of statements ?? []) {
4373
- const metrics = Array.isArray(statement.metrics) ? statement.metrics : statement.metrics?.toArray?.() ?? [];
4374
- for (const metric of metrics) {
4375
- const refs = wodWikiCore.getReferenceMetadata(metric.metadata)?.refs;
4376
- if (!refs) continue;
4377
- for (const ref of refs) {
4378
- if (ref.kind === "effort") pushUnique(efforts, seenEffort, ref.slug);
4379
- if (ref.kind === "equipment") pushUnique(equipment, seenEquipment, ref.slug);
4544
+ const bundled = Array.from(this.bundledEfforts.values()).filter((effort) => !this.userEfforts.has(effort.slug)).sort((a, b) => resolveEntityRank(b) - resolveEntityRank(a));
4545
+ for (const effort of bundled) {
4546
+ if (normalizeForFuzzy(effort.label) === normalized) return effort;
4547
+ for (const alias of effort.aliases) {
4548
+ if (normalizeForFuzzy(alias) === normalized) return effort;
4380
4549
  }
4381
4550
  }
4551
+ return null;
4552
+ }
4553
+ list() {
4554
+ const merged = new Map(this.bundledEfforts);
4555
+ for (const [slug, effort] of this.userEfforts) {
4556
+ merged.set(slug, effort);
4557
+ }
4558
+ return Array.from(merged.values());
4382
4559
  }
4383
- return { efforts, equipment };
4384
- }
4385
- function slugifyRefLabel(label) {
4386
- return normalizeForFuzzy(label).replace(/\s+/g, "-");
4560
+ listByOrigin(origin) {
4561
+ if (origin === "user") {
4562
+ return Array.from(this.userEfforts.values());
4563
+ }
4564
+ if (origin === "bundled") {
4565
+ return Array.from(this.bundledEfforts.values());
4566
+ }
4567
+ return this.list().filter((e) => e.registrySource === origin);
4568
+ }
4569
+ async upsert(effort) {
4570
+ if (effort.registrySource !== "user") {
4571
+ throw new Error(
4572
+ `CompositeEffortRegistry.upsert: only user efforts can be written. Received registrySource="${effort.registrySource}"`
4573
+ );
4574
+ }
4575
+ this.userEfforts.set(effort.slug, effort);
4576
+ if (this.storage) {
4577
+ try {
4578
+ await this.storage.save(effort);
4579
+ } catch (err) {
4580
+ console.warn("[CompositeEffortRegistry] Failed to persist effort to storage adapter:", err);
4581
+ }
4582
+ }
4583
+ }
4584
+ async delete(slug) {
4585
+ const existing = this.resolve(slug);
4586
+ if (existing && existing.registrySource !== "user") {
4587
+ throw new Error(
4588
+ `CompositeEffortRegistry.delete: cannot delete non-user effort "${slug}"`
4589
+ );
4590
+ }
4591
+ this.userEfforts.delete(slug);
4592
+ if (this.storage) {
4593
+ try {
4594
+ await this.storage.delete(slug);
4595
+ } catch (err) {
4596
+ console.warn("[CompositeEffortRegistry] Failed to delete effort from storage adapter:", err);
4597
+ }
4598
+ }
4599
+ }
4600
+ };
4601
+
4602
+ // src/effort-registry/aliasManagement.ts
4603
+ async function addAliasToEffort(registry, targetSlug, newAlias) {
4604
+ const trimmed = newAlias.trim();
4605
+ if (!trimmed) {
4606
+ throw new Error("Alias cannot be empty");
4607
+ }
4608
+ const existing = registry.resolve(targetSlug);
4609
+ if (!existing) {
4610
+ throw new Error(`Effort "${targetSlug}" not found in registry`);
4611
+ }
4612
+ const normalizedNew = normalizeForFuzzy(trimmed);
4613
+ const alreadyHas = normalizeForFuzzy(existing.label) === normalizedNew || existing.aliases.some((a) => normalizeForFuzzy(a) === normalizedNew);
4614
+ if (alreadyHas) {
4615
+ return existing;
4616
+ }
4617
+ const updatedAliases = [...existing.aliases, trimmed];
4618
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4619
+ if (existing.registrySource === "bundled") {
4620
+ const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? `effort-user-${crypto.randomUUID()}` : `effort-user-${Date.now()}`;
4621
+ const userEffort = {
4622
+ ...existing,
4623
+ id,
4624
+ registrySource: "user",
4625
+ aliases: updatedAliases,
4626
+ updatedAt: now,
4627
+ derivation: existing.derivation ?? {
4628
+ parentSlug: existing.slug,
4629
+ coefficients: {},
4630
+ hardOverrides: {}
4631
+ }
4632
+ };
4633
+ await registry.upsert(userEffort);
4634
+ return userEffort;
4635
+ }
4636
+ const updatedUserEffort = {
4637
+ ...existing,
4638
+ aliases: updatedAliases,
4639
+ updatedAt: now
4640
+ };
4641
+ await registry.upsert(updatedUserEffort);
4642
+ return updatedUserEffort;
4387
4643
  }
4388
4644
 
4389
4645
  // src/runtime/contracts/IRuntimeOptions.ts
@@ -6547,6 +6803,8 @@ var RuntimeBlock = class {
6547
6803
  blockType;
6548
6804
  context;
6549
6805
  sourceIds;
6806
+ /** The tags this block's statements referenced, resolved at compile time. */
6807
+ tagComposition;
6550
6808
  // List-based memory storage
6551
6809
  _memory = [];
6552
6810
  /**
@@ -6583,13 +6841,15 @@ var RuntimeBlock = class {
6583
6841
  key,
6584
6842
  blockType,
6585
6843
  label,
6586
- metrics
6844
+ metrics,
6845
+ tagComposition
6587
6846
  } = options;
6588
6847
  this._runtime = runtime;
6589
6848
  this.sourceIds = sourceIds;
6590
6849
  this.behaviors = behaviors;
6591
6850
  this.key = key ?? new wodWikiCore.BlockKey();
6592
6851
  this.blockType = blockType;
6852
+ this.tagComposition = tagComposition;
6593
6853
  this.context = context ?? new BlockContext(runtime, this.key.toString());
6594
6854
  if (label) {
6595
6855
  this._memory.push(new MemoryLocation("metric:label", [{
@@ -8553,61 +8813,153 @@ var SoundMetric = class {
8553
8813
  image;
8554
8814
  };
8555
8815
 
8556
- // src/runtime/behaviors/SoundCueBehavior.ts
8557
- var SoundCueBehavior = class {
8816
+ // src/runtime/sound/SoundProfile.ts
8817
+ function parseSoundTrigger(notation) {
8818
+ const trimmed = notation.trim();
8819
+ if (trimmed.length === 0) return void 0;
8820
+ let anchor;
8821
+ let magnitude;
8822
+ if (trimmed.startsWith("-")) {
8823
+ anchor = "end";
8824
+ magnitude = trimmed.slice(1);
8825
+ } else {
8826
+ anchor = "start";
8827
+ magnitude = trimmed;
8828
+ }
8829
+ if (magnitude.startsWith(":")) magnitude = magnitude.slice(1);
8830
+ const offsetMs = parseDurationMagnitude(magnitude);
8831
+ if (offsetMs === void 0) return void 0;
8832
+ return { anchor, offsetMs };
8833
+ }
8834
+ function parseDurationMagnitude(magnitude) {
8835
+ const cleaned = magnitude.trim().replace(/^:/, "");
8836
+ if (cleaned.length === 0) return 0;
8837
+ if (!/^\d+(:\d+)*$/.test(cleaned)) return void 0;
8838
+ const segments = cleaned.split(":").map(Number).reverse();
8839
+ const [seconds = 0, minutes = 0, hours = 0, days = 0] = segments;
8840
+ if (segments.length > 4) return void 0;
8841
+ if (minutes > 59 || hours > 59) return void 0;
8842
+ return ((days * 24 + hours) * 60 + minutes) * 6e4 + seconds * 1e3;
8843
+ }
8844
+ function formatSoundTrigger(trigger) {
8845
+ const totalSeconds = Math.round(trigger.offsetMs / 1e3);
8846
+ const seconds = totalSeconds % 60;
8847
+ const minutes = Math.floor(totalSeconds / 60) % 60;
8848
+ const hours = Math.floor(totalSeconds / 3600);
8849
+ let magnitude;
8850
+ if (hours > 0) {
8851
+ magnitude = `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
8852
+ } else if (minutes > 0) {
8853
+ magnitude = `${pad(minutes)}:${pad(seconds)}`;
8854
+ } else {
8855
+ magnitude = pad(seconds);
8856
+ }
8857
+ return `${trigger.anchor === "end" ? "-:" : ":"}${magnitude}`;
8858
+ }
8859
+ function pad(value) {
8860
+ return value < 10 ? `0${value}` : String(value);
8861
+ }
8862
+ function resolveProfileRules(profile, durationMs) {
8863
+ return profile.rules.filter((rule) => {
8864
+ const { anchor, offsetMs } = rule.trigger;
8865
+ if (anchor === "start") return true;
8866
+ if (offsetMs === 0) return true;
8867
+ if (durationMs === void 0) return false;
8868
+ return offsetMs <= durationMs;
8869
+ });
8870
+ }
8871
+ var DEFAULT_SOUND_PROFILE = {
8872
+ id: "default",
8873
+ name: "Default",
8874
+ rules: [
8875
+ { trigger: { anchor: "end", offsetMs: 3e3 }, sound: "countdown-beep" },
8876
+ { trigger: { anchor: "end", offsetMs: 2e3 }, sound: "countdown-beep" },
8877
+ { trigger: { anchor: "end", offsetMs: 1e3 }, sound: "countdown-beep" },
8878
+ { trigger: { anchor: "end", offsetMs: 0 }, sound: "timer-complete" }
8879
+ ]
8880
+ };
8881
+
8882
+ // src/runtime/behaviors/SoundProfileBehavior.ts
8883
+ var SOUND_PLAY_EVENT = "sound:play";
8884
+ var SoundProfileBehavior = class {
8558
8885
  constructor(config) {
8559
8886
  this.config = config;
8560
8887
  }
8561
8888
  config;
8889
+ rules = [];
8890
+ /** Rule indices already fired, so each rule fires exactly once. */
8891
+ fired = /* @__PURE__ */ new Set();
8892
+ durationMs;
8562
8893
  onMount(ctx) {
8563
- for (const cue of this.config.cues) {
8564
- if (cue.trigger === "mount") {
8565
- ctx.emitOutput("system", [
8566
- new SoundMetric(cue.sound, "mount")
8567
- ], { label: `Sound: ${cue.sound}` });
8568
- }
8569
- }
8570
- const countdownCues = this.config.cues.filter((c) => c.trigger === "countdown");
8571
- if (countdownCues.length > 0) {
8572
- const playedSeconds = /* @__PURE__ */ new Set();
8573
- ctx.subscribe("tick", (_event, tickCtx) => {
8574
- const timer = readTimer(tickCtx);
8575
- if (!timer || timer.direction !== "down" || !timer.durationMs) return [];
8576
- const now = tickCtx.clock.currentDate.getTime();
8577
- let elapsed = 0;
8578
- for (const span of timer.spans) {
8579
- const end = span.ended ?? now;
8580
- elapsed += end - span.started;
8581
- }
8582
- const remainingMs = timer.durationMs - elapsed;
8583
- const remainingSeconds = Math.ceil(remainingMs / 1e3);
8584
- for (const cue of countdownCues) {
8585
- if (cue.atSeconds?.includes(remainingSeconds) && !playedSeconds.has(remainingSeconds)) {
8586
- playedSeconds.add(remainingSeconds);
8587
- tickCtx.emitOutput("system", [
8588
- new SoundMetric(cue.sound, "countdown", { atSecond: remainingSeconds })
8589
- ], { label: `Countdown: ${remainingSeconds}s` });
8590
- }
8591
- }
8592
- return [];
8593
- }, { scope: "bubble" });
8594
- }
8894
+ const timer = readTimer(ctx);
8895
+ this.durationMs = timer?.durationMs;
8896
+ this.rules = resolveProfileRules(this.config.profile, this.durationMs);
8897
+ this.fireDue(ctx, 0, this.durationMs);
8898
+ ctx.subscribe("tick", (_event, tickCtx) => {
8899
+ const elapsed = this.elapsedMs(tickCtx);
8900
+ if (elapsed === void 0) return [];
8901
+ this.fireDue(tickCtx, elapsed, this.durationMs);
8902
+ return [];
8903
+ }, { scope: "bubble" });
8595
8904
  return [];
8596
8905
  }
8597
8906
  onNext(_ctx) {
8598
8907
  return [];
8599
8908
  }
8600
8909
  onUnmount(ctx) {
8601
- for (const cue of this.config.cues) {
8602
- if (cue.trigger === "unmount" || cue.trigger === "complete") {
8603
- ctx.emitOutput("system", [
8604
- new SoundMetric(cue.sound, cue.trigger)
8605
- ], { label: `Sound: ${cue.sound}` });
8606
- }
8607
- }
8910
+ this.fireDue(ctx, this.elapsedMs(ctx) ?? 0, this.durationMs, true);
8608
8911
  return [];
8609
8912
  }
8610
8913
  onDispose(_ctx) {
8914
+ this.fired.clear();
8915
+ }
8916
+ /**
8917
+ * Elapsed milliseconds for the block's own timer, or `undefined` when the
8918
+ * block has no timer to measure against.
8919
+ */
8920
+ elapsedMs(ctx) {
8921
+ const timer = readTimer(ctx);
8922
+ if (!timer) return void 0;
8923
+ return calculateElapsed(timer, ctx.clock.currentDate.getTime());
8924
+ }
8925
+ /**
8926
+ * Fire every rule whose trigger point has been reached.
8927
+ *
8928
+ * @param elapsed milliseconds elapsed on the block's timer
8929
+ * @param durationMs the block's total duration
8930
+ * @param ending whether the block is ending — a rule anchored to the end
8931
+ * with no offset fires then, even when the block has no duration to
8932
+ * count down from
8933
+ */
8934
+ fireDue(ctx, elapsed, durationMs, ending = false) {
8935
+ const remaining = durationMs === void 0 ? void 0 : Math.max(0, durationMs - elapsed);
8936
+ this.rules.forEach((rule, index) => {
8937
+ if (this.fired.has(index)) return;
8938
+ const { anchor, offsetMs } = rule.trigger;
8939
+ const due = anchor === "start" ? elapsed >= offsetMs : offsetMs === 0 ? ending || remaining !== void 0 && remaining <= 0 : remaining !== void 0 && remaining <= offsetMs;
8940
+ if (!due) return;
8941
+ this.fired.add(index);
8942
+ this.play(ctx, rule, anchor, offsetMs);
8943
+ });
8944
+ }
8945
+ /**
8946
+ * Record the cue and command its playback.
8947
+ */
8948
+ play(ctx, rule, anchor, offsetMs) {
8949
+ const trigger = anchor === "start" ? offsetMs === 0 ? "mount" : "countdown" : offsetMs === 0 ? "complete" : "countdown";
8950
+ ctx.emitOutput("system", [
8951
+ new SoundMetric(rule.sound, trigger, {
8952
+ atSecond: offsetMs / 1e3
8953
+ })
8954
+ ], { label: `Sound: ${rule.sound}` });
8955
+ ctx.emitEvent({
8956
+ name: SOUND_PLAY_EVENT,
8957
+ timestamp: ctx.clock.currentDate,
8958
+ data: {
8959
+ sound: rule.sound,
8960
+ volume: 1
8961
+ }
8962
+ });
8611
8963
  }
8612
8964
  };
8613
8965
  var ButtonBehavior = class {
@@ -8737,6 +9089,34 @@ var WaitingToStartInjectorBehavior = class {
8737
9089
  onDispose(_ctx) {
8738
9090
  }
8739
9091
  };
9092
+ var SOUND_PROFILE_METRIC = wodWikiCore.MetricType.Sound;
9093
+ function resolveSoundProfile(statements, options) {
9094
+ const blockProfileId = findBlockProfileId(statements);
9095
+ if (blockProfileId) {
9096
+ const match = findProfile(options?.profiles, blockProfileId);
9097
+ if (match) return match;
9098
+ }
9099
+ if (options?.noteProfileId) {
9100
+ const match = findProfile(options?.profiles, options.noteProfileId);
9101
+ if (match) return match;
9102
+ }
9103
+ return DEFAULT_SOUND_PROFILE;
9104
+ }
9105
+ function findBlockProfileId(statements) {
9106
+ for (const statement of statements) {
9107
+ const metric = statement.metrics.find((m) => m.type === SOUND_PROFILE_METRIC);
9108
+ if (!metric) continue;
9109
+ const value = metric.value;
9110
+ if (typeof value === "string" && value.trim().length > 0) {
9111
+ return value.trim();
9112
+ }
9113
+ }
9114
+ return void 0;
9115
+ }
9116
+ function findProfile(profiles, id) {
9117
+ if (!profiles) return void 0;
9118
+ return profiles.find((profile) => profile.id === id);
9119
+ }
8740
9120
 
8741
9121
  // src/runtime/blocks/RestBlock.ts
8742
9122
  var RestBlock = class _RestBlock extends RuntimeBlock {
@@ -8747,7 +9127,7 @@ var RestBlock = class _RestBlock extends RuntimeBlock {
8747
9127
  const restLabel = config.label ?? "Rest";
8748
9128
  const blockKey = new wodWikiCore.BlockKey("rest");
8749
9129
  const context = new BlockContext(runtime, blockKey.toString(), "Rest");
8750
- const behaviors = _RestBlock.buildBehaviors(config);
9130
+ const behaviors = _RestBlock.buildBehaviors(config, runtime.options?.soundOptions);
8751
9131
  super({
8752
9132
  runtime,
8753
9133
  sourceIds: [],
@@ -8762,7 +9142,7 @@ var RestBlock = class _RestBlock extends RuntimeBlock {
8762
9142
  /**
8763
9143
  * Builds the behavior list for a rest block.
8764
9144
  */
8765
- static buildBehaviors(config) {
9145
+ static buildBehaviors(config, soundOptions) {
8766
9146
  const behaviors = [];
8767
9147
  const restLabel = config.label ?? "Rest";
8768
9148
  behaviors.push(new ReportOutputBehavior({ label: restLabel }));
@@ -8781,18 +9161,8 @@ var RestBlock = class _RestBlock extends RuntimeBlock {
8781
9161
  mode: "timer",
8782
9162
  label: restLabel
8783
9163
  }));
8784
- behaviors.push(new SoundCueBehavior({
8785
- cues: [
8786
- {
8787
- sound: "rest-over",
8788
- trigger: "complete"
8789
- },
8790
- {
8791
- sound: "countdown-beep",
8792
- trigger: "countdown",
8793
- atSeconds: [3, 2, 1]
8794
- }
8795
- ]
9164
+ behaviors.push(new SoundProfileBehavior({
9165
+ profile: resolveSoundProfile([], soundOptions)
8796
9166
  }));
8797
9167
  return behaviors;
8798
9168
  }
@@ -8823,6 +9193,8 @@ var BlockBuilder = class {
8823
9193
  blockType = "Block";
8824
9194
  metrics;
8825
9195
  sourceIds = [];
9196
+ /** The tags the block's statements referenced, resolved at compile time. */
9197
+ tagComposition = [];
8826
9198
  /** Pending round config stored by asRepeater(), consumed by asContainer() */
8827
9199
  pendingRoundConfig;
8828
9200
  /** Declared exit intent — resolved into a single ExitBehavior in build(). */
@@ -8922,6 +9294,11 @@ var BlockBuilder = class {
8922
9294
  this.sourceIds = ids;
8923
9295
  return this;
8924
9296
  }
9297
+ /** The tags the block's statements referenced, and what each carried. */
9298
+ setTagComposition(composition) {
9299
+ this.tagComposition = composition;
9300
+ return this;
9301
+ }
8925
9302
  getLabel() {
8926
9303
  return this.label;
8927
9304
  }
@@ -9043,6 +9420,7 @@ var BlockBuilder = class {
9043
9420
  const block = new RuntimeBlock({
9044
9421
  runtime: this.runtime,
9045
9422
  sourceIds: this.sourceIds,
9423
+ tagComposition: this.tagComposition,
9046
9424
  behaviors: Array.from(this.behaviors.values()),
9047
9425
  context: this.context,
9048
9426
  key: this.key,
@@ -9132,6 +9510,11 @@ var JitCompiler = class {
9132
9510
  return void 0;
9133
9511
  }
9134
9512
  const builder = new BlockBuilder(runtime);
9513
+ const tagComposition = [];
9514
+ for (const node of effectiveNodes) {
9515
+ collectTagReferences(node.metrics, tagComposition);
9516
+ }
9517
+ builder.setTagComposition(tagComposition);
9135
9518
  for (const strategy of matchingStrategies) {
9136
9519
  strategy.apply(builder, effectiveNodes, runtime);
9137
9520
  }
@@ -10646,6 +11029,16 @@ var CalcEngine = class {
10646
11029
  const fenced = (d) => !d.fences || d.fences.includes(deps.dialect);
10647
11030
  this.segmentCalcs = registry.byScope("segment").filter(fenced);
10648
11031
  this.workoutCalcs = registry.byScope("workout").filter(fenced);
11032
+ for (const tag of deps.tags?.list() ?? []) {
11033
+ const resolved = resolveTagContributions(deps.tags, tag.slug);
11034
+ for (const name2 of resolved.functions) {
11035
+ const carriers = this.tagCarriedFunctions.get(name2);
11036
+ if (carriers) carriers.add(tag.slug);
11037
+ else this.tagCarriedFunctions.set(name2, /* @__PURE__ */ new Set([tag.slug]));
11038
+ }
11039
+ }
11040
+ this.runTags = [...deps.activeTags ?? []];
11041
+ for (const tag of this.runTags) this.activatedTags.add(tag);
10649
11042
  }
10650
11043
  registry;
10651
11044
  deps;
@@ -10659,6 +11052,43 @@ var CalcEngine = class {
10659
11052
  * calls (segments arrive in stream order, live and in replay).
10660
11053
  */
10661
11054
  lastSegmentEffort;
11055
+ /**
11056
+ * The tags that carry each function: function name → the tags that carry
11057
+ * it. A function no tag carries runs everywhere, as it always did.
11058
+ */
11059
+ tagCarriedFunctions = /* @__PURE__ */ new Map();
11060
+ /** The tags applied anywhere in the run so far. */
11061
+ activatedTags = /* @__PURE__ */ new Set();
11062
+ /** The tags the run activates — a note-level bundle of attachments. */
11063
+ runTags;
11064
+ /**
11065
+ * Whether a calculation runs here.
11066
+ *
11067
+ * A function a tag carries runs only where one of the tags that carry it
11068
+ * applies; every other function runs as it always did.
11069
+ */
11070
+ isActive(def, appliedTags) {
11071
+ const carriers = this.tagCarriedFunctions.get(def.id);
11072
+ if (!carriers) return true;
11073
+ for (const slug of carriers) {
11074
+ if (appliedTags.includes(slug)) return true;
11075
+ }
11076
+ return false;
11077
+ }
11078
+ /**
11079
+ * The tags applied to a segment: the tags its statement marked, plus the
11080
+ * tags the run activates — a note-level tag applies to every segment of
11081
+ * the note it belongs to.
11082
+ *
11083
+ * The applied tags are recorded, so a workout-scope function the tag
11084
+ * carries runs at the end of the workout.
11085
+ */
11086
+ appliedTags(output) {
11087
+ const slugs = tagMarkerSlugs(output.metrics.rawMetrics);
11088
+ for (const slug of slugs) this.activatedTags.add(slug);
11089
+ if (this.runTags.length === 0) return slugs;
11090
+ return [...slugs, ...this.runTags.filter((slug) => !slugs.includes(slug))];
11091
+ }
10662
11092
  // ── Phase 1: segment scope ─────────────────────────────────────────────
10663
11093
  process(output) {
10664
11094
  if (output.outputType !== "segment" || !output.isLeaf) return output;
@@ -10674,11 +11104,13 @@ var CalcEngine = class {
10674
11104
  if (discipline) effortTags.effortDiscipline = discipline;
10675
11105
  if (intensityTier) effortTags.effortIntensityTier = intensityTier;
10676
11106
  }
10677
- const climbGrade = [...output.metrics.rawMetrics].find((m) => m.type === "climb-grade")?.value;
11107
+ const climbGrade = output.metrics.rawMetrics.find((m) => m.type === "climb-grade")?.value;
10678
11108
  if (climbGrade && typeof climbGrade === "object" && "raw" in climbGrade) {
10679
11109
  effortTags.grade = String(climbGrade.raw);
10680
11110
  }
11111
+ const appliedTags = this.appliedTags(output);
10681
11112
  for (const def of this.segmentCalcs) {
11113
+ if (!this.isActive(def, appliedTags)) continue;
10682
11114
  const ctx = this.makeContext(atoms);
10683
11115
  if (!this.applicable(def.whenAst, ctx)) continue;
10684
11116
  const variant = this.selectVariant(def, ctx);
@@ -10720,7 +11152,15 @@ var CalcEngine = class {
10720
11152
  // ── Phase 2: workout scope ─────────────────────────────────────────────
10721
11153
  summarize(outputs) {
10722
11154
  const projections = [];
11155
+ for (const output of outputs) {
11156
+ if (output.outputType !== "segment") continue;
11157
+ for (const slug of tagMarkerSlugs(output.metrics.rawMetrics)) {
11158
+ this.activatedTags.add(slug);
11159
+ }
11160
+ }
11161
+ const appliedTags = [...this.activatedTags];
10723
11162
  for (const def of this.workoutCalcs) {
11163
+ if (!this.isActive(def, appliedTags)) continue;
10724
11164
  if (def.output?.isGrouped) {
10725
11165
  projections.push(...this.summarizeGrouped(def, outputs));
10726
11166
  } else {
@@ -11878,7 +12318,13 @@ function createCalcStack(options) {
11878
12318
  }
11879
12319
  function createCalcEngine(dialect, options) {
11880
12320
  const stack = createCalcStack(options);
11881
- return new CalcEngine(stack.registry, { dialect, lookups: stack.lookups, userProfile: options.userProfile });
12321
+ return new CalcEngine(stack.registry, {
12322
+ dialect,
12323
+ lookups: stack.lookups,
12324
+ userProfile: options.userProfile,
12325
+ tags: options.tags,
12326
+ activeTags: options.activeTags
12327
+ });
11882
12328
  }
11883
12329
 
11884
12330
  // src/analytics/StandardAnalyticsProfile.ts
@@ -11893,7 +12339,9 @@ var StandardAnalyticsProfile = class {
11893
12339
  const calcEngine = createCalcEngine(context.dialect, {
11894
12340
  effortResolver: context.analyticsContext.effortResolver,
11895
12341
  userProfile: context.userProfile,
11896
- calcs: context.calcs
12342
+ calcs: context.calcs,
12343
+ tags: context.tags,
12344
+ activeTags: context.activeTags
11897
12345
  });
11898
12346
  realtime.push(calcEngine);
11899
12347
  summary.push(calcEngine);
@@ -11945,7 +12393,9 @@ function createAnalyticsEngineForBlock(block, options) {
11945
12393
  scriptMetricTypes,
11946
12394
  analyticsContext,
11947
12395
  userProfile: options?.userProfile,
11948
- calcs: options?.calcs
12396
+ calcs: options?.calcs,
12397
+ tags: options?.tags,
12398
+ activeTags: options?.activeTags
11949
12399
  };
11950
12400
  const profile = new StandardAnalyticsProfile();
11951
12401
  const { realtime, summary } = profile.build(context);
@@ -12322,13 +12772,6 @@ var AmrapLogicStrategy = class {
12322
12772
  mode: "countdown",
12323
12773
  label
12324
12774
  }));
12325
- builder.addBehavior(new SoundCueBehavior({
12326
- cues: [
12327
- { sound: "start-beep", trigger: "mount" },
12328
- { sound: "countdown-beep", trigger: "countdown", atSeconds: [3, 2, 1] },
12329
- { sound: "timer-complete", trigger: "complete" }
12330
- ]
12331
- }));
12332
12775
  }
12333
12776
  };
12334
12777
  var IntervalLogicStrategy = class {
@@ -12386,13 +12829,6 @@ var IntervalLogicStrategy = class {
12386
12829
  mode: "countdown",
12387
12830
  label
12388
12831
  }));
12389
- builder.addBehavior(new SoundCueBehavior({
12390
- cues: [
12391
- { sound: "interval-start", trigger: "mount" },
12392
- { sound: "countdown-beep", trigger: "countdown", atSeconds: [3, 2, 1] },
12393
- { sound: "interval-complete", trigger: "complete" }
12394
- ]
12395
- }));
12396
12832
  const hasResistance = statements.some((s) => s.metrics.some((m) => m.type === wodWikiCore.MetricType.Resistance));
12397
12833
  const hasDistance = statements.some((s) => s.metrics.some((m) => m.type === wodWikiCore.MetricType.Distance));
12398
12834
  const promotions = [
@@ -12585,14 +13021,6 @@ var GenericTimerStrategy = class {
12585
13021
  } else {
12586
13022
  builder.addBehavior(new ExitBehavior({ mode: "immediate", onNext: true }));
12587
13023
  }
12588
- if (isCountdown) {
12589
- builder.addBehavior(new SoundCueBehavior({
12590
- cues: [
12591
- { sound: "countdown-beep", trigger: "countdown", atSeconds: [3, 2, 1] },
12592
- { sound: "timer-complete", trigger: "complete" }
12593
- ]
12594
- }));
12595
- }
12596
13024
  builder.addBehavior(new LabelingBehavior({
12597
13025
  mode: durationMs ? "countdown" : "clock",
12598
13026
  label
@@ -12653,29 +13081,17 @@ var SoundStrategy = class {
12653
13081
  priority = 20;
12654
13082
  id = "sound";
12655
13083
  match(statements, _runtime) {
12656
- return statements && statements.length > 0;
13084
+ return statements.length > 0;
12657
13085
  }
12658
- apply(builder, _statements, _runtime) {
12659
- if (builder.hasBehavior(SoundCueBehavior)) {
13086
+ apply(builder, statements, runtime) {
13087
+ if (builder.hasBehavior(SoundProfileBehavior)) {
12660
13088
  return;
12661
13089
  }
12662
- const countdown = builder.getBehavior(CountdownTimerBehavior);
12663
- if (countdown) {
12664
- builder.addBehavior(new SoundCueBehavior({
12665
- cues: [
12666
- { sound: "countdown-beep", trigger: "countdown", atSeconds: [3, 2, 1] },
12667
- { sound: "timer-complete", trigger: "complete" }
12668
- ]
12669
- }));
13090
+ if (!builder.hasTimerBehavior()) {
12670
13091
  return;
12671
13092
  }
12672
- if (builder.hasBehavior(CountupTimerBehavior)) {
12673
- builder.addBehavior(new SoundCueBehavior({
12674
- cues: [
12675
- { sound: "start-beep", trigger: "mount" }
12676
- ]
12677
- }));
12678
- }
13093
+ const profile = resolveSoundProfile(statements, runtime.options?.soundOptions);
13094
+ builder.addBehavior(new SoundProfileBehavior({ profile }));
12679
13095
  }
12680
13096
  };
12681
13097
 
@@ -12708,20 +13124,6 @@ var NextEvent = class _NextEvent {
12708
13124
  };
12709
13125
  }
12710
13126
  };
12711
-
12712
- // src/runtime/models/SoundModels.ts
12713
- var PREDEFINED_SOUNDS = {
12714
- /** Short single beep - good for countdown ticks */
12715
- BEEP: "beep",
12716
- /** Long buzz - good for timer completion */
12717
- BUZZER: "buzzer",
12718
- /** Pleasant chime - good for milestones */
12719
- CHIME: "chime",
12720
- /** Clock tick - good for second-by-second countdown */
12721
- TICK: "tick",
12722
- /** Ascending sequence - good for timer start */
12723
- START: "start"
12724
- };
12725
13127
  function metricsToLabel(metrics) {
12726
13128
  if (metrics instanceof wodWikiCore.MetricContainer) {
12727
13129
  return metricsToLabel(metrics.toArray());
@@ -13002,40 +13404,6 @@ var UnregisterEventHandlerAction = class {
13002
13404
  }
13003
13405
  };
13004
13406
 
13005
- // src/runtime/actions/audio/PlaySoundAction.ts
13006
- var PlaySoundAction = class {
13007
- constructor(sound, volume = 1, cueId, now = wallClockNow) {
13008
- this.sound = sound;
13009
- this.volume = volume;
13010
- this.cueId = cueId;
13011
- this.now = now;
13012
- if (volume < 0 || volume > 1) {
13013
- throw new RangeError(`Volume must be between 0.0 and 1.0, got: ${volume}`);
13014
- }
13015
- }
13016
- sound;
13017
- volume;
13018
- cueId;
13019
- now;
13020
- type = "play-sound";
13021
- /**
13022
- * Executes the sound playback by emitting a 'sound:play' event.
13023
- * UI components should register handlers for this event to play audio.
13024
- */
13025
- do(runtime) {
13026
- const event = {
13027
- name: "sound:play",
13028
- timestamp: this.now.now(),
13029
- data: {
13030
- sound: this.sound,
13031
- volume: this.volume,
13032
- cueId: this.cueId
13033
- }
13034
- };
13035
- return runtime.eventBus.dispatch(event, runtime);
13036
- }
13037
- };
13038
-
13039
13407
  // src/runtime/utils/MemoryUtils.ts
13040
13408
  function searchStackMemory(runtime, criteria) {
13041
13409
  const results = [];
@@ -14878,6 +15246,7 @@ exports.CrossFitDialect = CrossFitDialect;
14878
15246
  exports.DAY = DAY;
14879
15247
  exports.DEFAULT_RESOLVER_OPTIONS = DEFAULT_RESOLVER_OPTIONS;
14880
15248
  exports.DEFAULT_RUNTIME_OPTIONS = DEFAULT_RUNTIME_OPTIONS;
15249
+ exports.DEFAULT_SOUND_PROFILE = DEFAULT_SOUND_PROFILE;
14881
15250
  exports.DEFAULT_UNRESOLVED_EFFORT_MET = DEFAULT_UNRESOLVED_EFFORT_MET;
14882
15251
  exports.DIM_COUNT = DIM_COUNT;
14883
15252
  exports.DIM_ENERGY = DIM_ENERGY;
@@ -14912,6 +15281,7 @@ exports.LabelingBehavior = LabelingBehavior;
14912
15281
  exports.LocalEventProvider = LocalEventProvider;
14913
15282
  exports.LocalRuntimeSubscription = LocalRuntimeSubscription;
14914
15283
  exports.LookupRegistry = LookupRegistry;
15284
+ exports.MAX_TAG_CONTRIBUTION_DEPTH = MAX_TAG_CONTRIBUTION_DEPTH;
14915
15285
  exports.MdTimerRuntime = MdTimerRuntime;
14916
15286
  exports.MemoryLocation = MemoryLocation;
14917
15287
  exports.MemoryTypeEnum = MemoryTypeEnum;
@@ -14921,9 +15291,7 @@ exports.NextAction = NextAction;
14921
15291
  exports.NextEvent = NextEvent;
14922
15292
  exports.NextEventHandler = NextEventHandler;
14923
15293
  exports.OutputEmitter = OutputEmitter;
14924
- exports.PREDEFINED_SOUNDS = PREDEFINED_SOUNDS;
14925
15294
  exports.PRODUCTION_STRATEGIES = PRODUCTION_STRATEGIES;
14926
- exports.PlaySoundAction = PlaySoundAction;
14927
15295
  exports.PopBlockAction = PopBlockAction;
14928
15296
  exports.PushBlockAction = PushBlockAction;
14929
15297
  exports.PushRestBlockAction = PushRestBlockAction;
@@ -14945,6 +15313,8 @@ exports.RuntimeLogger = RuntimeLogger;
14945
15313
  exports.RuntimeMemory = RuntimeMemory;
14946
15314
  exports.RuntimeObservers = RuntimeObservers;
14947
15315
  exports.RuntimeStack = RuntimeStack;
15316
+ exports.SOUND_PLAY_EVENT = SOUND_PLAY_EVENT;
15317
+ exports.SOUND_PROFILE_METRIC = SOUND_PROFILE_METRIC;
14948
15318
  exports.STANDARD_UNITS = STANDARD_UNITS;
14949
15319
  exports.STORE_CALCS = STORE_CALCS;
14950
15320
  exports.STREAM_ATOMS = STREAM_ATOMS;
@@ -14952,13 +15322,14 @@ exports.ScriptRuntime = ScriptRuntime;
14952
15322
  exports.SessionRootBlock = SessionRootBlock;
14953
15323
  exports.SessionRootStrategy = SessionRootStrategy;
14954
15324
  exports.SnapshotClock = SnapshotClock;
14955
- exports.SoundCueBehavior = SoundCueBehavior;
15325
+ exports.SoundProfileBehavior = SoundProfileBehavior;
14956
15326
  exports.SoundStrategy = SoundStrategy;
14957
15327
  exports.SpanTrackingBehavior = SpanTrackingBehavior;
14958
15328
  exports.StandardAnalyticsProfile = StandardAnalyticsProfile;
14959
15329
  exports.StartSessionAction = StartSessionAction;
14960
15330
  exports.StartWorkoutAction = StartWorkoutAction;
14961
15331
  exports.SubscriptionManager = SubscriptionManager;
15332
+ exports.TAG_MARKER_PREFIX = TAG_MARKER_PREFIX;
14962
15333
  exports.ThrowError = ThrowError;
14963
15334
  exports.ThrowErrorAction = ThrowErrorAction;
14964
15335
  exports.TimeSpan = TimeSpan;
@@ -15035,6 +15406,7 @@ exports.extractStatementsRaw = extractStatementsRaw;
15035
15406
  exports.extractSyntaxFacts = extractSyntaxFacts;
15036
15407
  exports.filterTagsByVisibility = filterTagsByVisibility;
15037
15408
  exports.findBestFuzzyMatch = findBestFuzzyMatch;
15409
+ exports.findTagContributionCycles = findTagContributionCycles;
15038
15410
  exports.findUnresolvedChoices = findUnresolvedChoices;
15039
15411
  exports.fixtureBackSquat = fixtureBackSquat;
15040
15412
  exports.fixtureBurpee = fixtureBurpee;
@@ -15048,6 +15420,7 @@ exports.formatDuration = formatDuration;
15048
15420
  exports.formatRuntimeLogLine = formatRuntimeLogLine;
15049
15421
  exports.formatRuntimeLogRule = formatRuntimeLogRule;
15050
15422
  exports.formatRuntimeTimestamp = formatRuntimeTimestamp;
15423
+ exports.formatSoundTrigger = formatSoundTrigger;
15051
15424
  exports.fragmentsToLabel = metricsToLabel;
15052
15425
  exports.frozenNow = frozenNow;
15053
15426
  exports.fullFixtureSet = fullFixtureSet;
@@ -15087,6 +15460,7 @@ exports.parseEquipmentFile = parseEquipmentFile;
15087
15460
  exports.parseExpression = parseExpression;
15088
15461
  exports.parseMetricLine = parseMetricLine;
15089
15462
  exports.parseScript = parseScript;
15463
+ exports.parseSoundTrigger = parseSoundTrigger;
15090
15464
  exports.printCalculation = printCalculation;
15091
15465
  exports.readNestedSection = readNestedSection;
15092
15466
  exports.readTimer = readTimer;
@@ -15095,14 +15469,21 @@ exports.renderMetric = renderMetric;
15095
15469
  exports.resolveDominantOrigin = resolveDominantOrigin;
15096
15470
  exports.resolveEntityRank = resolveEntityRank;
15097
15471
  exports.resolveEntityTier = resolveEntityTier;
15472
+ exports.resolveProfileRules = resolveProfileRules;
15473
+ exports.resolveSoundProfile = resolveSoundProfile;
15098
15474
  exports.resolveSourceRank = resolveSourceRank;
15099
15475
  exports.resolveSourceTier = resolveSourceTier;
15476
+ exports.resolveTagContributions = resolveTagContributions;
15100
15477
  exports.slugifyRefLabel = slugifyRefLabel;
15101
15478
  exports.startSpan = startSpan;
15102
15479
  exports.str = str;
15103
15480
  exports.strategyRegistry = strategyRegistry;
15104
15481
  exports.subDim = subDim;
15105
15482
  exports.summaryProcessorRegistry = summaryProcessorRegistry;
15483
+ exports.tagContributionMetrics = tagContributionMetrics;
15484
+ exports.tagMarkerSlug = tagMarkerSlug;
15485
+ exports.tagMarkerSlugs = tagMarkerSlugs;
15486
+ exports.tagMarkerType = tagMarkerType;
15106
15487
  exports.toStoredOutputStatement = toStoredOutputStatement;
15107
15488
  exports.truthy = truthy;
15108
15489
  exports.validateOutputUnit = validateOutputUnit;