@bitcobblers/wod-wiki-lang 0.43.2153 → 0.44.2156

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { wallClockNow, formatDurationSmart } from './chunk-PFQDVQQO.mjs';
2
2
  export { createBrowserRuntimeNow, frozenNow, getRuntimeNowMs, wallClockNow } from './chunk-PFQDVQQO.mjs';
3
- import { MetricType, MetricContainer, BlockKey, REFERENCE_METADATA_KEY, ParsedCodeStatement, getReferenceMetadata, OutputStatement, valueKindOf, normalizeFieldPath } from '@bitcobblers/wod-wiki-core';
3
+ import { MetricType, MetricContainer, BlockKey, REFERENCE_METADATA_KEY, ParsedCodeStatement, TAG_REFERENCE_METADATA_KEY, getReferenceMetadata, OutputStatement, valueKindOf, normalizeFieldPath, getTagReference } from '@bitcobblers/wod-wiki-core';
4
4
  import { LRParser } from '@lezer/lr';
5
5
  import { LRLanguage, LanguageSupport } from '@codemirror/language';
6
6
  import { NodeProp } from '@lezer/common';
@@ -265,6 +265,125 @@ function computeColumnLabel(type) {
265
265
  const normalized = key.toLowerCase();
266
266
  return COLUMN_LABEL_MAP[normalized] ?? humanizeMetricKey(key);
267
267
  }
268
+ function tagReferenceFor(tagIndex, slug) {
269
+ const resolved = resolveTagContributions(tagIndex, slug);
270
+ const sound = resolved.metrics.find(
271
+ (contribution) => contribution.type === MetricType.Sound && typeof contribution.value === "string"
272
+ );
273
+ return {
274
+ slug,
275
+ label: slug,
276
+ ...resolved.functions.length > 0 ? { functions: [...resolved.functions] } : {},
277
+ ...sound ? { soundProfileId: sound.value } : {}
278
+ };
279
+ }
280
+ function collectTagReferences(metrics, into = []) {
281
+ for (const metric of metrics) {
282
+ const reference = getTagReference(metric.metadata);
283
+ if (!reference) continue;
284
+ if (into.some((existing) => existing.slug === reference.slug)) continue;
285
+ into.push(reference);
286
+ }
287
+ return into;
288
+ }
289
+ var TAG_MARKER_PREFIX = "tag.";
290
+ var MAX_TAG_CONTRIBUTION_DEPTH = 8;
291
+ var EMPTY = { metrics: [], functions: [], tags: [] };
292
+ function resolveTagContributions(tagIndex, slug) {
293
+ if (!tagIndex) return EMPTY;
294
+ const metrics = [];
295
+ const functions = [];
296
+ const tags2 = [];
297
+ const seenFunctions = /* @__PURE__ */ new Set();
298
+ const visited = /* @__PURE__ */ new Set();
299
+ const walk = (currentSlug, depth) => {
300
+ if (depth > MAX_TAG_CONTRIBUTION_DEPTH) return;
301
+ if (visited.has(currentSlug)) return;
302
+ const definition = tagIndex.resolve(currentSlug);
303
+ if (!definition) return;
304
+ visited.add(currentSlug);
305
+ tags2.push(currentSlug);
306
+ for (const contribution of definition.contributions) {
307
+ if (contribution.kind === "metric") metrics.push(contribution);
308
+ else if (contribution.kind === "function") {
309
+ if (!seenFunctions.has(contribution.name)) {
310
+ seenFunctions.add(contribution.name);
311
+ functions.push(contribution.name);
312
+ }
313
+ }
314
+ }
315
+ for (const contribution of definition.contributions) {
316
+ if (contribution.kind === "tag") walk(contribution.slug, depth + 1);
317
+ }
318
+ };
319
+ walk(slug, 0);
320
+ return { metrics, functions, tags: tags2 };
321
+ }
322
+ function findTagContributionCycles(tagIndex) {
323
+ if (!tagIndex) return [];
324
+ const cycles = [];
325
+ const seen = /* @__PURE__ */ new Set();
326
+ const walk = (slug, path, onPath) => {
327
+ if (onPath.has(slug)) {
328
+ const start = path.indexOf(slug);
329
+ if (start >= 0) {
330
+ const cycle = [...path.slice(start), slug];
331
+ const key = cycleKey(cycle);
332
+ if (!seen.has(key)) {
333
+ seen.add(key);
334
+ cycles.push(cycle);
335
+ }
336
+ }
337
+ return;
338
+ }
339
+ const definition = tagIndex.resolve(slug);
340
+ if (!definition) return;
341
+ const nextPath = [...path, slug];
342
+ const nextOnPath = new Set(onPath);
343
+ nextOnPath.add(slug);
344
+ for (const contribution of definition.contributions) {
345
+ if (contribution.kind === "tag") walk(contribution.slug, nextPath, nextOnPath);
346
+ }
347
+ };
348
+ for (const definition of tagIndex.list()) {
349
+ walk(definition.slug, [], /* @__PURE__ */ new Set());
350
+ }
351
+ return cycles;
352
+ }
353
+ function cycleKey(cycle) {
354
+ const nodes = cycle.slice(0, -1);
355
+ if (nodes.length === 0) return "";
356
+ let smallest = 0;
357
+ for (let index = 1; index < nodes.length; index++) {
358
+ if (nodes[index] < nodes[smallest]) smallest = index;
359
+ }
360
+ return [...nodes.slice(smallest), ...nodes.slice(0, smallest)].join(">");
361
+ }
362
+ function tagMarkerType(slug) {
363
+ return `${TAG_MARKER_PREFIX}${slug}`;
364
+ }
365
+ function tagMarkerSlug(type) {
366
+ if (!type.startsWith(TAG_MARKER_PREFIX)) return void 0;
367
+ const slug = type.slice(TAG_MARKER_PREFIX.length);
368
+ return slug.length > 0 ? slug : void 0;
369
+ }
370
+ function tagMarkerSlugs(metrics) {
371
+ const slugs = [];
372
+ for (const metric of metrics) {
373
+ const slug = tagMarkerSlug(metric.type);
374
+ if (slug !== void 0 && !slugs.includes(slug)) slugs.push(slug);
375
+ }
376
+ return slugs;
377
+ }
378
+ function tagContributionMetrics(contributions, origin = "parser") {
379
+ return contributions.map((contribution) => ({
380
+ type: contribution.type,
381
+ value: contribution.value,
382
+ origin,
383
+ image: String(contribution.value),
384
+ ...contribution.unit ? { unit: contribution.unit } : {}
385
+ }));
386
+ }
268
387
 
269
388
  // src/metrics/presentation/MetricPresentationPolicy.ts
270
389
  function isRestMetric(metric) {
@@ -328,6 +447,7 @@ function createPolicy() {
328
447
  }
329
448
  const t2 = metric.type;
330
449
  if ((t2 === MetricType.Sound || t2 === "sound") && surface !== "debug") return true;
450
+ if (t2.startsWith(TAG_MARKER_PREFIX) && surface !== "debug") return true;
331
451
  if (surface === "review-grid-column") {
332
452
  if (t2 === MetricType.Elapsed || t2 === "elapsed" || t2 === MetricType.Total || t2 === "total") return true;
333
453
  if (t2 === MetricType.System || t2 === "system") return true;
@@ -1465,15 +1585,16 @@ var TextMetric = class {
1465
1585
  origin = "parser";
1466
1586
  type = MetricType.Text;
1467
1587
  };
1468
-
1469
- // src/parser/semantic-classifier.ts
1470
1588
  var PROPERTY_KEY_TO_METRIC_TYPE = {
1471
1589
  rpe: MetricType.SessionRPE,
1472
1590
  rir: MetricType.RIR,
1473
1591
  intensity: MetricType.Intensity,
1474
1592
  load: MetricType.Load,
1475
1593
  volume: MetricType.Volume,
1476
- work: MetricType.Work
1594
+ work: MetricType.Work,
1595
+ // `sound: classic` names a sound profile, the same way a `#sound-classic`
1596
+ // tag reference does.
1597
+ sound: MetricType.Sound
1477
1598
  };
1478
1599
  var STRUCTURAL_EFFORT_WORDS = /* @__PURE__ */ new Set(["rest", "amrap", "emom", "tabata", "for time"]);
1479
1600
  var STANDARD_UNITS2 = UnitRegistry.standard();
@@ -1490,9 +1611,25 @@ function referenceMetadata(resolution) {
1490
1611
  function classifyStatements(facts, references) {
1491
1612
  return facts.statements.map((fact) => {
1492
1613
  const statement = new ParsedCodeStatement();
1493
- let metricPairs = [];
1614
+ const slots = [];
1494
1615
  for (const primitive of fact.primitives) {
1495
- metricPairs.push(...classifyPrimitive(primitive));
1616
+ slots.push(...classifySlots(primitive, references));
1617
+ }
1618
+ const explicitTypes = /* @__PURE__ */ new Set();
1619
+ for (const slot of slots) {
1620
+ if (slot.kind === "explicit") explicitTypes.add(slot.pair.metrics.type);
1621
+ }
1622
+ const contributedTypes = /* @__PURE__ */ new Set();
1623
+ const metricPairs = [];
1624
+ for (const slot of slots) {
1625
+ if (slot.kind !== "contributed") {
1626
+ metricPairs.push(slot.pair);
1627
+ continue;
1628
+ }
1629
+ const type = slot.pair.metrics.type;
1630
+ if (explicitTypes.has(type) || contributedTypes.has(type)) continue;
1631
+ contributedTypes.add(type);
1632
+ metricPairs.push(slot.pair);
1496
1633
  }
1497
1634
  const mergedPairs = mergeFragments(metricPairs);
1498
1635
  const finalPairs = references ? applyReferenceResolution(mergedPairs, fact, references) : mergedPairs;
@@ -1509,6 +1646,21 @@ function classifyStatements(facts, references) {
1509
1646
  return statement;
1510
1647
  });
1511
1648
  }
1649
+ function equipmentMeta(resolution, authored, meta2) {
1650
+ if (resolution.matched !== "split" || !resolution.remaining) return meta2;
1651
+ const prefixEnd = authored.trimEnd().length - resolution.remaining.length;
1652
+ if (prefixEnd <= 0) return meta2;
1653
+ const raw = meta2.raw.slice(0, prefixEnd).trimEnd();
1654
+ const length = raw.length;
1655
+ if (length <= 0) return meta2;
1656
+ return {
1657
+ ...meta2,
1658
+ endOffset: meta2.startOffset + length,
1659
+ length,
1660
+ columnEnd: meta2.columnStart + length,
1661
+ raw
1662
+ };
1663
+ }
1512
1664
  function applyReferenceResolution(pairs, fact, references) {
1513
1665
  const { index, defaults } = references;
1514
1666
  const result = [];
@@ -1533,7 +1685,10 @@ function applyReferenceResolution(pairs, fact, references) {
1533
1685
  diagnostics: resolution.diagnostics
1534
1686
  }
1535
1687
  ),
1536
- meta: meta2
1688
+ // The equipment is the prefix of the authored label; the effort is
1689
+ // what follows. Marking the whole phrase for both would let one mark
1690
+ // hide the other.
1691
+ meta: equipmentMeta(resolution, authored, meta2)
1537
1692
  });
1538
1693
  }
1539
1694
  };
