@bitcobblers/wod-wiki-lang 0.37.2137 → 0.37.2140

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
  /**
@@ -2839,6 +2998,42 @@ function whiteboardScript() {
2839
2998
 
2840
2999
  // src/effort-registry/types.ts
2841
3000
  var EFFORT_REGISTRY_ORIGINS = ["bundled", "user"];
3001
+ var CATALOG_PRECEDENCE_RANK = {
3002
+ core: 0,
3003
+ community: 1,
3004
+ imported: 2,
3005
+ user: 3
3006
+ };
3007
+ function resolveSourceTier(sourceIdOrMetaKey, explicitTier) {
3008
+ if (explicitTier) return explicitTier;
3009
+ if (!sourceIdOrMetaKey) return "core";
3010
+ if (sourceIdOrMetaKey === "user") return "user";
3011
+ const id = sourceIdOrMetaKey.startsWith("seed:") ? sourceIdOrMetaKey.slice(5) : sourceIdOrMetaKey;
3012
+ if (id === "core" || id === "efforts" || id === "equipment") return "core";
3013
+ if (id === "community" || id.startsWith("community-") || id === "common") return "community";
3014
+ return "imported";
3015
+ }
3016
+ function resolveSourceRank(sourceIdOrMetaKey, explicitTier) {
3017
+ return CATALOG_PRECEDENCE_RANK[resolveSourceTier(sourceIdOrMetaKey, explicitTier)];
3018
+ }
3019
+ function resolveEntityTier(entity) {
3020
+ if (entity.registrySource === "user") return "user";
3021
+ return resolveSourceTier(entity.sourceId);
3022
+ }
3023
+ function resolveEntityRank(entity) {
3024
+ return CATALOG_PRECEDENCE_RANK[resolveEntityTier(entity)];
3025
+ }
3026
+ function dedupeEntitiesByPrecedence(entities) {
3027
+ const bySlug = /* @__PURE__ */ new Map();
3028
+ for (const entity of entities) {
3029
+ const key = entity.slug.toLowerCase();
3030
+ const prior = bySlug.get(key);
3031
+ if (!prior || resolveEntityRank(entity) >= resolveEntityRank(prior)) {
3032
+ bySlug.set(key, entity);
3033
+ }
3034
+ }
3035
+ return Array.from(bySlug.values());
3036
+ }
2842
3037
  var INTENSITY_TIERS = ["low", "moderate", "high"];
