@bitcobblers/wod-wiki-lang 0.36.2136 → 0.37.2139

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -418,15 +418,19 @@ function createPolicy() {
418
418
  }
419
419
  var metricPresentation = createPolicy();
420
420
  var EffortMetric = class {
421
- constructor(effort) {
421
+ constructor(effort, options) {
422
422
  this.effort = effort;
423
- this.value = effort;
423
+ this.value = options?.resolvedSlug ?? effort;
424
424
  this.image = effort;
425
+ if (options?.references) {
426
+ this.metadata = { [wodWikiCore.REFERENCE_METADATA_KEY]: options.references };
427
+ }
425
428
  }
426
429
  effort;
427
430
  value;
428
431
  image;
429
432
  origin = "parser";
433
+ metadata;
430
434
  type = wodWikiCore.MetricType.Effort;
431
435
  };
432
436
  var DistanceMetric = class {
@@ -1394,6 +1398,27 @@ var DurationMetric = class {
1394
1398
  return this.value > 0 ? "down" : "up";
1395
1399
  }
1396
1400
  };
1401
+ var EquipmentMetric = class {
1402
+ type = wodWikiCore.MetricType.Equipment;
1403
+ origin;
1404
+ /** Canonical slugs, ordered and deduplicated. */
1405
+ value;
1406
+ image;
1407
+ name = "equipment";
1408
+ metadata;
1409
+ constructor(slugs, options) {
1410
+ this.value = [...new Set(slugs)];
1411
+ this.image = options?.source ?? this.value.join(" ");
1412
+ this.origin = options?.origin ?? "parser";
1413
+ const reference = {
1414
+ matched: options?.matched ?? "whole",
1415
+ source: options?.source ?? this.image,
1416
+ ...options?.refs ? { refs: options.refs } : {},
1417
+ ...options?.diagnostics?.length ? { diagnostics: options.diagnostics } : {}
1418
+ };
1419
+ this.metadata = { [wodWikiCore.REFERENCE_METADATA_KEY]: reference };
1420
+ }
1421
+ };
1397
1422
  var GroupMetric = class {
1398
1423
  constructor(group, image) {
1399
1424
  this.group = group;
@@ -1490,16 +1515,29 @@ var PROPERTY_KEY_TO_METRIC_TYPE = {
1490
1515
  volume: wodWikiCore.MetricType.Volume,
1491
1516
  work: wodWikiCore.MetricType.Work
1492
1517
  };
1493
- function classifyStatements(facts) {
1518
+ var STRUCTURAL_EFFORT_WORDS = /* @__PURE__ */ new Set(["rest", "amrap", "emom", "tabata", "for time"]);
1519
+ var STANDARD_UNITS2 = UnitRegistry.standard();
1520
+ var REFERENCE_PAIR_KEYS = /* @__PURE__ */ new Set(["effort", "equipment"]);
1521
+ function referenceMetadata(resolution) {
1522
+ return {
1523
+ matched: resolution.matched,
1524
+ source: resolution.label,
1525
+ ...resolution.effortRef || resolution.equipment.length > 0 ? { refs: [...resolution.effortRef ? [resolution.effortRef] : [], ...resolution.equipment] } : {},
1526
+ ...resolution.remaining ? { remaining: resolution.remaining } : {},
1527
+ ...resolution.diagnostics.length > 0 ? { diagnostics: resolution.diagnostics } : {}
1528
+ };
1529
+ }
1530
+ function classifyStatements(facts, references) {
1494
1531
  return facts.statements.map((fact) => {
1495
1532
  const statement = new wodWikiCore.ParsedCodeStatement();
1496
- const metricPairs = [];
1533
+ let metricPairs = [];
1497
1534
  for (const primitive of fact.primitives) {
1498
1535
  metricPairs.push(...classifyPrimitive(primitive));
1499
1536
  }
1500
1537
  const mergedPairs = mergeFragments(metricPairs);
1501
- statement.metrics = mergedPairs.map((pair) => pair.metrics);
1502
- statement.metricMeta = new Map(mergedPairs.map((pair) => [pair.metrics, pair.meta]));
1538
+ const finalPairs = references ? applyReferenceResolution(mergedPairs, fact, references) : mergedPairs;
1539
+ statement.metrics = finalPairs.map((pair) => pair.metrics);
1540
+ statement.metricMeta = new Map(finalPairs.map((pair) => [pair.metrics, pair.meta]));
1503
1541
  statement.id = fact.id;
1504
1542
  statement.line = fact.line;
1505
1543
  statement.text = fact.meta.raw;
@@ -1511,6 +1549,105 @@ function classifyStatements(facts) {
1511
1549
  return statement;
1512
1550
  });
1513
1551
  }
1552
+ function applyReferenceResolution(pairs, fact, references) {
1553
+ const { index, defaults } = references;
1554
+ const result = [];
1555
+ const emitResolution = (resolution, authored, meta2) => {
1556
+ if (resolution.effortRef || resolution.matched !== "none" || resolution.diagnostics.length > 0) {
1557
+ result.push({
1558
+ metrics: new EffortMetric(authored, {
1559
+ resolvedSlug: resolution.effortRef?.slug,
1560
+ references: referenceMetadata(resolution)
1561
+ }),
1562
+ meta: meta2
1563
+ });
1564
+ }
1565
+ if (resolution.equipment.length > 0) {
1566
+ result.push({
1567
+ metrics: new EquipmentMetric(
1568
+ resolution.equipment.map((ref) => ref.slug),
1569
+ {
1570
+ refs: resolution.equipment,
1571
+ source: authored,
1572
+ matched: resolution.matched,
1573
+ diagnostics: resolution.diagnostics
1574
+ }
1575
+ ),
1576
+ meta: meta2
1577
+ });
1578
+ }
1579
+ };
1580
+ const handledRawObjects = /* @__PURE__ */ new Set();
1581
+ const malformedHints = [];
1582
+ for (const primitive of fact.primitives) {
1583
+ if (primitive.kind !== "metric_object") continue;
1584
+ if (handledRawObjects.has(primitive.raw)) continue;
1585
+ handledRawObjects.add(primitive.raw);
1586
+ if (primitive.pairs.length === 0) {
1587
+ malformedHints.push({
1588
+ metrics: {
1589
+ ...hintMetric("reference.malformed_json", "parser"),
1590
+ metadata: referenceMetadata(index.resolve(primitive.raw))
1591
+ },
1592
+ meta: primitive.meta
1593
+ });
1594
+ continue;
1595
+ }
1596
+ const hasReferenceKeys = primitive.pairs.some((pair) => REFERENCE_PAIR_KEYS.has(pair.key.toLowerCase()));
1597
+ if (!hasReferenceKeys) continue;
1598
+ const resolution = index.resolve(primitive.raw);
1599
+ if (resolution.matched === "explicit") {
1600
+ emitResolution(resolution, primitive.raw, primitive.meta);
1601
+ }
1602
+ for (const pair of primitive.pairs) {
1603
+ if (REFERENCE_PAIR_KEYS.has(pair.key.toLowerCase())) continue;
1604
+ const metricType = PROPERTY_KEY_TO_METRIC_TYPE[pair.key.toLowerCase()] ?? wodWikiCore.MetricType.Custom;
1605
+ const rawValue = Array.isArray(pair.value) ? pair.value.join(", ") : pair.value;
1606
+ result.push({
1607
+ metrics: new PropertyMetric(pair.key, rawValue, { type: metricType, image: `${pair.key}: ${rawValue}` }),
1608
+ meta: primitive.meta
1609
+ });
1610
+ }
1611
+ }
1612
+ let sawEffortMetric = false;
1613
+ let sawDurationMetric = false;
1614
+ let sawMovementQuantity = false;
1615
+ let statementMeta;
1616
+ for (const pair of pairs) {
1617
+ const metric = pair.metrics;
1618
+ if (metric.type === wodWikiCore.MetricType.Custom && handledRawObjects.has(pair.meta.raw)) continue;
1619
+ if (metric.type === wodWikiCore.MetricType.Effort) {
1620
+ const authored = metric instanceof EffortMetric ? metric.effort : String(metric.value ?? "");
1621
+ const normalized = authored.trim().toLowerCase();
1622
+ if (STRUCTURAL_EFFORT_WORDS.has(normalized) || STANDARD_UNITS2.has(normalized)) {
1623
+ result.push(pair);
1624
+ continue;
1625
+ }
1626
+ sawEffortMetric = true;
1627
+ const resolution = index.resolve(authored, defaults);
1628
+ if (resolution.matched === "none" && resolution.diagnostics.length === 0) {
1629
+ result.push(pair);
1630
+ continue;
1631
+ }
1632
+ emitResolution(resolution, authored, pair.meta);
1633
+ continue;
1634
+ }
1635
+ if (metric.type === wodWikiCore.MetricType.Duration) sawDurationMetric = true;
1636
+ if (metric.type === wodWikiCore.MetricType.Rep || metric.type === wodWikiCore.MetricType.Rounds || metric.type === wodWikiCore.MetricType.Resistance || metric.type === wodWikiCore.MetricType.Distance) {
1637
+ sawMovementQuantity = true;
1638
+ }
1639
+ result.push(pair);
1640
+ statementMeta ??= pair.meta;
1641
+ }
1642
+ if (defaults && !sawEffortMetric && fact.isLeaf && sawMovementQuantity && !sawDurationMetric) {
1643
+ const resolution = index.resolve("", defaults);
1644
+ if (resolution.effortRef) {
1645
+ emitResolution(resolution, resolution.effortRef.label, statementMeta ?? fact.meta);
1646
+ }
1647
+ }
1648
+ result.push(...malformedHints);
1649
+ return result;
1650
+ }
1514
1651
  function classifyPrimitive(primitive) {
1515
1652
  switch (primitive.kind) {
1516
1653
  case "lap":
@@ -1579,10 +1716,11 @@ function classifyPrimitive(primitive) {
1579
1716
  const pairs = primitive.pairs;
1580
1717
  return pairs.map((pair) => {
1581
1718
  const metricType = PROPERTY_KEY_TO_METRIC_TYPE[pair.key.toLowerCase()] ?? wodWikiCore.MetricType.Custom;
1719
+ const rawValue = Array.isArray(pair.value) ? pair.value.join(", ") : pair.value;
1582
1720
  return {
1583
- metrics: new PropertyMetric(pair.key, pair.value, {
1721
+ metrics: new PropertyMetric(pair.key, rawValue, {
1584
1722
  type: metricType,
1585
- image: `${pair.key}: ${pair.value}`
1723
+ image: `${pair.key}: ${rawValue}`
1586
1724
  }),
1587
1725
  meta: primitive.meta
1588
1726
  };
@@ -1620,6 +1758,23 @@ function mergeFragments(pairs) {
1620
1758
  continue;
1621
1759
  }
1622
1760
  }
1761
+ if (current.metrics instanceof RepMetric && next.metrics instanceof EffortMetric && /^(reps?)$/i.test(next.metrics.effort)) {
1762
+ const gap = next.meta.startOffset - current.meta.endOffset;
1763
+ if (gap <= 1) {
1764
+ const mergedMeta = {
1765
+ ...current.meta,
1766
+ endOffset: next.meta.endOffset,
1767
+ columnEnd: next.meta.columnEnd,
1768
+ length: next.meta.endOffset - current.meta.startOffset,
1769
+ raw: current.meta.raw + (gap === 1 ? " " : "") + next.meta.raw
1770
+ };
1771
+ current = {
1772
+ metrics: current.metrics,
1773
+ meta: mergedMeta
1774
+ };
1775
+ continue;
1776
+ }
1777
+ }
1623
1778
  if (current.metrics instanceof RepMetric && (next.metrics instanceof ResistanceMetric || next.metrics instanceof DistanceMetric)) {
1624
1779
  const gap = next.meta.startOffset - current.meta.endOffset;
1625
1780
  if (gap <= 1) {
@@ -1995,12 +2150,12 @@ function resolveSourceInput(sourceOrTree, treeArg) {
1995
2150
  }
1996
2151
  return { source: "" };
1997
2152
  }
1998
- function extractStatementsRaw(sourceOrTree, treeArg) {
2153
+ function extractStatementsRaw(sourceOrTree, treeArg, references) {
1999
2154
  const { source, tree } = resolveSourceInput(sourceOrTree, treeArg);
2000
2155
  const facts = extractSyntaxFacts(source, tree);
2001
- return normalizeInlineRounds(classifyStatements(facts));
2156
+ return normalizeInlineRounds(classifyStatements(facts, references));
2002
2157
  }
2003
- function extractStatements(sourceOrTree, sportOrTree, sportArg) {
2158
+ function extractStatements(sourceOrTree, sportOrTree, sportArg, references) {
2004
2159
  let sport;
2005
2160
  let tree;
2006
2161
  if (typeof sportOrTree === "string") {
@@ -2011,7 +2166,7 @@ function extractStatements(sourceOrTree, sportOrTree, sportArg) {
2011
2166
  }
2012
2167
  const resolved = resolveSourceInput(sourceOrTree, tree);
2013
2168
  const facts = extractSyntaxFacts(resolved.source, resolved.tree);
2014
- const statements = classifyStatements(facts);
2169
+ const statements = classifyStatements(facts, references);
2015
2170
  dialectStack.processAll(statements, sport);
2016
2171
  return normalizeInlineRounds(statements);
2017
2172
  }
@@ -2143,7 +2298,8 @@ function parseScript(inputText, options = {}) {
2143
2298
  const doc = inputText.endsWith("\n") ? inputText : inputText + "\n";
2144
2299
  const tree = parser.parse(doc);
2145
2300
  const sport = options.sport ?? options.dialect;
2146
- const statements = options.withoutDialects ? extractStatementsRaw(inputText, tree) : extractStatements(inputText, tree, sport);
2301
+ const references = options.references ? { index: options.references, defaults: options.defaults } : void 0;
2302
+ const statements = options.withoutDialects ? extractStatementsRaw(inputText, tree, references) : extractStatements(inputText, tree, sport, references);
2147
2303
  return new WhiteboardScript(inputText, statements, []);
2148
2304
  } catch (error) {
2149
2305
  const msg = error instanceof Error ? error.message : "Unknown parse error";
@@ -2153,6 +2309,10 @@ function parseScript(inputText, options = {}) {
2153
2309
 
2154
2310
  // src/parser/md-timer.ts
2155
2311
  var MdTimerRuntime = class {
2312
+ config;
2313
+ constructor(config) {
2314
+ this.config = config;
2315
+ }
2156
2316
  /**
2157
2317
  * Parse WhiteboardScript source into a {@link WhiteboardScript}, running the
2158
2318
  * Dialect Stack on every statement.
@@ -2160,22 +2320,21 @@ var MdTimerRuntime = class {
2160
2320
  * @param sport - The block's `:sport` fence suffix (` ```log:climbing `).
2161
2321
  */
2162
2322
  read(inputText, sport) {
2163
- return parseScript(inputText, { sport });
2323
+ return parseScript(inputText, { sport, ...this.config });
2164
2324
  }
2165
2325
  /**
2166
2326
  * Parse without running the Dialect Stack. Used by the parser test harness
2167
2327
  * which applies its own Dialect set. Production consumers should use {@link read}.
2168
2328
  */
2169
2329
  readWithoutDialects(inputText) {
2170
- return parseScript(inputText, { withoutDialects: true });
2330
+ return parseScript(inputText, { withoutDialects: true, ...this.config });
2171
2331
  }
2172
2332
  };
2173
2333
 
2174
2334
  // src/parser/parserInstance.ts
2175
- function createParser() {
2176
- return new MdTimerRuntime();
2335
+ function createParser(config) {
2336
+ return new MdTimerRuntime(config);
2177
2337
  }
2178
- var sharedParser = /* @__PURE__ */ createParser();
2179
2338
  var nextTagID = 0;
2180
2339
  var Tag = class _Tag {
2181
2340
  /**
@@ -2878,6 +3037,426 @@ function disciplineFactorFor(discipline) {
2878
3037
  const key = discipline.toLowerCase();
2879
3038
  return isEffortDiscipline(key) ? DISCIPLINE_FACTORS[key] : 1;
2880
3039
  }
3040
+ var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
3041
+ var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3042
+ function unquote(value) {
3043
+ return value.replace(
3044
+ /^(['"])(.*)\1$/,
3045
+ (_match, quote, inner) => quote === '"' ? inner.replace(
3046
+ /\\(["\\nrt])/g,
3047
+ (_all, c) => c === "n" ? "\n" : c === "r" ? "\r" : c === "t" ? " " : c
3048
+ ) : inner
3049
+ );
3050
+ }
3051
+ function quoteYaml(val) {
3052
+ if (!val) return '""';
3053
+ if (/[:"'\n#{}[\],&*?|\-<>=%!@`]/.test(val) || val !== val.trim()) {
3054
+ return `"${val.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
3055
+ }
3056
+ return val;
3057
+ }
3058
+ function parseFrontmatter(raw) {
3059
+ const match = raw.match(FRONTMATTER_RE);
3060
+ if (!match) return { meta: {}, body: raw };
3061
+ const lines = match[1].split(/\r?\n/);
3062
+ const meta2 = {};
3063
+ for (let i = 0; i < lines.length; i++) {
3064
+ const keyMatch = lines[i].match(/^([A-Za-z][\w.-]*)\s*:(.*)$/);
3065
+ if (!keyMatch) continue;
3066
+ const key = keyMatch[1];
3067
+ const rawVal = keyMatch[2].trim();
3068
+ if (rawVal === "") {
3069
+ const items = [];
3070
+ while (i + 1 < lines.length) {
3071
+ const itemMatch = lines[i + 1].match(/^\s+-\s+(.+)$/);
3072
+ if (!itemMatch) break;
3073
+ items.push(unquote(itemMatch[1].trim()));
3074
+ i++;
3075
+ }
3076
+ meta2[key] = items.length > 0 ? items : "";
3077
+ } else {
3078
+ const wasQuoted = /^(['"])(.*)\1$/.test(rawVal);
3079
+ const value = unquote(rawVal);
3080
+ const num2 = Number(value);
3081
+ meta2[key] = !wasQuoted && value !== "" && !isNaN(num2) ? num2 : value;
3082
+ }
3083
+ }
3084
+ return { meta: meta2, body: raw.slice(match[0].length) };
3085
+ }
3086
+ function getScalar(meta2, key) {
3087
+ const value = meta2[key];
3088
+ return typeof value === "string" || typeof value === "number" ? value : void 0;
3089
+ }
3090
+ function getListStrict(meta2, key) {
3091
+ const value = meta2[key];
3092
+ if (value === void 0) return [];
3093
+ if (typeof value === "string" && value === "[]") return [];
3094
+ return Array.isArray(value) ? value : void 0;
3095
+ }
3096
+ function coerceNestedScalar(raw) {
3097
+ if (raw === "true") return true;
3098
+ if (raw === "false") return false;
3099
+ if (raw !== "" && !Number.isNaN(Number(raw))) return Number(raw);
3100
+ return unquote(raw);
3101
+ }
3102
+ function readNestedSection(lines, key) {
3103
+ const start = lines.findIndex((l) => new RegExp(`^${key}:\\s*$`).test(l));
3104
+ if (start === -1) return void 0;
3105
+ const result = {};
3106
+ let subKey = null;
3107
+ for (let i = start + 1; i < lines.length; i++) {
3108
+ const line = lines[i];
3109
+ if (/^\S/.test(line)) break;
3110
+ const trimmed = line.trim();
3111
+ if (!trimmed || trimmed.startsWith("#")) continue;
3112
+ const indent = line.length - line.trimStart().length;
3113
+ const m = trimmed.match(/^([\w.-]+):\s*(.*)$/);
3114
+ if (!m) continue;
3115
+ const [, k, rawVal] = m;
3116
+ if (indent === 2) {
3117
+ if (rawVal === "") {
3118
+ subKey = k;
3119
+ result[k] = {};
3120
+ } else {
3121
+ subKey = null;
3122
+ result[k] = coerceNestedScalar(rawVal);
3123
+ }
3124
+ } else if (indent >= 4 && subKey !== null) {
3125
+ result[subKey][k] = coerceNestedScalar(rawVal);
3126
+ }
3127
+ }
3128
+ return Object.keys(result).length > 0 ? result : void 0;
3129
+ }
3130
+ function parseDiscipline(val) {
3131
+ if (!val) return void 0;
3132
+ const normalized = unquote(val.trim()).toLowerCase();
3133
+ if (isEffortDiscipline(normalized)) return normalized;
3134
+ console.warn(`[definition-markdown] unknown discipline '${val}' \u2014 expected one of the canonical values; treating as unset`);
3135
+ return void 0;
3136
+ }
3137
+ function identityProblems(slug, label) {
3138
+ const problems = [];
3139
+ const slugText = slug === void 0 ? "" : String(slug);
3140
+ if (!slugText) problems.push("Missing required field: slug");
3141
+ else if (!SLUG_RE.test(slugText)) problems.push("Invalid slug: must be lowercase letters, numbers, and hyphens only");
3142
+ if (label === void 0 || !String(label).trim()) problems.push("Missing required field: label");
3143
+ return problems;
3144
+ }
3145
+ function stringListProblems(values, kind) {
3146
+ const problems = [];
3147
+ for (const value of values) {
3148
+ if (!value.trim()) problems.push(`Invalid ${kind}: blank entries are not allowed`);
3149
+ else if (kind === "equipment" && !SLUG_RE.test(value)) {
3150
+ problems.push(`Invalid ${kind}: '${value}' must be a canonical slug (lowercase letters, numbers, hyphens)`);
3151
+ }
3152
+ }
3153
+ const duplicate = values.find((v, i) => values.indexOf(v) !== i);
3154
+ if (duplicate !== void 0) problems.push(`Invalid ${kind}: duplicate entry '${duplicate}'`);
3155
+ return problems;
3156
+ }
3157
+ function metProblem(value) {
3158
+ if (value === void 0) return "Missing required field: baseAttributes.met";
3159
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
3160
+ return "Invalid baseAttributes.met: must be a positive number";
3161
+ }
3162
+ return null;
3163
+ }
3164
+ function parseEffortFile(raw) {
3165
+ const blockMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
3166
+ if (!blockMatch) return null;
3167
+ const innerLines = blockMatch[1].split(/\r?\n/);
3168
+ const { meta: meta2, body } = parseFrontmatter(raw);
3169
+ const type = getScalar(meta2, "type");
3170
+ if (type !== void 0 && type !== "effort") return null;
3171
+ const id = getScalar(meta2, "id");
3172
+ const slug = getScalar(meta2, "slug");
3173
+ const label = getScalar(meta2, "label");
3174
+ const aliases = getListStrict(meta2, "aliases");
3175
+ const equipment = getListStrict(meta2, "equipment");
3176
+ if (aliases === void 0 || equipment === void 0) return null;
3177
+ if (identityProblems(slug, label).length > 0) return null;
3178
+ if (stringListProblems(aliases, "aliases").length > 0) return null;
3179
+ if (equipment.length > 0 && stringListProblems(equipment, "equipment").length > 0) return null;
3180
+ const baseAttrs = readNestedSection(innerLines, "baseAttributes") ?? {};
3181
+ if (metProblem(baseAttrs.met) !== null) return null;
3182
+ const met = baseAttrs.met;
3183
+ const discipline = parseDiscipline(
3184
+ typeof baseAttrs.discipline === "string" ? baseAttrs.discipline : void 0
3185
+ );
3186
+ const intensityTier = baseAttrs.intensityTier;
3187
+ const disciplineFactor = typeof baseAttrs.disciplineFactor === "number" && Number.isFinite(baseAttrs.disciplineFactor) ? baseAttrs.disciplineFactor : void 0;
3188
+ const hints = readNestedSection(innerLines, "hints");
3189
+ const derivation = readNestedSection(innerLines, "derivation");
3190
+ const trimmedBody = body.trim();
3191
+ return {
3192
+ id: String(id ?? ""),
3193
+ slug: String(slug),
3194
+ label: String(label),
3195
+ aliases,
3196
+ baseAttributes: {
3197
+ met,
3198
+ ...discipline ? { discipline } : {},
3199
+ ...disciplineFactor !== void 0 ? { disciplineFactor } : {},
3200
+ ...intensityTier ? { intensityTier } : {}
3201
+ },
3202
+ registrySource: "bundled",
3203
+ ...equipment.length > 0 ? { equipment } : {},
3204
+ ...trimmedBody ? { body: trimmedBody } : {},
3205
+ ...hints ? { hints } : {},
3206
+ ...derivation ? { derivation } : {}
3207
+ };
3208
+ }
3209
+ function parseEquipmentFile(raw) {
3210
+ const { meta: meta2, body } = parseFrontmatter(raw);
3211
+ if (getScalar(meta2, "type") !== "equipment") return null;
3212
+ const id = getScalar(meta2, "id");
3213
+ const slug = getScalar(meta2, "slug");
3214
+ const label = getScalar(meta2, "label");
3215
+ const aliases = getListStrict(meta2, "aliases");
3216
+ if (id === void 0 || aliases === void 0) return null;
3217
+ if (identityProblems(slug, label).length > 0) return null;
3218
+ if (stringListProblems(aliases, "aliases").length > 0) return null;
3219
+ const trimmedBody = body.trim();
3220
+ return {
3221
+ id: String(id),
3222
+ slug: String(slug),
3223
+ label: String(label),
3224
+ aliases,
3225
+ registrySource: "bundled",
3226
+ ...trimmedBody ? { body: trimmedBody } : {}
3227
+ };
3228
+ }
3229
+ function createDefaultEffort() {
3230
+ return {
3231
+ id: `effort-user-${uuid.v7()}`,
3232
+ slug: "",
3233
+ label: "",
3234
+ aliases: [],
3235
+ baseAttributes: { met: 5 },
3236
+ registrySource: "user"
3237
+ };
3238
+ }
3239
+ function documentToEffort(doc, baseEffort) {
3240
+ const errors = [];
3241
+ const blockMatch = doc.match(/^---\r?\n([\s\S]*?)\r?\n---/);
3242
+ if (!blockMatch) {
3243
+ errors.push("Missing YAML frontmatter delimiters (---)");
3244
+ return { effort: baseEffort ?? createDefaultEffort(), errors };
3245
+ }
3246
+ const innerLines = blockMatch[1].split(/\r?\n/);
3247
+ const { meta: meta2, body } = parseFrontmatter(doc);
3248
+ const bodyText = body.replace(/^\r?\n+/, "") || void 0;
3249
+ const id = getScalar(meta2, "id");
3250
+ const slug = getScalar(meta2, "slug");
3251
+ const label = getScalar(meta2, "label");
3252
+ const registrySource = getScalar(meta2, "registrySource");
3253
+ const createdAt = getScalar(meta2, "createdAt");
3254
+ const updatedAt = getScalar(meta2, "updatedAt");
3255
+ const aliases = getListStrict(meta2, "aliases");
3256
+ const equipmentRaw = meta2["equipment"];
3257
+ const equipment = getListStrict(meta2, "equipment");
3258
+ if (aliases === void 0) {
3259
+ errors.push("Invalid aliases: must be a list (`aliases:` followed by `- item` lines)");
3260
+ }
3261
+ if (equipment === void 0) {
3262
+ errors.push("Invalid equipment: must be a list (`equipment:` followed by `- slug` lines)");
3263
+ }
3264
+ const baseAttrs = readNestedSection(innerLines, "baseAttributes") ?? {};
3265
+ const baseAttributes = {};
3266
+ const metRaw = baseAttrs.met;
3267
+ if (metRaw === void 0) {
3268
+ const fallbackMet = baseEffort?.baseAttributes.met;
3269
+ const fallbackProblem = fallbackMet === void 0 ? null : metProblem(fallbackMet);
3270
+ if (fallbackProblem) errors.push(fallbackProblem);
3271
+ else if (fallbackMet !== void 0) baseAttributes.met = fallbackMet;
3272
+ else errors.push("Missing required field: baseAttributes.met");
3273
+ } else {
3274
+ const problem = metProblem(metRaw);
3275
+ if (problem) errors.push(problem);
3276
+ else baseAttributes.met = Number(metRaw);
3277
+ }
3278
+ if (getScalar(meta2, "met") !== void 0) {
3279
+ errors.push(
3280
+ "Invalid met placement: 'met' must be nested under baseAttributes (missing baseAttributes.met)"
3281
+ );
3282
+ }
3283
+ const discipline = parseDiscipline(
3284
+ typeof baseAttrs.discipline === "string" ? baseAttrs.discipline : void 0
3285
+ );
3286
+ if (discipline) baseAttributes.discipline = discipline;
3287
+ if (typeof baseAttrs.disciplineFactor === "number" && Number.isFinite(baseAttrs.disciplineFactor)) {
3288
+ baseAttributes.disciplineFactor = baseAttrs.disciplineFactor;
3289
+ }
3290
+ if (baseAttrs.intensityTier) baseAttributes.intensityTier = baseAttrs.intensityTier;
3291
+ const hints = readNestedSection(innerLines, "hints") ?? baseEffort?.hints;
3292
+ const derivation = readNestedSection(innerLines, "derivation");
3293
+ errors.push(...identityProblems(slug, label));
3294
+ if (aliases !== void 0) errors.push(...stringListProblems(aliases, "aliases"));
3295
+ if (equipment !== void 0 && equipment.length > 0) {
3296
+ errors.push(...stringListProblems(equipment, "equipment"));
3297
+ }
3298
+ const resolvedEquipment = equipmentRaw !== void 0 ? equipment ?? [] : baseEffort?.equipment?.length ? baseEffort.equipment : void 0;
3299
+ const effort = {
3300
+ id: String(id ?? "") || baseEffort?.id || `effort-user-${uuid.v7()}`,
3301
+ slug: String(slug ?? ""),
3302
+ label: String(label ?? ""),
3303
+ aliases: aliases ?? [],
3304
+ baseAttributes,
3305
+ registrySource: registrySource || baseEffort?.registrySource || "user",
3306
+ ...resolvedEquipment ? { equipment: resolvedEquipment } : {},
3307
+ ...createdAt ? { createdAt: String(createdAt) } : {},
3308
+ ...updatedAt ? { updatedAt: String(updatedAt) } : {},
3309
+ ...bodyText ? { body: bodyText } : {},
3310
+ ...hints && Object.keys(hints).length > 0 ? { hints } : {},
3311
+ ...derivation ? { derivation } : {}
3312
+ };
3313
+ return { effort, errors };
3314
+ }
3315
+ function effortToDocument(effort) {
3316
+ const lines = ["---"];
3317
+ lines.push(`id: ${effort.id}`);
3318
+ lines.push(`slug: ${effort.slug}`);
3319
+ lines.push(`label: ${quoteYaml(effort.label)}`);
3320
+ if (effort.aliases.length > 0) {
3321
+ lines.push("aliases:");
3322
+ for (const alias of effort.aliases) {
3323
+ lines.push(` - ${quoteYaml(alias)}`);
3324
+ }
3325
+ } else {
3326
+ lines.push("aliases: []");
3327
+ }
3328
+ lines.push("baseAttributes:");
3329
+ lines.push(` met: ${effort.baseAttributes.met}`);
3330
+ if (effort.baseAttributes.discipline) {
3331
+ lines.push(` discipline: ${effort.baseAttributes.discipline}`);
3332
+ }
3333
+ if (effort.baseAttributes.disciplineFactor !== void 0) {
3334
+ lines.push(` disciplineFactor: ${effort.baseAttributes.disciplineFactor}`);
3335
+ }
3336
+ if (effort.baseAttributes.intensityTier) {
3337
+ lines.push(` intensityTier: ${effort.baseAttributes.intensityTier}`);
3338
+ }
3339
+ lines.push(`registrySource: ${effort.registrySource}`);
3340
+ if (effort.equipment && effort.equipment.length > 0) {
3341
+ lines.push("equipment:");
3342
+ for (const slug of effort.equipment) {
3343
+ lines.push(` - ${quoteYaml(slug)}`);
3344
+ }
3345
+ }
3346
+ if (effort.derivation) {
3347
+ lines.push("derivation:");
3348
+ if (effort.derivation.parentSlug) {
3349
+ lines.push(` parentSlug: ${effort.derivation.parentSlug}`);
3350
+ }
3351
+ if (effort.derivation.coefficients && Object.keys(effort.derivation.coefficients).length > 0) {
3352
+ lines.push(" coefficients:");
3353
+ for (const [k, v] of Object.entries(effort.derivation.coefficients)) {
3354
+ lines.push(` ${k}: ${v}`);
3355
+ }
3356
+ }
3357
+ if (effort.derivation.hardOverrides && Object.keys(effort.derivation.hardOverrides).length > 0) {
3358
+ lines.push(" hardOverrides:");
3359
+ for (const [k, v] of Object.entries(effort.derivation.hardOverrides)) {
3360
+ lines.push(` ${k}: ${JSON.stringify(v)}`);
3361
+ }
3362
+ }
3363
+ }
3364
+ if (effort.createdAt) {
3365
+ lines.push(`createdAt: ${effort.createdAt}`);
3366
+ }
3367
+ if (effort.updatedAt) {
3368
+ lines.push(`updatedAt: ${effort.updatedAt}`);
3369
+ }
3370
+ if (effort.hints && Object.keys(effort.hints).length > 0) {
3371
+ lines.push("hints:");
3372
+ for (const [k, v] of Object.entries(effort.hints)) {
3373
+ lines.push(` ${k}: ${v !== null && typeof v === "object" ? JSON.stringify(v) : String(v)}`);
3374
+ }
3375
+ }
3376
+ lines.push("---");
3377
+ if (effort.body) {
3378
+ lines.push("");
3379
+ lines.push(effort.body);
3380
+ }
3381
+ return lines.join("\n");
3382
+ }
3383
+ function createDefaultEquipment() {
3384
+ return {
3385
+ id: `equipment-user-${uuid.v7()}`,
3386
+ slug: "",
3387
+ label: "",
3388
+ aliases: [],
3389
+ registrySource: "user"
3390
+ };
3391
+ }
3392
+ function documentToEquipment(doc, baseEquipment) {
3393
+ const errors = [];
3394
+ const blockMatch = doc.match(FRONTMATTER_RE);
3395
+ if (!blockMatch) {
3396
+ errors.push("Missing YAML frontmatter delimiters (---)");
3397
+ return { equipment: baseEquipment ?? createDefaultEquipment(), errors };
3398
+ }
3399
+ const { meta: meta2, body } = parseFrontmatter(doc);
3400
+ const bodyText = body.replace(/^\r?\n+/, "") || void 0;
3401
+ const type = getScalar(meta2, "type");
3402
+ const id = getScalar(meta2, "id");
3403
+ const slug = getScalar(meta2, "slug");
3404
+ const label = getScalar(meta2, "label");
3405
+ const registrySource = getScalar(meta2, "registrySource");
3406
+ const createdAt = getScalar(meta2, "createdAt");
3407
+ const updatedAt = getScalar(meta2, "updatedAt");
3408
+ const aliases = getListStrict(meta2, "aliases");
3409
+ if (type !== "equipment") errors.push("Missing or invalid discriminator: frontmatter must declare `type: equipment`");
3410
+ if (!slug || !SLUG_RE.test(String(slug))) errors.push("Invalid slug: must be a canonical slug (lowercase letters, numbers, hyphens)");
3411
+ if (!label) errors.push("Missing required field: label");
3412
+ if (aliases === void 0) {
3413
+ errors.push("Invalid aliases: must be a list (`aliases:` followed by `- item` lines)");
3414
+ } else {
3415
+ errors.push(...stringListProblems(aliases, "aliases"));
3416
+ }
3417
+ if (registrySource !== void 0 && registrySource !== "bundled" && registrySource !== "user") {
3418
+ errors.push("Invalid registrySource: must be 'bundled' or 'user'");
3419
+ }
3420
+ const equipment = {
3421
+ id: String(id ?? "") || baseEquipment?.id || `equipment-user-${uuid.v7()}`,
3422
+ slug: String(slug ?? ""),
3423
+ label: String(label ?? ""),
3424
+ aliases: aliases ?? [],
3425
+ registrySource: registrySource || baseEquipment?.registrySource || "user",
3426
+ ...createdAt ? { createdAt: String(createdAt) } : {},
3427
+ ...updatedAt ? { updatedAt: String(updatedAt) } : {},
3428
+ ...bodyText ? { body: bodyText } : {}
3429
+ };
3430
+ return { equipment, errors };
3431
+ }
3432
+ function equipmentToDocument(equipment) {
3433
+ const lines = ["---"];
3434
+ lines.push("type: equipment");
3435
+ lines.push(`id: ${equipment.id}`);
3436
+ lines.push(`slug: ${equipment.slug}`);
3437
+ lines.push(`label: ${quoteYaml(equipment.label)}`);
3438
+ if (equipment.aliases.length > 0) {
3439
+ lines.push("aliases:");
3440
+ for (const alias of equipment.aliases) {
3441
+ lines.push(` - ${quoteYaml(alias)}`);
3442
+ }
3443
+ } else {
3444
+ lines.push("aliases: []");
3445
+ }
3446
+ lines.push(`registrySource: ${equipment.registrySource}`);
3447
+ if (equipment.createdAt) {
3448
+ lines.push(`createdAt: ${equipment.createdAt}`);
3449
+ }
3450
+ if (equipment.updatedAt) {
3451
+ lines.push(`updatedAt: ${equipment.updatedAt}`);
3452
+ }
3453
+ lines.push("---");
3454
+ if (equipment.body) {
3455
+ lines.push("");
3456
+ lines.push(equipment.body);
3457
+ }
3458
+ return lines.join("\n");
3459
+ }
2881
3460
 
2882
3461
  // src/effort-registry/fuzzyMatch.ts
2883
3462
  function levenshteinDistance(a, b) {
@@ -3262,12 +3841,27 @@ var CompositeEffortRegistry = class {
3262
3841
  * Initialize the registry:
3263
3842
  * 1. Load bundled efforts into memory
3264
3843
  * 2. Load user efforts from storage adapter into memory
3844
+ *
3845
+ * An explicit `customBundled` REPLACES the bundled tier — passing an empty
3846
+ * set clears it — so a seed source removal never leaves stale bundled
3847
+ * efforts cached. The no-argument call keeps the constructor/default set.
3848
+ * Both tiers rebuild only AFTER a successful adapter load: a failed load
3849
+ * propagates (readiness must never report over stale data), and deleted
3850
+ * custom rows disappear because the user map is cleared too.
3265
3851
  */
3266
3852
  async loadBundled(customBundled) {
3267
- const seed = customBundled ?? this.initialBundled ?? bundledEfforts;
3268
- for (const effort of seed) {
3269
- this.bundledEfforts.set(effort.slug, effort);
3853
+ this.bundledEfforts.clear();
3854
+ if (customBundled) {
3855
+ for (const effort of customBundled) {
3856
+ this.bundledEfforts.set(effort.slug, effort);
3857
+ }
3858
+ } else {
3859
+ const seed = this.initialBundled ?? bundledEfforts;
3860
+ for (const effort of seed) {
3861
+ this.bundledEfforts.set(effort.slug, effort);
3862
+ }
3270
3863
  }
3864
+ this.userEfforts.clear();
3271
3865
  if (this.storage) {
3272
3866
  try {
3273
3867
  const userEfforts = await this.storage.load();
@@ -3397,6 +3991,358 @@ async function addAliasToEffort(registry, targetSlug, newAlias) {
3397
3991
  await registry.upsert(updatedUserEffort);
3398
3992
  return updatedUserEffort;
3399
3993
  }
3994
+ function buildExactIndex(entities) {
3995
+ const byKey = /* @__PURE__ */ new Map();
3996
+ const add = (key, entity) => {
3997
+ if (!key) return;
3998
+ const existing = byKey.get(key);
3999
+ if (existing) {
4000
+ if (!existing.includes(entity)) existing.push(entity);
4001
+ } else {
4002
+ byKey.set(key, [entity]);
4003
+ }
4004
+ };
4005
+ for (const entity of entities) {
4006
+ add(entity.slug.toLowerCase(), entity);
4007
+ add(normalizeForFuzzy(entity.label), entity);
4008
+ for (const alias of entity.aliases) add(normalizeForFuzzy(alias), entity);
4009
+ }
4010
+ return {
4011
+ byKey,
4012
+ lookup(key) {
4013
+ const direct = byKey.get(key) ?? byKey.get(normalizeForFuzzy(key));
4014
+ if (!direct || direct.length === 0) return { entity: null, ambiguous: [] };
4015
+ if (direct.length === 1) return { entity: direct[0], ambiguous: [] };
4016
+ return { entity: null, ambiguous: direct };
4017
+ }
4018
+ };
4019
+ }
4020
+ function equipmentRef(entity) {
4021
+ return { kind: "equipment", slug: entity.slug, label: entity.label };
4022
+ }
4023
+ function dedupeRefs(refs) {
4024
+ const seen = /* @__PURE__ */ new Set();
4025
+ const result = [];
4026
+ for (const ref of refs) {
4027
+ const key = `${ref.kind}:${ref.slug}`;
4028
+ if (seen.has(key)) continue;
4029
+ seen.add(key);
4030
+ result.push(ref);
4031
+ }
4032
+ return result;
4033
+ }
4034
+ function createReferenceIndex(catalog, materialize) {
4035
+ const efforts = buildExactIndex(catalog.efforts ?? []);
4036
+ const equipment = buildExactIndex(catalog.equipment ?? []);
4037
+ const prefixCandidates = (catalog.equipment ?? []).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);
4038
+ const materializer = materialize ?? ((effort) => ({
4039
+ effort,
4040
+ definition: effort,
4041
+ slug: effort.slug,
4042
+ label: effort.label,
4043
+ met: effort.baseAttributes.met,
4044
+ baseAttributes: effort.baseAttributes,
4045
+ discipline: effort.baseAttributes.discipline,
4046
+ disciplineFactor: effort.baseAttributes.disciplineFactor ?? 1,
4047
+ intensityTier: effort.baseAttributes.intensityTier,
4048
+ modifiers: {},
4049
+ registrySource: effort.registrySource,
4050
+ resolvedFrom: effort.registrySource === "user" ? "user" : effort.registrySource === "bundled" ? "bundled" : "default",
4051
+ isEstimated: false
4052
+ }));
4053
+ const resolveEffortExact = (key) => {
4054
+ const found = efforts.lookup(key.trim());
4055
+ if (found.entity) return { entity: found.entity };
4056
+ if (found.ambiguous.length > 0) {
4057
+ return {
4058
+ entity: null,
4059
+ diagnostic: {
4060
+ reason: "ambiguous-effort",
4061
+ message: `"${key}" matches multiple efforts.`,
4062
+ token: key,
4063
+ candidates: found.ambiguous.map((e) => e.slug)
4064
+ }
4065
+ };
4066
+ }
4067
+ return {
4068
+ entity: null,
4069
+ diagnostic: { reason: "unknown-effort", message: `Unknown effort "${key}".`, token: key }
4070
+ };
4071
+ };
4072
+ const resolveEquipmentValues = (values) => {
4073
+ const refs = [];
4074
+ const diagnostics = [];
4075
+ for (const value of values) {
4076
+ if (typeof value !== "string" || !value.trim()) {
4077
+ diagnostics.push({
4078
+ reason: "invalid-explicit",
4079
+ message: `Equipment reference must be a slug string, got ${JSON.stringify(value) ?? "null"}.`,
4080
+ token: String(value)
4081
+ });
4082
+ continue;
4083
+ }
4084
+ const found = equipment.lookup(value.trim());
4085
+ if (found.entity) {
4086
+ refs.push(equipmentRef(found.entity));
4087
+ } else if (found.ambiguous.length > 0) {
4088
+ diagnostics.push({
4089
+ reason: "ambiguous-equipment",
4090
+ message: `"${value}" matches multiple equipment entries.`,
4091
+ token: value,
4092
+ candidates: found.ambiguous.map((e) => e.slug)
4093
+ });
4094
+ } else {
4095
+ diagnostics.push({
4096
+ reason: "invalid-explicit",
4097
+ message: `Unknown equipment "${value}".`,
4098
+ token: value
4099
+ });
4100
+ }
4101
+ }
4102
+ return { refs: dedupeRefs(refs), diagnostics };
4103
+ };
4104
+ const index = {
4105
+ resolveEffort(slugOrAlias) {
4106
+ return efforts.lookup(slugOrAlias).entity;
4107
+ },
4108
+ resolveEquipment(slugOrAlias) {
4109
+ return equipment.lookup(slugOrAlias).entity;
4110
+ },
4111
+ materialize(effort) {
4112
+ return materializer(effort);
4113
+ },
4114
+ resolve(label, defaults) {
4115
+ const source = (label ?? "").trim();
4116
+ const diagnostics = [];
4117
+ const push = (diagnostic) => {
4118
+ if (diagnostic) diagnostics.push(diagnostic);
4119
+ };
4120
+ let matched = "none";
4121
+ let effortEntity = null;
4122
+ let remaining = "";
4123
+ let equipmentRefs = [];
4124
+ let equipmentAuthored = false;
4125
+ const fillEquipment = () => {
4126
+ if (equipmentRefs.length > 0 || equipmentAuthored) return;
4127
+ if (effortEntity === null) return;
4128
+ if (defaults?.equipment?.length) {
4129
+ const resolvedDefaults = resolveEquipmentValues(defaults.equipment);
4130
+ equipmentRefs = resolvedDefaults.refs;
4131
+ for (const diagnostic of resolvedDefaults.diagnostics) {
4132
+ push({ ...diagnostic, reason: "unknown-equipment" });
4133
+ }
4134
+ if (equipmentRefs.length > 0) return;
4135
+ }
4136
+ if (effortEntity.equipment?.length) {
4137
+ const resolvedDefEquipment = resolveEquipmentValues(effortEntity.equipment);
4138
+ equipmentRefs = resolvedDefEquipment.refs;
4139
+ for (const diagnostic of resolvedDefEquipment.diagnostics) {
4140
+ push({ ...diagnostic, reason: "unknown-equipment" });
4141
+ }
4142
+ }
4143
+ };
4144
+ if (!source) {
4145
+ if (defaults?.effort) {
4146
+ const found = resolveEffortExact(defaults.effort);
4147
+ if (found.entity) {
4148
+ effortEntity = found.entity;
4149
+ matched = "default";
4150
+ } else {
4151
+ push({
4152
+ reason: found.diagnostic?.reason === "ambiguous-effort" ? "ambiguous-effort" : "unknown-effort",
4153
+ message: `Default effort "${defaults.effort}" is not in the catalog.`,
4154
+ token: defaults.effort,
4155
+ candidates: found.diagnostic?.candidates
4156
+ });
4157
+ }
4158
+ }
4159
+ fillEquipment();
4160
+ return {
4161
+ matched,
4162
+ label: source,
4163
+ effort: effortEntity ? materializer(effortEntity) : null,
4164
+ effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
4165
+ equipment: equipmentRefs,
4166
+ remaining: "",
4167
+ diagnostics
4168
+ };
4169
+ }
4170
+ if (source.startsWith("{")) {
4171
+ let parsed;
4172
+ try {
4173
+ parsed = JSON.parse(source);
4174
+ } catch {
4175
+ return {
4176
+ matched: "none",
4177
+ label: source,
4178
+ effort: null,
4179
+ effortRef: null,
4180
+ equipment: [],
4181
+ remaining: "",
4182
+ diagnostics: [{
4183
+ reason: "malformed-json",
4184
+ message: "Reference object is not valid JSON.",
4185
+ token: source
4186
+ }]
4187
+ };
4188
+ }
4189
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && ("effort" in parsed || "equipment" in parsed)) {
4190
+ matched = "explicit";
4191
+ const obj = parsed;
4192
+ if (obj.effort !== void 0) {
4193
+ if (typeof obj.effort === "string" && obj.effort.trim()) {
4194
+ const found = resolveEffortExact(obj.effort);
4195
+ effortEntity = found.entity;
4196
+ if (found.diagnostic) {
4197
+ push(found.diagnostic.reason === "ambiguous-effort" ? found.diagnostic : { ...found.diagnostic, reason: "invalid-explicit", message: `Explicit effort "${obj.effort}" is not in the catalog.` });
4198
+ }
4199
+ } else {
4200
+ push({
4201
+ reason: "invalid-explicit",
4202
+ message: 'Explicit "effort" must be a slug string.',
4203
+ token: String(obj.effort)
4204
+ });
4205
+ }
4206
+ }
4207
+ if (obj.equipment !== void 0) {
4208
+ equipmentAuthored = true;
4209
+ const values = Array.isArray(obj.equipment) ? obj.equipment : [obj.equipment];
4210
+ const resolvedEquipment = resolveEquipmentValues(values);
4211
+ equipmentRefs = resolvedEquipment.refs;
4212
+ diagnostics.push(...resolvedEquipment.diagnostics);
4213
+ if (effortEntity?.equipment?.length) {
4214
+ const definitionSlugs = new Set(effortEntity.equipment);
4215
+ const authoredSlugs = equipmentRefs.map((ref) => ref.slug);
4216
+ if (authoredSlugs.length > 0 && authoredSlugs.some((slug) => !definitionSlugs.has(slug))) {
4217
+ push({
4218
+ reason: "conflict",
4219
+ message: `Explicit equipment [${authoredSlugs.join(", ")}] contradicts effort "${effortEntity.slug}" definition equipment [${effortEntity.equipment.join(", ")}].`,
4220
+ token: source
4221
+ });
4222
+ }
4223
+ }
4224
+ }
4225
+ fillEquipment();
4226
+ return {
4227
+ matched,
4228
+ label: source,
4229
+ effort: effortEntity ? materializer(effortEntity) : null,
4230
+ effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
4231
+ equipment: equipmentRefs,
4232
+ remaining: "",
4233
+ diagnostics
4234
+ };
4235
+ }
4236
+ }
4237
+ const lowerSource = source.toLowerCase();
4238
+ for (const candidate of prefixCandidates) {
4239
+ if (!lowerSource.startsWith(candidate.lower)) continue;
4240
+ const after = source.slice(candidate.lower.length);
4241
+ if (after && !/^\s/.test(after)) continue;
4242
+ const candidateRemaining = after.trim();
4243
+ if (!candidateRemaining) continue;
4244
+ const equipmentFound = equipment.lookup(candidate.lower);
4245
+ if (equipmentFound.ambiguous.length > 0) {
4246
+ push({
4247
+ reason: "ambiguous-equipment",
4248
+ message: `"${candidate.lower}" matches multiple equipment entries.`,
4249
+ token: source,
4250
+ candidates: equipmentFound.ambiguous.map((e) => e.slug)
4251
+ });
4252
+ return {
4253
+ matched: "none",
4254
+ label: source,
4255
+ effort: null,
4256
+ effortRef: null,
4257
+ equipment: [],
4258
+ remaining: "",
4259
+ diagnostics
4260
+ };
4261
+ }
4262
+ if (!equipmentFound.entity) continue;
4263
+ const effortFound = resolveEffortExact(candidateRemaining);
4264
+ if (effortFound.entity) {
4265
+ matched = "split";
4266
+ effortEntity = effortFound.entity;
4267
+ equipmentRefs = [equipmentRef(equipmentFound.entity)];
4268
+ equipmentAuthored = true;
4269
+ remaining = candidateRemaining;
4270
+ break;
4271
+ }
4272
+ if (effortFound.diagnostic?.reason === "ambiguous-effort") {
4273
+ push(effortFound.diagnostic);
4274
+ return {
4275
+ matched: "none",
4276
+ label: source,
4277
+ effort: null,
4278
+ effortRef: null,
4279
+ equipment: [],
4280
+ remaining: "",
4281
+ diagnostics
4282
+ };
4283
+ }
4284
+ }
4285
+ if (matched === "none") {
4286
+ const found = resolveEffortExact(source);
4287
+ if (found.entity) {
4288
+ effortEntity = found.entity;
4289
+ matched = "whole";
4290
+ } else if (found.diagnostic?.reason === "ambiguous-effort") {
4291
+ push(found.diagnostic);
4292
+ return {
4293
+ matched: "none",
4294
+ label: source,
4295
+ effort: null,
4296
+ effortRef: null,
4297
+ equipment: [],
4298
+ remaining: "",
4299
+ diagnostics
4300
+ };
4301
+ } else if (found.diagnostic) {
4302
+ push(found.diagnostic);
4303
+ }
4304
+ }
4305
+ fillEquipment();
4306
+ return {
4307
+ matched,
4308
+ label: source,
4309
+ effort: effortEntity ? materializer(effortEntity) : null,
4310
+ effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
4311
+ equipment: equipmentRefs,
4312
+ remaining,
4313
+ diagnostics
4314
+ };
4315
+ }
4316
+ };
4317
+ return index;
4318
+ }
4319
+ function extractStatementRefs(statements) {
4320
+ const efforts = [];
4321
+ const equipment = [];
4322
+ const seenEffort = /* @__PURE__ */ new Set();
4323
+ const seenEquipment = /* @__PURE__ */ new Set();
4324
+ const pushUnique = (list, seen, value) => {
4325
+ if (!value) return;
4326
+ if (seen.has(value)) return;
4327
+ seen.add(value);
4328
+ list.push(value);
4329
+ };
4330
+ for (const statement of statements ?? []) {
4331
+ const metrics = Array.isArray(statement.metrics) ? statement.metrics : statement.metrics?.toArray?.() ?? [];
4332
+ for (const metric of metrics) {
4333
+ const refs = wodWikiCore.getReferenceMetadata(metric.metadata)?.refs;
4334
+ if (!refs) continue;
4335
+ for (const ref of refs) {
4336
+ if (ref.kind === "effort") pushUnique(efforts, seenEffort, ref.slug);
4337
+ if (ref.kind === "equipment") pushUnique(equipment, seenEquipment, ref.slug);
4338
+ }
4339
+ }
4340
+ }
4341
+ return { efforts, equipment };
4342
+ }
4343
+ function slugifyRefLabel(label) {
4344
+ return normalizeForFuzzy(label).replace(/\s+/g, "-");
4345
+ }
3400
4346
 
3401
4347
  // src/runtime/contracts/IRuntimeOptions.ts
3402
4348
  var DEFAULT_RUNTIME_OPTIONS = {
@@ -6735,14 +7681,12 @@ function resolveDominantOrigin(origins) {
6735
7681
  if (origins.every((o) => o === "compiler")) return "compiler";
6736
7682
  return "analyzed";
6737
7683
  }
6738
-
6739
- // src/runtime/compiler/EffortEnrichmentPass.ts
6740
7684
  var logTrace = (message) => console.log(message);
6741
7685
  function applyEffortEnrichment(block, options) {
6742
- const { resolver, overwrite = false, debug = false } = options;
6743
- tryEnrichBlock(block, resolver, overwrite, debug);
7686
+ const { resolver, references, overwrite = false, debug = false } = options;
7687
+ tryEnrichBlock(block, resolver, references, overwrite, debug);
6744
7688
  }
6745
- function tryEnrichBlock(block, resolver, overwrite, debug) {
7689
+ function tryEnrichBlock(block, resolver, references, overwrite, debug) {
6746
7690
  const normalizedBlockType = block.blockType?.toLowerCase();
6747
7691
  if (normalizedBlockType !== "effort" && normalizedBlockType !== "exercise") {
6748
7692
  return;
@@ -6760,7 +7704,32 @@ function tryEnrichBlock(block, resolver, overwrite, debug) {
6760
7704
  return;
6761
7705
  }
6762
7706
  try {
6763
- const resolved = resolver.resolveEffort(label);
7707
+ let resolved;
7708
+ let matchedKind = "none";
7709
+ let equipmentRefs = [];
7710
+ let diagnostics = [];
7711
+ if (references) {
7712
+ const resolution = references.resolve(label);
7713
+ resolved = resolution.effort ?? void 0;
7714
+ matchedKind = resolution.matched;
7715
+ equipmentRefs = resolution.equipment;
7716
+ diagnostics = resolution.diagnostics;
7717
+ } else {
7718
+ resolved = resolver.resolveEffort(label);
7719
+ }
7720
+ if (!resolved) {
7721
+ if (diagnostics.length > 0) {
7722
+ block.pushMemory(new MemoryLocation("metric:tracked", [{
7723
+ type: wodWikiCore.MetricType.Hint,
7724
+ value: "reference.unresolved",
7725
+ image: label,
7726
+ origin: "compiler",
7727
+ metadata: { [wodWikiCore.REFERENCE_METADATA_KEY]: { matched: "none", source: label, diagnostics } }
7728
+ }]));
7729
+ }
7730
+ if (debug) logTrace(`[EffortEnrichment] Unresolved effort: ${label}`);
7731
+ return;
7732
+ }
6764
7733
  const effortHints = resolved.effort.hints;
6765
7734
  if (effortHints && Object.keys(effortHints).length > 0) {
6766
7735
  block.mergeHints(effortHints);
@@ -6773,7 +7742,8 @@ function tryEnrichBlock(block, resolver, overwrite, debug) {
6773
7742
  image: resolved.label,
6774
7743
  value: resolved,
6775
7744
  origin: "compiler",
6776
- timestamp: /* @__PURE__ */ new Date()
7745
+ timestamp: /* @__PURE__ */ new Date(),
7746
+ ...equipmentRefs.length > 0 || diagnostics.length > 0 ? { metadata: { [wodWikiCore.REFERENCE_METADATA_KEY]: { matched: matchedKind, source: label, refs: [{ kind: "effort", slug: resolved.slug, label: resolved.label }, ...equipmentRefs], ...diagnostics.length > 0 ? { diagnostics } : {} } } } : {}
6777
7747
  };
6778
7748
  const privateMetrics = block.getMetricMemoryByVisibility("private");
6779
7749
  const existingLoc = privateMetrics.find(
@@ -6788,10 +7758,31 @@ function tryEnrichBlock(block, resolver, overwrite, debug) {
6788
7758
  const location = new MemoryLocation("metric:tracked", [effortDataMetric]);
6789
7759
  block.pushMemory(location);
6790
7760
  }
7761
+ if (equipmentRefs.length > 0) {
7762
+ pushPrivateMetrics(block, wodWikiCore.MetricType.Equipment, [
7763
+ new EquipmentMetric(equipmentRefs.map((ref) => ref.slug), {
7764
+ refs: equipmentRefs,
7765
+ source: label,
7766
+ origin: "compiler"
7767
+ })
7768
+ ], wodWikiCore.MetricType.Equipment, overwrite);
7769
+ }
6791
7770
  } catch (err) {
6792
7771
  console.error(`[EffortEnrichment] Failed to resolve effort "${label}":`, err);
6793
7772
  }
6794
7773
  }
7774
+ function pushPrivateMetrics(block, markerType, metrics, replaceType, overwrite) {
7775
+ const privateMetrics = block.getMetricMemoryByVisibility("private");
7776
+ const existingLoc = privateMetrics.find(
7777
+ (loc) => loc.metrics.toArray().some((m) => m.type === markerType)
7778
+ );
7779
+ if (existingLoc && overwrite) {
7780
+ const filtered = existingLoc.metrics.toArray().filter((m) => m.type !== replaceType);
7781
+ existingLoc.update([...filtered, ...metrics]);
7782
+ } else if (!existingLoc) {
7783
+ block.pushMemory(new MemoryLocation("metric:tracked", metrics));
7784
+ }
7785
+ }
6795
7786
 
6796
7787
  // src/runtime/actions/stack/CompileAndPushBlockAction.ts
6797
7788
  var CompileAndPushBlockAction = class {
@@ -6817,7 +7808,8 @@ var CompileAndPushBlockAction = class {
6817
7808
  }
6818
7809
  if (runtime.analyticsContext?.effortResolver) {
6819
7810
  applyEffortEnrichment(compiledBlock, {
6820
- resolver: runtime.analyticsContext.effortResolver
7811
+ resolver: runtime.analyticsContext.effortResolver,
7812
+ references: runtime.analyticsContext.references
6821
7813
  });
6822
7814
  }
6823
7815
  return [new PushBlockAction(compiledBlock, this.options)];
@@ -10843,7 +11835,8 @@ function createAnalyticsEngineForBlock(block, options) {
10843
11835
  effortResolver = new EffortResolver(registry);
10844
11836
  }
10845
11837
  const analyticsContext = {
10846
- effortResolver
11838
+ effortResolver,
11839
+ ...options?.references ? { references: options.references } : {}
10847
11840
  };
10848
11841
  const context = {
10849
11842
  dialect,
@@ -13914,6 +14907,7 @@ exports.createMetricPresentationPolicy = createPolicy;
13914
14907
  exports.createMockClock = createMockClock;
13915
14908
  exports.createParser = createParser;
13916
14909
  exports.createProfileTable = createProfileTable;
14910
+ exports.createReferenceIndex = createReferenceIndex;
13917
14911
  exports.createRpeLabelsTable = createRpeLabelsTable;
13918
14912
  exports.dailySessionLoads = dailySessionLoads;
13919
14913
  exports.dayBucket = dayBucket;
@@ -13922,10 +14916,15 @@ exports.dialectStack = dialectStack;
13922
14916
  exports.diffStatement = diffStatement;
13923
14917
  exports.dimEquals = dimEquals;
13924
14918
  exports.disciplineFactorFor = disciplineFactorFor;
14919
+ exports.documentToEffort = documentToEffort;
14920
+ exports.documentToEquipment = documentToEquipment;
14921
+ exports.effortToDocument = effortToDocument;
14922
+ exports.equipmentToDocument = equipmentToDocument;
13925
14923
  exports.evaluate = evaluate;
13926
14924
  exports.evaluateCalculateDefinitions = evaluateCalculateDefinitions;
13927
14925
  exports.extractEffortData = extractEffortData;
13928
14926
  exports.extractMetrics = extractMetrics;
14927
+ exports.extractStatementRefs = extractStatementRefs;
13929
14928
  exports.extractStatements = extractStatements;
13930
14929
  exports.extractStatementsRaw = extractStatementsRaw;
13931
14930
  exports.extractSyntaxFacts = extractSyntaxFacts;
@@ -13973,15 +14972,18 @@ exports.outputNodeId = outputNodeId;
13973
14972
  exports.outputNodeIds = outputNodeIds;
13974
14973
  exports.parseCalcLine = parseCalcLine;
13975
14974
  exports.parseCalculateBlock = parseCalculateBlock;
14975
+ exports.parseEffortFile = parseEffortFile;
14976
+ exports.parseEquipmentFile = parseEquipmentFile;
13976
14977
  exports.parseExpression = parseExpression;
13977
14978
  exports.parseMetricLine = parseMetricLine;
13978
14979
  exports.parseScript = parseScript;
13979
14980
  exports.printCalculation = printCalculation;
14981
+ exports.readNestedSection = readNestedSection;
13980
14982
  exports.readTimer = readTimer;
13981
14983
  exports.realtimeProcessorRegistry = realtimeProcessorRegistry;
13982
14984
  exports.renderMetric = renderMetric;
13983
14985
  exports.resolveDominantOrigin = resolveDominantOrigin;
13984
- exports.sharedParser = sharedParser;
14986
+ exports.slugifyRefLabel = slugifyRefLabel;
13985
14987
  exports.startSpan = startSpan;
13986
14988
  exports.str = str;
13987
14989
  exports.strategyRegistry = strategyRegistry;