@@ -1663,14 +1818,7 @@ function classifyPrimitive(primitive) {
1663
1818
  case "effort":
1664
1819
  return [{ metrics: new EffortMetric(primitive.raw), meta: primitive.meta }];
1665
1820
  case "property": {
1666
- const metricType = PROPERTY_KEY_TO_METRIC_TYPE[primitive.key.toLowerCase()] ?? MetricType.Custom;
1667
- return [{
1668
- metrics: new PropertyMetric(primitive.key, primitive.value, {
1669
- type: metricType,
1670
- image: primitive.raw
1671
- }),
1672
- meta: primitive.meta
1673
- }];
1821
+ return [propertyPair(primitive)];
1674
1822
  }
1675
1823
  case "metric_object": {
1676
1824
  const pairs = primitive.pairs;
@@ -1690,6 +1838,85 @@ function classifyPrimitive(primitive) {
1690
1838
  return [];
1691
1839
  }
1692
1840
  }
1841
+ function classifySlots(primitive, references) {
1842
+ if (primitive.kind === "property" && primitive.tagRefs !== void 0) {
1843
+ return propertyTagSlots(primitive, references);
1844
+ }
1845
+ if (primitive.kind === "tag_ref") {
1846
+ return fragmentTagSlots(primitive, references);
1847
+ }
1848
+ return classifyPrimitive(primitive).map((pair) => ({ kind: "explicit", pair }));
1849
+ }
1850
+ function propertyTagSlots(primitive, references) {
1851
+ const { slots, resolved } = referenceSlots(primitive.tagRefs ?? [], primitive.meta, references);
1852
+ if (resolved) return slots;
1853
+ return [{ kind: "explicit", pair: propertyPair(primitive) }];
1854
+ }
1855
+ function fragmentTagSlots(primitive, references) {
1856
+ const { slots, resolved } = referenceSlots([primitive.slug], primitive.meta, references);
1857
+ if (resolved) return slots;
1858
+ return [{
1859
+ kind: "explicit",
1860
+ pair: {
1861
+ metrics: {
1862
+ type: MetricType.Custom,
1863
+ value: primitive.raw,
1864
+ origin: "parser",
1865
+ image: primitive.raw
1866
+ },
1867
+ meta: primitive.meta
1868
+ }
1869
+ }];
1870
+ }
1871
+ function referenceSlots(slugs, meta2, references) {
1872
+ const slots = [];
1873
+ let resolved = false;
1874
+ for (const slug of slugs) {
1875
+ const contributions = resolveTagContributions(references?.tags, slug);
1876
+ const reference = tagReferenceFor(references?.tags, slug);
1877
+ const contributed = tagContributionMetrics(contributions.metrics);
1878
+ if (contributed.length > 0) resolved = true;
1879
+ for (const metric of contributed) {
1880
+ slots.push({
1881
+ kind: "contributed",
1882
+ pair: { metrics: withTagReference(metric, reference), meta: meta2 }
1883
+ });
1884
+ }
1885
+ for (const tag of contributions.tags) {
1886
+ resolved = true;
1887
+ const tagReference = tag === slug ? reference : tagReferenceFor(references?.tags, tag);
1888
+ slots.push({
1889
+ kind: "marker",
1890
+ pair: {
1891
+ metrics: withTagReference({
1892
+ type: tagMarkerType(tag),
1893
+ value: tag,
1894
+ origin: "parser",
1895
+ image: tag
1896
+ }, tagReference),
1897
+ meta: meta2
1898
+ }
1899
+ });
1900
+ }
1901
+ }
1902
+ return { slots, resolved };
1903
+ }
1904
+ function withTagReference(metric, reference) {
1905
+ return {
1906
+ ...metric,
1907
+ metadata: { ...metric.metadata ?? {}, [TAG_REFERENCE_METADATA_KEY]: reference }
1908
+ };
1909
+ }
1910
+ function propertyPair(primitive) {
1911
+ const metricType = PROPERTY_KEY_TO_METRIC_TYPE[primitive.key.toLowerCase()] ?? MetricType.Custom;
1912
+ return {
1913
+ metrics: new PropertyMetric(primitive.key, primitive.value, {
1914
+ type: metricType,
1915
+ image: primitive.raw
1916
+ }),
1917
+ meta: primitive.meta
1918
+ };
1919
+ }
1693
1920
  function mergeFragments(pairs) {
1694
1921
  if (pairs.length < 2) return pairs;
1695
1922
  const result = [];
@@ -1769,33 +1996,34 @@ function mergeFragments(pairs) {
1769
1996
  }
1770
1997
  var parser = LRParser.deserialize({
1771
1998
  version: 14,
1772
- 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+$^",
1773
- 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~",
1774
- 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",
1775
- 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",
1776
- maxTerm: 60,
1999
+ 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+$_",
2000
+ 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~",
2001
+ 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",
2002
+ 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",
2003
+ maxTerm: 62,
1777
2004
  skippedNodes: [0],
1778
- repeatNodeCount: 6,
1779
- 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!]~",
2005
+ repeatNodeCount: 7,
2006
+ 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!_~",
1780
2007
  tokenizers: [0],
1781
2008
  topRules: { "Program": [0, 1] },
1782
- tokenPrec: 424
2009
+ tokenPrec: 439
1783
2010
  });
1784
2011
 
1785
2012
  // src/grammar/parser.terms.ts
1786
- var Lap = 7;
1787
- var Fragment = 8;
1788
- var Duration = 9;
1789
- var Rounds = 13;
1790
- var RoundScheme = 15;
1791
- var Action = 16;
1792
- var Text = 17;
1793
- var Heading = 18;
1794
- var Quantity = 19;
1795
- var Slash = 22;
1796
- var Pipe = 23;
1797
- var Effort = 24;
1798
- var MetricObject = 25;
2013
+ var TagRef = 6;
2014
+ var Lap = 8;
2015
+ var Fragment = 9;
2016
+ var Duration = 10;
2017
+ var Rounds = 14;
2018
+ var RoundScheme = 16;
2019
+ var Action = 17;
2020
+ var Text = 18;
2021
+ var Heading = 19;
2022
+ var Quantity = 20;
2023
+ var Slash = 23;
2024
+ var Pipe = 24;
2025
+ var Effort = 25;
2026
+ var MetricObject = 26;
1799
2027
 
1800
2028
  // src/parser/syntax-parser.ts
1801
2029
  function createLineLocator(source) {
@@ -1901,15 +2129,28 @@ function createPropertyPrimitive(locator, source, from, to) {
1901
2129
  const match = raw.match(/^\s*([A-Za-z][A-Za-z0-9]*)\s*:\s*(.*?)\s*$/);
1902
2130
  if (!match) return null;
1903
2131
  const [, key, rawValue] = match;
2132
+ const tagRefs = parseTagRefs(rawValue);
1904
2133
  return {
1905
2134
  kind: "property",
1906
2135
  raw,
1907
2136
  meta: meta2,
1908
2137
  key,
1909
2138
  valueRaw: rawValue,
1910
- value: parsePropertyValue(rawValue)
2139
+ value: tagRefs !== void 0 ? rawValue.trim() : parsePropertyValue(rawValue),
2140
+ ...tagRefs !== void 0 ? { tagRefs } : {}
1911
2141
  };
1912
2142
  }
2143
+ function parseTagRefs(rawValue) {
2144
+ const parts = rawValue.trim().split(/\s+/).filter((part) => part.length > 0);
2145
+ if (parts.length === 0) return void 0;
2146
+ const slugs = [];
2147
+ for (const part of parts) {
2148
+ const match = part.match(/^#([a-zA-Z0-9][a-zA-Z0-9-]*)$/);
2149
+ if (!match) return void 0;
2150
+ slugs.push(match[1]);
2151
+ }
2152
+ return slugs;
2153
+ }
1913
2154
  function parsePropertyValue(rawValue) {
1914
2155
  const trimmed = rawValue.trim();
1915
2156
  if (/^"(?:[^"\\]|\\.)*"$/.test(trimmed)) {
@@ -2009,6 +2250,17 @@ function mapFragmentToPrimitive(locator, source, node) {
2009
2250
  };
2010
2251
  return primitive;
2011
2252
  }
2253
+ case TagRef: {
2254
+ const match = raw.match(/^#([a-zA-Z0-9][a-zA-Z0-9-]*)$/);
2255
+ if (!match) return null;
2256
+ const primitive = {
2257
+ kind: "tag_ref",
2258
+ raw,
2259
+ meta: meta2,
2260
+ slug: match[1]
2261
+ };
2262
+ return primitive;
2263
+ }
2012
2264
  case Slash: {
2013
2265
  const primitive = {
2014
2266
  kind: "effort",
@@ -2249,269 +2501,743 @@ var WhiteboardScript = class {
2249
2501
  }
2250
2502
  };
2251
2503
 
2252
- // src/parser/parseScript.ts
2253
- function parseScript(inputText, options = {}) {
2254
- if (!inputText || !inputText.trim()) {
2255
- return new WhiteboardScript(inputText, [], []);
2256
- }
2257
- try {
2258
- const doc = inputText.endsWith("\n") ? inputText : inputText + "\n";
2259
- const tree = parser.parse(doc);
2260
- const sport = options.sport ?? options.dialect;
2261
- const references = options.references ? { index: options.references, defaults: options.defaults } : void 0;
2262
- const statements = options.withoutDialects ? extractStatementsRaw(inputText, tree, references) : extractStatements(inputText, tree, sport, references);
2263
- return new WhiteboardScript(inputText, statements, []);
2264
- } catch (error) {
2265
- const msg = error instanceof Error ? error.message : "Unknown parse error";
2266
- return new WhiteboardScript(inputText, [], [{ message: msg }]);
2504
+ // src/effort-registry/types.ts
2505
+ var EFFORT_REGISTRY_ORIGINS = ["bundled", "user"];
2506
+ var CATALOG_PRECEDENCE_RANK = {
2507
+ core: 0,
2508
+ community: 1,
2509
+ imported: 2,
2510
+ user: 3
2511
+ };
2512
+ function resolveSourceTier(sourceIdOrMetaKey, explicitTier) {
2513
+ if (explicitTier) return explicitTier;
2514
+ if (!sourceIdOrMetaKey) return "core";
2515
+ if (sourceIdOrMetaKey === "user") return "user";
2516
+ const id = sourceIdOrMetaKey.startsWith("seed:") ? sourceIdOrMetaKey.slice(5) : sourceIdOrMetaKey;
2517
+ if (id === "core" || id === "efforts" || id === "equipment") return "core";
2518
+ if (id === "community" || id.startsWith("community-") || id === "common") return "community";
2519
+ return "imported";
2520
+ }
2521
+ function resolveSourceRank(sourceIdOrMetaKey, explicitTier) {
2522
+ return CATALOG_PRECEDENCE_RANK[resolveSourceTier(sourceIdOrMetaKey, explicitTier)];
2523
+ }
2524
+ function resolveEntityTier(entity) {
2525
+ if (entity.registrySource === "user") return "user";
2526
+ return resolveSourceTier(entity.sourceId);
2527
+ }
2528
+ function resolveEntityRank(entity) {
2529
+ return CATALOG_PRECEDENCE_RANK[resolveEntityTier(entity)];
2530
+ }
2531
+ function dedupeEntitiesByPrecedence(entities) {
2532
+ const bySlug = /* @__PURE__ */ new Map();
2533
+ for (const entity of entities) {
2534
+ const key = entity.slug.toLowerCase();
2535
+ const prior = bySlug.get(key);
2536
+ if (!prior || resolveEntityRank(entity) >= resolveEntityRank(prior)) {
2537
+ bySlug.set(key, entity);
2538
+ }
2267
2539
  }
2540
+ return Array.from(bySlug.values());
2268
2541
  }
2542
+ var INTENSITY_TIERS = ["low", "moderate", "high"];
2543
+ var DEFAULT_RESOLVER_OPTIONS = {
2544
+ defaultMet: 5,
2545
+ defaultThreshold: 2
2546
+ };
2269
2547
 
2270
- // src/parser/md-timer.ts
2271
- var MdTimerRuntime = class {
2272
- config;
2273
- constructor(config) {
2274
- this.config = config;
2548
+ // src/effort-registry/fuzzyMatch.ts
2549
+ function levenshteinDistance(a, b) {
2550
+ if (a === b) return 0;
2551
+ if (a.length === 0) return b.length;
2552
+ if (b.length === 0) return a.length;
2553
+ if (a.length > b.length) {
2554
+ const tmp = a;
2555
+ a = b;
2556
+ b = tmp;
2275
2557
  }
2276
- /**
2277
- * Parse WhiteboardScript source into a {@link WhiteboardScript}, running the
2278
- * Dialect Stack on every statement.
2279
- *
2280
- * @param sport - The block's `:sport` fence suffix (` ```log:climbing `).
2281
- */
2282
- read(inputText, sport) {
2283
- return parseScript(inputText, { sport, ...this.config });
2558
+ const m = a.length;
2559
+ const n = b.length;
2560
+ let prev = new Uint32Array(m + 1);
2561
+ let curr = new Uint32Array(m + 1);
2562
+ for (let i = 0; i <= m; i++) {
2563
+ prev[i] = i;
2284
2564
  }
2285
- /**
2286
- * Parse without running the Dialect Stack. Used by the parser test harness
2287
- * which applies its own Dialect set. Production consumers should use {@link read}.
2288
- */
2289
- readWithoutDialects(inputText) {
2290
- return parseScript(inputText, { withoutDialects: true, ...this.config });
2565
+ for (let j = 1; j <= n; j++) {
2566
+ curr[0] = j;
2567
+ const bj = b.charCodeAt(j - 1);
2568
+ for (let i = 1; i <= m; i++) {
2569
+ const cost = a.charCodeAt(i - 1) === bj ? 0 : 1;
2570
+ curr[i] = Math.min(
2571
+ prev[i] + 1,
2572
+ // deletion
2573
+ curr[i - 1] + 1,
2574
+ // insertion
2575
+ prev[i - 1] + cost
2576
+ // substitution
2577
+ );
2578
+ }
2579
+ const swap = prev;
2580
+ prev = curr;
2581
+ curr = swap;
2291
2582
  }
2292
- };
2293
-
2294
- // src/parser/parserInstance.ts
2295
- function createParser(config) {
2296
- return new MdTimerRuntime(config);
2583
+ return prev[m];
2297
2584
  }
2298
- var nextTagID = 0;
2299
- var Tag = class _Tag {
2300
- /**
2301
- @internal
2302
- */
2303
- constructor(name2, set, base, modified) {
2304
- this.name = name2;
2305
- this.set = set;
2306
- this.base = base;
2307
- this.modified = modified;
2308
- this.id = nextTagID++;
2309
- }
2310
- toString() {
2311
- let { name: name2 } = this;
2312
- for (let mod of this.modified)
2313
- if (mod.name)
2314
- name2 = `${mod.name}(${name2})`;
2315
- return name2;
2585
+ function normalizeForFuzzy(input) {
2586
+ return input.toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, " ").replace(/[-_]+/g, " ").trim().replace(/\s+/g, " ");
2587
+ }
2588
+ function isWithinThreshold(a, b, threshold) {
2589
+ const na = normalizeForFuzzy(a);
2590
+ const nb = normalizeForFuzzy(b);
2591
+ if (na === nb) return true;
2592
+ if (Math.abs(na.length - nb.length) > threshold) return false;
2593
+ return levenshteinDistance(na, nb) <= threshold;
2594
+ }
2595
+ function findBestFuzzyMatch(query, candidates, threshold) {
2596
+ const nq = normalizeForFuzzy(query);
2597
+ let best = null;
2598
+ for (const candidate of candidates) {
2599
+ const nc = normalizeForFuzzy(candidate);
2600
+ if (Math.abs(nq.length - nc.length) > threshold) continue;
2601
+ const distance = levenshteinDistance(nq, nc);
2602
+ if (distance > threshold) continue;
2603
+ if (!best || distance < best.distance) {
2604
+ best = { target: candidate, distance };
2605
+ if (distance === 0) break;
2606
+ }
2316
2607
  }
2317
- static define(nameOrParent, parent) {
2318
- let name2 = typeof nameOrParent == "string" ? nameOrParent : "?";
2319
- if (nameOrParent instanceof _Tag)
2320
- parent = nameOrParent;
2321
- if (parent === null || parent === void 0 ? void 0 : parent.base)
2322
- throw new Error("Can not derive from a modified tag");
2323
- let tag = new _Tag(name2, [], null, []);
2324
- tag.set.push(tag);
2325
- if (parent)
2326
- for (let t2 of parent.set)
2327
- tag.set.push(t2);
2328
- return tag;
2608
+ return best;
2609
+ }
2610
+
2611
+ // src/effort-registry/reference-resolution.ts
2612
+ function buildExactIndex(entities) {
2613
+ const deduped = dedupeEntitiesByPrecedence(entities);
2614
+ const byKey = /* @__PURE__ */ new Map();
2615
+ const add = (key, entity) => {
2616
+ if (!key) return;
2617
+ const existing = byKey.get(key);
2618
+ if (existing) {
2619
+ if (!existing.includes(entity)) existing.push(entity);
2620
+ } else {
2621
+ byKey.set(key, [entity]);
2622
+ }
2623
+ };
2624
+ for (const entity of deduped) {
2625
+ add(entity.slug.toLowerCase(), entity);
2626
+ add(normalizeForFuzzy(entity.label), entity);
2627
+ for (const alias of entity.aliases) add(normalizeForFuzzy(alias), entity);
2329
2628
  }
2330
- /**
2331
- Define a tag _modifier_, which is a function that, given a tag,
2332
- will return a tag that is a subtag of the original. Applying the
2333
- same modifier to a twice tag will return the same value (`m1(t1)
2334
- == m1(t1)`) and applying multiple modifiers will, regardless or
2335
- order, produce the same tag (`m1(m2(t1)) == m2(m1(t1))`).
2336
-
2337
- When multiple modifiers are applied to a given base tag, each
2338
- smaller set of modifiers is registered as a parent, so that for
2339
- example `m1(m2(m3(t1)))` is a subtype of `m1(m2(t1))`,
2340
- `m1(m3(t1)`, and so on.
2341
- */
2342
- static defineModifier(name2) {
2343
- let mod = new Modifier(name2);
2344
- return (tag) => {
2345
- if (tag.modified.indexOf(mod) > -1)
2346
- return tag;
2347
- return Modifier.get(tag.base || tag, tag.modified.concat(mod).sort((a, b) => a.id - b.id));
2348
- };
2349
- }
2350
- };
2351
- var nextModifierID = 0;
2352
- var Modifier = class _Modifier {
2353
- constructor(name2) {
2354
- this.name = name2;
2355
- this.instances = [];
2356
- this.id = nextModifierID++;
2357
- }
2358
- static get(base, mods) {
2359
- if (!mods.length)
2360
- return base;
2361
- let exists = mods[0].instances.find((t2) => t2.base == base && sameArray(mods, t2.modified));
2362
- if (exists)
2363
- return exists;
2364
- let set = [], tag = new Tag(base.name, set, base, mods);
2365
- for (let m of mods)
2366
- m.instances.push(tag);
2367
- let configs = powerSet(mods);
2368
- for (let parent of base.set)
2369
- if (!parent.modified.length)
2370
- for (let config of configs)
2371
- set.push(_Modifier.get(parent, config));
2372
- return tag;
2373
- }
2374
- };
2375
- function sameArray(a, b) {
2376
- return a.length == b.length && a.every((x, i) => x == b[i]);
2377
- }
2378
- function powerSet(array) {
2379
- let sets = [[]];
2380
- for (let i = 0; i < array.length; i++) {
2381
- for (let j = 0, e = sets.length; j < e; j++) {
2382
- sets.push(sets[j].concat(array[i]));
2629
+ return {
2630
+ byKey,
2631
+ lookup(key) {
2632
+ const direct = byKey.get(key) ?? byKey.get(normalizeForFuzzy(key));
2633
+ if (!direct || direct.length === 0) return { entity: null, ambiguous: [] };
2634
+ if (direct.length === 1) return { entity: direct[0], ambiguous: [] };
2635
+ let maxRank = -1;
2636
+ for (const candidate of direct) {
2637
+ const rank = resolveEntityRank(candidate);
2638
+ if (rank > maxRank) maxRank = rank;
2639
+ }
2640
+ const topTier = direct.filter((candidate) => resolveEntityRank(candidate) === maxRank);
2641
+ if (topTier.length === 1) return { entity: topTier[0], ambiguous: [] };
2642
+ return { entity: null, ambiguous: topTier };
2383
2643
  }
2644
+ };
2645
+ }
2646
+ function equipmentRef(entity) {
2647
+ return { kind: "equipment", slug: entity.slug, label: entity.label };
2648
+ }
2649
+ function dedupeRefs(refs) {
2650
+ const seen = /* @__PURE__ */ new Set();
2651
+ const result = [];
2652
+ for (const ref of refs) {
2653
+ const key = `${ref.kind}:${ref.slug}`;
2654
+ if (seen.has(key)) continue;
2655
+ seen.add(key);
2656
+ result.push(ref);
2384
2657
  }
2385
- return sets.sort((a, b) => b.length - a.length);
2658
+ return result;
2386
2659
  }
2387
- function styleTags(spec) {
2388
- let byName = /* @__PURE__ */ Object.create(null);
2389
- for (let prop in spec) {
2390
- let tags2 = spec[prop];
2391
- if (!Array.isArray(tags2))
2392
- tags2 = [tags2];
2393
- for (let part of prop.split(" "))
2394
- if (part) {
2395
- let pieces = [], mode = 2, rest = part;
2396
- for (let pos = 0; ; ) {
2397
- if (rest == "..." && pos > 0 && pos + 3 == part.length) {
2398
- mode = 1;
2399
- break;
2400
- }
2401
- let m = /^"(?:[^"\\]|\\.)*?"|[^\/!]+/.exec(rest);
2402
- if (!m)
2403
- throw new RangeError("Invalid path: " + part);
2404
- pieces.push(m[0] == "*" ? "" : m[0][0] == '"' ? JSON.parse(m[0]) : m[0]);
2405
- pos += m[0].length;
2406
- if (pos == part.length)
2407
- break;
2408
- let next = part[pos++];
2409
- if (pos == part.length && next == "!") {
2410
- mode = 0;
2411
- break;
2412
- }
2413
- if (next != "/")
2414
- throw new RangeError("Invalid path: " + part);
2415
- rest = part.slice(pos);
2660
+ function createReferenceIndex(catalog, materialize) {
2661
+ const dedupedEquipment = dedupeEntitiesByPrecedence(catalog.equipment ?? []);
2662
+ const efforts = buildExactIndex(catalog.efforts ?? []);
2663
+ const equipment = buildExactIndex(dedupedEquipment);
2664
+ 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);
2665
+ const materializer = materialize ?? ((effort) => ({
2666
+ effort,
2667
+ definition: effort,
2668
+ slug: effort.slug,
2669
+ label: effort.label,
2670
+ met: effort.baseAttributes.met,
2671
+ baseAttributes: effort.baseAttributes,
2672
+ discipline: effort.baseAttributes.discipline,
2673
+ disciplineFactor: effort.baseAttributes.disciplineFactor ?? 1,
2674
+ intensityTier: effort.baseAttributes.intensityTier,
2675
+ modifiers: {},
2676
+ registrySource: effort.registrySource,
2677
+ resolvedFrom: effort.registrySource === "user" ? "user" : effort.registrySource === "bundled" ? "bundled" : "default",
2678
+ isEstimated: false
2679
+ }));
2680
+ const resolveEffortExact = (key) => {
2681
+ const found = efforts.lookup(key.trim());
2682
+ if (found.entity) return { entity: found.entity };
2683
+ if (found.ambiguous.length > 0) {
2684
+ return {
2685
+ entity: null,
2686
+ diagnostic: {
2687
+ reason: "ambiguous-effort",
2688
+ message: `"${key}" matches multiple efforts.`,
2689
+ token: key,
2690
+ candidates: found.ambiguous.map((e) => e.slug)
2416
2691
  }
2417
- let last = pieces.length - 1, inner = pieces[last];
2418
- if (!inner)
2419
- throw new RangeError("Invalid path: " + part);
2420
- let rule = new Rule(tags2, mode, last > 0 ? pieces.slice(0, last) : null);
2421
- byName[inner] = rule.sort(byName[inner]);
2692
+ };
2693
+ }
2694
+ return {
2695
+ entity: null,
2696
+ diagnostic: { reason: "unknown-effort", message: `Unknown effort "${key}".`, token: key }
2697
+ };
2698
+ };
2699
+ const resolveEquipmentValues = (values) => {
2700
+ const refs = [];
2701
+ const diagnostics = [];
2702
+ for (const value of values) {
2703
+ if (typeof value !== "string" || !value.trim()) {
2704
+ diagnostics.push({
2705
+ reason: "invalid-explicit",
2706
+ message: `Equipment reference must be a slug string, got ${JSON.stringify(value) ?? "null"}.`,
2707
+ token: String(value)
2708
+ });
2709
+ continue;
2422
2710
  }
2423
- }
2424
- return ruleNodeProp.add(byName);
2425
- }
2426
- var ruleNodeProp = new NodeProp({
2427
- combine(a, b) {
2428
- let cur, root, take;
2429
- while (a || b) {
2430
- if (!a || b && a.depth >= b.depth) {
2431
- take = b;
2432
- b = b.next;
2711
+ const found = equipment.lookup(value.trim());
2712
+ if (found.entity) {
2713
+ refs.push(equipmentRef(found.entity));
2714
+ } else if (found.ambiguous.length > 0) {
2715
+ diagnostics.push({
2716
+ reason: "ambiguous-equipment",
2717
+ message: `"${value}" matches multiple equipment entries.`,
2718
+ token: value,
2719
+ candidates: found.ambiguous.map((e) => e.slug)
2720
+ });
2433
2721
  } else {
2434
- take = a;
2435
- a = a.next;
2722
+ diagnostics.push({
2723
+ reason: "invalid-explicit",
2724
+ message: `Unknown equipment "${value}".`,
2725
+ token: value
2726
+ });
2436
2727
  }
2437
- if (cur && cur.mode == take.mode && !take.context && !cur.context)
2438
- continue;
2439
- let copy = new Rule(take.tags, take.mode, take.context);
2440
- if (cur)
2441
- cur.next = copy;
2442
- else
2443
- root = copy;
2444
- cur = copy;
2445
- }
2446
- return root;
2447
- }
2448
- });
2449
- var Rule = class {
2450
- constructor(tags2, mode, context, next) {
2451
- this.tags = tags2;
2452
- this.mode = mode;
2453
- this.context = context;
2454
- this.next = next;
2455
- }
2456
- get opaque() {
2457
- return this.mode == 0;
2458
- }
2459
- get inherit() {
2460
- return this.mode == 1;
2461
- }
2462
- sort(other) {
2463
- if (!other || other.depth < this.depth) {
2464
- this.next = other;
2465
- return this;
2466
2728
  }
2467
- other.next = this.sort(other.next);
2468
- return other;
2469
- }
2470
- get depth() {
2471
- return this.context ? this.context.length : 0;
2472
- }
2473
- };
2474
- Rule.empty = new Rule([], 2, null);
2475
- function tagHighlighter(tags2, options) {
2476
- let map = /* @__PURE__ */ Object.create(null);
2477
- for (let style of tags2) {
2478
- if (!Array.isArray(style.tag))
2479
- map[style.tag.id] = style.class;
2480
- else
2481
- for (let tag of style.tag)
2482
- map[tag.id] = style.class;
2483
- }
2484
- let { scope, all = null } = {};
2485
- return {
2486
- style: (tags3) => {
2487
- let cls = all;
2488
- for (let tag of tags3) {
2489
- for (let sub of tag.set) {
2490
- let tagClass = map[sub.id];
2491
- if (tagClass) {
2492
- cls = cls ? cls + " " + tagClass : tagClass;
2493
- break;
2494
- }
2495
- }
2496
- }
2497
- return cls;
2498
- },
2499
- scope
2729
+ return { refs: dedupeRefs(refs), diagnostics };
2500
2730
  };
2501
- }
2502
- var t = Tag.define;
2503
- var comment = t();
2504
- var name = t();
2505
- var typeName = t(name);
2506
- var propertyName = t(name);
2507
- var literal = t();
2508
- var string = t(literal);
2509
- var number = t(literal);
2510
- var content = t();
2511
- var heading = t(content);
2512
- var keyword = t();
2513
- var operator = t();
2514
- var punctuation = t();
2731
+ const index = {
2732
+ resolveEffort(slugOrAlias) {
2733
+ return efforts.lookup(slugOrAlias).entity;
2734
+ },
2735
+ resolveEquipment(slugOrAlias) {
2736
+ return equipment.lookup(slugOrAlias).entity;
2737
+ },
2738
+ materialize(effort) {
2739
+ return materializer(effort);
2740
+ },
2741
+ resolve(label, defaults) {
2742
+ const source = (label ?? "").trim();
2743
+ const diagnostics = [];
2744
+ const push = (diagnostic) => {
2745
+ if (diagnostic) diagnostics.push(diagnostic);
2746
+ };
2747
+ let matched = "none";
2748
+ let effortEntity = null;
2749
+ let remaining = "";
2750
+ let equipmentRefs = [];
2751
+ let equipmentAuthored = false;
2752
+ const fillEquipment = () => {
2753
+ if (equipmentRefs.length > 0 || equipmentAuthored) return;
2754
+ if (effortEntity === null) return;
2755
+ if (defaults?.equipment?.length) {
2756
+ const resolvedDefaults = resolveEquipmentValues(defaults.equipment);
2757
+ equipmentRefs = resolvedDefaults.refs;
2758
+ for (const diagnostic of resolvedDefaults.diagnostics) {
2759
+ push({ ...diagnostic, reason: "unknown-equipment" });
2760
+ }
2761
+ if (equipmentRefs.length > 0) return;
2762
+ }
2763
+ if (effortEntity.equipment?.length) {
2764
+ const resolvedDefEquipment = resolveEquipmentValues(effortEntity.equipment);
2765
+ equipmentRefs = resolvedDefEquipment.refs;
2766
+ for (const diagnostic of resolvedDefEquipment.diagnostics) {
2767
+ push({ ...diagnostic, reason: "unknown-equipment" });
2768
+ }
2769
+ }
2770
+ };
2771
+ if (!source) {
2772
+ if (defaults?.effort) {
2773
+ const found = resolveEffortExact(defaults.effort);
2774
+ if (found.entity) {
2775
+ effortEntity = found.entity;
2776
+ matched = "default";
2777
+ } else {
2778
+ push({
2779
+ reason: found.diagnostic?.reason === "ambiguous-effort" ? "ambiguous-effort" : "unknown-effort",
2780
+ message: `Default effort "${defaults.effort}" is not in the catalog.`,
2781
+ token: defaults.effort,
2782
+ candidates: found.diagnostic?.candidates
2783
+ });
2784
+ }
2785
+ }
2786
+ fillEquipment();
2787
+ return {
2788
+ matched,
2789
+ label: source,
2790
+ effort: effortEntity ? materializer(effortEntity) : null,
2791
+ effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
2792
+ equipment: equipmentRefs,
2793
+ remaining: "",
2794
+ diagnostics
2795
+ };
2796
+ }
2797
+ if (source.startsWith("{")) {
2798
+ let parsed;
2799
+ try {
2800
+ parsed = JSON.parse(source);
2801
+ } catch {
2802
+ return {
2803
+ matched: "none",
2804
+ label: source,
2805
+ effort: null,
2806
+ effortRef: null,
2807
+ equipment: [],
2808
+ remaining: "",
2809
+ diagnostics: [{
2810
+ reason: "malformed-json",
2811
+ message: "Reference object is not valid JSON.",
2812
+ token: source
2813
+ }]
2814
+ };
2815
+ }
2816
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && ("effort" in parsed || "equipment" in parsed)) {
2817
+ matched = "explicit";
2818
+ const obj = parsed;
2819
+ if (obj.effort !== void 0) {
2820
+ if (typeof obj.effort === "string" && obj.effort.trim()) {
2821
+ const found = resolveEffortExact(obj.effort);
2822
+ effortEntity = found.entity;
2823
+ if (found.diagnostic) {
2824
+ push(found.diagnostic.reason === "ambiguous-effort" ? found.diagnostic : { ...found.diagnostic, reason: "invalid-explicit", message: `Explicit effort "${obj.effort}" is not in the catalog.` });
2825
+ }
2826
+ } else {
2827
+ push({
2828
+ reason: "invalid-explicit",
2829
+ message: 'Explicit "effort" must be a slug string.',
2830
+ token: String(obj.effort)
2831
+ });
2832
+ }
2833
+ }
2834
+ if (obj.equipment !== void 0) {
2835
+ equipmentAuthored = true;
2836
+ const values = Array.isArray(obj.equipment) ? obj.equipment : [obj.equipment];
2837
+ const resolvedEquipment = resolveEquipmentValues(values);
2838
+ equipmentRefs = resolvedEquipment.refs;
2839
+ diagnostics.push(...resolvedEquipment.diagnostics);
2840
+ if (effortEntity?.equipment?.length) {
2841
+ const definitionSlugs = new Set(effortEntity.equipment);
2842
+ const authoredSlugs = equipmentRefs.map((ref) => ref.slug);
2843
+ if (authoredSlugs.length > 0 && authoredSlugs.some((slug) => !definitionSlugs.has(slug))) {
2844
+ push({
2845
+ reason: "conflict",
2846
+ message: `Explicit equipment [${authoredSlugs.join(", ")}] contradicts effort "${effortEntity.slug}" definition equipment [${effortEntity.equipment.join(", ")}].`,
2847
+ token: source
2848
+ });
2849
+ }
2850
+ }
2851
+ }
2852
+ fillEquipment();
2853
+ return {
2854
+ matched,
2855
+ label: source,
2856
+ effort: effortEntity ? materializer(effortEntity) : null,
2857
+ effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
2858
+ equipment: equipmentRefs,
2859
+ remaining: "",
2860
+ diagnostics
2861
+ };
2862
+ }
2863
+ }
2864
+ const lowerSource = source.toLowerCase();
2865
+ for (const candidate of prefixCandidates) {
2866
+ if (!lowerSource.startsWith(candidate.lower)) continue;
2867
+ const after = source.slice(candidate.lower.length);
2868
+ if (after && !/^\s/.test(after)) continue;
2869
+ const candidateRemaining = after.trim();
2870
+ if (!candidateRemaining) continue;
2871
+ const equipmentFound = equipment.lookup(candidate.lower);
2872
+ if (equipmentFound.ambiguous.length > 0) {
2873
+ push({
2874
+ reason: "ambiguous-equipment",
2875
+ message: `"${candidate.lower}" matches multiple equipment entries.`,
2876
+ token: source,
2877
+ candidates: equipmentFound.ambiguous.map((e) => e.slug)
2878
+ });
2879
+ return {
2880
+ matched: "none",
2881
+ label: source,
2882
+ effort: null,
2883
+ effortRef: null,
2884
+ equipment: [],
2885
+ remaining: "",
2886
+ diagnostics
2887
+ };
2888
+ }
2889
+ if (!equipmentFound.entity) continue;
2890
+ const effortFound = resolveEffortExact(candidateRemaining);
2891
+ if (effortFound.entity) {
2892
+ matched = "split";
2893
+ effortEntity = effortFound.entity;
2894
+ equipmentRefs = [equipmentRef(equipmentFound.entity)];
2895
+ equipmentAuthored = true;
2896
+ remaining = candidateRemaining;
2897
+ break;
2898
+ }
2899
+ if (effortFound.diagnostic?.reason === "ambiguous-effort") {
2900
+ push(effortFound.diagnostic);
2901
+ return {
2902
+ matched: "none",
2903
+ label: source,
2904
+ effort: null,
2905
+ effortRef: null,
2906
+ equipment: [],
2907
+ remaining: "",
2908
+ diagnostics
2909
+ };
2910
+ }
2911
+ }
2912
+ if (matched === "none") {
2913
+ const found = resolveEffortExact(source);
2914
+ if (found.entity) {
2915
+ effortEntity = found.entity;
2916
+ matched = "whole";
2917
+ } else if (found.diagnostic?.reason === "ambiguous-effort") {
2918
+ push(found.diagnostic);
2919
+ return {
2920
+ matched: "none",
2921
+ label: source,
2922
+ effort: null,
2923
+ effortRef: null,
2924
+ equipment: [],
2925
+ remaining: "",
2926
+ diagnostics
2927
+ };
2928
+ } else if (found.diagnostic) {
2929
+ push(found.diagnostic);
2930
+ }
2931
+ }
2932
+ fillEquipment();
2933
+ return {
2934
+ matched,
2935
+ label: source,
2936
+ effort: effortEntity ? materializer(effortEntity) : null,
2937
+ effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
2938
+ equipment: equipmentRefs,
2939
+ remaining,
2940
+ diagnostics
2941
+ };
2942
+ }
2943
+ };
2944
+ return index;
2945
+ }
2946
+ function extractStatementRefs(statements) {
2947
+ const efforts = [];
2948
+ const equipment = [];
2949
+ const seenEffort = /* @__PURE__ */ new Set();
2950
+ const seenEquipment = /* @__PURE__ */ new Set();
2951
+ const pushUnique = (list, seen, value) => {
2952
+ if (!value) return;
2953
+ if (seen.has(value)) return;
2954
+ seen.add(value);
2955
+ list.push(value);
2956
+ };
2957
+ for (const statement of statements ?? []) {
2958
+ const metrics = Array.isArray(statement.metrics) ? statement.metrics : statement.metrics?.toArray?.() ?? [];
2959
+ for (const metric of metrics) {
2960
+ const refs = getReferenceMetadata(metric.metadata)?.refs;
2961
+ if (!refs) continue;
2962
+ for (const ref of refs) {
2963
+ if (ref.kind === "effort") pushUnique(efforts, seenEffort, ref.slug);
2964
+ if (ref.kind === "equipment") pushUnique(equipment, seenEquipment, ref.slug);
2965
+ }
2966
+ }
2967
+ }
2968
+ return { efforts, equipment };
2969
+ }
2970
+ function slugifyRefLabel(label) {
2971
+ return normalizeForFuzzy(label).replace(/\s+/g, "-");
2972
+ }
2973
+
2974
+ // src/parser/parseScript.ts
2975
+ function parseScript(inputText, options = {}) {
2976
+ if (!inputText || !inputText.trim()) {
2977
+ return new WhiteboardScript(inputText, [], []);
2978
+ }
2979
+ try {
2980
+ const doc = inputText.endsWith("\n") ? inputText : inputText + "\n";
2981
+ const tree = parser.parse(doc);
2982
+ const sport = options.sport ?? options.dialect;
2983
+ const references = options.references || options.tags ? {
2984
+ index: options.references ?? createReferenceIndex({ efforts: [], equipment: [] }),
2985
+ defaults: options.defaults,
2986
+ tags: options.tags
2987
+ } : void 0;
2988
+ const statements = options.withoutDialects ? extractStatementsRaw(inputText, tree, references) : extractStatements(inputText, tree, sport, references);
2989
+ return new WhiteboardScript(inputText, statements, []);
2990
+ } catch (error) {
2991
+ const msg = error instanceof Error ? error.message : "Unknown parse error";
2992
+ return new WhiteboardScript(inputText, [], [{ message: msg }]);
2993
+ }
2994
+ }
2995
+
2996
+ // src/parser/md-timer.ts
2997
+ var MdTimerRuntime = class {
2998
+ config;
2999
+ constructor(config) {
3000
+ this.config = config;
3001
+ }
3002
+ /**
3003
+ * Parse WhiteboardScript source into a {@link WhiteboardScript}, running the
3004
+ * Dialect Stack on every statement.
3005
+ *
3006
+ * @param sport - The block's `:sport` fence suffix (` ```log:climbing `).
3007
+ */
3008
+ read(inputText, sport) {
3009
+ return parseScript(inputText, { sport, ...this.config });
3010
+ }
3011
+ /**
3012
+ * Parse without running the Dialect Stack. Used by the parser test harness
3013
+ * which applies its own Dialect set. Production consumers should use {@link read}.
3014
+ */
3015
+ readWithoutDialects(inputText) {
3016
+ return parseScript(inputText, { withoutDialects: true, ...this.config });
3017
+ }
3018
+ };
3019
+
3020
+ // src/parser/parserInstance.ts
3021
+ function createParser(config) {
3022
+ return new MdTimerRuntime(config);
3023
+ }
3024
+ var nextTagID = 0;
3025
+ var Tag = class _Tag {
3026
+ /**
3027
+ @internal
3028
+ */
3029
+ constructor(name2, set, base, modified) {
3030
+ this.name = name2;
3031
+ this.set = set;
3032
+ this.base = base;
3033
+ this.modified = modified;
3034
+ this.id = nextTagID++;
3035
+ }
3036
+ toString() {
3037
+ let { name: name2 } = this;
3038
+ for (let mod of this.modified)
3039
+ if (mod.name)
3040
+ name2 = `${mod.name}(${name2})`;
3041
+ return name2;
3042
+ }
3043
+ static define(nameOrParent, parent) {
3044
+ let name2 = typeof nameOrParent == "string" ? nameOrParent : "?";
3045
+ if (nameOrParent instanceof _Tag)
3046
+ parent = nameOrParent;
3047
+ if (parent === null || parent === void 0 ? void 0 : parent.base)
3048
+ throw new Error("Can not derive from a modified tag");
3049
+ let tag = new _Tag(name2, [], null, []);
3050
+ tag.set.push(tag);
3051
+ if (parent)
3052
+ for (let t2 of parent.set)
3053
+ tag.set.push(t2);
3054
+ return tag;
3055
+ }
3056
+ /**
3057
+ Define a tag _modifier_, which is a function that, given a tag,
3058
+ will return a tag that is a subtag of the original. Applying the
3059
+ same modifier to a twice tag will return the same value (`m1(t1)
3060
+ == m1(t1)`) and applying multiple modifiers will, regardless or
3061
+ order, produce the same tag (`m1(m2(t1)) == m2(m1(t1))`).
3062
+
3063
+ When multiple modifiers are applied to a given base tag, each
3064
+ smaller set of modifiers is registered as a parent, so that for
3065
+ example `m1(m2(m3(t1)))` is a subtype of `m1(m2(t1))`,
3066
+ `m1(m3(t1)`, and so on.
3067
+ */
3068
+ static defineModifier(name2) {
3069
+ let mod = new Modifier(name2);
3070
+ return (tag) => {
3071
+ if (tag.modified.indexOf(mod) > -1)
3072
+ return tag;
3073
+ return Modifier.get(tag.base || tag, tag.modified.concat(mod).sort((a, b) => a.id - b.id));
3074
+ };
3075
+ }
3076
+ };
3077
+ var nextModifierID = 0;
3078
+ var Modifier = class _Modifier {
3079
+ constructor(name2) {
3080
+ this.name = name2;
3081
+ this.instances = [];
3082
+ this.id = nextModifierID++;
3083
+ }
3084
+ static get(base, mods) {
3085
+ if (!mods.length)
3086
+ return base;
3087
+ let exists = mods[0].instances.find((t2) => t2.base == base && sameArray(mods, t2.modified));
3088
+ if (exists)
3089
+ return exists;
3090
+ let set = [], tag = new Tag(base.name, set, base, mods);
3091
+ for (let m of mods)
3092
+ m.instances.push(tag);
3093
+ let configs = powerSet(mods);
3094
+ for (let parent of base.set)
3095
+ if (!parent.modified.length)
3096
+ for (let config of configs)
3097
+ set.push(_Modifier.get(parent, config));
3098
+ return tag;
3099
+ }
3100
+ };
3101
+ function sameArray(a, b) {
3102
+ return a.length == b.length && a.every((x, i) => x == b[i]);
3103
+ }
3104
+ function powerSet(array) {
3105
+ let sets = [[]];
3106
+ for (let i = 0; i < array.length; i++) {
3107
+ for (let j = 0, e = sets.length; j < e; j++) {
3108
+ sets.push(sets[j].concat(array[i]));
3109
+ }
3110
+ }
3111
+ return sets.sort((a, b) => b.length - a.length);
3112
+ }
3113
+ function styleTags(spec) {
3114
+ let byName = /* @__PURE__ */ Object.create(null);
3115
+ for (let prop in spec) {
3116
+ let tags2 = spec[prop];
3117
+ if (!Array.isArray(tags2))
3118
+ tags2 = [tags2];
3119
+ for (let part of prop.split(" "))
3120
+ if (part) {
3121
+ let pieces = [], mode = 2, rest = part;
3122
+ for (let pos = 0; ; ) {
3123
+ if (rest == "..." && pos > 0 && pos + 3 == part.length) {
3124
+ mode = 1;
3125
+ break;
3126
+ }
3127
+ let m = /^"(?:[^"\\]|\\.)*?"|[^\/!]+/.exec(rest);
3128
+ if (!m)
3129
+ throw new RangeError("Invalid path: " + part);
3130
+ pieces.push(m[0] == "*" ? "" : m[0][0] == '"' ? JSON.parse(m[0]) : m[0]);
3131
+ pos += m[0].length;
3132
+ if (pos == part.length)
3133
+ break;
3134
+ let next = part[pos++];
3135
+ if (pos == part.length && next == "!") {
3136
+ mode = 0;
3137
+ break;
3138
+ }
3139
+ if (next != "/")
3140
+ throw new RangeError("Invalid path: " + part);
3141
+ rest = part.slice(pos);
3142
+ }
3143
+ let last = pieces.length - 1, inner = pieces[last];
3144
+ if (!inner)
3145
+ throw new RangeError("Invalid path: " + part);
3146
+ let rule = new Rule(tags2, mode, last > 0 ? pieces.slice(0, last) : null);
3147
+ byName[inner] = rule.sort(byName[inner]);
3148
+ }
3149
+ }
3150
+ return ruleNodeProp.add(byName);
3151
+ }
3152
+ var ruleNodeProp = new NodeProp({
3153
+ combine(a, b) {
3154
+ let cur, root, take;
3155
+ while (a || b) {
3156
+ if (!a || b && a.depth >= b.depth) {
3157
+ take = b;
3158
+ b = b.next;
3159
+ } else {
3160
+ take = a;
3161
+ a = a.next;
3162
+ }
3163
+ if (cur && cur.mode == take.mode && !take.context && !cur.context)
3164
+ continue;
3165
+ let copy = new Rule(take.tags, take.mode, take.context);
3166
+ if (cur)
3167
+ cur.next = copy;
3168
+ else
3169
+ root = copy;
3170
+ cur = copy;
3171
+ }
3172
+ return root;
3173
+ }
3174
+ });
3175
+ var Rule = class {
3176
+ constructor(tags2, mode, context, next) {
3177
+ this.tags = tags2;
3178
+ this.mode = mode;
3179
+ this.context = context;
3180
+ this.next = next;
3181
+ }
3182
+ get opaque() {
3183
+ return this.mode == 0;
3184
+ }
3185
+ get inherit() {
3186
+ return this.mode == 1;
3187
+ }
3188
+ sort(other) {
3189
+ if (!other || other.depth < this.depth) {
3190
+ this.next = other;
3191
+ return this;
3192
+ }
3193
+ other.next = this.sort(other.next);
3194
+ return other;
3195
+ }
3196
+ get depth() {
3197
+ return this.context ? this.context.length : 0;
3198
+ }
3199
+ };
3200
+ Rule.empty = new Rule([], 2, null);
3201
+ function tagHighlighter(tags2, options) {
3202
+ let map = /* @__PURE__ */ Object.create(null);
3203
+ for (let style of tags2) {
3204
+ if (!Array.isArray(style.tag))
3205
+ map[style.tag.id] = style.class;
3206
+ else
3207
+ for (let tag of style.tag)
3208
+ map[tag.id] = style.class;
3209
+ }
3210
+ let { scope, all = null } = {};
3211
+ return {
3212
+ style: (tags3) => {
3213
+ let cls = all;
3214
+ for (let tag of tags3) {
3215
+ for (let sub of tag.set) {
3216
+ let tagClass = map[sub.id];
3217
+ if (tagClass) {
3218
+ cls = cls ? cls + " " + tagClass : tagClass;
3219
+ break;
3220
+ }
3221
+ }
3222
+ }
3223
+ return cls;
3224
+ },
3225
+ scope
3226
+ };
3227
+ }
3228
+ var t = Tag.define;
3229
+ var comment = t();
3230
+ var name = t();
3231
+ var typeName = t(name);
3232
+ var propertyName = t(name);
3233
+ var literal = t();
3234
+ var string = t(literal);
3235
+ var number = t(literal);
3236
+ var content = t();
3237
+ var heading = t(content);
3238
+ var keyword = t();
3239
+ var operator = t();
3240
+ var punctuation = t();
2515
3241
  var bracket = t(punctuation);
2516
3242
  var meta = t();
2517
3243
  var tags = {
@@ -2956,50 +3682,6 @@ function whiteboardScript() {
2956
3682
  return new LanguageSupport(whiteboardScriptLanguage);
2957
3683
  }
2958
3684
 
2959
- // src/effort-registry/types.ts
2960
- var EFFORT_REGISTRY_ORIGINS = ["bundled", "user"];
2961
- var CATALOG_PRECEDENCE_RANK = {
2962
- core: 0,
2963
- community: 1,
2964
- imported: 2,
2965
- user: 3
2966
- };
2967
- function resolveSourceTier(sourceIdOrMetaKey, explicitTier) {
2968
- if (explicitTier) return explicitTier;
2969
- if (!sourceIdOrMetaKey) return "core";
2970
- if (sourceIdOrMetaKey === "user") return "user";
2971
- const id = sourceIdOrMetaKey.startsWith("seed:") ? sourceIdOrMetaKey.slice(5) : sourceIdOrMetaKey;
2972
- if (id === "core" || id === "efforts" || id === "equipment") return "core";
2973
- if (id === "community" || id.startsWith("community-") || id === "common") return "community";
2974
- return "imported";
2975
- }
2976
- function resolveSourceRank(sourceIdOrMetaKey, explicitTier) {
2977
- return CATALOG_PRECEDENCE_RANK[resolveSourceTier(sourceIdOrMetaKey, explicitTier)];
2978
- }
2979
- function resolveEntityTier(entity) {
2980
- if (entity.registrySource === "user") return "user";
2981
- return resolveSourceTier(entity.sourceId);
2982
- }
2983
- function resolveEntityRank(entity) {
2984
- return CATALOG_PRECEDENCE_RANK[resolveEntityTier(entity)];
2985
- }
2986
- function dedupeEntitiesByPrecedence(entities) {
2987
- const bySlug = /* @__PURE__ */ new Map();
2988
- for (const entity of entities) {
2989
- const key = entity.slug.toLowerCase();
2990
- const prior = bySlug.get(key);
2991
- if (!prior || resolveEntityRank(entity) >= resolveEntityRank(prior)) {
2992
- bySlug.set(key, entity);
2993
- }
2994
- }
2995
- return Array.from(bySlug.values());
2996
- }
2997
- var INTENSITY_TIERS = ["low", "moderate", "high"];
2998
- var DEFAULT_RESOLVER_OPTIONS = {
2999
- defaultMet: 5,
3000
- defaultThreshold: 2
3001
- };
3002
-
3003
3685
  // src/effort-registry/disciplines.ts
3004
3686
  var EFFORT_DISCIPLINES = [
3005
3687
  "bodyweight",
@@ -3453,71 +4135,6 @@ function equipmentToDocument(equipment) {
3453
4135
  }
3454
4136
  return lines.join("\n");
3455
4137
  }
3456
-
3457
- // src/effort-registry/fuzzyMatch.ts
3458
- function levenshteinDistance(a, b) {
3459
- if (a === b) return 0;
3460
- if (a.length === 0) return b.length;
3461
- if (b.length === 0) return a.length;
3462
- if (a.length > b.length) {
3463
- const tmp = a;
3464
- a = b;
3465
- b = tmp;
3466
- }
3467
- const m = a.length;
3468
- const n = b.length;
3469
- let prev = new Uint32Array(m + 1);
3470
- let curr = new Uint32Array(m + 1);
3471
- for (let i = 0; i <= m; i++) {
3472
- prev[i] = i;
3473
- }
3474
- for (let j = 1; j <= n; j++) {
3475
- curr[0] = j;
3476
- const bj = b.charCodeAt(j - 1);
3477
- for (let i = 1; i <= m; i++) {
3478
- const cost = a.charCodeAt(i - 1) === bj ? 0 : 1;
3479
- curr[i] = Math.min(
3480
- prev[i] + 1,
3481
- // deletion
3482
- curr[i - 1] + 1,
3483
- // insertion
3484
- prev[i - 1] + cost
3485
- // substitution
3486
- );
3487
- }
3488
- const swap = prev;
3489
- prev = curr;
3490
- curr = swap;
3491
- }
3492
- return prev[m];
3493
- }
3494
- function normalizeForFuzzy(input) {
3495
- return input.toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, " ").replace(/[-_]+/g, " ").trim().replace(/\s+/g, " ");
3496
- }
3497
- function isWithinThreshold(a, b, threshold) {
3498
- const na = normalizeForFuzzy(a);
3499
- const nb = normalizeForFuzzy(b);
3500
- if (na === nb) return true;
3501
- if (Math.abs(na.length - nb.length) > threshold) return false;
3502
- return levenshteinDistance(na, nb) <= threshold;
3503
- }
3504
- function findBestFuzzyMatch(query, candidates, threshold) {
3505
- const nq = normalizeForFuzzy(query);
3506
- let best = null;
3507
- for (const candidate of candidates) {
3508
- const nc = normalizeForFuzzy(candidate);
3509
- if (Math.abs(nq.length - nc.length) > threshold) continue;
3510
- const distance = levenshteinDistance(nq, nc);
3511
- if (distance > threshold) continue;
3512
- if (!best || distance < best.distance) {
3513
- best = { target: candidate, distance };
3514
- if (distance === 0) break;
3515
- }
3516
- }
3517
- return best;
3518
- }
3519
-
3520
- // src/effort-registry/EffortResolver.ts
3521
4138
  function normalizeSlug(label) {
3522
4139
  return normalizeForFuzzy(label).replace(/\s+/g, "-");
3523
4140
  }
@@ -3730,162 +4347,64 @@ var fixtureUserOverride = {
3730
4347
  label: "Rowing (Calibrated)",
3731
4348
  aliases: ["row", "rower", "erg"],
3732
4349
  baseAttributes: { met: 8.5, discipline: "rowing", intensityTier: "high" },
3733
- registrySource: "user",
3734
- createdAt: "2024-02-01T08:00:00Z",
3735
- updatedAt: "2024-02-01T08:00:00Z"
3736
- };
3737
- var commonFixtureSet = [
3738
- fixtureRunning,
3739
- fixtureRowing,
3740
- fixtureBackSquat,
3741
- fixtureBurpee,
3742
- fixturePlank
3743
- ];
3744
- var fullFixtureSet = [
3745
- ...commonFixtureSet,
3746
- fixtureUserCustom,
3747
- fixtureUserOverride
3748
- ];
3749
-
3750
- // src/effort-registry/data/bundled-efforts.ts
3751
- var bundledEfforts = commonFixtureSet;
3752
- var BUNDLED_EFFORT_COUNT = bundledEfforts.length;
3753
-
3754
- // src/effort-registry/InMemoryEffortRegistry.ts
3755
- var InMemoryEffortRegistry = class {
3756
- efforts = /* @__PURE__ */ new Map();
3757
- sourceFilter = null;
3758
- initialized = false;
3759
- constructor(sourceFilter) {
3760
- this.sourceFilter = sourceFilter ?? null;
3761
- }
3762
- /** Seed with bundled efforts */
3763
- async loadBundled() {
3764
- for (const effort of bundledEfforts) {
3765
- if (!this.sourceFilter || effort.registrySource === this.sourceFilter) {
3766
- this.efforts.set(effort.slug, effort);
3767
- }
3768
- }
3769
- this.initialized = true;
3770
- }
3771
- isInitialized() {
3772
- return this.initialized;
3773
- }
3774
- /** Seed with arbitrary efforts (useful for tests) */
3775
- seed(efforts) {
3776
- for (const effort of efforts) {
3777
- this.efforts.set(effort.slug, effort);
3778
- }
3779
- }
3780
- /** Clear all efforts */
3781
- clear() {
3782
- this.efforts.clear();
3783
- this.initialized = false;
3784
- }
3785
- resolve(slug) {
3786
- return this.efforts.get(slug) ?? null;
3787
- }
3788
- resolveByAlias(label) {
3789
- const normalized = normalizeForFuzzy(label);
3790
- for (const effort of this.efforts.values()) {
3791
- if (normalizeForFuzzy(effort.label) === normalized) return effort;
3792
- for (const alias of effort.aliases) {
3793
- if (normalizeForFuzzy(alias) === normalized) return effort;
3794
- }
3795
- }
3796
- return null;
3797
- }
3798
- list() {
3799
- return Array.from(this.efforts.values());
3800
- }
3801
- listByOrigin(origin) {
3802
- return this.list().filter((e) => e.registrySource === origin);
3803
- }
3804
- async upsert(effort) {
3805
- if (effort.registrySource !== "user") {
3806
- throw new Error(
3807
- `InMemoryEffortRegistry.upsert: only user efforts can be written. Received registrySource="${effort.registrySource}"`
3808
- );
3809
- }
3810
- this.efforts.set(effort.slug, effort);
3811
- }
3812
- async delete(slug) {
3813
- const existing = this.efforts.get(slug);
3814
- if (existing && existing.registrySource !== "user") {
3815
- throw new Error(
3816
- `InMemoryEffortRegistry.delete: cannot delete non-user effort "${slug}"`
3817
- );
3818
- }
3819
- this.efforts.delete(slug);
3820
- }
3821
- };
3822
-
3823
- // src/effort-registry/CompositeEffortRegistry.ts
3824
- var CompositeEffortRegistry = class {
3825
- userEfforts = /* @__PURE__ */ new Map();
3826
- bundledEfforts = /* @__PURE__ */ new Map();
3827
- initialized = false;
3828
- initialBundled;
3829
- storage;
3830
- constructor(options) {
3831
- if (options) {
3832
- this.initialBundled = options.bundled;
3833
- this.storage = options.storage;
3834
- }
3835
- }
3836
- /**
3837
- * Initialize the registry:
3838
- * 1. Load bundled efforts into memory
3839
- * 2. Load user efforts from storage adapter into memory
3840
- *
3841
- * An explicit `customBundled` REPLACES the bundled tier — passing an empty
3842
- * set clears it — so a seed source removal never leaves stale bundled
3843
- * efforts cached. The no-argument call keeps the constructor/default set.
3844
- * Both tiers rebuild only AFTER a successful adapter load: a failed load
3845
- * propagates (readiness must never report over stale data), and deleted
3846
- * custom rows disappear because the user map is cleared too.
3847
- */
3848
- async loadBundled(customBundled) {
3849
- this.bundledEfforts.clear();
3850
- const source = customBundled ?? this.initialBundled ?? bundledEfforts;
3851
- for (const effort of source) {
3852
- const prior = this.bundledEfforts.get(effort.slug);
3853
- if (!prior || resolveEntityRank(effort) >= resolveEntityRank(prior)) {
3854
- this.bundledEfforts.set(effort.slug, effort);
3855
- }
3856
- }
3857
- this.userEfforts.clear();
3858
- if (this.storage) {
3859
- try {
3860
- const userEfforts = await this.storage.load();
3861
- for (const effort of userEfforts) {
3862
- if (effort.registrySource === "user") {
3863
- this.userEfforts.set(effort.slug, effort);
3864
- }
3865
- }
3866
- } catch (err) {
3867
- console.warn("[CompositeEffortRegistry] Failed to load user efforts from storage adapter:", err);
4350
+ registrySource: "user",
4351
+ createdAt: "2024-02-01T08:00:00Z",
4352
+ updatedAt: "2024-02-01T08:00:00Z"
4353
+ };
4354
+ var commonFixtureSet = [
4355
+ fixtureRunning,
4356
+ fixtureRowing,
4357
+ fixtureBackSquat,
4358
+ fixtureBurpee,
4359
+ fixturePlank
4360
+ ];
4361
+ var fullFixtureSet = [
4362
+ ...commonFixtureSet,
4363
+ fixtureUserCustom,
4364
+ fixtureUserOverride
4365
+ ];
4366
+
4367
+ // src/effort-registry/data/bundled-efforts.ts
4368
+ var bundledEfforts = commonFixtureSet;
4369
+ var BUNDLED_EFFORT_COUNT = bundledEfforts.length;
4370
+
4371
+ // src/effort-registry/InMemoryEffortRegistry.ts
4372
+ var InMemoryEffortRegistry = class {
4373
+ efforts = /* @__PURE__ */ new Map();
4374
+ sourceFilter = null;
4375
+ initialized = false;
4376
+ constructor(sourceFilter) {
4377
+ this.sourceFilter = sourceFilter ?? null;
4378
+ }
4379
+ /** Seed with bundled efforts */
4380
+ async loadBundled() {
4381
+ for (const effort of bundledEfforts) {
4382
+ if (!this.sourceFilter || effort.registrySource === this.sourceFilter) {
4383
+ this.efforts.set(effort.slug, effort);
3868
4384
  }
3869
4385
  }
3870
4386
  this.initialized = true;
3871
4387
  }
3872
- /** True after loadBundled() has resolved */
3873
4388
  isInitialized() {
3874
4389
  return this.initialized;
3875
4390
  }
4391
+ /** Seed with arbitrary efforts (useful for tests) */
4392
+ seed(efforts) {
4393
+ for (const effort of efforts) {
4394
+ this.efforts.set(effort.slug, effort);
4395
+ }
4396
+ }
4397
+ /** Clear all efforts */
4398
+ clear() {
4399
+ this.efforts.clear();
4400
+ this.initialized = false;
4401
+ }
3876
4402
  resolve(slug) {
3877
- return this.userEfforts.get(slug) ?? this.bundledEfforts.get(slug) ?? null;
4403
+ return this.efforts.get(slug) ?? null;
3878
4404
  }
3879
4405
  resolveByAlias(label) {
3880
4406
  const normalized = normalizeForFuzzy(label);
3881
- for (const effort of this.userEfforts.values()) {
3882
- if (normalizeForFuzzy(effort.label) === normalized) return effort;
3883
- for (const alias of effort.aliases) {
3884
- if (normalizeForFuzzy(alias) === normalized) return effort;
3885
- }
3886
- }
3887
- const bundled = Array.from(this.bundledEfforts.values()).filter((effort) => !this.userEfforts.has(effort.slug)).sort((a, b) => resolveEntityRank(b) - resolveEntityRank(a));
3888
- for (const effort of bundled) {
4407
+ for (const effort of this.efforts.values()) {
3889
4408
  if (normalizeForFuzzy(effort.label) === normalized) return effort;
3890
4409
  for (const alias of effort.aliases) {
3891
4410
  if (normalizeForFuzzy(alias) === normalized) return effort;
@@ -3894,456 +4413,193 @@ var CompositeEffortRegistry = class {
3894
4413
  return null;
3895
4414
  }
3896
4415
  list() {
3897
- const merged = new Map(this.bundledEfforts);
3898
- for (const [slug, effort] of this.userEfforts) {
3899
- merged.set(slug, effort);
3900
- }
3901
- return Array.from(merged.values());
4416
+ return Array.from(this.efforts.values());
3902
4417
  }
3903
4418
  listByOrigin(origin) {
3904
- if (origin === "user") {
3905
- return Array.from(this.userEfforts.values());
3906
- }
3907
- if (origin === "bundled") {
3908
- return Array.from(this.bundledEfforts.values());
3909
- }
3910
4419
  return this.list().filter((e) => e.registrySource === origin);
3911
4420
  }
3912
4421
  async upsert(effort) {
3913
4422
  if (effort.registrySource !== "user") {
3914
4423
  throw new Error(
3915
- `CompositeEffortRegistry.upsert: only user efforts can be written. Received registrySource="${effort.registrySource}"`
3916
- );
3917
- }
3918
- this.userEfforts.set(effort.slug, effort);
3919
- if (this.storage) {
3920
- try {
3921
- await this.storage.save(effort);
3922
- } catch (err) {
3923
- console.warn("[CompositeEffortRegistry] Failed to persist effort to storage adapter:", err);
3924
- }
3925
- }
3926
- }
3927
- async delete(slug) {
3928
- const existing = this.resolve(slug);
3929
- if (existing && existing.registrySource !== "user") {
3930
- throw new Error(
3931
- `CompositeEffortRegistry.delete: cannot delete non-user effort "${slug}"`
4424
+ `InMemoryEffortRegistry.upsert: only user efforts can be written. Received registrySource="${effort.registrySource}"`
3932
4425
  );
3933
4426
  }
3934
- this.userEfforts.delete(slug);
3935
- if (this.storage) {
3936
- try {
3937
- await this.storage.delete(slug);
3938
- } catch (err) {
3939
- console.warn("[CompositeEffortRegistry] Failed to delete effort from storage adapter:", err);
3940
- }
3941
- }
3942
- }
3943
- };
3944
-
3945
- // src/effort-registry/aliasManagement.ts
3946
- async function addAliasToEffort(registry, targetSlug, newAlias) {
3947
- const trimmed = newAlias.trim();
3948
- if (!trimmed) {
3949
- throw new Error("Alias cannot be empty");
3950
- }
3951
- const existing = registry.resolve(targetSlug);
3952
- if (!existing) {
3953
- throw new Error(`Effort "${targetSlug}" not found in registry`);
3954
- }
3955
- const normalizedNew = normalizeForFuzzy(trimmed);
3956
- const alreadyHas = normalizeForFuzzy(existing.label) === normalizedNew || existing.aliases.some((a) => normalizeForFuzzy(a) === normalizedNew);
3957
- if (alreadyHas) {
3958
- return existing;
3959
- }
3960
- const updatedAliases = [...existing.aliases, trimmed];
3961
- const now = (/* @__PURE__ */ new Date()).toISOString();
3962
- if (existing.registrySource === "bundled") {
3963
- const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? `effort-user-${crypto.randomUUID()}` : `effort-user-${Date.now()}`;
3964
- const userEffort = {
3965
- ...existing,
3966
- id,
3967
- registrySource: "user",
3968
- aliases: updatedAliases,
3969
- updatedAt: now,
3970
- derivation: existing.derivation ?? {
3971
- parentSlug: existing.slug,
3972
- coefficients: {},
3973
- hardOverrides: {}
3974
- }
3975
- };
3976
- await registry.upsert(userEffort);
3977
- return userEffort;
3978
- }
3979
- const updatedUserEffort = {
3980
- ...existing,
3981
- aliases: updatedAliases,
3982
- updatedAt: now
3983
- };
3984
- await registry.upsert(updatedUserEffort);
3985
- return updatedUserEffort;
3986
- }
3987
- function buildExactIndex(entities) {
3988
- const deduped = dedupeEntitiesByPrecedence(entities);
3989
- const byKey = /* @__PURE__ */ new Map();
3990
- const add = (key, entity) => {
3991
- if (!key) return;
3992
- const existing = byKey.get(key);
3993
- if (existing) {
3994
- if (!existing.includes(entity)) existing.push(entity);
3995
- } else {
3996
- byKey.set(key, [entity]);
3997
- }
3998
- };
3999
- for (const entity of deduped) {
4000
- add(entity.slug.toLowerCase(), entity);
4001
- add(normalizeForFuzzy(entity.label), entity);
4002
- for (const alias of entity.aliases) add(normalizeForFuzzy(alias), entity);
4003
- }
4004
- return {
4005
- byKey,
4006
- lookup(key) {
4007
- const direct = byKey.get(key) ?? byKey.get(normalizeForFuzzy(key));
4008
- if (!direct || direct.length === 0) return { entity: null, ambiguous: [] };
4009
- if (direct.length === 1) return { entity: direct[0], ambiguous: [] };
4010
- let maxRank = -1;
4011
- for (const candidate of direct) {
4012
- const rank = resolveEntityRank(candidate);
4013
- if (rank > maxRank) maxRank = rank;
4014
- }
4015
- const topTier = direct.filter((candidate) => resolveEntityRank(candidate) === maxRank);
4016
- if (topTier.length === 1) return { entity: topTier[0], ambiguous: [] };
4017
- return { entity: null, ambiguous: topTier };
4018
- }
4019
- };
4020
- }
4021
- function equipmentRef(entity) {
4022
- return { kind: "equipment", slug: entity.slug, label: entity.label };
4023
- }
4024
- function dedupeRefs(refs) {
4025
- const seen = /* @__PURE__ */ new Set();
4026
- const result = [];
4027
- for (const ref of refs) {
4028
- const key = `${ref.kind}:${ref.slug}`;
4029
- if (seen.has(key)) continue;
4030
- seen.add(key);
4031
- result.push(ref);
4427
+ this.efforts.set(effort.slug, effort);
4032
4428
  }
4033
- return result;
4034
- }
4035
- function createReferenceIndex(catalog, materialize) {
4036
- const dedupedEquipment = dedupeEntitiesByPrecedence(catalog.equipment ?? []);
4037
- const efforts = buildExactIndex(catalog.efforts ?? []);
4038
- const equipment = buildExactIndex(dedupedEquipment);
4039
- 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);
4040
- const materializer = materialize ?? ((effort) => ({
4041
- effort,
4042
- definition: effort,
4043
- slug: effort.slug,
4044
- label: effort.label,
4045
- met: effort.baseAttributes.met,
4046
- baseAttributes: effort.baseAttributes,
4047
- discipline: effort.baseAttributes.discipline,
4048
- disciplineFactor: effort.baseAttributes.disciplineFactor ?? 1,
4049
- intensityTier: effort.baseAttributes.intensityTier,
4050
- modifiers: {},
4051
- registrySource: effort.registrySource,
4052
- resolvedFrom: effort.registrySource === "user" ? "user" : effort.registrySource === "bundled" ? "bundled" : "default",
4053
- isEstimated: false
4054
- }));
4055
- const resolveEffortExact = (key) => {
4056
- const found = efforts.lookup(key.trim());
4057
- if (found.entity) return { entity: found.entity };
4058
- if (found.ambiguous.length > 0) {
4059
- return {
4060
- entity: null,
4061
- diagnostic: {
4062
- reason: "ambiguous-effort",
4063
- message: `"${key}" matches multiple efforts.`,
4064
- token: key,
4065
- candidates: found.ambiguous.map((e) => e.slug)
4066
- }
4067
- };
4068
- }
4069
- return {
4070
- entity: null,
4071
- diagnostic: { reason: "unknown-effort", message: `Unknown effort "${key}".`, token: key }
4072
- };
4073
- };
4074
- const resolveEquipmentValues = (values) => {
4075
- const refs = [];
4076
- const diagnostics = [];
4077
- for (const value of values) {
4078
- if (typeof value !== "string" || !value.trim()) {
4079
- diagnostics.push({
4080
- reason: "invalid-explicit",
4081
- message: `Equipment reference must be a slug string, got ${JSON.stringify(value) ?? "null"}.`,
4082
- token: String(value)
4083
- });
4084
- continue;
4085
- }
4086
- const found = equipment.lookup(value.trim());
4087
- if (found.entity) {
4088
- refs.push(equipmentRef(found.entity));
4089
- } else if (found.ambiguous.length > 0) {
4090
- diagnostics.push({
4091
- reason: "ambiguous-equipment",
4092
- message: `"${value}" matches multiple equipment entries.`,
4093
- token: value,
4094
- candidates: found.ambiguous.map((e) => e.slug)
4095
- });
4096
- } else {
4097
- diagnostics.push({
4098
- reason: "invalid-explicit",
4099
- message: `Unknown equipment "${value}".`,
4100
- token: value
4101
- });
4102
- }
4429
+ async delete(slug) {
4430
+ const existing = this.efforts.get(slug);
4431
+ if (existing && existing.registrySource !== "user") {
4432
+ throw new Error(
4433
+ `InMemoryEffortRegistry.delete: cannot delete non-user effort "${slug}"`
4434
+ );
4103
4435
  }
4104
- return { refs: dedupeRefs(refs), diagnostics };
4105
- };
4106
- const index = {
4107
- resolveEffort(slugOrAlias) {
4108
- return efforts.lookup(slugOrAlias).entity;
4109
- },
4110
- resolveEquipment(slugOrAlias) {
4111
- return equipment.lookup(slugOrAlias).entity;
4112
- },
4113
- materialize(effort) {
4114
- return materializer(effort);
4115
- },
4116
- resolve(label, defaults) {
4117
- const source = (label ?? "").trim();
4118
- const diagnostics = [];
4119
- const push = (diagnostic) => {
4120
- if (diagnostic) diagnostics.push(diagnostic);
4121
- };
4122
- let matched = "none";
4123
- let effortEntity = null;
4124
- let remaining = "";
4125
- let equipmentRefs = [];
4126
- let equipmentAuthored = false;
4127
- const fillEquipment = () => {
4128
- if (equipmentRefs.length > 0 || equipmentAuthored) return;
4129
- if (effortEntity === null) return;
4130
- if (defaults?.equipment?.length) {
4131
- const resolvedDefaults = resolveEquipmentValues(defaults.equipment);
4132
- equipmentRefs = resolvedDefaults.refs;
4133
- for (const diagnostic of resolvedDefaults.diagnostics) {
4134
- push({ ...diagnostic, reason: "unknown-equipment" });
4135
- }
4136
- if (equipmentRefs.length > 0) return;
4137
- }
4138
- if (effortEntity.equipment?.length) {
4139
- const resolvedDefEquipment = resolveEquipmentValues(effortEntity.equipment);
4140
- equipmentRefs = resolvedDefEquipment.refs;
4141
- for (const diagnostic of resolvedDefEquipment.diagnostics) {
4142
- push({ ...diagnostic, reason: "unknown-equipment" });
4143
- }
4144
- }
4145
- };
4146
- if (!source) {
4147
- if (defaults?.effort) {
4148
- const found = resolveEffortExact(defaults.effort);
4149
- if (found.entity) {
4150
- effortEntity = found.entity;
4151
- matched = "default";
4152
- } else {
4153
- push({
4154
- reason: found.diagnostic?.reason === "ambiguous-effort" ? "ambiguous-effort" : "unknown-effort",
4155
- message: `Default effort "${defaults.effort}" is not in the catalog.`,
4156
- token: defaults.effort,
4157
- candidates: found.diagnostic?.candidates
4158
- });
4159
- }
4160
- }
4161
- fillEquipment();
4162
- return {
4163
- matched,
4164
- label: source,
4165
- effort: effortEntity ? materializer(effortEntity) : null,
4166
- effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
4167
- equipment: equipmentRefs,
4168
- remaining: "",
4169
- diagnostics
4170
- };
4436
+ this.efforts.delete(slug);
4437
+ }
4438
+ };
4439
+
4440
+ // src/effort-registry/CompositeEffortRegistry.ts
4441
+ var CompositeEffortRegistry = class {
4442
+ userEfforts = /* @__PURE__ */ new Map();
4443
+ bundledEfforts = /* @__PURE__ */ new Map();
4444
+ initialized = false;
4445
+ initialBundled;
4446
+ storage;
4447
+ constructor(options) {
4448
+ if (options) {
4449
+ this.initialBundled = options.bundled;
4450
+ this.storage = options.storage;
4451
+ }
4452
+ }
4453
+ /**
4454
+ * Initialize the registry:
4455
+ * 1. Load bundled efforts into memory
4456
+ * 2. Load user efforts from storage adapter into memory
4457
+ *
4458
+ * An explicit `customBundled` REPLACES the bundled tier — passing an empty
4459
+ * set clears it — so a seed source removal never leaves stale bundled
4460
+ * efforts cached. The no-argument call keeps the constructor/default set.
4461
+ * Both tiers rebuild only AFTER a successful adapter load: a failed load
4462
+ * propagates (readiness must never report over stale data), and deleted
4463
+ * custom rows disappear because the user map is cleared too.
4464
+ */
4465
+ async loadBundled(customBundled) {
4466
+ this.bundledEfforts.clear();
4467
+ const source = customBundled ?? this.initialBundled ?? bundledEfforts;
4468
+ for (const effort of source) {
4469
+ const prior = this.bundledEfforts.get(effort.slug);
4470
+ if (!prior || resolveEntityRank(effort) >= resolveEntityRank(prior)) {
4471
+ this.bundledEfforts.set(effort.slug, effort);
4171
4472
  }
4172
- if (source.startsWith("{")) {
4173
- let parsed;
4174
- try {
4175
- parsed = JSON.parse(source);
4176
- } catch {
4177
- return {
4178
- matched: "none",
4179
- label: source,
4180
- effort: null,
4181
- effortRef: null,
4182
- equipment: [],
4183
- remaining: "",
4184
- diagnostics: [{
4185
- reason: "malformed-json",
4186
- message: "Reference object is not valid JSON.",
4187
- token: source
4188
- }]
4189
- };
4190
- }
4191
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && ("effort" in parsed || "equipment" in parsed)) {
4192
- matched = "explicit";
4193
- const obj = parsed;
4194
- if (obj.effort !== void 0) {
4195
- if (typeof obj.effort === "string" && obj.effort.trim()) {
4196
- const found = resolveEffortExact(obj.effort);
4197
- effortEntity = found.entity;
4198
- if (found.diagnostic) {
4199
- push(found.diagnostic.reason === "ambiguous-effort" ? found.diagnostic : { ...found.diagnostic, reason: "invalid-explicit", message: `Explicit effort "${obj.effort}" is not in the catalog.` });
4200
- }
4201
- } else {
4202
- push({
4203
- reason: "invalid-explicit",
4204
- message: 'Explicit "effort" must be a slug string.',
4205
- token: String(obj.effort)
4206
- });
4207
- }
4208
- }
4209
- if (obj.equipment !== void 0) {
4210
- equipmentAuthored = true;
4211
- const values = Array.isArray(obj.equipment) ? obj.equipment : [obj.equipment];
4212
- const resolvedEquipment = resolveEquipmentValues(values);
4213
- equipmentRefs = resolvedEquipment.refs;
4214
- diagnostics.push(...resolvedEquipment.diagnostics);
4215
- if (effortEntity?.equipment?.length) {
4216
- const definitionSlugs = new Set(effortEntity.equipment);
4217
- const authoredSlugs = equipmentRefs.map((ref) => ref.slug);
4218
- if (authoredSlugs.length > 0 && authoredSlugs.some((slug) => !definitionSlugs.has(slug))) {
4219
- push({
4220
- reason: "conflict",
4221
- message: `Explicit equipment [${authoredSlugs.join(", ")}] contradicts effort "${effortEntity.slug}" definition equipment [${effortEntity.equipment.join(", ")}].`,
4222
- token: source
4223
- });
4224
- }
4225
- }
4473
+ }
4474
+ this.userEfforts.clear();
4475
+ if (this.storage) {
4476
+ try {
4477
+ const userEfforts = await this.storage.load();
4478
+ for (const effort of userEfforts) {
4479
+ if (effort.registrySource === "user") {
4480
+ this.userEfforts.set(effort.slug, effort);
4226
4481
  }
4227
- fillEquipment();
4228
- return {
4229
- matched,
4230
- label: source,
4231
- effort: effortEntity ? materializer(effortEntity) : null,
4232
- effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
4233
- equipment: equipmentRefs,
4234
- remaining: "",
4235
- diagnostics
4236
- };
4237
- }
4238
- }
4239
- const lowerSource = source.toLowerCase();
4240
- for (const candidate of prefixCandidates) {
4241
- if (!lowerSource.startsWith(candidate.lower)) continue;
4242
- const after = source.slice(candidate.lower.length);
4243
- if (after && !/^\s/.test(after)) continue;
4244
- const candidateRemaining = after.trim();
4245
- if (!candidateRemaining) continue;
4246
- const equipmentFound = equipment.lookup(candidate.lower);
4247
- if (equipmentFound.ambiguous.length > 0) {
4248
- push({
4249
- reason: "ambiguous-equipment",
4250
- message: `"${candidate.lower}" matches multiple equipment entries.`,
4251
- token: source,
4252
- candidates: equipmentFound.ambiguous.map((e) => e.slug)
4253
- });
4254
- return {
4255
- matched: "none",
4256
- label: source,
4257
- effort: null,
4258
- effortRef: null,
4259
- equipment: [],
4260
- remaining: "",
4261
- diagnostics
4262
- };
4263
- }
4264
- if (!equipmentFound.entity) continue;
4265
- const effortFound = resolveEffortExact(candidateRemaining);
4266
- if (effortFound.entity) {
4267
- matched = "split";
4268
- effortEntity = effortFound.entity;
4269
- equipmentRefs = [equipmentRef(equipmentFound.entity)];
4270
- equipmentAuthored = true;
4271
- remaining = candidateRemaining;
4272
- break;
4273
- }
4274
- if (effortFound.diagnostic?.reason === "ambiguous-effort") {
4275
- push(effortFound.diagnostic);
4276
- return {
4277
- matched: "none",
4278
- label: source,
4279
- effort: null,
4280
- effortRef: null,
4281
- equipment: [],
4282
- remaining: "",
4283
- diagnostics
4284
- };
4285
4482
  }
4483
+ } catch (err) {
4484
+ console.warn("[CompositeEffortRegistry] Failed to load user efforts from storage adapter:", err);
4286
4485
  }
4287
- if (matched === "none") {
4288
- const found = resolveEffortExact(source);
4289
- if (found.entity) {
4290
- effortEntity = found.entity;
4291
- matched = "whole";
4292
- } else if (found.diagnostic?.reason === "ambiguous-effort") {
4293
- push(found.diagnostic);
4294
- return {
4295
- matched: "none",
4296
- label: source,
4297
- effort: null,
4298
- effortRef: null,
4299
- equipment: [],
4300
- remaining: "",
4301
- diagnostics
4302
- };
4303
- } else if (found.diagnostic) {
4304
- push(found.diagnostic);
4305
- }
4486
+ }
4487
+ this.initialized = true;
4488
+ }
4489
+ /** True after loadBundled() has resolved */
4490
+ isInitialized() {
4491
+ return this.initialized;
4492
+ }
4493
+ resolve(slug) {
4494
+ return this.userEfforts.get(slug) ?? this.bundledEfforts.get(slug) ?? null;
4495
+ }
4496
+ resolveByAlias(label) {
4497
+ const normalized = normalizeForFuzzy(label);
4498
+ for (const effort of this.userEfforts.values()) {
4499
+ if (normalizeForFuzzy(effort.label) === normalized) return effort;
4500
+ for (const alias of effort.aliases) {
4501
+ if (normalizeForFuzzy(alias) === normalized) return effort;
4306
4502
  }
4307
- fillEquipment();
4308
- return {
4309
- matched,
4310
- label: source,
4311
- effort: effortEntity ? materializer(effortEntity) : null,
4312
- effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
4313
- equipment: equipmentRefs,
4314
- remaining,
4315
- diagnostics
4316
- };
4317
4503
  }
4318
- };
4319
- return index;
4320
- }
4321
- function extractStatementRefs(statements) {
4322
- const efforts = [];
4323
- const equipment = [];
4324
- const seenEffort = /* @__PURE__ */ new Set();
4325
- const seenEquipment = /* @__PURE__ */ new Set();
4326
- const pushUnique = (list, seen, value) => {
4327
- if (!value) return;
4328
- if (seen.has(value)) return;
4329
- seen.add(value);
4330
- list.push(value);
4331
- };
4332
- for (const statement of statements ?? []) {
4333
- const metrics = Array.isArray(statement.metrics) ? statement.metrics : statement.metrics?.toArray?.() ?? [];
4334
- for (const metric of metrics) {
4335
- const refs = getReferenceMetadata(metric.metadata)?.refs;
4336
- if (!refs) continue;
4337
- for (const ref of refs) {
4338
- if (ref.kind === "effort") pushUnique(efforts, seenEffort, ref.slug);
4339
- if (ref.kind === "equipment") pushUnique(equipment, seenEquipment, ref.slug);
4504
+ const bundled = Array.from(this.bundledEfforts.values()).filter((effort) => !this.userEfforts.has(effort.slug)).sort((a, b) => resolveEntityRank(b) - resolveEntityRank(a));
4505
+ for (const effort of bundled) {
4506
+ if (normalizeForFuzzy(effort.label) === normalized) return effort;
4507
+ for (const alias of effort.aliases) {
4508
+ if (normalizeForFuzzy(alias) === normalized) return effort;
4340
4509
  }
4341
4510
  }
4511
+ return null;
4512
+ }
4513
+ list() {
4514
+ const merged = new Map(this.bundledEfforts);
4515
+ for (const [slug, effort] of this.userEfforts) {
4516
+ merged.set(slug, effort);
4517
+ }
4518
+ return Array.from(merged.values());
4342
4519
  }
4343
- return { efforts, equipment };
4344
- }
4345
- function slugifyRefLabel(label) {
4346
- return normalizeForFuzzy(label).replace(/\s+/g, "-");
4520
+ listByOrigin(origin) {
4521
+ if (origin === "user") {
4522
+ return Array.from(this.userEfforts.values());
4523
+ }
4524
+ if (origin === "bundled") {
4525
+ return Array.from(this.bundledEfforts.values());
4526
+ }
4527
+ return this.list().filter((e) => e.registrySource === origin);
4528
+ }
4529
+ async upsert(effort) {
4530
+ if (effort.registrySource !== "user") {
4531
+ throw new Error(
4532
+ `CompositeEffortRegistry.upsert: only user efforts can be written. Received registrySource="${effort.registrySource}"`
4533
+ );
4534
+ }
4535
+ this.userEfforts.set(effort.slug, effort);
4536
+ if (this.storage) {
4537
+ try {
4538
+ await this.storage.save(effort);
4539
+ } catch (err) {
4540
+ console.warn("[CompositeEffortRegistry] Failed to persist effort to storage adapter:", err);
4541
+ }
4542
+ }
4543
+ }
4544
+ async delete(slug) {
4545
+ const existing = this.resolve(slug);
4546
+ if (existing && existing.registrySource !== "user") {
4547
+ throw new Error(
4548
+ `CompositeEffortRegistry.delete: cannot delete non-user effort "${slug}"`
4549
+ );
4550
+ }
4551
+ this.userEfforts.delete(slug);
4552
+ if (this.storage) {
4553
+ try {
4554
+ await this.storage.delete(slug);
4555
+ } catch (err) {
4556
+ console.warn("[CompositeEffortRegistry] Failed to delete effort from storage adapter:", err);
4557
+ }
4558
+ }
4559
+ }
4560
+ };
4561
+
4562
+ // src/effort-registry/aliasManagement.ts
4563
+ async function addAliasToEffort(registry, targetSlug, newAlias) {
4564
+ const trimmed = newAlias.trim();
4565
+ if (!trimmed) {
4566
+ throw new Error("Alias cannot be empty");
4567
+ }
4568
+ const existing = registry.resolve(targetSlug);
4569
+ if (!existing) {
4570
+ throw new Error(`Effort "${targetSlug}" not found in registry`);
4571
+ }
4572
+ const normalizedNew = normalizeForFuzzy(trimmed);
4573
+ const alreadyHas = normalizeForFuzzy(existing.label) === normalizedNew || existing.aliases.some((a) => normalizeForFuzzy(a) === normalizedNew);
4574
+ if (alreadyHas) {
4575
+ return existing;
4576
+ }
4577
+ const updatedAliases = [...existing.aliases, trimmed];
4578
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4579
+ if (existing.registrySource === "bundled") {
4580
+ const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? `effort-user-${crypto.randomUUID()}` : `effort-user-${Date.now()}`;
4581
+ const userEffort = {
4582
+ ...existing,
4583
+ id,
4584
+ registrySource: "user",
4585
+ aliases: updatedAliases,
4586
+ updatedAt: now,
4587
+ derivation: existing.derivation ?? {
4588
+ parentSlug: existing.slug,
4589
+ coefficients: {},
4590
+ hardOverrides: {}
4591
+ }
4592
+ };
4593
+ await registry.upsert(userEffort);
4594
+ return userEffort;
4595
+ }
4596
+ const updatedUserEffort = {
4597
+ ...existing,
4598
+ aliases: updatedAliases,
4599
+ updatedAt: now
4600
+ };
4601
+ await registry.upsert(updatedUserEffort);
4602
+ return updatedUserEffort;
4347
4603
  }
4348
4604
 
4349
4605
  // src/runtime/contracts/IRuntimeOptions.ts
@@ -6498,6 +6754,8 @@ var RuntimeBlock = class {
6498
6754
  blockType;
6499
6755
  context;
6500
6756
  sourceIds;
6757
+ /** The tags this block's statements referenced, resolved at compile time. */
6758
+ tagComposition;
6501
6759
  // List-based memory storage
6502
6760
  _memory = [];
6503
6761
  /**
@@ -6534,13 +6792,15 @@ var RuntimeBlock = class {
6534
6792
  key,
6535
6793
  blockType,
6536
6794
  label,
6537
- metrics
6795
+ metrics,
6796
+ tagComposition
6538
6797
  } = options;
6539
6798
  this._runtime = runtime;
6540
6799
  this.sourceIds = sourceIds;
6541
6800
  this.behaviors = behaviors;
6542
6801
  this.key = key ?? new BlockKey();
6543
6802
  this.blockType = blockType;
6803
+ this.tagComposition = tagComposition;
6544
6804
  this.context = context ?? new BlockContext(runtime, this.key.toString());
6545
6805
  if (label) {
6546
6806
  this._memory.push(new MemoryLocation("metric:label", [{
@@ -8481,61 +8741,153 @@ var SoundMetric = class {
8481
8741
  image;
8482
8742
  };
8483
8743
 
8484
- // src/runtime/behaviors/SoundCueBehavior.ts
8485
- var SoundCueBehavior = class {
8744
+ // src/runtime/sound/SoundProfile.ts
8745
+ function parseSoundTrigger(notation) {
8746
+ const trimmed = notation.trim();
8747
+ if (trimmed.length === 0) return void 0;
8748
+ let anchor;
8749
+ let magnitude;
8750
+ if (trimmed.startsWith("-")) {
8751
+ anchor = "end";
8752
+ magnitude = trimmed.slice(1);
8753
+ } else {
8754
+ anchor = "start";
8755
+ magnitude = trimmed;
8756
+ }
8757
+ if (magnitude.startsWith(":")) magnitude = magnitude.slice(1);
8758
+ const offsetMs = parseDurationMagnitude(magnitude);
8759
+ if (offsetMs === void 0) return void 0;
8760
+ return { anchor, offsetMs };
8761
+ }
8762
+ function parseDurationMagnitude(magnitude) {
8763
+ const cleaned = magnitude.trim().replace(/^:/, "");
8764
+ if (cleaned.length === 0) return 0;
8765
+ if (!/^\d+(:\d+)*$/.test(cleaned)) return void 0;
8766
+ const segments = cleaned.split(":").map(Number).reverse();
8767
+ const [seconds = 0, minutes = 0, hours = 0, days = 0] = segments;
8768
+ if (segments.length > 4) return void 0;
8769
+ if (minutes > 59 || hours > 59) return void 0;
8770
+ return ((days * 24 + hours) * 60 + minutes) * 6e4 + seconds * 1e3;
8771
+ }
8772
+ function formatSoundTrigger(trigger) {
8773
+ const totalSeconds = Math.round(trigger.offsetMs / 1e3);
8774
+ const seconds = totalSeconds % 60;
8775
+ const minutes = Math.floor(totalSeconds / 60) % 60;
8776
+ const hours = Math.floor(totalSeconds / 3600);
8777
+ let magnitude;
8778
+ if (hours > 0) {
8779
+ magnitude = `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
8780
+ } else if (minutes > 0) {
8781
+ magnitude = `${pad(minutes)}:${pad(seconds)}`;
8782
+ } else {
8783
+ magnitude = pad(seconds);
8784
+ }
8785
+ return `${trigger.anchor === "end" ? "-:" : ":"}${magnitude}`;
8786
+ }
8787
+ function pad(value) {
8788
+ return value < 10 ? `0${value}` : String(value);
8789
+ }
8790
+ function resolveProfileRules(profile, durationMs) {
8791
+ return profile.rules.filter((rule) => {
8792
+ const { anchor, offsetMs } = rule.trigger;
8793
+ if (anchor === "start") return true;
8794
+ if (offsetMs === 0) return true;
8795
+ if (durationMs === void 0) return false;
8796
+ return offsetMs <= durationMs;
8797
+ });
8798
+ }
8799
+ var DEFAULT_SOUND_PROFILE = {
8800
+ id: "default",
8801
+ name: "Default",
8802
+ rules: [
8803
+ { trigger: { anchor: "end", offsetMs: 3e3 }, sound: "countdown-beep" },
8804
+ { trigger: { anchor: "end", offsetMs: 2e3 }, sound: "countdown-beep" },
8805
+ { trigger: { anchor: "end", offsetMs: 1e3 }, sound: "countdown-beep" },
8806
+ { trigger: { anchor: "end", offsetMs: 0 }, sound: "timer-complete" }
8807
+ ]
8808
+ };
8809
+
8810
+ // src/runtime/behaviors/SoundProfileBehavior.ts
8811
+ var SOUND_PLAY_EVENT = "sound:play";
8812
+ var SoundProfileBehavior = class {
8486
8813
  constructor(config) {
8487
8814
  this.config = config;
8488
8815
  }
8489
8816
  config;
8817
+ rules = [];
8818
+ /** Rule indices already fired, so each rule fires exactly once. */
8819
+ fired = /* @__PURE__ */ new Set();
8820
+ durationMs;
8490
8821
  onMount(ctx) {
8491
- for (const cue of this.config.cues) {
8492
- if (cue.trigger === "mount") {
8493
- ctx.emitOutput("system", [
8494
- new SoundMetric(cue.sound, "mount")
8495
- ], { label: `Sound: ${cue.sound}` });
8496
- }
8497
- }
8498
- const countdownCues = this.config.cues.filter((c) => c.trigger === "countdown");
8499
- if (countdownCues.length > 0) {
8500
- const playedSeconds = /* @__PURE__ */ new Set();
8501
- ctx.subscribe("tick", (_event, tickCtx) => {
8502
- const timer = readTimer(tickCtx);
8503
- if (!timer || timer.direction !== "down" || !timer.durationMs) return [];
8504
- const now = tickCtx.clock.currentDate.getTime();
8505
- let elapsed = 0;
8506
- for (const span of timer.spans) {
8507
- const end = span.ended ?? now;
8508
- elapsed += end - span.started;
8509
- }
8510
- const remainingMs = timer.durationMs - elapsed;
8511
- const remainingSeconds = Math.ceil(remainingMs / 1e3);
8512
- for (const cue of countdownCues) {
8513
- if (cue.atSeconds?.includes(remainingSeconds) && !playedSeconds.has(remainingSeconds)) {
8514
- playedSeconds.add(remainingSeconds);
8515
- tickCtx.emitOutput("system", [
8516
- new SoundMetric(cue.sound, "countdown", { atSecond: remainingSeconds })
8517
- ], { label: `Countdown: ${remainingSeconds}s` });
8518
- }
8519
- }
8520
- return [];
8521
- }, { scope: "bubble" });
8522
- }
8822
+ const timer = readTimer(ctx);
8823
+ this.durationMs = timer?.durationMs;
8824
+ this.rules = resolveProfileRules(this.config.profile, this.durationMs);
8825
+ this.fireDue(ctx, 0, this.durationMs);
8826
+ ctx.subscribe("tick", (_event, tickCtx) => {
8827
+ const elapsed = this.elapsedMs(tickCtx);
8828
+ if (elapsed === void 0) return [];
8829
+ this.fireDue(tickCtx, elapsed, this.durationMs);
8830
+ return [];
8831
+ }, { scope: "bubble" });
8523
8832
  return [];
8524
8833
  }
8525
8834
  onNext(_ctx) {
8526
8835
  return [];
8527
8836
  }
8528
8837
  onUnmount(ctx) {
8529
- for (const cue of this.config.cues) {
8530
- if (cue.trigger === "unmount" || cue.trigger === "complete") {
8531
- ctx.emitOutput("system", [
8532
- new SoundMetric(cue.sound, cue.trigger)
8533
- ], { label: `Sound: ${cue.sound}` });
8534
- }
8535
- }
8838
+ this.fireDue(ctx, this.elapsedMs(ctx) ?? 0, this.durationMs, true);
8536
8839
  return [];
8537
8840
  }
8538
8841
  onDispose(_ctx) {
8842
+ this.fired.clear();
8843
+ }
8844
+ /**
8845
+ * Elapsed milliseconds for the block's own timer, or `undefined` when the
8846
+ * block has no timer to measure against.
8847
+ */
8848
+ elapsedMs(ctx) {
8849
+ const timer = readTimer(ctx);
8850
+ if (!timer) return void 0;
8851
+ return calculateElapsed(timer, ctx.clock.currentDate.getTime());
8852
+ }
8853
+ /**
8854
+ * Fire every rule whose trigger point has been reached.
8855
+ *
8856
+ * @param elapsed milliseconds elapsed on the block's timer
8857
+ * @param durationMs the block's total duration
8858
+ * @param ending whether the block is ending — a rule anchored to the end
8859
+ * with no offset fires then, even when the block has no duration to
8860
+ * count down from
8861
+ */
8862
+ fireDue(ctx, elapsed, durationMs, ending = false) {
8863
+ const remaining = durationMs === void 0 ? void 0 : Math.max(0, durationMs - elapsed);
8864
+ this.rules.forEach((rule, index) => {
8865
+ if (this.fired.has(index)) return;
8866
+ const { anchor, offsetMs } = rule.trigger;
8867
+ const due = anchor === "start" ? elapsed >= offsetMs : offsetMs === 0 ? ending || remaining !== void 0 && remaining <= 0 : remaining !== void 0 && remaining <= offsetMs;
8868
+ if (!due) return;
8869
+ this.fired.add(index);
8870
+ this.play(ctx, rule, anchor, offsetMs);
8871
+ });
8872
+ }
8873
+ /**
8874
+ * Record the cue and command its playback.
8875
+ */
8876
+ play(ctx, rule, anchor, offsetMs) {
8877
+ const trigger = anchor === "start" ? offsetMs === 0 ? "mount" : "countdown" : offsetMs === 0 ? "complete" : "countdown";
8878
+ ctx.emitOutput("system", [
8879
+ new SoundMetric(rule.sound, trigger, {
8880
+ atSecond: offsetMs / 1e3
8881
+ })
8882
+ ], { label: `Sound: ${rule.sound}` });
8883
+ ctx.emitEvent({
8884
+ name: SOUND_PLAY_EVENT,
8885
+ timestamp: ctx.clock.currentDate,
8886
+ data: {
8887
+ sound: rule.sound,
8888
+ volume: 1
8889
+ }
8890
+ });
8539
8891
  }
8540
8892
  };
8541
8893
  var ButtonBehavior = class {
@@ -8665,6 +9017,34 @@ var WaitingToStartInjectorBehavior = class {
8665
9017
  onDispose(_ctx) {
8666
9018
  }
8667
9019
  };
9020
+ var SOUND_PROFILE_METRIC = MetricType.Sound;
9021
+ function resolveSoundProfile(statements, options) {
9022
+ const blockProfileId = findBlockProfileId(statements);
9023
+ if (blockProfileId) {
9024
+ const match = findProfile(options?.profiles, blockProfileId);
9025
+ if (match) return match;
9026
+ }
9027
+ if (options?.noteProfileId) {
9028
+ const match = findProfile(options?.profiles, options.noteProfileId);
9029
+ if (match) return match;
9030
+ }
9031
+ return DEFAULT_SOUND_PROFILE;
9032
+ }
9033
+ function findBlockProfileId(statements) {
9034
+ for (const statement of statements) {
9035
+ const metric = statement.metrics.find((m) => m.type === SOUND_PROFILE_METRIC);
9036
+ if (!metric) continue;
9037
+ const value = metric.value;
9038
+ if (typeof value === "string" && value.trim().length > 0) {
9039
+ return value.trim();
9040
+ }
9041
+ }
9042
+ return void 0;
9043
+ }
9044
+ function findProfile(profiles, id) {
9045
+ if (!profiles) return void 0;
9046
+ return profiles.find((profile) => profile.id === id);
9047
+ }
8668
9048
 
8669
9049
  // src/runtime/blocks/RestBlock.ts
8670
9050
  var RestBlock = class _RestBlock extends RuntimeBlock {
@@ -8675,7 +9055,7 @@ var RestBlock = class _RestBlock extends RuntimeBlock {
8675
9055
  const restLabel = config.label ?? "Rest";
8676
9056
  const blockKey = new BlockKey("rest");
8677
9057
  const context = new BlockContext(runtime, blockKey.toString(), "Rest");
8678
- const behaviors = _RestBlock.buildBehaviors(config);
9058
+ const behaviors = _RestBlock.buildBehaviors(config, runtime.options?.soundOptions);
8679
9059
  super({
8680
9060
  runtime,
8681
9061
  sourceIds: [],
@@ -8690,7 +9070,7 @@ var RestBlock = class _RestBlock extends RuntimeBlock {
8690
9070
  /**
8691
9071
  * Builds the behavior list for a rest block.
8692
9072
  */
8693
- static buildBehaviors(config) {
9073
+ static buildBehaviors(config, soundOptions) {
8694
9074
  const behaviors = [];
8695
9075
  const restLabel = config.label ?? "Rest";
8696
9076
  behaviors.push(new ReportOutputBehavior({ label: restLabel }));
@@ -8709,18 +9089,8 @@ var RestBlock = class _RestBlock extends RuntimeBlock {
8709
9089
  mode: "timer",
8710
9090
  label: restLabel
8711
9091
  }));
8712
- behaviors.push(new SoundCueBehavior({
8713
- cues: [
8714
- {
8715
- sound: "rest-over",
8716
- trigger: "complete"
8717
- },
8718
- {
8719
- sound: "countdown-beep",
8720
- trigger: "countdown",
8721
- atSeconds: [3, 2, 1]
8722
- }
8723
- ]
9092
+ behaviors.push(new SoundProfileBehavior({
9093
+ profile: resolveSoundProfile([], soundOptions)
8724
9094
  }));
8725
9095
  return behaviors;
8726
9096
  }
@@ -8751,6 +9121,8 @@ var BlockBuilder = class {
8751
9121
  blockType = "Block";
8752
9122
  metrics;
8753
9123
  sourceIds = [];
9124
+ /** The tags the block's statements referenced, resolved at compile time. */
9125
+ tagComposition = [];
8754
9126
  /** Pending round config stored by asRepeater(), consumed by asContainer() */
8755
9127
  pendingRoundConfig;
8756
9128
  /** Declared exit intent — resolved into a single ExitBehavior in build(). */
@@ -8850,6 +9222,11 @@ var BlockBuilder = class {
8850
9222
  this.sourceIds = ids;
8851
9223
  return this;
8852
9224
  }
9225
+ /** The tags the block's statements referenced, and what each carried. */
9226
+ setTagComposition(composition) {
9227
+ this.tagComposition = composition;
9228
+ return this;
9229
+ }
8853
9230
  getLabel() {
8854
9231
  return this.label;
8855
9232
  }
@@ -8971,6 +9348,7 @@ var BlockBuilder = class {
8971
9348
  const block = new RuntimeBlock({
8972
9349
  runtime: this.runtime,
8973
9350
  sourceIds: this.sourceIds,
9351
+ tagComposition: this.tagComposition,
8974
9352
  behaviors: Array.from(this.behaviors.values()),
8975
9353
  context: this.context,
8976
9354
  key: this.key,
@@ -9060,6 +9438,11 @@ var JitCompiler = class {
9060
9438
  return void 0;
9061
9439
  }
9062
9440
  const builder = new BlockBuilder(runtime);
9441
+ const tagComposition = [];
9442
+ for (const node of effectiveNodes) {
9443
+ collectTagReferences(node.metrics, tagComposition);
9444
+ }
9445
+ builder.setTagComposition(tagComposition);
9063
9446
  for (const strategy of matchingStrategies) {
9064
9447
  strategy.apply(builder, effectiveNodes, runtime);
9065
9448
  }
@@ -10574,6 +10957,16 @@ var CalcEngine = class {
10574
10957
  const fenced = (d) => !d.fences || d.fences.includes(deps.dialect);
10575
10958
  this.segmentCalcs = registry.byScope("segment").filter(fenced);
10576
10959
  this.workoutCalcs = registry.byScope("workout").filter(fenced);
10960
+ for (const tag of deps.tags?.list() ?? []) {
10961
+ const resolved = resolveTagContributions(deps.tags, tag.slug);
10962
+ for (const name2 of resolved.functions) {
10963
+ const carriers = this.tagCarriedFunctions.get(name2);
10964
+ if (carriers) carriers.add(tag.slug);
10965
+ else this.tagCarriedFunctions.set(name2, /* @__PURE__ */ new Set([tag.slug]));
10966
+ }
10967
+ }
10968
+ this.runTags = [...deps.activeTags ?? []];
10969
+ for (const tag of this.runTags) this.activatedTags.add(tag);
10577
10970
  }
10578
10971
  registry;
10579
10972
  deps;
@@ -10587,6 +10980,43 @@ var CalcEngine = class {
10587
10980
  * calls (segments arrive in stream order, live and in replay).
10588
10981
  */
10589
10982
  lastSegmentEffort;
10983
+ /**
10984
+ * The tags that carry each function: function name → the tags that carry
10985
+ * it. A function no tag carries runs everywhere, as it always did.
10986
+ */
10987
+ tagCarriedFunctions = /* @__PURE__ */ new Map();
10988
+ /** The tags applied anywhere in the run so far. */
10989
+ activatedTags = /* @__PURE__ */ new Set();
10990
+ /** The tags the run activates — a note-level bundle of attachments. */
10991
+ runTags;
10992
+ /**
10993
+ * Whether a calculation runs here.
10994
+ *
10995
+ * A function a tag carries runs only where one of the tags that carry it
10996
+ * applies; every other function runs as it always did.
10997
+ */
10998
+ isActive(def, appliedTags) {
10999
+ const carriers = this.tagCarriedFunctions.get(def.id);
11000
+ if (!carriers) return true;
11001
+ for (const slug of carriers) {
11002
+ if (appliedTags.includes(slug)) return true;
11003
+ }
11004
+ return false;
11005
+ }
11006
+ /**
11007
+ * The tags applied to a segment: the tags its statement marked, plus the
11008
+ * tags the run activates — a note-level tag applies to every segment of
11009
+ * the note it belongs to.
11010
+ *
11011
+ * The applied tags are recorded, so a workout-scope function the tag
11012
+ * carries runs at the end of the workout.
11013
+ */
11014
+ appliedTags(output) {
11015
+ const slugs = tagMarkerSlugs(output.metrics.rawMetrics);
11016
+ for (const slug of slugs) this.activatedTags.add(slug);
11017
+ if (this.runTags.length === 0) return slugs;
11018
+ return [...slugs, ...this.runTags.filter((slug) => !slugs.includes(slug))];
11019
+ }
10590
11020
  // ── Phase 1: segment scope ─────────────────────────────────────────────
10591
11021
  process(output) {
10592
11022
  if (output.outputType !== "segment" || !output.isLeaf) return output;
@@ -10602,11 +11032,13 @@ var CalcEngine = class {
10602
11032
  if (discipline) effortTags.effortDiscipline = discipline;
10603
11033
  if (intensityTier) effortTags.effortIntensityTier = intensityTier;
10604
11034
  }
10605
- const climbGrade = [...output.metrics.rawMetrics].find((m) => m.type === "climb-grade")?.value;
11035
+ const climbGrade = output.metrics.rawMetrics.find((m) => m.type === "climb-grade")?.value;
10606
11036
  if (climbGrade && typeof climbGrade === "object" && "raw" in climbGrade) {
10607
11037
  effortTags.grade = String(climbGrade.raw);
10608
11038
  }
11039
+ const appliedTags = this.appliedTags(output);
10609
11040
  for (const def of this.segmentCalcs) {
11041
+ if (!this.isActive(def, appliedTags)) continue;
10610
11042
  const ctx = this.makeContext(atoms);
10611
11043
  if (!this.applicable(def.whenAst, ctx)) continue;
10612
11044
  const variant = this.selectVariant(def, ctx);
@@ -10648,7 +11080,15 @@ var CalcEngine = class {
10648
11080
  // ── Phase 2: workout scope ─────────────────────────────────────────────
10649
11081
  summarize(outputs) {
10650
11082
  const projections = [];
11083
+ for (const output of outputs) {
11084
+ if (output.outputType !== "segment") continue;
11085
+ for (const slug of tagMarkerSlugs(output.metrics.rawMetrics)) {
11086
+ this.activatedTags.add(slug);
11087
+ }
11088
+ }
11089
+ const appliedTags = [...this.activatedTags];
10651
11090
  for (const def of this.workoutCalcs) {
11091
+ if (!this.isActive(def, appliedTags)) continue;
10652
11092
  if (def.output?.isGrouped) {
10653
11093
  projections.push(...this.summarizeGrouped(def, outputs));
10654
11094
  } else {
@@ -11806,7 +12246,13 @@ function createCalcStack(options) {
11806
12246
  }
11807
12247
  function createCalcEngine(dialect, options) {
11808
12248
  const stack = createCalcStack(options);
11809
- return new CalcEngine(stack.registry, { dialect, lookups: stack.lookups, userProfile: options.userProfile });
12249
+ return new CalcEngine(stack.registry, {
12250
+ dialect,
12251
+ lookups: stack.lookups,
12252
+ userProfile: options.userProfile,
12253
+ tags: options.tags,
12254
+ activeTags: options.activeTags
12255
+ });
11810
12256
  }
11811
12257
 
11812
12258
  // src/analytics/StandardAnalyticsProfile.ts
@@ -11821,7 +12267,9 @@ var StandardAnalyticsProfile = class {
11821
12267
  const calcEngine = createCalcEngine(context.dialect, {
11822
12268
  effortResolver: context.analyticsContext.effortResolver,
11823
12269
  userProfile: context.userProfile,
11824
- calcs: context.calcs
12270
+ calcs: context.calcs,
12271
+ tags: context.tags,
12272
+ activeTags: context.activeTags
11825
12273
  });
11826
12274
  realtime.push(calcEngine);
11827
12275
  summary.push(calcEngine);
@@ -11873,7 +12321,9 @@ function createAnalyticsEngineForBlock(block, options) {
11873
12321
  scriptMetricTypes,
11874
12322
  analyticsContext,
11875
12323
  userProfile: options?.userProfile,
11876
- calcs: options?.calcs
12324
+ calcs: options?.calcs,
12325
+ tags: options?.tags,
12326
+ activeTags: options?.activeTags
11877
12327
  };
11878
12328
  const profile = new StandardAnalyticsProfile();
11879
12329
  const { realtime, summary } = profile.build(context);
@@ -12250,13 +12700,6 @@ var AmrapLogicStrategy = class {
12250
12700
  mode: "countdown",
12251
12701
  label
12252
12702
  }));
12253
- builder.addBehavior(new SoundCueBehavior({
12254
- cues: [
12255
- { sound: "start-beep", trigger: "mount" },
12256
- { sound: "countdown-beep", trigger: "countdown", atSeconds: [3, 2, 1] },
12257
- { sound: "timer-complete", trigger: "complete" }
12258
- ]
12259
- }));
12260
12703
  }
12261
12704
  };
12262
12705
  var IntervalLogicStrategy = class {
@@ -12314,13 +12757,6 @@ var IntervalLogicStrategy = class {
12314
12757
  mode: "countdown",
12315
12758
  label
12316
12759
  }));
12317
- builder.addBehavior(new SoundCueBehavior({
12318
- cues: [
12319
- { sound: "interval-start", trigger: "mount" },
12320
- { sound: "countdown-beep", trigger: "countdown", atSeconds: [3, 2, 1] },
12321
- { sound: "interval-complete", trigger: "complete" }
12322
- ]
12323
- }));
12324
12760
  const hasResistance = statements.some((s) => s.metrics.some((m) => m.type === MetricType.Resistance));
12325
12761
  const hasDistance = statements.some((s) => s.metrics.some((m) => m.type === MetricType.Distance));
12326
12762
  const promotions = [
@@ -12513,14 +12949,6 @@ var GenericTimerStrategy = class {
12513
12949
  } else {
12514
12950
  builder.addBehavior(new ExitBehavior({ mode: "immediate", onNext: true }));
12515
12951
  }
12516
- if (isCountdown) {
12517
- builder.addBehavior(new SoundCueBehavior({
12518
- cues: [
12519
- { sound: "countdown-beep", trigger: "countdown", atSeconds: [3, 2, 1] },
12520
- { sound: "timer-complete", trigger: "complete" }
12521
- ]
12522
- }));
12523
- }
12524
12952
  builder.addBehavior(new LabelingBehavior({
12525
12953
  mode: durationMs ? "countdown" : "clock",
12526
12954
  label
@@ -12581,29 +13009,17 @@ var SoundStrategy = class {
12581
13009
  priority = 20;
12582
13010
  id = "sound";
12583
13011
  match(statements, _runtime) {
12584
- return statements && statements.length > 0;
13012
+ return statements.length > 0;
12585
13013
  }
12586
- apply(builder, _statements, _runtime) {
12587
- if (builder.hasBehavior(SoundCueBehavior)) {
13014
+ apply(builder, statements, runtime) {
13015
+ if (builder.hasBehavior(SoundProfileBehavior)) {
12588
13016
  return;
12589
13017
  }
12590
- const countdown = builder.getBehavior(CountdownTimerBehavior);
12591
- if (countdown) {
12592
- builder.addBehavior(new SoundCueBehavior({
12593
- cues: [
12594
- { sound: "countdown-beep", trigger: "countdown", atSeconds: [3, 2, 1] },
12595
- { sound: "timer-complete", trigger: "complete" }
12596
- ]
12597
- }));
13018
+ if (!builder.hasTimerBehavior()) {
12598
13019
  return;
12599
13020
  }
12600
- if (builder.hasBehavior(CountupTimerBehavior)) {
12601
- builder.addBehavior(new SoundCueBehavior({
12602
- cues: [
12603
- { sound: "start-beep", trigger: "mount" }
12604
- ]
12605
- }));
12606
- }
13021
+ const profile = resolveSoundProfile(statements, runtime.options?.soundOptions);
13022
+ builder.addBehavior(new SoundProfileBehavior({ profile }));
12607
13023
  }
12608
13024
  };
12609
13025
 
@@ -12636,20 +13052,6 @@ var NextEvent = class _NextEvent {
12636
13052
  };
12637
13053
  }
12638
13054
  };
12639
-
12640
- // src/runtime/models/SoundModels.ts
12641
- var PREDEFINED_SOUNDS = {
12642
- /** Short single beep - good for countdown ticks */
12643
- BEEP: "beep",
12644
- /** Long buzz - good for timer completion */
12645
- BUZZER: "buzzer",
12646
- /** Pleasant chime - good for milestones */
12647
- CHIME: "chime",
12648
- /** Clock tick - good for second-by-second countdown */
12649
- TICK: "tick",
12650
- /** Ascending sequence - good for timer start */
12651
- START: "start"
12652
- };
12653
13055
  function metricsToLabel(metrics) {
12654
13056
  if (metrics instanceof MetricContainer) {
12655
13057
  return metricsToLabel(metrics.toArray());
@@ -12930,40 +13332,6 @@ var UnregisterEventHandlerAction = class {
12930
13332
  }
12931
13333
  };
12932
13334
 
12933
- // src/runtime/actions/audio/PlaySoundAction.ts
12934
- var PlaySoundAction = class {
12935
- constructor(sound, volume = 1, cueId, now = wallClockNow) {
12936
- this.sound = sound;
12937
- this.volume = volume;
12938
- this.cueId = cueId;
12939
- this.now = now;
12940
- if (volume < 0 || volume > 1) {
12941
- throw new RangeError(`Volume must be between 0.0 and 1.0, got: ${volume}`);
12942
- }
12943
- }
12944
- sound;
12945
- volume;
12946
- cueId;
12947
- now;
12948
- type = "play-sound";
12949
- /**
12950
- * Executes the sound playback by emitting a 'sound:play' event.
12951
- * UI components should register handlers for this event to play audio.
12952
- */
12953
- do(runtime) {
12954
- const event = {
12955
- name: "sound:play",
12956
- timestamp: this.now.now(),
12957
- data: {
12958
- sound: this.sound,
12959
- volume: this.volume,
12960
- cueId: this.cueId
12961
- }
12962
- };
12963
- return runtime.eventBus.dispatch(event, runtime);
12964
- }
12965
- };
12966
-
12967
13335
  // src/runtime/utils/MemoryUtils.ts
12968
13336
  function searchStackMemory(runtime, criteria) {
12969
13337
  const results = [];
@@ -14766,6 +15134,6 @@ function compareStatement(expectations, actual, mode) {
14766
15134
  return diffs;
14767
15135
  }
14768
15136
 
14769
- export { ABSENT, AGGREGATE_BUILTINS, AUTHORITATIVE_CASTS, AbortSessionAction, AmrapLogicStrategy, AnalyticsEngine, AnalyticsTransformer, BUILTIN_CALCS, BUNDLED_EFFORT_COUNT, BehaviorContext, BlockBuilder, BlockContext, ButtonBehavior, CATALOG_PRECEDENCE_RANK, CONSUMED_HINTS, CONSUMED_HINT_KEYS, CONTEXT_ATOMS, CalcEngine, CalcEvalError, CalcParseError, CalcRegistrationError, CalcUnitError, CalculateBlockProcessor, CalculationRegistry, CardioDialect, ChildSelectionBehavior, ChildrenStrategy, ChoiceGroupMetric, ClearChildrenAction, ClimbDialect, ClimbMetricType, CompileAndPushBlockAction, CompletionTimestampBehavior, CompositeEffortRegistry, CountdownTimerBehavior, CountupTimerBehavior, CrossFitDialect, DAY, DEFAULT_RESOLVER_OPTIONS, DEFAULT_RUNTIME_OPTIONS, DEFAULT_UNRESOLVED_EFFORT_MET, DIM_COUNT, DIM_ENERGY, DIM_LENGTH, DIM_MASS, DIM_TIME, DIM_ZERO, DISCIPLINE_FACTORS, DialectStack, EFFORT_DATA_METRIC_TYPE, EFFORT_DISCIPLINES, EFFORT_REGISTRY_ORIGINS, EffortBlock, EffortFallbackStrategy, EffortResolver, EmitEventAction, EmitSystemOutputAction, ErrorAction, EventBus, ExecutionContext, ExitBehavior, FixtureSyntaxError, GenericGroupStrategy, GenericLoopStrategy, GenericTimerStrategy, HabitsDialect, INTENSITY_TIERS, InMemoryEffortRegistry, IntervalLogicStrategy, JitCompiler, LabelingBehavior, LocalEventProvider, LocalRuntimeSubscription, LookupRegistry, MdTimerRuntime, MemoryLocation, MemoryTypeEnum, MetricPromotionBehavior, NAMED_COMPOUNDS, NextAction, NextEvent, NextEventHandler, OutputEmitter, PREDEFINED_SOUNDS, PRODUCTION_STRATEGIES, PlaySoundAction, PopBlockAction, PushBlockAction, PushRestBlockAction, REST_EXCLUSION, RPE_LABELS, RUNTIME_LOG_SUBJECT_WIDTH, RegisterEventHandlerAction, Registry, ReportOutputBehavior, ReportOutputStrategy, RestBlock, RoundsMetric, RuntimeAdapter, RuntimeBlock, RuntimeBuilder, RuntimeClock, RuntimeFactory, RuntimeLogger, RuntimeMemory, RuntimeObservers, RuntimeStack, STANDARD_UNITS, STORE_CALCS, STREAM_ATOMS, ScriptRuntime, SessionRootBlock, SessionRootStrategy, SnapshotClock, SoundCueBehavior, SoundStrategy, SpanTrackingBehavior, StandardAnalyticsProfile, StartSessionAction, StartWorkoutAction, SubscriptionManager, ThrowError, ThrowErrorAction, TimeSpan, TwoPassEffortResolutionProcess, TypedMemoryReference, UNITS, UnitRegistry, UnitSet, UnitsDialect, UnregisterEventHandlerAction, UpdateNextPreviewAction, VISIBILITY_ICONS, VISIBILITY_LABELS, WaitingToStartBlock, WaitingToStartInjectorBehavior, WhiteboardScript, WodDialect, YogaDialect, addAliasToEffort, addDim, buildStaticLookups, buildTooltip, bundledEfforts, calculateElapsed, canonicalizeType, checkCalcLine, classifyStatements, closeCurrentSpan, collapseUnresolvedChoices, commonFixtureSet, compareStatement, compileLineForm, composeDivUnit, composeMulUnit, compoundName, computeColumnLabel, computeLabel, computeWorkloadRollups, convertScalar, createAnalyticsEngineForBlock, createCalcEngine, createCalcStack, createCompiler, createDialectStack, createDisciplinesTable, createEffortTable, createLineLocator, createPolicy as createMetricPresentationPolicy, createMockClock, createParser, createProfileTable, createReferenceIndex, createRpeLabelsTable, dailySessionLoads, dayBucket, dedupeEntitiesByPrecedence, dialectRegistry, dialectStack, diffStatement, dimEquals, disciplineFactorFor, documentToEffort, documentToEquipment, effortToDocument, equipmentToDocument, evaluate, evaluateCalculateDefinitions, extractEffortData, extractMetrics, extractStatementRefs, extractStatements, extractStatementsRaw, extractSyntaxFacts, filterTagsByVisibility, findBestFuzzyMatch, findUnresolvedChoices, fixtureBackSquat, fixtureBurpee, fixturePlank, fixtureRowing, fixtureRunning, fixtureUserCustom, fixtureUserOverride, formatDim, formatDuration, formatRuntimeLogLine, formatRuntimeLogRule, formatRuntimeTimestamp, metricsToLabel as fragmentsToLabel, fullFixtureSet, fuseUnits, fuseUnitsInMetrics, getAnalyticsFromLogs, getAnalyticsFromRuntime, getHints, getMetricVisibility, getUnit, hasHint, hintMetric, hintsToContainer, inferDim, isCastSubscription, isChoiceResolved, isDimensionless, isEffortDiscipline, isFragmentTag, isTimeLikeMetric, isWithinThreshold, levenshteinDistance, metricPresentation, mutateTimerSpans, normalizeForFuzzy, num, openSpan, outputNodeId, outputNodeIds, padRuntimeLogEvent, padRuntimeLogSubject, parseCalcLine, parseCalculateBlock, parseEffortFile, parseEquipmentFile, parseExpression, parseMetricLine, parseScript, printCalculation, readNestedSection, readTimer, realtimeProcessorRegistry, renderMetric, resolveDominantOrigin, resolveEntityRank, resolveEntityTier, resolveSourceRank, resolveSourceTier, slugifyRefLabel, startSpan, str, strategyRegistry, subDim, summaryProcessorRegistry, toStoredOutputStatement, truthy, validateOutputUnit, parser as whiteboardParser, whiteboardScript, whiteboardScriptLanguage, writeChoiceSelection };
15137
+ export { ABSENT, AGGREGATE_BUILTINS, AUTHORITATIVE_CASTS, AbortSessionAction, AmrapLogicStrategy, AnalyticsEngine, AnalyticsTransformer, BUILTIN_CALCS, BUNDLED_EFFORT_COUNT, BehaviorContext, BlockBuilder, BlockContext, ButtonBehavior, CATALOG_PRECEDENCE_RANK, CONSUMED_HINTS, CONSUMED_HINT_KEYS, CONTEXT_ATOMS, CalcEngine, CalcEvalError, CalcParseError, CalcRegistrationError, CalcUnitError, CalculateBlockProcessor, CalculationRegistry, CardioDialect, ChildSelectionBehavior, ChildrenStrategy, ChoiceGroupMetric, ClearChildrenAction, ClimbDialect, ClimbMetricType, CompileAndPushBlockAction, CompletionTimestampBehavior, CompositeEffortRegistry, CountdownTimerBehavior, CountupTimerBehavior, CrossFitDialect, DAY, DEFAULT_RESOLVER_OPTIONS, DEFAULT_RUNTIME_OPTIONS, DEFAULT_SOUND_PROFILE, DEFAULT_UNRESOLVED_EFFORT_MET, DIM_COUNT, DIM_ENERGY, DIM_LENGTH, DIM_MASS, DIM_TIME, DIM_ZERO, DISCIPLINE_FACTORS, DialectStack, EFFORT_DATA_METRIC_TYPE, EFFORT_DISCIPLINES, EFFORT_REGISTRY_ORIGINS, EffortBlock, EffortFallbackStrategy, EffortResolver, EmitEventAction, EmitSystemOutputAction, ErrorAction, EventBus, ExecutionContext, ExitBehavior, FixtureSyntaxError, GenericGroupStrategy, GenericLoopStrategy, GenericTimerStrategy, HabitsDialect, INTENSITY_TIERS, InMemoryEffortRegistry, IntervalLogicStrategy, JitCompiler, LabelingBehavior, LocalEventProvider, LocalRuntimeSubscription, LookupRegistry, MAX_TAG_CONTRIBUTION_DEPTH, MdTimerRuntime, MemoryLocation, MemoryTypeEnum, MetricPromotionBehavior, NAMED_COMPOUNDS, NextAction, NextEvent, NextEventHandler, OutputEmitter, PRODUCTION_STRATEGIES, PopBlockAction, PushBlockAction, PushRestBlockAction, REST_EXCLUSION, RPE_LABELS, RUNTIME_LOG_SUBJECT_WIDTH, RegisterEventHandlerAction, Registry, ReportOutputBehavior, ReportOutputStrategy, RestBlock, RoundsMetric, RuntimeAdapter, RuntimeBlock, RuntimeBuilder, RuntimeClock, RuntimeFactory, RuntimeLogger, RuntimeMemory, RuntimeObservers, RuntimeStack, SOUND_PLAY_EVENT, SOUND_PROFILE_METRIC, STANDARD_UNITS, STORE_CALCS, STREAM_ATOMS, ScriptRuntime, SessionRootBlock, SessionRootStrategy, SnapshotClock, SoundProfileBehavior, SoundStrategy, SpanTrackingBehavior, StandardAnalyticsProfile, StartSessionAction, StartWorkoutAction, SubscriptionManager, TAG_MARKER_PREFIX, ThrowError, ThrowErrorAction, TimeSpan, TwoPassEffortResolutionProcess, TypedMemoryReference, UNITS, UnitRegistry, UnitSet, UnitsDialect, UnregisterEventHandlerAction, UpdateNextPreviewAction, VISIBILITY_ICONS, VISIBILITY_LABELS, WaitingToStartBlock, WaitingToStartInjectorBehavior, WhiteboardScript, WodDialect, YogaDialect, addAliasToEffort, addDim, buildStaticLookups, buildTooltip, bundledEfforts, calculateElapsed, canonicalizeType, checkCalcLine, classifyStatements, closeCurrentSpan, collapseUnresolvedChoices, commonFixtureSet, compareStatement, compileLineForm, composeDivUnit, composeMulUnit, compoundName, computeColumnLabel, computeLabel, computeWorkloadRollups, convertScalar, createAnalyticsEngineForBlock, createCalcEngine, createCalcStack, createCompiler, createDialectStack, createDisciplinesTable, createEffortTable, createLineLocator, createPolicy as createMetricPresentationPolicy, createMockClock, createParser, createProfileTable, createReferenceIndex, createRpeLabelsTable, dailySessionLoads, dayBucket, dedupeEntitiesByPrecedence, dialectRegistry, dialectStack, diffStatement, dimEquals, disciplineFactorFor, documentToEffort, documentToEquipment, effortToDocument, equipmentToDocument, evaluate, evaluateCalculateDefinitions, extractEffortData, extractMetrics, extractStatementRefs, extractStatements, extractStatementsRaw, extractSyntaxFacts, filterTagsByVisibility, findBestFuzzyMatch, findTagContributionCycles, findUnresolvedChoices, fixtureBackSquat, fixtureBurpee, fixturePlank, fixtureRowing, fixtureRunning, fixtureUserCustom, fixtureUserOverride, formatDim, formatDuration, formatRuntimeLogLine, formatRuntimeLogRule, formatRuntimeTimestamp, formatSoundTrigger, metricsToLabel as fragmentsToLabel, fullFixtureSet, fuseUnits, fuseUnitsInMetrics, getAnalyticsFromLogs, getAnalyticsFromRuntime, getHints, getMetricVisibility, getUnit, hasHint, hintMetric, hintsToContainer, inferDim, isCastSubscription, isChoiceResolved, isDimensionless, isEffortDiscipline, isFragmentTag, isTimeLikeMetric, isWithinThreshold, levenshteinDistance, metricPresentation, mutateTimerSpans, normalizeForFuzzy, num, openSpan, outputNodeId, outputNodeIds, padRuntimeLogEvent, padRuntimeLogSubject, parseCalcLine, parseCalculateBlock, parseEffortFile, parseEquipmentFile, parseExpression, parseMetricLine, parseScript, parseSoundTrigger, printCalculation, readNestedSection, readTimer, realtimeProcessorRegistry, renderMetric, resolveDominantOrigin, resolveEntityRank, resolveEntityTier, resolveProfileRules, resolveSoundProfile, resolveSourceRank, resolveSourceTier, resolveTagContributions, slugifyRefLabel, startSpan, str, strategyRegistry, subDim, summaryProcessorRegistry, tagContributionMetrics, tagMarkerSlug, tagMarkerSlugs, tagMarkerType, toStoredOutputStatement, truthy, validateOutputUnit, parser as whiteboardParser, whiteboardScript, whiteboardScriptLanguage, writeChoiceSelection };
14770
15138
  //# sourceMappingURL=index.mjs.map
14771
15139
  //# sourceMappingURL=index.mjs.map