2843
3038
  var DEFAULT_RESOLVER_OPTIONS = {
2844
3039
  defaultMet: 5,
@@ -2878,6 +3073,426 @@ function disciplineFactorFor(discipline) {
2878
3073
  const key = discipline.toLowerCase();
2879
3074
  return isEffortDiscipline(key) ? DISCIPLINE_FACTORS[key] : 1;
2880
3075
  }
3076
+ var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
3077
+ var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3078
+ function unquote(value) {
3079
+ return value.replace(
3080
+ /^(['"])(.*)\1$/,
3081
+ (_match, quote, inner) => quote === '"' ? inner.replace(
3082
+ /\\(["\\nrt])/g,
3083
+ (_all, c) => c === "n" ? "\n" : c === "r" ? "\r" : c === "t" ? " " : c
3084
+ ) : inner
3085
+ );
3086
+ }
3087
+ function quoteYaml(val) {
3088
+ if (!val) return '""';
3089
+ if (/[:"'\n#{}[\],&*?|\-<>=%!@`]/.test(val) || val !== val.trim()) {
3090
+ return `"${val.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
3091
+ }
3092
+ return val;
3093
+ }
3094
+ function parseFrontmatter(raw) {
3095
+ const match = raw.match(FRONTMATTER_RE);
3096
+ if (!match) return { meta: {}, body: raw };
3097
+ const lines = match[1].split(/\r?\n/);
3098
+ const meta2 = {};
3099
+ for (let i = 0; i < lines.length; i++) {
3100
+ const keyMatch = lines[i].match(/^([A-Za-z][\w.-]*)\s*:(.*)$/);
3101
+ if (!keyMatch) continue;
3102
+ const key = keyMatch[1];
3103
+ const rawVal = keyMatch[2].trim();
3104
+ if (rawVal === "") {
3105
+ const items = [];
3106
+ while (i + 1 < lines.length) {
3107
+ const itemMatch = lines[i + 1].match(/^\s+-\s+(.+)$/);
3108
+ if (!itemMatch) break;
3109
+ items.push(unquote(itemMatch[1].trim()));
3110
+ i++;
3111
+ }
3112
+ meta2[key] = items.length > 0 ? items : "";
3113
+ } else {
3114
+ const wasQuoted = /^(['"])(.*)\1$/.test(rawVal);
3115
+ const value = unquote(rawVal);
3116
+ const num2 = Number(value);
3117
+ meta2[key] = !wasQuoted && value !== "" && !isNaN(num2) ? num2 : value;
3118
+ }
3119
+ }
3120
+ return { meta: meta2, body: raw.slice(match[0].length) };
3121
+ }
3122
+ function getScalar(meta2, key) {
3123
+ const value = meta2[key];
3124
+ return typeof value === "string" || typeof value === "number" ? value : void 0;
3125
+ }
3126
+ function getListStrict(meta2, key) {
3127
+ const value = meta2[key];
3128
+ if (value === void 0) return [];
3129
+ if (typeof value === "string" && value === "[]") return [];
3130
+ return Array.isArray(value) ? value : void 0;
3131
+ }
3132
+ function coerceNestedScalar(raw) {
3133
+ if (raw === "true") return true;
3134
+ if (raw === "false") return false;
3135
+ if (raw !== "" && !Number.isNaN(Number(raw))) return Number(raw);
3136
+ return unquote(raw);
3137
+ }
3138
+ function readNestedSection(lines, key) {
3139
+ const start = lines.findIndex((l) => new RegExp(`^${key}:\\s*$`).test(l));
3140
+ if (start === -1) return void 0;
3141
+ const result = {};
3142
+ let subKey = null;
3143
+ for (let i = start + 1; i < lines.length; i++) {
3144
+ const line = lines[i];
3145
+ if (/^\S/.test(line)) break;
3146
+ const trimmed = line.trim();
3147
+ if (!trimmed || trimmed.startsWith("#")) continue;
3148
+ const indent = line.length - line.trimStart().length;
3149
+ const m = trimmed.match(/^([\w.-]+):\s*(.*)$/);
3150
+ if (!m) continue;
3151
+ const [, k, rawVal] = m;
3152
+ if (indent === 2) {
3153
+ if (rawVal === "") {
3154
+ subKey = k;
3155
+ result[k] = {};
3156
+ } else {
3157
+ subKey = null;
3158
+ result[k] = coerceNestedScalar(rawVal);
3159
+ }
3160
+ } else if (indent >= 4 && subKey !== null) {
3161
+ result[subKey][k] = coerceNestedScalar(rawVal);
3162
+ }
3163
+ }
3164
+ return Object.keys(result).length > 0 ? result : void 0;
3165
+ }
3166
+ function parseDiscipline(val) {
3167
+ if (!val) return void 0;
3168
+ const normalized = unquote(val.trim()).toLowerCase();
3169
+ if (isEffortDiscipline(normalized)) return normalized;
3170
+ console.warn(`[definition-markdown] unknown discipline '${val}' \u2014 expected one of the canonical values; treating as unset`);
3171
+ return void 0;
3172
+ }
3173
+ function identityProblems(slug, label) {
3174
+ const problems = [];
3175
+ const slugText = slug === void 0 ? "" : String(slug);
3176
+ if (!slugText) problems.push("Missing required field: slug");
3177
+ else if (!SLUG_RE.test(slugText)) problems.push("Invalid slug: must be lowercase letters, numbers, and hyphens only");
3178
+ if (label === void 0 || !String(label).trim()) problems.push("Missing required field: label");
3179
+ return problems;
3180
+ }
3181
+ function stringListProblems(values, kind) {
3182
+ const problems = [];
3183
+ for (const value of values) {
3184
+ if (!value.trim()) problems.push(`Invalid ${kind}: blank entries are not allowed`);
3185
+ else if (kind === "equipment" && !SLUG_RE.test(value)) {
3186
+ problems.push(`Invalid ${kind}: '${value}' must be a canonical slug (lowercase letters, numbers, hyphens)`);
3187
+ }
3188
+ }
3189
+ const duplicate = values.find((v, i) => values.indexOf(v) !== i);
3190
+ if (duplicate !== void 0) problems.push(`Invalid ${kind}: duplicate entry '${duplicate}'`);
3191
+ return problems;
3192
+ }
3193
+ function metProblem(value) {
3194
+ if (value === void 0) return "Missing required field: baseAttributes.met";
3195
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
3196
+ return "Invalid baseAttributes.met: must be a positive number";
3197
+ }
3198
+ return null;
3199
+ }
3200
+ function parseEffortFile(raw) {
3201
+ const blockMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
3202
+ if (!blockMatch) return null;
3203
+ const innerLines = blockMatch[1].split(/\r?\n/);
3204
+ const { meta: meta2, body } = parseFrontmatter(raw);
3205
+ const type = getScalar(meta2, "type");
3206
+ if (type !== void 0 && type !== "effort") return null;
3207
+ const id = getScalar(meta2, "id");
3208
+ const slug = getScalar(meta2, "slug");
3209
+ const label = getScalar(meta2, "label");
3210
+ const aliases = getListStrict(meta2, "aliases");
3211
+ const equipment = getListStrict(meta2, "equipment");
3212
+ if (aliases === void 0 || equipment === void 0) return null;
3213
+ if (identityProblems(slug, label).length > 0) return null;
3214
+ if (stringListProblems(aliases, "aliases").length > 0) return null;
3215
+ if (equipment.length > 0 && stringListProblems(equipment, "equipment").length > 0) return null;
3216
+ const baseAttrs = readNestedSection(innerLines, "baseAttributes") ?? {};
3217
+ if (metProblem(baseAttrs.met) !== null) return null;
3218
+ const met = baseAttrs.met;
3219
+ const discipline = parseDiscipline(
3220
+ typeof baseAttrs.discipline === "string" ? baseAttrs.discipline : void 0
3221
+ );
3222
+ const intensityTier = baseAttrs.intensityTier;
3223
+ const disciplineFactor = typeof baseAttrs.disciplineFactor === "number" && Number.isFinite(baseAttrs.disciplineFactor) ? baseAttrs.disciplineFactor : void 0;
3224
+ const hints = readNestedSection(innerLines, "hints");
3225
+ const derivation = readNestedSection(innerLines, "derivation");
3226
+ const trimmedBody = body.trim();
3227
+ return {
3228
+ id: String(id ?? ""),
3229
+ slug: String(slug),
3230
+ label: String(label),
3231
+ aliases,
3232
+ baseAttributes: {
3233
+ met,
3234
+ ...discipline ? { discipline } : {},
3235
+ ...disciplineFactor !== void 0 ? { disciplineFactor } : {},
3236
+ ...intensityTier ? { intensityTier } : {}
3237
+ },
3238
+ registrySource: "bundled",
3239
+ ...equipment.length > 0 ? { equipment } : {},
3240
+ ...trimmedBody ? { body: trimmedBody } : {},
3241
+ ...hints ? { hints } : {},
3242
+ ...derivation ? { derivation } : {}
3243
+ };
3244
+ }
3245
+ function parseEquipmentFile(raw) {
3246
+ const { meta: meta2, body } = parseFrontmatter(raw);
3247
+ if (getScalar(meta2, "type") !== "equipment") return null;
3248
+ const id = getScalar(meta2, "id");
3249
+ const slug = getScalar(meta2, "slug");
3250
+ const label = getScalar(meta2, "label");
3251
+ const aliases = getListStrict(meta2, "aliases");
3252
+ if (id === void 0 || aliases === void 0) return null;
3253
+ if (identityProblems(slug, label).length > 0) return null;
3254
+ if (stringListProblems(aliases, "aliases").length > 0) return null;
3255
+ const trimmedBody = body.trim();
3256
+ return {
3257
+ id: String(id),
3258
+ slug: String(slug),
3259
+ label: String(label),
3260
+ aliases,
3261
+ registrySource: "bundled",
3262
+ ...trimmedBody ? { body: trimmedBody } : {}
3263
+ };
3264
+ }
3265
+ function createDefaultEffort() {
3266
+ return {
3267
+ id: `effort-user-${uuid.v7()}`,
3268
+ slug: "",
3269
+ label: "",
3270
+ aliases: [],
3271
+ baseAttributes: { met: 5 },
3272
+ registrySource: "user"
3273
+ };
3274
+ }
3275
+ function documentToEffort(doc, baseEffort) {
3276
+ const errors = [];
3277
+ const blockMatch = doc.match(/^---\r?\n([\s\S]*?)\r?\n---/);
3278
+ if (!blockMatch) {
3279
+ errors.push("Missing YAML frontmatter delimiters (---)");
3280
+ return { effort: baseEffort ?? createDefaultEffort(), errors };
3281
+ }
3282
+ const innerLines = blockMatch[1].split(/\r?\n/);
3283
+ const { meta: meta2, body } = parseFrontmatter(doc);
3284
+ const bodyText = body.replace(/^\r?\n+/, "") || void 0;
3285
+ const id = getScalar(meta2, "id");
3286
+ const slug = getScalar(meta2, "slug");
3287
+ const label = getScalar(meta2, "label");
3288
+ const registrySource = getScalar(meta2, "registrySource");
3289
+ const createdAt = getScalar(meta2, "createdAt");
3290
+ const updatedAt = getScalar(meta2, "updatedAt");
3291
+ const aliases = getListStrict(meta2, "aliases");
3292
+ const equipmentRaw = meta2["equipment"];
3293
+ const equipment = getListStrict(meta2, "equipment");
3294
+ if (aliases === void 0) {
3295
+ errors.push("Invalid aliases: must be a list (`aliases:` followed by `- item` lines)");
3296
+ }
3297
+ if (equipment === void 0) {
3298
+ errors.push("Invalid equipment: must be a list (`equipment:` followed by `- slug` lines)");
3299
+ }
3300
+ const baseAttrs = readNestedSection(innerLines, "baseAttributes") ?? {};
3301
+ const baseAttributes = {};
3302
+ const metRaw = baseAttrs.met;
3303
+ if (metRaw === void 0) {
3304
+ const fallbackMet = baseEffort?.baseAttributes.met;
3305
+ const fallbackProblem = fallbackMet === void 0 ? null : metProblem(fallbackMet);
3306
+ if (fallbackProblem) errors.push(fallbackProblem);
3307
+ else if (fallbackMet !== void 0) baseAttributes.met = fallbackMet;
3308
+ else errors.push("Missing required field: baseAttributes.met");
3309
+ } else {
3310
+ const problem = metProblem(metRaw);
3311
+ if (problem) errors.push(problem);
3312
+ else baseAttributes.met = Number(metRaw);
3313
+ }
3314
+ if (getScalar(meta2, "met") !== void 0) {
3315
+ errors.push(
3316
+ "Invalid met placement: 'met' must be nested under baseAttributes (missing baseAttributes.met)"
3317
+ );
3318
+ }
3319
+ const discipline = parseDiscipline(
3320
+ typeof baseAttrs.discipline === "string" ? baseAttrs.discipline : void 0
3321
+ );
3322
+ if (discipline) baseAttributes.discipline = discipline;
3323
+ if (typeof baseAttrs.disciplineFactor === "number" && Number.isFinite(baseAttrs.disciplineFactor)) {
3324
+ baseAttributes.disciplineFactor = baseAttrs.disciplineFactor;
3325
+ }
3326
+ if (baseAttrs.intensityTier) baseAttributes.intensityTier = baseAttrs.intensityTier;
3327
+ const hints = readNestedSection(innerLines, "hints") ?? baseEffort?.hints;
3328
+ const derivation = readNestedSection(innerLines, "derivation");
3329
+ errors.push(...identityProblems(slug, label));
3330
+ if (aliases !== void 0) errors.push(...stringListProblems(aliases, "aliases"));
3331
+ if (equipment !== void 0 && equipment.length > 0) {
3332
+ errors.push(...stringListProblems(equipment, "equipment"));
3333
+ }
3334
+ const resolvedEquipment = equipmentRaw !== void 0 ? equipment ?? [] : baseEffort?.equipment?.length ? baseEffort.equipment : void 0;
3335
+ const effort = {
3336
+ id: String(id ?? "") || baseEffort?.id || `effort-user-${uuid.v7()}`,
3337
+ slug: String(slug ?? ""),
3338
+ label: String(label ?? ""),
3339
+ aliases: aliases ?? [],
3340
+ baseAttributes,
3341
+ registrySource: registrySource || baseEffort?.registrySource || "user",
3342
+ ...resolvedEquipment ? { equipment: resolvedEquipment } : {},
3343
+ ...createdAt ? { createdAt: String(createdAt) } : {},
3344
+ ...updatedAt ? { updatedAt: String(updatedAt) } : {},
3345
+ ...bodyText ? { body: bodyText } : {},
3346
+ ...hints && Object.keys(hints).length > 0 ? { hints } : {},
3347
+ ...derivation ? { derivation } : {}
3348
+ };
3349
+ return { effort, errors };
3350
+ }
3351
+ function effortToDocument(effort) {
3352
+ const lines = ["---"];
3353
+ lines.push(`id: ${effort.id}`);
3354
+ lines.push(`slug: ${effort.slug}`);
3355
+ lines.push(`label: ${quoteYaml(effort.label)}`);
3356
+ if (effort.aliases.length > 0) {
3357
+ lines.push("aliases:");
3358
+ for (const alias of effort.aliases) {
3359
+ lines.push(` - ${quoteYaml(alias)}`);
3360
+ }
3361
+ } else {
3362
+ lines.push("aliases: []");
3363
+ }
3364
+ lines.push("baseAttributes:");
3365
+ lines.push(` met: ${effort.baseAttributes.met}`);
3366
+ if (effort.baseAttributes.discipline) {
3367
+ lines.push(` discipline: ${effort.baseAttributes.discipline}`);
3368
+ }
3369
+ if (effort.baseAttributes.disciplineFactor !== void 0) {
3370
+ lines.push(` disciplineFactor: ${effort.baseAttributes.disciplineFactor}`);
3371
+ }
3372
+ if (effort.baseAttributes.intensityTier) {
3373
+ lines.push(` intensityTier: ${effort.baseAttributes.intensityTier}`);
3374
+ }
3375
+ lines.push(`registrySource: ${effort.registrySource}`);
3376
+ if (effort.equipment && effort.equipment.length > 0) {
3377
+ lines.push("equipment:");
3378
+ for (const slug of effort.equipment) {
3379
+ lines.push(` - ${quoteYaml(slug)}`);
3380
+ }
3381
+ }
3382
+ if (effort.derivation) {
3383
+ lines.push("derivation:");
3384
+ if (effort.derivation.parentSlug) {
3385
+ lines.push(` parentSlug: ${effort.derivation.parentSlug}`);
3386
+ }
3387
+ if (effort.derivation.coefficients && Object.keys(effort.derivation.coefficients).length > 0) {
3388
+ lines.push(" coefficients:");
3389
+ for (const [k, v] of Object.entries(effort.derivation.coefficients)) {
3390
+ lines.push(` ${k}: ${v}`);
3391
+ }
3392
+ }
3393
+ if (effort.derivation.hardOverrides && Object.keys(effort.derivation.hardOverrides).length > 0) {
3394
+ lines.push(" hardOverrides:");
3395
+ for (const [k, v] of Object.entries(effort.derivation.hardOverrides)) {
3396
+ lines.push(` ${k}: ${JSON.stringify(v)}`);
3397
+ }
3398
+ }
3399
+ }
3400
+ if (effort.createdAt) {
3401
+ lines.push(`createdAt: ${effort.createdAt}`);
3402
+ }
3403
+ if (effort.updatedAt) {
3404
+ lines.push(`updatedAt: ${effort.updatedAt}`);
3405
+ }
3406
+ if (effort.hints && Object.keys(effort.hints).length > 0) {
3407
+ lines.push("hints:");
3408
+ for (const [k, v] of Object.entries(effort.hints)) {
3409
+ lines.push(` ${k}: ${v !== null && typeof v === "object" ? JSON.stringify(v) : String(v)}`);
3410
+ }
3411
+ }
3412
+ lines.push("---");
3413
+ if (effort.body) {
3414
+ lines.push("");
3415
+ lines.push(effort.body);
3416
+ }
3417
+ return lines.join("\n");
3418
+ }
3419
+ function createDefaultEquipment() {
3420
+ return {
3421
+ id: `equipment-user-${uuid.v7()}`,
3422
+ slug: "",
3423
+ label: "",
3424
+ aliases: [],
3425
+ registrySource: "user"
3426
+ };
3427
+ }
3428
+ function documentToEquipment(doc, baseEquipment) {
3429
+ const errors = [];
3430
+ const blockMatch = doc.match(FRONTMATTER_RE);
3431
+ if (!blockMatch) {
3432
+ errors.push("Missing YAML frontmatter delimiters (---)");
3433
+ return { equipment: baseEquipment ?? createDefaultEquipment(), errors };
3434
+ }
3435
+ const { meta: meta2, body } = parseFrontmatter(doc);
3436
+ const bodyText = body.replace(/^\r?\n+/, "") || void 0;
3437
+ const type = getScalar(meta2, "type");
3438
+ const id = getScalar(meta2, "id");
3439
+ const slug = getScalar(meta2, "slug");
3440
+ const label = getScalar(meta2, "label");
3441
+ const registrySource = getScalar(meta2, "registrySource");
3442
+ const createdAt = getScalar(meta2, "createdAt");
3443
+ const updatedAt = getScalar(meta2, "updatedAt");
3444
+ const aliases = getListStrict(meta2, "aliases");
3445
+ if (type !== "equipment") errors.push("Missing or invalid discriminator: frontmatter must declare `type: equipment`");
3446
+ if (!slug || !SLUG_RE.test(String(slug))) errors.push("Invalid slug: must be a canonical slug (lowercase letters, numbers, hyphens)");
3447
+ if (!label) errors.push("Missing required field: label");
3448
+ if (aliases === void 0) {
3449
+ errors.push("Invalid aliases: must be a list (`aliases:` followed by `- item` lines)");
3450
+ } else {
3451
+ errors.push(...stringListProblems(aliases, "aliases"));
3452
+ }
3453
+ if (registrySource !== void 0 && registrySource !== "bundled" && registrySource !== "user") {
3454
+ errors.push("Invalid registrySource: must be 'bundled' or 'user'");
3455
+ }
3456
+ const equipment = {
3457
+ id: String(id ?? "") || baseEquipment?.id || `equipment-user-${uuid.v7()}`,
3458
+ slug: String(slug ?? ""),
3459
+ label: String(label ?? ""),
3460
+ aliases: aliases ?? [],
3461
+ registrySource: registrySource || baseEquipment?.registrySource || "user",
3462
+ ...createdAt ? { createdAt: String(createdAt) } : {},
3463
+ ...updatedAt ? { updatedAt: String(updatedAt) } : {},
3464
+ ...bodyText ? { body: bodyText } : {}
3465
+ };
3466
+ return { equipment, errors };
3467
+ }
3468
+ function equipmentToDocument(equipment) {
3469
+ const lines = ["---"];
3470
+ lines.push("type: equipment");
3471
+ lines.push(`id: ${equipment.id}`);
3472
+ lines.push(`slug: ${equipment.slug}`);
3473
+ lines.push(`label: ${quoteYaml(equipment.label)}`);
3474
+ if (equipment.aliases.length > 0) {
3475
+ lines.push("aliases:");
3476
+ for (const alias of equipment.aliases) {
3477
+ lines.push(` - ${quoteYaml(alias)}`);
3478
+ }
3479
+ } else {
3480
+ lines.push("aliases: []");
3481
+ }
3482
+ lines.push(`registrySource: ${equipment.registrySource}`);
3483
+ if (equipment.createdAt) {
3484
+ lines.push(`createdAt: ${equipment.createdAt}`);
3485
+ }
3486
+ if (equipment.updatedAt) {
3487
+ lines.push(`updatedAt: ${equipment.updatedAt}`);
3488
+ }
3489
+ lines.push("---");
3490
+ if (equipment.body) {
3491
+ lines.push("");
3492
+ lines.push(equipment.body);
3493
+ }
3494
+ return lines.join("\n");
3495
+ }
2881
3496
 
2882
3497
  // src/effort-registry/fuzzyMatch.ts
2883
3498
  function levenshteinDistance(a, b) {
@@ -3262,12 +3877,24 @@ var CompositeEffortRegistry = class {
3262
3877
  * Initialize the registry:
3263
3878
  * 1. Load bundled efforts into memory
3264
3879
  * 2. Load user efforts from storage adapter into memory
3880
+ *
3881
+ * An explicit `customBundled` REPLACES the bundled tier — passing an empty
3882
+ * set clears it — so a seed source removal never leaves stale bundled
3883
+ * efforts cached. The no-argument call keeps the constructor/default set.
3884
+ * Both tiers rebuild only AFTER a successful adapter load: a failed load
3885
+ * propagates (readiness must never report over stale data), and deleted
3886
+ * custom rows disappear because the user map is cleared too.
3265
3887
  */
3266
3888
  async loadBundled(customBundled) {
3267
- const seed = customBundled ?? this.initialBundled ?? bundledEfforts;
3268
- for (const effort of seed) {
3269
- this.bundledEfforts.set(effort.slug, effort);
3889
+ this.bundledEfforts.clear();
3890
+ const source = customBundled ?? this.initialBundled ?? bundledEfforts;
3891
+ for (const effort of source) {
3892
+ const prior = this.bundledEfforts.get(effort.slug);
3893
+ if (!prior || resolveEntityRank(effort) >= resolveEntityRank(prior)) {
3894
+ this.bundledEfforts.set(effort.slug, effort);
3895
+ }
3270
3896
  }
3897
+ this.userEfforts.clear();
3271
3898
  if (this.storage) {
3272
3899
  try {
3273
3900
  const userEfforts = await this.storage.load();
@@ -3297,8 +3924,8 @@ var CompositeEffortRegistry = class {
3297
3924
  if (normalizeForFuzzy(alias) === normalized) return effort;
3298
3925
  }
3299
3926
  }
3300
- for (const effort of this.bundledEfforts.values()) {
3301
- if (this.userEfforts.has(effort.slug)) continue;
3927
+ const bundled = Array.from(this.bundledEfforts.values()).filter((effort) => !this.userEfforts.has(effort.slug)).sort((a, b) => resolveEntityRank(b) - resolveEntityRank(a));
3928
+ for (const effort of bundled) {
3302
3929
  if (normalizeForFuzzy(effort.label) === normalized) return effort;
3303
3930
  for (const alias of effort.aliases) {
3304
3931
  if (normalizeForFuzzy(alias) === normalized) return effort;
@@ -3397,6 +4024,367 @@ async function addAliasToEffort(registry, targetSlug, newAlias) {
3397
4024
  await registry.upsert(updatedUserEffort);
3398
4025
  return updatedUserEffort;
3399
4026
  }
4027
+ function buildExactIndex(entities) {
4028
+ const deduped = dedupeEntitiesByPrecedence(entities);
4029
+ const byKey = /* @__PURE__ */ new Map();
4030
+ const add = (key, entity) => {
4031
+ if (!key) return;
4032
+ const existing = byKey.get(key);
4033
+ if (existing) {
4034
+ if (!existing.includes(entity)) existing.push(entity);
4035
+ } else {
4036
+ byKey.set(key, [entity]);
4037
+ }
4038
+ };
4039
+ for (const entity of deduped) {
4040
+ add(entity.slug.toLowerCase(), entity);
4041
+ add(normalizeForFuzzy(entity.label), entity);
4042
+ for (const alias of entity.aliases) add(normalizeForFuzzy(alias), entity);
4043
+ }
4044
+ return {
4045
+ byKey,
4046
+ lookup(key) {
4047
+ const direct = byKey.get(key) ?? byKey.get(normalizeForFuzzy(key));
4048
+ if (!direct || direct.length === 0) return { entity: null, ambiguous: [] };
4049
+ if (direct.length === 1) return { entity: direct[0], ambiguous: [] };
4050
+ let maxRank = -1;
4051
+ for (const candidate of direct) {
4052
+ const rank = resolveEntityRank(candidate);
4053
+ if (rank > maxRank) maxRank = rank;
4054
+ }
4055
+ const topTier = direct.filter((candidate) => resolveEntityRank(candidate) === maxRank);
4056
+ if (topTier.length === 1) return { entity: topTier[0], ambiguous: [] };
4057
+ return { entity: null, ambiguous: topTier };
4058
+ }
4059
+ };
4060
+ }
4061
+ function equipmentRef(entity) {
4062
+ return { kind: "equipment", slug: entity.slug, label: entity.label };
4063
+ }
4064
+ function dedupeRefs(refs) {
4065
+ const seen = /* @__PURE__ */ new Set();
4066
+ const result = [];
4067
+ for (const ref of refs) {
4068
+ const key = `${ref.kind}:${ref.slug}`;
4069
+ if (seen.has(key)) continue;
4070
+ seen.add(key);
4071
+ result.push(ref);
4072
+ }
4073
+ return result;
4074
+ }
4075
+ function createReferenceIndex(catalog, materialize) {
4076
+ const dedupedEquipment = dedupeEntitiesByPrecedence(catalog.equipment ?? []);
4077
+ const efforts = buildExactIndex(catalog.efforts ?? []);
4078
+ const equipment = buildExactIndex(dedupedEquipment);
4079
+ const prefixCandidates = dedupedEquipment.flatMap((entity) => [entity.label, ...entity.aliases].filter((name2) => !!name2 && !!name2.trim()).map((name2) => ({ lower: name2.trim().toLowerCase(), entity }))).sort((a, b) => b.lower.length - a.lower.length);
4080
+ const materializer = materialize ?? ((effort) => ({
4081
+ effort,
4082
+ definition: effort,
4083
+ slug: effort.slug,
4084
+ label: effort.label,
4085
+ met: effort.baseAttributes.met,
4086
+ baseAttributes: effort.baseAttributes,
4087
+ discipline: effort.baseAttributes.discipline,
4088
+ disciplineFactor: effort.baseAttributes.disciplineFactor ?? 1,
4089
+ intensityTier: effort.baseAttributes.intensityTier,
4090
+ modifiers: {},
4091
+ registrySource: effort.registrySource,
4092
+ resolvedFrom: effort.registrySource === "user" ? "user" : effort.registrySource === "bundled" ? "bundled" : "default",
4093
+ isEstimated: false
4094
+ }));
4095
+ const resolveEffortExact = (key) => {
4096
+ const found = efforts.lookup(key.trim());
4097
+ if (found.entity) return { entity: found.entity };
4098
+ if (found.ambiguous.length > 0) {
4099
+ return {
4100
+ entity: null,
4101
+ diagnostic: {
4102
+ reason: "ambiguous-effort",
4103
+ message: `"${key}" matches multiple efforts.`,
4104
+ token: key,
4105
+ candidates: found.ambiguous.map((e) => e.slug)
4106
+ }
4107
+ };
4108
+ }
4109
+ return {
4110
+ entity: null,
4111
+ diagnostic: { reason: "unknown-effort", message: `Unknown effort "${key}".`, token: key }
4112
+ };
4113
+ };
4114
+ const resolveEquipmentValues = (values) => {
4115
+ const refs = [];
4116
+ const diagnostics = [];
4117
+ for (const value of values) {
4118
+ if (typeof value !== "string" || !value.trim()) {
4119
+ diagnostics.push({
4120
+ reason: "invalid-explicit",
4121
+ message: `Equipment reference must be a slug string, got ${JSON.stringify(value) ?? "null"}.`,
4122
+ token: String(value)
4123
+ });
4124
+ continue;
4125
+ }
4126
+ const found = equipment.lookup(value.trim());
4127
+ if (found.entity) {
4128
+ refs.push(equipmentRef(found.entity));
4129
+ } else if (found.ambiguous.length > 0) {
4130
+ diagnostics.push({
4131
+ reason: "ambiguous-equipment",
4132
+ message: `"${value}" matches multiple equipment entries.`,
4133
+ token: value,
4134
+ candidates: found.ambiguous.map((e) => e.slug)
4135
+ });
4136
+ } else {
4137
+ diagnostics.push({
4138
+ reason: "invalid-explicit",
4139
+ message: `Unknown equipment "${value}".`,
4140
+ token: value
4141
+ });
4142
+ }
4143
+ }
4144
+ return { refs: dedupeRefs(refs), diagnostics };
4145
+ };
4146
+ const index = {
4147
+ resolveEffort(slugOrAlias) {
4148
+ return efforts.lookup(slugOrAlias).entity;
4149
+ },
4150
+ resolveEquipment(slugOrAlias) {
4151
+ return equipment.lookup(slugOrAlias).entity;
4152
+ },
4153
+ materialize(effort) {
4154
+ return materializer(effort);
4155
+ },
4156
+ resolve(label, defaults) {
4157
+ const source = (label ?? "").trim();
4158
+ const diagnostics = [];
4159
+ const push = (diagnostic) => {
4160
+ if (diagnostic) diagnostics.push(diagnostic);
4161
+ };
4162
+ let matched = "none";
4163
+ let effortEntity = null;
4164
+ let remaining = "";
4165
+ let equipmentRefs = [];
4166
+ let equipmentAuthored = false;
4167
+ const fillEquipment = () => {
4168
+ if (equipmentRefs.length > 0 || equipmentAuthored) return;
4169
+ if (effortEntity === null) return;
4170
+ if (defaults?.equipment?.length) {
4171
+ const resolvedDefaults = resolveEquipmentValues(defaults.equipment);
4172
+ equipmentRefs = resolvedDefaults.refs;
4173
+ for (const diagnostic of resolvedDefaults.diagnostics) {
4174
+ push({ ...diagnostic, reason: "unknown-equipment" });
4175
+ }
4176
+ if (equipmentRefs.length > 0) return;
4177
+ }
4178
+ if (effortEntity.equipment?.length) {
4179
+ const resolvedDefEquipment = resolveEquipmentValues(effortEntity.equipment);
4180
+ equipmentRefs = resolvedDefEquipment.refs;
4181
+ for (const diagnostic of resolvedDefEquipment.diagnostics) {
4182
+ push({ ...diagnostic, reason: "unknown-equipment" });
4183
+ }
4184
+ }
4185
+ };
4186
+ if (!source) {
4187
+ if (defaults?.effort) {
4188
+ const found = resolveEffortExact(defaults.effort);
4189
+ if (found.entity) {
4190
+ effortEntity = found.entity;
4191
+ matched = "default";
4192
+ } else {
4193
+ push({
4194
+ reason: found.diagnostic?.reason === "ambiguous-effort" ? "ambiguous-effort" : "unknown-effort",
4195
+ message: `Default effort "${defaults.effort}" is not in the catalog.`,
4196
+ token: defaults.effort,
4197
+ candidates: found.diagnostic?.candidates
4198
+ });
4199
+ }
4200
+ }
4201
+ fillEquipment();
4202
+ return {
4203
+ matched,
4204
+ label: source,
4205
+ effort: effortEntity ? materializer(effortEntity) : null,
4206
+ effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
4207
+ equipment: equipmentRefs,
4208
+ remaining: "",
4209
+ diagnostics
4210
+ };
4211
+ }
4212
+ if (source.startsWith("{")) {
4213
+ let parsed;
4214
+ try {
4215
+ parsed = JSON.parse(source);
4216
+ } catch {
4217
+ return {
4218
+ matched: "none",
4219
+ label: source,
4220
+ effort: null,
4221
+ effortRef: null,
4222
+ equipment: [],
4223
+ remaining: "",
4224
+ diagnostics: [{
4225
+ reason: "malformed-json",
4226
+ message: "Reference object is not valid JSON.",
4227
+ token: source
4228
+ }]
4229
+ };
4230
+ }
4231
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && ("effort" in parsed || "equipment" in parsed)) {
4232
+ matched = "explicit";
4233
+ const obj = parsed;
4234
+ if (obj.effort !== void 0) {
4235
+ if (typeof obj.effort === "string" && obj.effort.trim()) {
4236
+ const found = resolveEffortExact(obj.effort);
4237
+ effortEntity = found.entity;
4238
+ if (found.diagnostic) {
4239
+ push(found.diagnostic.reason === "ambiguous-effort" ? found.diagnostic : { ...found.diagnostic, reason: "invalid-explicit", message: `Explicit effort "${obj.effort}" is not in the catalog.` });
4240
+ }
4241
+ } else {
4242
+ push({
4243
+ reason: "invalid-explicit",
4244
+ message: 'Explicit "effort" must be a slug string.',
4245
+ token: String(obj.effort)
4246
+ });
4247
+ }
4248
+ }
4249
+ if (obj.equipment !== void 0) {
4250
+ equipmentAuthored = true;
4251
+ const values = Array.isArray(obj.equipment) ? obj.equipment : [obj.equipment];
4252
+ const resolvedEquipment = resolveEquipmentValues(values);
4253
+ equipmentRefs = resolvedEquipment.refs;
4254
+ diagnostics.push(...resolvedEquipment.diagnostics);
4255
+ if (effortEntity?.equipment?.length) {
4256
+ const definitionSlugs = new Set(effortEntity.equipment);
4257
+ const authoredSlugs = equipmentRefs.map((ref) => ref.slug);
4258
+ if (authoredSlugs.length > 0 && authoredSlugs.some((slug) => !definitionSlugs.has(slug))) {
4259
+ push({
4260
+ reason: "conflict",
4261
+ message: `Explicit equipment [${authoredSlugs.join(", ")}] contradicts effort "${effortEntity.slug}" definition equipment [${effortEntity.equipment.join(", ")}].`,
4262
+ token: source
4263
+ });
4264
+ }
4265
+ }
4266
+ }
4267
+ fillEquipment();
4268
+ return {
4269
+ matched,
4270
+ label: source,
4271
+ effort: effortEntity ? materializer(effortEntity) : null,
4272
+ effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
4273
+ equipment: equipmentRefs,
4274
+ remaining: "",
4275
+ diagnostics
4276
+ };
4277
+ }
4278
+ }
4279
+ const lowerSource = source.toLowerCase();
4280
+ for (const candidate of prefixCandidates) {
4281
+ if (!lowerSource.startsWith(candidate.lower)) continue;
4282
+ const after = source.slice(candidate.lower.length);
4283
+ if (after && !/^\s/.test(after)) continue;
4284
+ const candidateRemaining = after.trim();
4285
+ if (!candidateRemaining) continue;
4286
+ const equipmentFound = equipment.lookup(candidate.lower);
4287
+ if (equipmentFound.ambiguous.length > 0) {
4288
+ push({
4289
+ reason: "ambiguous-equipment",
4290
+ message: `"${candidate.lower}" matches multiple equipment entries.`,
4291
+ token: source,
4292
+ candidates: equipmentFound.ambiguous.map((e) => e.slug)
4293
+ });
4294
+ return {
4295
+ matched: "none",
4296
+ label: source,
4297
+ effort: null,
4298
+ effortRef: null,
4299
+ equipment: [],
4300
+ remaining: "",
4301
+ diagnostics
4302
+ };
4303
+ }
4304
+ if (!equipmentFound.entity) continue;
4305
+ const effortFound = resolveEffortExact(candidateRemaining);
4306
+ if (effortFound.entity) {
4307
+ matched = "split";
4308
+ effortEntity = effortFound.entity;
4309
+ equipmentRefs = [equipmentRef(equipmentFound.entity)];
4310
+ equipmentAuthored = true;
4311
+ remaining = candidateRemaining;
4312
+ break;
4313
+ }
4314
+ if (effortFound.diagnostic?.reason === "ambiguous-effort") {
4315
+ push(effortFound.diagnostic);
4316
+ return {
4317
+ matched: "none",
4318
+ label: source,
4319
+ effort: null,
4320
+ effortRef: null,
4321
+ equipment: [],
4322
+ remaining: "",
4323
+ diagnostics
4324
+ };
4325
+ }
4326
+ }
4327
+ if (matched === "none") {
4328
+ const found = resolveEffortExact(source);
4329
+ if (found.entity) {
4330
+ effortEntity = found.entity;
4331
+ matched = "whole";
4332
+ } else if (found.diagnostic?.reason === "ambiguous-effort") {
4333
+ push(found.diagnostic);
4334
+ return {
4335
+ matched: "none",
4336
+ label: source,
4337
+ effort: null,
4338
+ effortRef: null,
4339
+ equipment: [],
4340
+ remaining: "",
4341
+ diagnostics
4342
+ };
4343
+ } else if (found.diagnostic) {
4344
+ push(found.diagnostic);
4345
+ }
4346
+ }
4347
+ fillEquipment();
4348
+ return {
4349
+ matched,
4350
+ label: source,
4351
+ effort: effortEntity ? materializer(effortEntity) : null,
4352
+ effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
4353
+ equipment: equipmentRefs,
4354
+ remaining,
4355
+ diagnostics
4356
+ };
4357
+ }
4358
+ };
4359
+ return index;
4360
+ }
4361
+ function extractStatementRefs(statements) {
4362
+ const efforts = [];
4363
+ const equipment = [];
4364
+ const seenEffort = /* @__PURE__ */ new Set();
4365
+ const seenEquipment = /* @__PURE__ */ new Set();
4366
+ const pushUnique = (list, seen, value) => {
4367
+ if (!value) return;
4368
+ if (seen.has(value)) return;
4369
+ seen.add(value);
4370
+ list.push(value);
4371
+ };
4372
+ for (const statement of statements ?? []) {
4373
+ const metrics = Array.isArray(statement.metrics) ? statement.metrics : statement.metrics?.toArray?.() ?? [];
4374
+ for (const metric of metrics) {
4375
+ const refs = wodWikiCore.getReferenceMetadata(metric.metadata)?.refs;
4376
+ if (!refs) continue;
4377
+ for (const ref of refs) {
4378
+ if (ref.kind === "effort") pushUnique(efforts, seenEffort, ref.slug);
4379
+ if (ref.kind === "equipment") pushUnique(equipment, seenEquipment, ref.slug);
4380
+ }
4381
+ }
4382
+ }
4383
+ return { efforts, equipment };
4384
+ }
4385
+ function slugifyRefLabel(label) {
4386
+ return normalizeForFuzzy(label).replace(/\s+/g, "-");
4387
+ }
3400
4388
 
3401
4389
  // src/runtime/contracts/IRuntimeOptions.ts
3402
4390
  var DEFAULT_RUNTIME_OPTIONS = {
@@ -6735,14 +7723,12 @@ function resolveDominantOrigin(origins) {
6735
7723
  if (origins.every((o) => o === "compiler")) return "compiler";
6736
7724
  return "analyzed";
6737
7725
  }
6738
-
6739
- // src/runtime/compiler/EffortEnrichmentPass.ts
6740
7726
  var logTrace = (message) => console.log(message);
6741
7727
  function applyEffortEnrichment(block, options) {
6742
- const { resolver, overwrite = false, debug = false } = options;
6743
- tryEnrichBlock(block, resolver, overwrite, debug);
7728
+ const { resolver, references, overwrite = false, debug = false } = options;
7729
+ tryEnrichBlock(block, resolver, references, overwrite, debug);
6744
7730
  }
6745
- function tryEnrichBlock(block, resolver, overwrite, debug) {
7731
+ function tryEnrichBlock(block, resolver, references, overwrite, debug) {
6746
7732
  const normalizedBlockType = block.blockType?.toLowerCase();
6747
7733
  if (normalizedBlockType !== "effort" && normalizedBlockType !== "exercise") {
6748
7734
  return;
@@ -6760,7 +7746,32 @@ function tryEnrichBlock(block, resolver, overwrite, debug) {
6760
7746
  return;
6761
7747
  }
6762
7748
  try {
6763
- const resolved = resolver.resolveEffort(label);
7749
+ let resolved;
7750
+ let matchedKind = "none";
7751
+ let equipmentRefs = [];
7752
+ let diagnostics = [];
7753
+ if (references) {
7754
+ const resolution = references.resolve(label);
7755
+ resolved = resolution.effort ?? void 0;
7756
+ matchedKind = resolution.matched;
7757
+ equipmentRefs = resolution.equipment;
7758
+ diagnostics = resolution.diagnostics;
7759
+ } else {
7760
+ resolved = resolver.resolveEffort(label);
7761
+ }
7762
+ if (!resolved) {
7763
+ if (diagnostics.length > 0) {
7764
+ block.pushMemory(new MemoryLocation("metric:tracked", [{
7765
+ type: wodWikiCore.MetricType.Hint,
7766
+ value: "reference.unresolved",
7767
+ image: label,
7768
+ origin: "compiler",
7769
+ metadata: { [wodWikiCore.REFERENCE_METADATA_KEY]: { matched: "none", source: label, diagnostics } }
7770
+ }]));
7771
+ }
7772
+ if (debug) logTrace(`[EffortEnrichment] Unresolved effort: ${label}`);
7773
+ return;
7774
+ }
6764
7775
  const effortHints = resolved.effort.hints;
6765
7776
  if (effortHints && Object.keys(effortHints).length > 0) {
6766
7777
  block.mergeHints(effortHints);
@@ -6773,7 +7784,8 @@ function tryEnrichBlock(block, resolver, overwrite, debug) {
6773
7784
  image: resolved.label,
6774
7785
  value: resolved,
6775
7786
  origin: "compiler",
6776
- timestamp: /* @__PURE__ */ new Date()
7787
+ timestamp: /* @__PURE__ */ new Date(),
7788
+ ...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
7789
  };
6778
7790
  const privateMetrics = block.getMetricMemoryByVisibility("private");
6779
7791
  const existingLoc = privateMetrics.find(
@@ -6788,10 +7800,31 @@ function tryEnrichBlock(block, resolver, overwrite, debug) {
6788
7800
  const location = new MemoryLocation("metric:tracked", [effortDataMetric]);
6789
7801
  block.pushMemory(location);
6790
7802
  }
7803
+ if (equipmentRefs.length > 0) {
7804
+ pushPrivateMetrics(block, wodWikiCore.MetricType.Equipment, [
7805
+ new EquipmentMetric(equipmentRefs.map((ref) => ref.slug), {
7806
+ refs: equipmentRefs,
7807
+ source: label,
7808
+ origin: "compiler"
7809
+ })
7810
+ ], wodWikiCore.MetricType.Equipment, overwrite);
7811
+ }
6791
7812
  } catch (err) {
6792
7813
  console.error(`[EffortEnrichment] Failed to resolve effort "${label}":`, err);
6793
7814
  }
6794
7815
  }
7816
+ function pushPrivateMetrics(block, markerType, metrics, replaceType, overwrite) {
7817
+ const privateMetrics = block.getMetricMemoryByVisibility("private");
7818
+ const existingLoc = privateMetrics.find(
7819
+ (loc) => loc.metrics.toArray().some((m) => m.type === markerType)
7820
+ );
7821
+ if (existingLoc && overwrite) {
7822
+ const filtered = existingLoc.metrics.toArray().filter((m) => m.type !== replaceType);
7823
+ existingLoc.update([...filtered, ...metrics]);
7824
+ } else if (!existingLoc) {
7825
+ block.pushMemory(new MemoryLocation("metric:tracked", metrics));
7826
+ }
7827
+ }
6795
7828
 
6796
7829
  // src/runtime/actions/stack/CompileAndPushBlockAction.ts
6797
7830
  var CompileAndPushBlockAction = class {
@@ -6817,7 +7850,8 @@ var CompileAndPushBlockAction = class {
6817
7850
  }
6818
7851
  if (runtime.analyticsContext?.effortResolver) {
6819
7852
  applyEffortEnrichment(compiledBlock, {
6820
- resolver: runtime.analyticsContext.effortResolver
7853
+ resolver: runtime.analyticsContext.effortResolver,
7854
+ references: runtime.analyticsContext.references
6821
7855
  });
6822
7856
  }
6823
7857
  return [new PushBlockAction(compiledBlock, this.options)];
@@ -10843,7 +11877,8 @@ function createAnalyticsEngineForBlock(block, options) {
10843
11877
  effortResolver = new EffortResolver(registry);
10844
11878
  }
10845
11879
  const analyticsContext = {
10846
- effortResolver
11880
+ effortResolver,
11881
+ ...options?.references ? { references: options.references } : {}
10847
11882
  };
10848
11883
  const context = {
10849
11884
  dialect,
@@ -13756,6 +14791,7 @@ exports.BehaviorContext = BehaviorContext;
13756
14791
  exports.BlockBuilder = BlockBuilder;
13757
14792
  exports.BlockContext = BlockContext;
13758
14793
  exports.ButtonBehavior = ButtonBehavior;
14794
+ exports.CATALOG_PRECEDENCE_RANK = CATALOG_PRECEDENCE_RANK;
13759
14795
  exports.CONSUMED_HINTS = CONSUMED_HINTS;
13760
14796
  exports.CONSUMED_HINT_KEYS = CONSUMED_HINT_KEYS;
13761
14797
  exports.CONTEXT_ATOMS = CONTEXT_ATOMS;
@@ -13914,18 +14950,25 @@ exports.createMetricPresentationPolicy = createPolicy;
13914
14950
  exports.createMockClock = createMockClock;
13915
14951
  exports.createParser = createParser;
13916
14952
  exports.createProfileTable = createProfileTable;
14953
+ exports.createReferenceIndex = createReferenceIndex;
13917
14954
  exports.createRpeLabelsTable = createRpeLabelsTable;
13918
14955
  exports.dailySessionLoads = dailySessionLoads;
13919
14956
  exports.dayBucket = dayBucket;
14957
+ exports.dedupeEntitiesByPrecedence = dedupeEntitiesByPrecedence;
13920
14958
  exports.dialectRegistry = dialectRegistry;
13921
14959
  exports.dialectStack = dialectStack;
13922
14960
  exports.diffStatement = diffStatement;
13923
14961
  exports.dimEquals = dimEquals;
13924
14962
  exports.disciplineFactorFor = disciplineFactorFor;
14963
+ exports.documentToEffort = documentToEffort;
14964
+ exports.documentToEquipment = documentToEquipment;
14965
+ exports.effortToDocument = effortToDocument;
14966
+ exports.equipmentToDocument = equipmentToDocument;
13925
14967
  exports.evaluate = evaluate;
13926
14968
  exports.evaluateCalculateDefinitions = evaluateCalculateDefinitions;
13927
14969
  exports.extractEffortData = extractEffortData;
13928
14970
  exports.extractMetrics = extractMetrics;
14971
+ exports.extractStatementRefs = extractStatementRefs;
13929
14972
  exports.extractStatements = extractStatements;
13930
14973
  exports.extractStatementsRaw = extractStatementsRaw;
13931
14974
  exports.extractSyntaxFacts = extractSyntaxFacts;
@@ -13973,15 +15016,22 @@ exports.outputNodeId = outputNodeId;
13973
15016
  exports.outputNodeIds = outputNodeIds;
13974
15017
  exports.parseCalcLine = parseCalcLine;
13975
15018
  exports.parseCalculateBlock = parseCalculateBlock;
15019
+ exports.parseEffortFile = parseEffortFile;
15020
+ exports.parseEquipmentFile = parseEquipmentFile;
13976
15021
  exports.parseExpression = parseExpression;
13977
15022
  exports.parseMetricLine = parseMetricLine;
13978
15023
  exports.parseScript = parseScript;
13979
15024
  exports.printCalculation = printCalculation;
15025
+ exports.readNestedSection = readNestedSection;
13980
15026
  exports.readTimer = readTimer;
13981
15027
  exports.realtimeProcessorRegistry = realtimeProcessorRegistry;
13982
15028
  exports.renderMetric = renderMetric;
13983
15029
  exports.resolveDominantOrigin = resolveDominantOrigin;
13984
- exports.sharedParser = sharedParser;
15030
+ exports.resolveEntityRank = resolveEntityRank;
15031
+ exports.resolveEntityTier = resolveEntityTier;
15032
+ exports.resolveSourceRank = resolveSourceRank;
15033
+ exports.resolveSourceTier = resolveSourceTier;
15034
+ exports.slugifyRefLabel = slugifyRefLabel;
13985
15035
  exports.startSpan = startSpan;
13986
15036
  exports.str = str;
13987
15037
  exports.strategyRegistry = strategyRegistry;