@bitcobblers/wod-wiki-lang 0.37.2137 → 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.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { wallClockNow, formatDurationSmart } from './chunk-PFQDVQQO.mjs';
2
2
  export { createBrowserRuntimeNow, frozenNow, getRuntimeNowMs, wallClockNow } from './chunk-PFQDVQQO.mjs';
3
- import { MetricType, MetricContainer, BlockKey, ParsedCodeStatement, OutputStatement, valueKindOf, normalizeFieldPath } from '@bitcobblers/wod-wiki-core';
3
+ import { MetricType, MetricContainer, BlockKey, REFERENCE_METADATA_KEY, ParsedCodeStatement, getReferenceMetadata, OutputStatement, valueKindOf, normalizeFieldPath } from '@bitcobblers/wod-wiki-core';
4
4
  import { LRParser } from '@lezer/lr';
5
5
  import { LRLanguage, LanguageSupport } from '@codemirror/language';
6
6
  import { NodeProp } from '@lezer/common';
@@ -378,15 +378,19 @@ function createPolicy() {
378
378
  }
379
379
  var metricPresentation = createPolicy();
380
380
  var EffortMetric = class {
381
- constructor(effort) {
381
+ constructor(effort, options) {
382
382
  this.effort = effort;
383
- this.value = effort;
383
+ this.value = options?.resolvedSlug ?? effort;
384
384
  this.image = effort;
385
+ if (options?.references) {
386
+ this.metadata = { [REFERENCE_METADATA_KEY]: options.references };
387
+ }
385
388
  }
386
389
  effort;
387
390
  value;
388
391
  image;
389
392
  origin = "parser";
393
+ metadata;
390
394
  type = MetricType.Effort;
391
395
  };
392
396
  var DistanceMetric = class {
@@ -1354,6 +1358,27 @@ var DurationMetric = class {
1354
1358
  return this.value > 0 ? "down" : "up";
1355
1359
  }
1356
1360
  };
1361
+ var EquipmentMetric = class {
1362
+ type = MetricType.Equipment;
1363
+ origin;
1364
+ /** Canonical slugs, ordered and deduplicated. */
1365
+ value;
1366
+ image;
1367
+ name = "equipment";
1368
+ metadata;
1369
+ constructor(slugs, options) {
1370
+ this.value = [...new Set(slugs)];
1371
+ this.image = options?.source ?? this.value.join(" ");
1372
+ this.origin = options?.origin ?? "parser";
1373
+ const reference = {
1374
+ matched: options?.matched ?? "whole",
1375
+ source: options?.source ?? this.image,
1376
+ ...options?.refs ? { refs: options.refs } : {},
1377
+ ...options?.diagnostics?.length ? { diagnostics: options.diagnostics } : {}
1378
+ };
1379
+ this.metadata = { [REFERENCE_METADATA_KEY]: reference };
1380
+ }
1381
+ };
1357
1382
  var GroupMetric = class {
1358
1383
  constructor(group, image) {
1359
1384
  this.group = group;
@@ -1450,16 +1475,29 @@ var PROPERTY_KEY_TO_METRIC_TYPE = {
1450
1475
  volume: MetricType.Volume,
1451
1476
  work: MetricType.Work
1452
1477
  };
1453
- function classifyStatements(facts) {
1478
+ var STRUCTURAL_EFFORT_WORDS = /* @__PURE__ */ new Set(["rest", "amrap", "emom", "tabata", "for time"]);
1479
+ var STANDARD_UNITS2 = UnitRegistry.standard();
1480
+ var REFERENCE_PAIR_KEYS = /* @__PURE__ */ new Set(["effort", "equipment"]);
1481
+ function referenceMetadata(resolution) {
1482
+ return {
1483
+ matched: resolution.matched,
1484
+ source: resolution.label,
1485
+ ...resolution.effortRef || resolution.equipment.length > 0 ? { refs: [...resolution.effortRef ? [resolution.effortRef] : [], ...resolution.equipment] } : {},
1486
+ ...resolution.remaining ? { remaining: resolution.remaining } : {},
1487
+ ...resolution.diagnostics.length > 0 ? { diagnostics: resolution.diagnostics } : {}
1488
+ };
1489
+ }
1490
+ function classifyStatements(facts, references) {
1454
1491
  return facts.statements.map((fact) => {
1455
1492
  const statement = new ParsedCodeStatement();
1456
- const metricPairs = [];
1493
+ let metricPairs = [];
1457
1494
  for (const primitive of fact.primitives) {
1458
1495
  metricPairs.push(...classifyPrimitive(primitive));
1459
1496
  }
1460
1497
  const mergedPairs = mergeFragments(metricPairs);
1461
- statement.metrics = mergedPairs.map((pair) => pair.metrics);
1462
- statement.metricMeta = new Map(mergedPairs.map((pair) => [pair.metrics, pair.meta]));
1498
+ const finalPairs = references ? applyReferenceResolution(mergedPairs, fact, references) : mergedPairs;
1499
+ statement.metrics = finalPairs.map((pair) => pair.metrics);
1500
+ statement.metricMeta = new Map(finalPairs.map((pair) => [pair.metrics, pair.meta]));
1463
1501
  statement.id = fact.id;
1464
1502
  statement.line = fact.line;
1465
1503
  statement.text = fact.meta.raw;
@@ -1471,6 +1509,105 @@ function classifyStatements(facts) {
1471
1509
  return statement;
1472
1510
  });
1473
1511
  }
1512
+ function applyReferenceResolution(pairs, fact, references) {
1513
+ const { index, defaults } = references;
1514
+ const result = [];
1515
+ const emitResolution = (resolution, authored, meta2) => {
1516
+ if (resolution.effortRef || resolution.matched !== "none" || resolution.diagnostics.length > 0) {
1517
+ result.push({
1518
+ metrics: new EffortMetric(authored, {
1519
+ resolvedSlug: resolution.effortRef?.slug,
1520
+ references: referenceMetadata(resolution)
1521
+ }),
1522
+ meta: meta2
1523
+ });
1524
+ }
1525
+ if (resolution.equipment.length > 0) {
1526
+ result.push({
1527
+ metrics: new EquipmentMetric(
1528
+ resolution.equipment.map((ref) => ref.slug),
1529
+ {
1530
+ refs: resolution.equipment,
1531
+ source: authored,
1532
+ matched: resolution.matched,
1533
+ diagnostics: resolution.diagnostics
1534
+ }
1535
+ ),
1536
+ meta: meta2
1537
+ });
1538
+ }
1539
+ };
1540
+ const handledRawObjects = /* @__PURE__ */ new Set();
1541
+ const malformedHints = [];
1542
+ for (const primitive of fact.primitives) {
1543
+ if (primitive.kind !== "metric_object") continue;
1544
+ if (handledRawObjects.has(primitive.raw)) continue;
1545
+ handledRawObjects.add(primitive.raw);
1546
+ if (primitive.pairs.length === 0) {
1547
+ malformedHints.push({
1548
+ metrics: {
1549
+ ...hintMetric("reference.malformed_json", "parser"),
1550
+ metadata: referenceMetadata(index.resolve(primitive.raw))
1551
+ },
1552
+ meta: primitive.meta
1553
+ });
1554
+ continue;
1555
+ }
1556
+ const hasReferenceKeys = primitive.pairs.some((pair) => REFERENCE_PAIR_KEYS.has(pair.key.toLowerCase()));
1557
+ if (!hasReferenceKeys) continue;
1558
+ const resolution = index.resolve(primitive.raw);
1559
+ if (resolution.matched === "explicit") {
1560
+ emitResolution(resolution, primitive.raw, primitive.meta);
1561
+ }
1562
+ for (const pair of primitive.pairs) {
1563
+ if (REFERENCE_PAIR_KEYS.has(pair.key.toLowerCase())) continue;
1564
+ const metricType = PROPERTY_KEY_TO_METRIC_TYPE[pair.key.toLowerCase()] ?? MetricType.Custom;
1565
+ const rawValue = Array.isArray(pair.value) ? pair.value.join(", ") : pair.value;
1566
+ result.push({
1567
+ metrics: new PropertyMetric(pair.key, rawValue, { type: metricType, image: `${pair.key}: ${rawValue}` }),
1568
+ meta: primitive.meta
1569
+ });
1570
+ }
1571
+ }
1572
+ let sawEffortMetric = false;
1573
+ let sawDurationMetric = false;
1574
+ let sawMovementQuantity = false;
1575
+ let statementMeta;
1576
+ for (const pair of pairs) {
1577
+ const metric = pair.metrics;
1578
+ if (metric.type === MetricType.Custom && handledRawObjects.has(pair.meta.raw)) continue;
1579
+ if (metric.type === MetricType.Effort) {
1580
+ const authored = metric instanceof EffortMetric ? metric.effort : String(metric.value ?? "");
1581
+ const normalized = authored.trim().toLowerCase();
1582
+ if (STRUCTURAL_EFFORT_WORDS.has(normalized) || STANDARD_UNITS2.has(normalized)) {
1583
+ result.push(pair);
1584
+ continue;
1585
+ }
1586
+ sawEffortMetric = true;
1587
+ const resolution = index.resolve(authored, defaults);
1588
+ if (resolution.matched === "none" && resolution.diagnostics.length === 0) {
1589
+ result.push(pair);
1590
+ continue;
1591
+ }
1592
+ emitResolution(resolution, authored, pair.meta);
1593
+ continue;
1594
+ }
1595
+ if (metric.type === MetricType.Duration) sawDurationMetric = true;
1596
+ if (metric.type === MetricType.Rep || metric.type === MetricType.Rounds || metric.type === MetricType.Resistance || metric.type === MetricType.Distance) {
1597
+ sawMovementQuantity = true;
1598
+ }
1599
+ result.push(pair);
1600
+ statementMeta ??= pair.meta;
1601
+ }
1602
+ if (defaults && !sawEffortMetric && fact.isLeaf && sawMovementQuantity && !sawDurationMetric) {
1603
+ const resolution = index.resolve("", defaults);
1604
+ if (resolution.effortRef) {
1605
+ emitResolution(resolution, resolution.effortRef.label, statementMeta ?? fact.meta);
1606
+ }
1607
+ }
1608
+ result.push(...malformedHints);
1609
+ return result;
1610
+ }
1474
1611
  function classifyPrimitive(primitive) {
1475
1612
  switch (primitive.kind) {
1476
1613
  case "lap":
@@ -1539,10 +1676,11 @@ function classifyPrimitive(primitive) {
1539
1676
  const pairs = primitive.pairs;
1540
1677
  return pairs.map((pair) => {
1541
1678
  const metricType = PROPERTY_KEY_TO_METRIC_TYPE[pair.key.toLowerCase()] ?? MetricType.Custom;
1679
+ const rawValue = Array.isArray(pair.value) ? pair.value.join(", ") : pair.value;
1542
1680
  return {
1543
- metrics: new PropertyMetric(pair.key, pair.value, {
1681
+ metrics: new PropertyMetric(pair.key, rawValue, {
1544
1682
  type: metricType,
1545
- image: `${pair.key}: ${pair.value}`
1683
+ image: `${pair.key}: ${rawValue}`
1546
1684
  }),
1547
1685
  meta: primitive.meta
1548
1686
  };
@@ -1580,6 +1718,23 @@ function mergeFragments(pairs) {
1580
1718
  continue;
1581
1719
  }
1582
1720
  }
1721
+ if (current.metrics instanceof RepMetric && next.metrics instanceof EffortMetric && /^(reps?)$/i.test(next.metrics.effort)) {
1722
+ const gap = next.meta.startOffset - current.meta.endOffset;
1723
+ if (gap <= 1) {
1724
+ const mergedMeta = {
1725
+ ...current.meta,
1726
+ endOffset: next.meta.endOffset,
1727
+ columnEnd: next.meta.columnEnd,
1728
+ length: next.meta.endOffset - current.meta.startOffset,
1729
+ raw: current.meta.raw + (gap === 1 ? " " : "") + next.meta.raw
1730
+ };
1731
+ current = {
1732
+ metrics: current.metrics,
1733
+ meta: mergedMeta
1734
+ };
1735
+ continue;
1736
+ }
1737
+ }
1583
1738
  if (current.metrics instanceof RepMetric && (next.metrics instanceof ResistanceMetric || next.metrics instanceof DistanceMetric)) {
1584
1739
  const gap = next.meta.startOffset - current.meta.endOffset;
1585
1740
  if (gap <= 1) {
@@ -1955,12 +2110,12 @@ function resolveSourceInput(sourceOrTree, treeArg) {
1955
2110
  }
1956
2111
  return { source: "" };
1957
2112
  }
1958
- function extractStatementsRaw(sourceOrTree, treeArg) {
2113
+ function extractStatementsRaw(sourceOrTree, treeArg, references) {
1959
2114
  const { source, tree } = resolveSourceInput(sourceOrTree, treeArg);
1960
2115
  const facts = extractSyntaxFacts(source, tree);
1961
- return normalizeInlineRounds(classifyStatements(facts));
2116
+ return normalizeInlineRounds(classifyStatements(facts, references));
1962
2117
  }
1963
- function extractStatements(sourceOrTree, sportOrTree, sportArg) {
2118
+ function extractStatements(sourceOrTree, sportOrTree, sportArg, references) {
1964
2119
  let sport;
1965
2120
  let tree;
1966
2121
  if (typeof sportOrTree === "string") {
@@ -1971,7 +2126,7 @@ function extractStatements(sourceOrTree, sportOrTree, sportArg) {
1971
2126
  }
1972
2127
  const resolved = resolveSourceInput(sourceOrTree, tree);
1973
2128
  const facts = extractSyntaxFacts(resolved.source, resolved.tree);
1974
- const statements = classifyStatements(facts);
2129
+ const statements = classifyStatements(facts, references);
1975
2130
  dialectStack.processAll(statements, sport);
1976
2131
  return normalizeInlineRounds(statements);
1977
2132
  }
@@ -2103,7 +2258,8 @@ function parseScript(inputText, options = {}) {
2103
2258
  const doc = inputText.endsWith("\n") ? inputText : inputText + "\n";
2104
2259
  const tree = parser.parse(doc);
2105
2260
  const sport = options.sport ?? options.dialect;
2106
- const statements = options.withoutDialects ? extractStatementsRaw(inputText, tree) : extractStatements(inputText, tree, sport);
2261
+ const references = options.references ? { index: options.references, defaults: options.defaults } : void 0;
2262
+ const statements = options.withoutDialects ? extractStatementsRaw(inputText, tree, references) : extractStatements(inputText, tree, sport, references);
2107
2263
  return new WhiteboardScript(inputText, statements, []);
2108
2264
  } catch (error) {
2109
2265
  const msg = error instanceof Error ? error.message : "Unknown parse error";
@@ -2113,6 +2269,10 @@ function parseScript(inputText, options = {}) {
2113
2269
 
2114
2270
  // src/parser/md-timer.ts
2115
2271
  var MdTimerRuntime = class {
2272
+ config;
2273
+ constructor(config) {
2274
+ this.config = config;
2275
+ }
2116
2276
  /**
2117
2277
  * Parse WhiteboardScript source into a {@link WhiteboardScript}, running the
2118
2278
  * Dialect Stack on every statement.
@@ -2120,22 +2280,21 @@ var MdTimerRuntime = class {
2120
2280
  * @param sport - The block's `:sport` fence suffix (` ```log:climbing `).
2121
2281
  */
2122
2282
  read(inputText, sport) {
2123
- return parseScript(inputText, { sport });
2283
+ return parseScript(inputText, { sport, ...this.config });
2124
2284
  }
2125
2285
  /**
2126
2286
  * Parse without running the Dialect Stack. Used by the parser test harness
2127
2287
  * which applies its own Dialect set. Production consumers should use {@link read}.
2128
2288
  */
2129
2289
  readWithoutDialects(inputText) {
2130
- return parseScript(inputText, { withoutDialects: true });
2290
+ return parseScript(inputText, { withoutDialects: true, ...this.config });
2131
2291
  }
2132
2292
  };
2133
2293
 
2134
2294
  // src/parser/parserInstance.ts
2135
- function createParser() {
2136
- return new MdTimerRuntime();
2295
+ function createParser(config) {
2296
+ return new MdTimerRuntime(config);
2137
2297
  }
2138
- var sharedParser = /* @__PURE__ */ createParser();
2139
2298
  var nextTagID = 0;
2140
2299
  var Tag = class _Tag {
2141
2300
  /**
@@ -2838,6 +2997,426 @@ function disciplineFactorFor(discipline) {
2838
2997
  const key = discipline.toLowerCase();
2839
2998
  return isEffortDiscipline(key) ? DISCIPLINE_FACTORS[key] : 1;
2840
2999
  }
3000
+ var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
3001
+ var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3002
+ function unquote(value) {
3003
+ return value.replace(
3004
+ /^(['"])(.*)\1$/,
3005
+ (_match, quote, inner) => quote === '"' ? inner.replace(
3006
+ /\\(["\\nrt])/g,
3007
+ (_all, c) => c === "n" ? "\n" : c === "r" ? "\r" : c === "t" ? " " : c
3008
+ ) : inner
3009
+ );
3010
+ }
3011
+ function quoteYaml(val) {
3012
+ if (!val) return '""';
3013
+ if (/[:"'\n#{}[\],&*?|\-<>=%!@`]/.test(val) || val !== val.trim()) {
3014
+ return `"${val.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
3015
+ }
3016
+ return val;
3017
+ }
3018
+ function parseFrontmatter(raw) {
3019
+ const match = raw.match(FRONTMATTER_RE);
3020
+ if (!match) return { meta: {}, body: raw };
3021
+ const lines = match[1].split(/\r?\n/);
3022
+ const meta2 = {};
3023
+ for (let i = 0; i < lines.length; i++) {
3024
+ const keyMatch = lines[i].match(/^([A-Za-z][\w.-]*)\s*:(.*)$/);
3025
+ if (!keyMatch) continue;
3026
+ const key = keyMatch[1];
3027
+ const rawVal = keyMatch[2].trim();
3028
+ if (rawVal === "") {
3029
+ const items = [];
3030
+ while (i + 1 < lines.length) {
3031
+ const itemMatch = lines[i + 1].match(/^\s+-\s+(.+)$/);
3032
+ if (!itemMatch) break;
3033
+ items.push(unquote(itemMatch[1].trim()));
3034
+ i++;
3035
+ }
3036
+ meta2[key] = items.length > 0 ? items : "";
3037
+ } else {
3038
+ const wasQuoted = /^(['"])(.*)\1$/.test(rawVal);
3039
+ const value = unquote(rawVal);
3040
+ const num2 = Number(value);
3041
+ meta2[key] = !wasQuoted && value !== "" && !isNaN(num2) ? num2 : value;
3042
+ }
3043
+ }
3044
+ return { meta: meta2, body: raw.slice(match[0].length) };
3045
+ }
3046
+ function getScalar(meta2, key) {
3047
+ const value = meta2[key];
3048
+ return typeof value === "string" || typeof value === "number" ? value : void 0;
3049
+ }
3050
+ function getListStrict(meta2, key) {
3051
+ const value = meta2[key];
3052
+ if (value === void 0) return [];
3053
+ if (typeof value === "string" && value === "[]") return [];
3054
+ return Array.isArray(value) ? value : void 0;
3055
+ }
3056
+ function coerceNestedScalar(raw) {
3057
+ if (raw === "true") return true;
3058
+ if (raw === "false") return false;
3059
+ if (raw !== "" && !Number.isNaN(Number(raw))) return Number(raw);
3060
+ return unquote(raw);
3061
+ }
3062
+ function readNestedSection(lines, key) {
3063
+ const start = lines.findIndex((l) => new RegExp(`^${key}:\\s*$`).test(l));
3064
+ if (start === -1) return void 0;
3065
+ const result = {};
3066
+ let subKey = null;
3067
+ for (let i = start + 1; i < lines.length; i++) {
3068
+ const line = lines[i];
3069
+ if (/^\S/.test(line)) break;
3070
+ const trimmed = line.trim();
3071
+ if (!trimmed || trimmed.startsWith("#")) continue;
3072
+ const indent = line.length - line.trimStart().length;
3073
+ const m = trimmed.match(/^([\w.-]+):\s*(.*)$/);
3074
+ if (!m) continue;
3075
+ const [, k, rawVal] = m;
3076
+ if (indent === 2) {
3077
+ if (rawVal === "") {
3078
+ subKey = k;
3079
+ result[k] = {};
3080
+ } else {
3081
+ subKey = null;
3082
+ result[k] = coerceNestedScalar(rawVal);
3083
+ }
3084
+ } else if (indent >= 4 && subKey !== null) {
3085
+ result[subKey][k] = coerceNestedScalar(rawVal);
3086
+ }
3087
+ }
3088
+ return Object.keys(result).length > 0 ? result : void 0;
3089
+ }
3090
+ function parseDiscipline(val) {
3091
+ if (!val) return void 0;
3092
+ const normalized = unquote(val.trim()).toLowerCase();
3093
+ if (isEffortDiscipline(normalized)) return normalized;
3094
+ console.warn(`[definition-markdown] unknown discipline '${val}' \u2014 expected one of the canonical values; treating as unset`);
3095
+ return void 0;
3096
+ }
3097
+ function identityProblems(slug, label) {
3098
+ const problems = [];
3099
+ const slugText = slug === void 0 ? "" : String(slug);
3100
+ if (!slugText) problems.push("Missing required field: slug");
3101
+ else if (!SLUG_RE.test(slugText)) problems.push("Invalid slug: must be lowercase letters, numbers, and hyphens only");
3102
+ if (label === void 0 || !String(label).trim()) problems.push("Missing required field: label");
3103
+ return problems;
3104
+ }
3105
+ function stringListProblems(values, kind) {
3106
+ const problems = [];
3107
+ for (const value of values) {
3108
+ if (!value.trim()) problems.push(`Invalid ${kind}: blank entries are not allowed`);
3109
+ else if (kind === "equipment" && !SLUG_RE.test(value)) {
3110
+ problems.push(`Invalid ${kind}: '${value}' must be a canonical slug (lowercase letters, numbers, hyphens)`);
3111
+ }
3112
+ }
3113
+ const duplicate = values.find((v, i) => values.indexOf(v) !== i);
3114
+ if (duplicate !== void 0) problems.push(`Invalid ${kind}: duplicate entry '${duplicate}'`);
3115
+ return problems;
3116
+ }
3117
+ function metProblem(value) {
3118
+ if (value === void 0) return "Missing required field: baseAttributes.met";
3119
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
3120
+ return "Invalid baseAttributes.met: must be a positive number";
3121
+ }
3122
+ return null;
3123
+ }
3124
+ function parseEffortFile(raw) {
3125
+ const blockMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
3126
+ if (!blockMatch) return null;
3127
+ const innerLines = blockMatch[1].split(/\r?\n/);
3128
+ const { meta: meta2, body } = parseFrontmatter(raw);
3129
+ const type = getScalar(meta2, "type");
3130
+ if (type !== void 0 && type !== "effort") return null;
3131
+ const id = getScalar(meta2, "id");
3132
+ const slug = getScalar(meta2, "slug");
3133
+ const label = getScalar(meta2, "label");
3134
+ const aliases = getListStrict(meta2, "aliases");
3135
+ const equipment = getListStrict(meta2, "equipment");
3136
+ if (aliases === void 0 || equipment === void 0) return null;
3137
+ if (identityProblems(slug, label).length > 0) return null;
3138
+ if (stringListProblems(aliases, "aliases").length > 0) return null;
3139
+ if (equipment.length > 0 && stringListProblems(equipment, "equipment").length > 0) return null;
3140
+ const baseAttrs = readNestedSection(innerLines, "baseAttributes") ?? {};
3141
+ if (metProblem(baseAttrs.met) !== null) return null;
3142
+ const met = baseAttrs.met;
3143
+ const discipline = parseDiscipline(
3144
+ typeof baseAttrs.discipline === "string" ? baseAttrs.discipline : void 0
3145
+ );
3146
+ const intensityTier = baseAttrs.intensityTier;
3147
+ const disciplineFactor = typeof baseAttrs.disciplineFactor === "number" && Number.isFinite(baseAttrs.disciplineFactor) ? baseAttrs.disciplineFactor : void 0;
3148
+ const hints = readNestedSection(innerLines, "hints");
3149
+ const derivation = readNestedSection(innerLines, "derivation");
3150
+ const trimmedBody = body.trim();
3151
+ return {
3152
+ id: String(id ?? ""),
3153
+ slug: String(slug),
3154
+ label: String(label),
3155
+ aliases,
3156
+ baseAttributes: {
3157
+ met,
3158
+ ...discipline ? { discipline } : {},
3159
+ ...disciplineFactor !== void 0 ? { disciplineFactor } : {},
3160
+ ...intensityTier ? { intensityTier } : {}
3161
+ },
3162
+ registrySource: "bundled",
3163
+ ...equipment.length > 0 ? { equipment } : {},
3164
+ ...trimmedBody ? { body: trimmedBody } : {},
3165
+ ...hints ? { hints } : {},
3166
+ ...derivation ? { derivation } : {}
3167
+ };
3168
+ }
3169
+ function parseEquipmentFile(raw) {
3170
+ const { meta: meta2, body } = parseFrontmatter(raw);
3171
+ if (getScalar(meta2, "type") !== "equipment") return null;
3172
+ const id = getScalar(meta2, "id");
3173
+ const slug = getScalar(meta2, "slug");
3174
+ const label = getScalar(meta2, "label");
3175
+ const aliases = getListStrict(meta2, "aliases");
3176
+ if (id === void 0 || aliases === void 0) return null;
3177
+ if (identityProblems(slug, label).length > 0) return null;
3178
+ if (stringListProblems(aliases, "aliases").length > 0) return null;
3179
+ const trimmedBody = body.trim();
3180
+ return {
3181
+ id: String(id),
3182
+ slug: String(slug),
3183
+ label: String(label),
3184
+ aliases,
3185
+ registrySource: "bundled",
3186
+ ...trimmedBody ? { body: trimmedBody } : {}
3187
+ };
3188
+ }
3189
+ function createDefaultEffort() {
3190
+ return {
3191
+ id: `effort-user-${v7()}`,
3192
+ slug: "",
3193
+ label: "",
3194
+ aliases: [],
3195
+ baseAttributes: { met: 5 },
3196
+ registrySource: "user"
3197
+ };
3198
+ }
3199
+ function documentToEffort(doc, baseEffort) {
3200
+ const errors = [];
3201
+ const blockMatch = doc.match(/^---\r?\n([\s\S]*?)\r?\n---/);
3202
+ if (!blockMatch) {
3203
+ errors.push("Missing YAML frontmatter delimiters (---)");
3204
+ return { effort: baseEffort ?? createDefaultEffort(), errors };
3205
+ }
3206
+ const innerLines = blockMatch[1].split(/\r?\n/);
3207
+ const { meta: meta2, body } = parseFrontmatter(doc);
3208
+ const bodyText = body.replace(/^\r?\n+/, "") || void 0;
3209
+ const id = getScalar(meta2, "id");
3210
+ const slug = getScalar(meta2, "slug");
3211
+ const label = getScalar(meta2, "label");
3212
+ const registrySource = getScalar(meta2, "registrySource");
3213
+ const createdAt = getScalar(meta2, "createdAt");
3214
+ const updatedAt = getScalar(meta2, "updatedAt");
3215
+ const aliases = getListStrict(meta2, "aliases");
3216
+ const equipmentRaw = meta2["equipment"];
3217
+ const equipment = getListStrict(meta2, "equipment");
3218
+ if (aliases === void 0) {
3219
+ errors.push("Invalid aliases: must be a list (`aliases:` followed by `- item` lines)");
3220
+ }
3221
+ if (equipment === void 0) {
3222
+ errors.push("Invalid equipment: must be a list (`equipment:` followed by `- slug` lines)");
3223
+ }
3224
+ const baseAttrs = readNestedSection(innerLines, "baseAttributes") ?? {};
3225
+ const baseAttributes = {};
3226
+ const metRaw = baseAttrs.met;
3227
+ if (metRaw === void 0) {
3228
+ const fallbackMet = baseEffort?.baseAttributes.met;
3229
+ const fallbackProblem = fallbackMet === void 0 ? null : metProblem(fallbackMet);
3230
+ if (fallbackProblem) errors.push(fallbackProblem);
3231
+ else if (fallbackMet !== void 0) baseAttributes.met = fallbackMet;
3232
+ else errors.push("Missing required field: baseAttributes.met");
3233
+ } else {
3234
+ const problem = metProblem(metRaw);
3235
+ if (problem) errors.push(problem);
3236
+ else baseAttributes.met = Number(metRaw);
3237
+ }
3238
+ if (getScalar(meta2, "met") !== void 0) {
3239
+ errors.push(
3240
+ "Invalid met placement: 'met' must be nested under baseAttributes (missing baseAttributes.met)"
3241
+ );
3242
+ }
3243
+ const discipline = parseDiscipline(
3244
+ typeof baseAttrs.discipline === "string" ? baseAttrs.discipline : void 0
3245
+ );
3246
+ if (discipline) baseAttributes.discipline = discipline;
3247
+ if (typeof baseAttrs.disciplineFactor === "number" && Number.isFinite(baseAttrs.disciplineFactor)) {
3248
+ baseAttributes.disciplineFactor = baseAttrs.disciplineFactor;
3249
+ }
3250
+ if (baseAttrs.intensityTier) baseAttributes.intensityTier = baseAttrs.intensityTier;
3251
+ const hints = readNestedSection(innerLines, "hints") ?? baseEffort?.hints;
3252
+ const derivation = readNestedSection(innerLines, "derivation");
3253
+ errors.push(...identityProblems(slug, label));
3254
+ if (aliases !== void 0) errors.push(...stringListProblems(aliases, "aliases"));
3255
+ if (equipment !== void 0 && equipment.length > 0) {
3256
+ errors.push(...stringListProblems(equipment, "equipment"));
3257
+ }
3258
+ const resolvedEquipment = equipmentRaw !== void 0 ? equipment ?? [] : baseEffort?.equipment?.length ? baseEffort.equipment : void 0;
3259
+ const effort = {
3260
+ id: String(id ?? "") || baseEffort?.id || `effort-user-${v7()}`,
3261
+ slug: String(slug ?? ""),
3262
+ label: String(label ?? ""),
3263
+ aliases: aliases ?? [],
3264
+ baseAttributes,
3265
+ registrySource: registrySource || baseEffort?.registrySource || "user",
3266
+ ...resolvedEquipment ? { equipment: resolvedEquipment } : {},
3267
+ ...createdAt ? { createdAt: String(createdAt) } : {},
3268
+ ...updatedAt ? { updatedAt: String(updatedAt) } : {},
3269
+ ...bodyText ? { body: bodyText } : {},
3270
+ ...hints && Object.keys(hints).length > 0 ? { hints } : {},
3271
+ ...derivation ? { derivation } : {}
3272
+ };
3273
+ return { effort, errors };
3274
+ }
3275
+ function effortToDocument(effort) {
3276
+ const lines = ["---"];
3277
+ lines.push(`id: ${effort.id}`);
3278
+ lines.push(`slug: ${effort.slug}`);
3279
+ lines.push(`label: ${quoteYaml(effort.label)}`);
3280
+ if (effort.aliases.length > 0) {
3281
+ lines.push("aliases:");
3282
+ for (const alias of effort.aliases) {
3283
+ lines.push(` - ${quoteYaml(alias)}`);
3284
+ }
3285
+ } else {
3286
+ lines.push("aliases: []");
3287
+ }
3288
+ lines.push("baseAttributes:");
3289
+ lines.push(` met: ${effort.baseAttributes.met}`);
3290
+ if (effort.baseAttributes.discipline) {
3291
+ lines.push(` discipline: ${effort.baseAttributes.discipline}`);
3292
+ }
3293
+ if (effort.baseAttributes.disciplineFactor !== void 0) {
3294
+ lines.push(` disciplineFactor: ${effort.baseAttributes.disciplineFactor}`);
3295
+ }
3296
+ if (effort.baseAttributes.intensityTier) {
3297
+ lines.push(` intensityTier: ${effort.baseAttributes.intensityTier}`);
3298
+ }
3299
+ lines.push(`registrySource: ${effort.registrySource}`);
3300
+ if (effort.equipment && effort.equipment.length > 0) {
3301
+ lines.push("equipment:");
3302
+ for (const slug of effort.equipment) {
3303
+ lines.push(` - ${quoteYaml(slug)}`);
3304
+ }
3305
+ }
3306
+ if (effort.derivation) {
3307
+ lines.push("derivation:");
3308
+ if (effort.derivation.parentSlug) {
3309
+ lines.push(` parentSlug: ${effort.derivation.parentSlug}`);
3310
+ }
3311
+ if (effort.derivation.coefficients && Object.keys(effort.derivation.coefficients).length > 0) {
3312
+ lines.push(" coefficients:");
3313
+ for (const [k, v] of Object.entries(effort.derivation.coefficients)) {
3314
+ lines.push(` ${k}: ${v}`);
3315
+ }
3316
+ }
3317
+ if (effort.derivation.hardOverrides && Object.keys(effort.derivation.hardOverrides).length > 0) {
3318
+ lines.push(" hardOverrides:");
3319
+ for (const [k, v] of Object.entries(effort.derivation.hardOverrides)) {
3320
+ lines.push(` ${k}: ${JSON.stringify(v)}`);
3321
+ }
3322
+ }
3323
+ }
3324
+ if (effort.createdAt) {
3325
+ lines.push(`createdAt: ${effort.createdAt}`);
3326
+ }
3327
+ if (effort.updatedAt) {
3328
+ lines.push(`updatedAt: ${effort.updatedAt}`);
3329
+ }
3330
+ if (effort.hints && Object.keys(effort.hints).length > 0) {
3331
+ lines.push("hints:");
3332
+ for (const [k, v] of Object.entries(effort.hints)) {
3333
+ lines.push(` ${k}: ${v !== null && typeof v === "object" ? JSON.stringify(v) : String(v)}`);
3334
+ }
3335
+ }
3336
+ lines.push("---");
3337
+ if (effort.body) {
3338
+ lines.push("");
3339
+ lines.push(effort.body);
3340
+ }
3341
+ return lines.join("\n");
3342
+ }
3343
+ function createDefaultEquipment() {
3344
+ return {
3345
+ id: `equipment-user-${v7()}`,
3346
+ slug: "",
3347
+ label: "",
3348
+ aliases: [],
3349
+ registrySource: "user"
3350
+ };
3351
+ }
3352
+ function documentToEquipment(doc, baseEquipment) {
3353
+ const errors = [];
3354
+ const blockMatch = doc.match(FRONTMATTER_RE);
3355
+ if (!blockMatch) {
3356
+ errors.push("Missing YAML frontmatter delimiters (---)");
3357
+ return { equipment: baseEquipment ?? createDefaultEquipment(), errors };
3358
+ }
3359
+ const { meta: meta2, body } = parseFrontmatter(doc);
3360
+ const bodyText = body.replace(/^\r?\n+/, "") || void 0;
3361
+ const type = getScalar(meta2, "type");
3362
+ const id = getScalar(meta2, "id");
3363
+ const slug = getScalar(meta2, "slug");
3364
+ const label = getScalar(meta2, "label");
3365
+ const registrySource = getScalar(meta2, "registrySource");
3366
+ const createdAt = getScalar(meta2, "createdAt");
3367
+ const updatedAt = getScalar(meta2, "updatedAt");
3368
+ const aliases = getListStrict(meta2, "aliases");
3369
+ if (type !== "equipment") errors.push("Missing or invalid discriminator: frontmatter must declare `type: equipment`");
3370
+ if (!slug || !SLUG_RE.test(String(slug))) errors.push("Invalid slug: must be a canonical slug (lowercase letters, numbers, hyphens)");
3371
+ if (!label) errors.push("Missing required field: label");
3372
+ if (aliases === void 0) {
3373
+ errors.push("Invalid aliases: must be a list (`aliases:` followed by `- item` lines)");
3374
+ } else {
3375
+ errors.push(...stringListProblems(aliases, "aliases"));
3376
+ }
3377
+ if (registrySource !== void 0 && registrySource !== "bundled" && registrySource !== "user") {
3378
+ errors.push("Invalid registrySource: must be 'bundled' or 'user'");
3379
+ }
3380
+ const equipment = {
3381
+ id: String(id ?? "") || baseEquipment?.id || `equipment-user-${v7()}`,
3382
+ slug: String(slug ?? ""),
3383
+ label: String(label ?? ""),
3384
+ aliases: aliases ?? [],
3385
+ registrySource: registrySource || baseEquipment?.registrySource || "user",
3386
+ ...createdAt ? { createdAt: String(createdAt) } : {},
3387
+ ...updatedAt ? { updatedAt: String(updatedAt) } : {},
3388
+ ...bodyText ? { body: bodyText } : {}
3389
+ };
3390
+ return { equipment, errors };
3391
+ }
3392
+ function equipmentToDocument(equipment) {
3393
+ const lines = ["---"];
3394
+ lines.push("type: equipment");
3395
+ lines.push(`id: ${equipment.id}`);
3396
+ lines.push(`slug: ${equipment.slug}`);
3397
+ lines.push(`label: ${quoteYaml(equipment.label)}`);
3398
+ if (equipment.aliases.length > 0) {
3399
+ lines.push("aliases:");
3400
+ for (const alias of equipment.aliases) {
3401
+ lines.push(` - ${quoteYaml(alias)}`);
3402
+ }
3403
+ } else {
3404
+ lines.push("aliases: []");
3405
+ }
3406
+ lines.push(`registrySource: ${equipment.registrySource}`);
3407
+ if (equipment.createdAt) {
3408
+ lines.push(`createdAt: ${equipment.createdAt}`);
3409
+ }
3410
+ if (equipment.updatedAt) {
3411
+ lines.push(`updatedAt: ${equipment.updatedAt}`);
3412
+ }
3413
+ lines.push("---");
3414
+ if (equipment.body) {
3415
+ lines.push("");
3416
+ lines.push(equipment.body);
3417
+ }
3418
+ return lines.join("\n");
3419
+ }
2841
3420
 
2842
3421
  // src/effort-registry/fuzzyMatch.ts
2843
3422
  function levenshteinDistance(a, b) {
@@ -3222,12 +3801,27 @@ var CompositeEffortRegistry = class {
3222
3801
  * Initialize the registry:
3223
3802
  * 1. Load bundled efforts into memory
3224
3803
  * 2. Load user efforts from storage adapter into memory
3804
+ *
3805
+ * An explicit `customBundled` REPLACES the bundled tier — passing an empty
3806
+ * set clears it — so a seed source removal never leaves stale bundled
3807
+ * efforts cached. The no-argument call keeps the constructor/default set.
3808
+ * Both tiers rebuild only AFTER a successful adapter load: a failed load
3809
+ * propagates (readiness must never report over stale data), and deleted
3810
+ * custom rows disappear because the user map is cleared too.
3225
3811
  */
3226
3812
  async loadBundled(customBundled) {
3227
- const seed = customBundled ?? this.initialBundled ?? bundledEfforts;
3228
- for (const effort of seed) {
3229
- this.bundledEfforts.set(effort.slug, effort);
3813
+ this.bundledEfforts.clear();
3814
+ if (customBundled) {
3815
+ for (const effort of customBundled) {
3816
+ this.bundledEfforts.set(effort.slug, effort);
3817
+ }
3818
+ } else {
3819
+ const seed = this.initialBundled ?? bundledEfforts;
3820
+ for (const effort of seed) {
3821
+ this.bundledEfforts.set(effort.slug, effort);
3822
+ }
3230
3823
  }
3824
+ this.userEfforts.clear();
3231
3825
  if (this.storage) {
3232
3826
  try {
3233
3827
  const userEfforts = await this.storage.load();
@@ -3357,6 +3951,358 @@ async function addAliasToEffort(registry, targetSlug, newAlias) {
3357
3951
  await registry.upsert(updatedUserEffort);
3358
3952
  return updatedUserEffort;
3359
3953
  }
3954
+ function buildExactIndex(entities) {
3955
+ const byKey = /* @__PURE__ */ new Map();
3956
+ const add = (key, entity) => {
3957
+ if (!key) return;
3958
+ const existing = byKey.get(key);
3959
+ if (existing) {
3960
+ if (!existing.includes(entity)) existing.push(entity);
3961
+ } else {
3962
+ byKey.set(key, [entity]);
3963
+ }
3964
+ };
3965
+ for (const entity of entities) {
3966
+ add(entity.slug.toLowerCase(), entity);
3967
+ add(normalizeForFuzzy(entity.label), entity);
3968
+ for (const alias of entity.aliases) add(normalizeForFuzzy(alias), entity);
3969
+ }
3970
+ return {
3971
+ byKey,
3972
+ lookup(key) {
3973
+ const direct = byKey.get(key) ?? byKey.get(normalizeForFuzzy(key));
3974
+ if (!direct || direct.length === 0) return { entity: null, ambiguous: [] };
3975
+ if (direct.length === 1) return { entity: direct[0], ambiguous: [] };
3976
+ return { entity: null, ambiguous: direct };
3977
+ }
3978
+ };
3979
+ }
3980
+ function equipmentRef(entity) {
3981
+ return { kind: "equipment", slug: entity.slug, label: entity.label };
3982
+ }
3983
+ function dedupeRefs(refs) {
3984
+ const seen = /* @__PURE__ */ new Set();
3985
+ const result = [];
3986
+ for (const ref of refs) {
3987
+ const key = `${ref.kind}:${ref.slug}`;
3988
+ if (seen.has(key)) continue;
3989
+ seen.add(key);
3990
+ result.push(ref);
3991
+ }
3992
+ return result;
3993
+ }
3994
+ function createReferenceIndex(catalog, materialize) {
3995
+ const efforts = buildExactIndex(catalog.efforts ?? []);
3996
+ const equipment = buildExactIndex(catalog.equipment ?? []);
3997
+ 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);
3998
+ const materializer = materialize ?? ((effort) => ({
3999
+ effort,
4000
+ definition: effort,
4001
+ slug: effort.slug,
4002
+ label: effort.label,
4003
+ met: effort.baseAttributes.met,
4004
+ baseAttributes: effort.baseAttributes,
4005
+ discipline: effort.baseAttributes.discipline,
4006
+ disciplineFactor: effort.baseAttributes.disciplineFactor ?? 1,
4007
+ intensityTier: effort.baseAttributes.intensityTier,
4008
+ modifiers: {},
4009
+ registrySource: effort.registrySource,
4010
+ resolvedFrom: effort.registrySource === "user" ? "user" : effort.registrySource === "bundled" ? "bundled" : "default",
4011
+ isEstimated: false
4012
+ }));
4013
+ const resolveEffortExact = (key) => {
4014
+ const found = efforts.lookup(key.trim());
4015
+ if (found.entity) return { entity: found.entity };
4016
+ if (found.ambiguous.length > 0) {
4017
+ return {
4018
+ entity: null,
4019
+ diagnostic: {
4020
+ reason: "ambiguous-effort",
4021
+ message: `"${key}" matches multiple efforts.`,
4022
+ token: key,
4023
+ candidates: found.ambiguous.map((e) => e.slug)
4024
+ }
4025
+ };
4026
+ }
4027
+ return {
4028
+ entity: null,
4029
+ diagnostic: { reason: "unknown-effort", message: `Unknown effort "${key}".`, token: key }
4030
+ };
4031
+ };
4032
+ const resolveEquipmentValues = (values) => {
4033
+ const refs = [];
4034
+ const diagnostics = [];
4035
+ for (const value of values) {
4036
+ if (typeof value !== "string" || !value.trim()) {
4037
+ diagnostics.push({
4038
+ reason: "invalid-explicit",
4039
+ message: `Equipment reference must be a slug string, got ${JSON.stringify(value) ?? "null"}.`,
4040
+ token: String(value)
4041
+ });
4042
+ continue;
4043
+ }
4044
+ const found = equipment.lookup(value.trim());
4045
+ if (found.entity) {
4046
+ refs.push(equipmentRef(found.entity));
4047
+ } else if (found.ambiguous.length > 0) {
4048
+ diagnostics.push({
4049
+ reason: "ambiguous-equipment",
4050
+ message: `"${value}" matches multiple equipment entries.`,
4051
+ token: value,
4052
+ candidates: found.ambiguous.map((e) => e.slug)
4053
+ });
4054
+ } else {
4055
+ diagnostics.push({
4056
+ reason: "invalid-explicit",
4057
+ message: `Unknown equipment "${value}".`,
4058
+ token: value
4059
+ });
4060
+ }
4061
+ }
4062
+ return { refs: dedupeRefs(refs), diagnostics };
4063
+ };
4064
+ const index = {
4065
+ resolveEffort(slugOrAlias) {
4066
+ return efforts.lookup(slugOrAlias).entity;
4067
+ },
4068
+ resolveEquipment(slugOrAlias) {
4069
+ return equipment.lookup(slugOrAlias).entity;
4070
+ },
4071
+ materialize(effort) {
4072
+ return materializer(effort);
4073
+ },
4074
+ resolve(label, defaults) {
4075
+ const source = (label ?? "").trim();
4076
+ const diagnostics = [];
4077
+ const push = (diagnostic) => {
4078
+ if (diagnostic) diagnostics.push(diagnostic);
4079
+ };
4080
+ let matched = "none";
4081
+ let effortEntity = null;
4082
+ let remaining = "";
4083
+ let equipmentRefs = [];
4084
+ let equipmentAuthored = false;
4085
+ const fillEquipment = () => {
4086
+ if (equipmentRefs.length > 0 || equipmentAuthored) return;
4087
+ if (effortEntity === null) return;
4088
+ if (defaults?.equipment?.length) {
4089
+ const resolvedDefaults = resolveEquipmentValues(defaults.equipment);
4090
+ equipmentRefs = resolvedDefaults.refs;
4091
+ for (const diagnostic of resolvedDefaults.diagnostics) {
4092
+ push({ ...diagnostic, reason: "unknown-equipment" });
4093
+ }
4094
+ if (equipmentRefs.length > 0) return;
4095
+ }
4096
+ if (effortEntity.equipment?.length) {
4097
+ const resolvedDefEquipment = resolveEquipmentValues(effortEntity.equipment);
4098
+ equipmentRefs = resolvedDefEquipment.refs;
4099
+ for (const diagnostic of resolvedDefEquipment.diagnostics) {
4100
+ push({ ...diagnostic, reason: "unknown-equipment" });
4101
+ }
4102
+ }
4103
+ };
4104
+ if (!source) {
4105
+ if (defaults?.effort) {
4106
+ const found = resolveEffortExact(defaults.effort);
4107
+ if (found.entity) {
4108
+ effortEntity = found.entity;
4109
+ matched = "default";
4110
+ } else {
4111
+ push({
4112
+ reason: found.diagnostic?.reason === "ambiguous-effort" ? "ambiguous-effort" : "unknown-effort",
4113
+ message: `Default effort "${defaults.effort}" is not in the catalog.`,
4114
+ token: defaults.effort,
4115
+ candidates: found.diagnostic?.candidates
4116
+ });
4117
+ }
4118
+ }
4119
+ fillEquipment();
4120
+ return {
4121
+ matched,
4122
+ label: source,
4123
+ effort: effortEntity ? materializer(effortEntity) : null,
4124
+ effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
4125
+ equipment: equipmentRefs,
4126
+ remaining: "",
4127
+ diagnostics
4128
+ };
4129
+ }
4130
+ if (source.startsWith("{")) {
4131
+ let parsed;
4132
+ try {
4133
+ parsed = JSON.parse(source);
4134
+ } catch {
4135
+ return {
4136
+ matched: "none",
4137
+ label: source,
4138
+ effort: null,
4139
+ effortRef: null,
4140
+ equipment: [],
4141
+ remaining: "",
4142
+ diagnostics: [{
4143
+ reason: "malformed-json",
4144
+ message: "Reference object is not valid JSON.",
4145
+ token: source
4146
+ }]
4147
+ };
4148
+ }
4149
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && ("effort" in parsed || "equipment" in parsed)) {
4150
+ matched = "explicit";
4151
+ const obj = parsed;
4152
+ if (obj.effort !== void 0) {
4153
+ if (typeof obj.effort === "string" && obj.effort.trim()) {
4154
+ const found = resolveEffortExact(obj.effort);
4155
+ effortEntity = found.entity;
4156
+ if (found.diagnostic) {
4157
+ push(found.diagnostic.reason === "ambiguous-effort" ? found.diagnostic : { ...found.diagnostic, reason: "invalid-explicit", message: `Explicit effort "${obj.effort}" is not in the catalog.` });
4158
+ }
4159
+ } else {
4160
+ push({
4161
+ reason: "invalid-explicit",
4162
+ message: 'Explicit "effort" must be a slug string.',
4163
+ token: String(obj.effort)
4164
+ });
4165
+ }
4166
+ }
4167
+ if (obj.equipment !== void 0) {
4168
+ equipmentAuthored = true;
4169
+ const values = Array.isArray(obj.equipment) ? obj.equipment : [obj.equipment];
4170
+ const resolvedEquipment = resolveEquipmentValues(values);
4171
+ equipmentRefs = resolvedEquipment.refs;
4172
+ diagnostics.push(...resolvedEquipment.diagnostics);
4173
+ if (effortEntity?.equipment?.length) {
4174
+ const definitionSlugs = new Set(effortEntity.equipment);
4175
+ const authoredSlugs = equipmentRefs.map((ref) => ref.slug);
4176
+ if (authoredSlugs.length > 0 && authoredSlugs.some((slug) => !definitionSlugs.has(slug))) {
4177
+ push({
4178
+ reason: "conflict",
4179
+ message: `Explicit equipment [${authoredSlugs.join(", ")}] contradicts effort "${effortEntity.slug}" definition equipment [${effortEntity.equipment.join(", ")}].`,
4180
+ token: source
4181
+ });
4182
+ }
4183
+ }
4184
+ }
4185
+ fillEquipment();
4186
+ return {
4187
+ matched,
4188
+ label: source,
4189
+ effort: effortEntity ? materializer(effortEntity) : null,
4190
+ effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
4191
+ equipment: equipmentRefs,
4192
+ remaining: "",
4193
+ diagnostics
4194
+ };
4195
+ }
4196
+ }
4197
+ const lowerSource = source.toLowerCase();
4198
+ for (const candidate of prefixCandidates) {
4199
+ if (!lowerSource.startsWith(candidate.lower)) continue;
4200
+ const after = source.slice(candidate.lower.length);
4201
+ if (after && !/^\s/.test(after)) continue;
4202
+ const candidateRemaining = after.trim();
4203
+ if (!candidateRemaining) continue;
4204
+ const equipmentFound = equipment.lookup(candidate.lower);
4205
+ if (equipmentFound.ambiguous.length > 0) {
4206
+ push({
4207
+ reason: "ambiguous-equipment",
4208
+ message: `"${candidate.lower}" matches multiple equipment entries.`,
4209
+ token: source,
4210
+ candidates: equipmentFound.ambiguous.map((e) => e.slug)
4211
+ });
4212
+ return {
4213
+ matched: "none",
4214
+ label: source,
4215
+ effort: null,
4216
+ effortRef: null,
4217
+ equipment: [],
4218
+ remaining: "",
4219
+ diagnostics
4220
+ };
4221
+ }
4222
+ if (!equipmentFound.entity) continue;
4223
+ const effortFound = resolveEffortExact(candidateRemaining);
4224
+ if (effortFound.entity) {
4225
+ matched = "split";
4226
+ effortEntity = effortFound.entity;
4227
+ equipmentRefs = [equipmentRef(equipmentFound.entity)];
4228
+ equipmentAuthored = true;
4229
+ remaining = candidateRemaining;
4230
+ break;
4231
+ }
4232
+ if (effortFound.diagnostic?.reason === "ambiguous-effort") {
4233
+ push(effortFound.diagnostic);
4234
+ return {
4235
+ matched: "none",
4236
+ label: source,
4237
+ effort: null,
4238
+ effortRef: null,
4239
+ equipment: [],
4240
+ remaining: "",
4241
+ diagnostics
4242
+ };
4243
+ }
4244
+ }
4245
+ if (matched === "none") {
4246
+ const found = resolveEffortExact(source);
4247
+ if (found.entity) {
4248
+ effortEntity = found.entity;
4249
+ matched = "whole";
4250
+ } else if (found.diagnostic?.reason === "ambiguous-effort") {
4251
+ push(found.diagnostic);
4252
+ return {
4253
+ matched: "none",
4254
+ label: source,
4255
+ effort: null,
4256
+ effortRef: null,
4257
+ equipment: [],
4258
+ remaining: "",
4259
+ diagnostics
4260
+ };
4261
+ } else if (found.diagnostic) {
4262
+ push(found.diagnostic);
4263
+ }
4264
+ }
4265
+ fillEquipment();
4266
+ return {
4267
+ matched,
4268
+ label: source,
4269
+ effort: effortEntity ? materializer(effortEntity) : null,
4270
+ effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
4271
+ equipment: equipmentRefs,
4272
+ remaining,
4273
+ diagnostics
4274
+ };
4275
+ }
4276
+ };
4277
+ return index;
4278
+ }
4279
+ function extractStatementRefs(statements) {
4280
+ const efforts = [];
4281
+ const equipment = [];
4282
+ const seenEffort = /* @__PURE__ */ new Set();
4283
+ const seenEquipment = /* @__PURE__ */ new Set();
4284
+ const pushUnique = (list, seen, value) => {
4285
+ if (!value) return;
4286
+ if (seen.has(value)) return;
4287
+ seen.add(value);
4288
+ list.push(value);
4289
+ };
4290
+ for (const statement of statements ?? []) {
4291
+ const metrics = Array.isArray(statement.metrics) ? statement.metrics : statement.metrics?.toArray?.() ?? [];
4292
+ for (const metric of metrics) {
4293
+ const refs = getReferenceMetadata(metric.metadata)?.refs;
4294
+ if (!refs) continue;
4295
+ for (const ref of refs) {
4296
+ if (ref.kind === "effort") pushUnique(efforts, seenEffort, ref.slug);
4297
+ if (ref.kind === "equipment") pushUnique(equipment, seenEquipment, ref.slug);
4298
+ }
4299
+ }
4300
+ }
4301
+ return { efforts, equipment };
4302
+ }
4303
+ function slugifyRefLabel(label) {
4304
+ return normalizeForFuzzy(label).replace(/\s+/g, "-");
4305
+ }
3360
4306
 
3361
4307
  // src/runtime/contracts/IRuntimeOptions.ts
3362
4308
  var DEFAULT_RUNTIME_OPTIONS = {
@@ -6663,14 +7609,12 @@ function resolveDominantOrigin(origins) {
6663
7609
  if (origins.every((o) => o === "compiler")) return "compiler";
6664
7610
  return "analyzed";
6665
7611
  }
6666
-
6667
- // src/runtime/compiler/EffortEnrichmentPass.ts
6668
7612
  var logTrace = (message) => console.log(message);
6669
7613
  function applyEffortEnrichment(block, options) {
6670
- const { resolver, overwrite = false, debug = false } = options;
6671
- tryEnrichBlock(block, resolver, overwrite, debug);
7614
+ const { resolver, references, overwrite = false, debug = false } = options;
7615
+ tryEnrichBlock(block, resolver, references, overwrite, debug);
6672
7616
  }
6673
- function tryEnrichBlock(block, resolver, overwrite, debug) {
7617
+ function tryEnrichBlock(block, resolver, references, overwrite, debug) {
6674
7618
  const normalizedBlockType = block.blockType?.toLowerCase();
6675
7619
  if (normalizedBlockType !== "effort" && normalizedBlockType !== "exercise") {
6676
7620
  return;
@@ -6688,7 +7632,32 @@ function tryEnrichBlock(block, resolver, overwrite, debug) {
6688
7632
  return;
6689
7633
  }
6690
7634
  try {
6691
- const resolved = resolver.resolveEffort(label);
7635
+ let resolved;
7636
+ let matchedKind = "none";
7637
+ let equipmentRefs = [];
7638
+ let diagnostics = [];
7639
+ if (references) {
7640
+ const resolution = references.resolve(label);
7641
+ resolved = resolution.effort ?? void 0;
7642
+ matchedKind = resolution.matched;
7643
+ equipmentRefs = resolution.equipment;
7644
+ diagnostics = resolution.diagnostics;
7645
+ } else {
7646
+ resolved = resolver.resolveEffort(label);
7647
+ }
7648
+ if (!resolved) {
7649
+ if (diagnostics.length > 0) {
7650
+ block.pushMemory(new MemoryLocation("metric:tracked", [{
7651
+ type: MetricType.Hint,
7652
+ value: "reference.unresolved",
7653
+ image: label,
7654
+ origin: "compiler",
7655
+ metadata: { [REFERENCE_METADATA_KEY]: { matched: "none", source: label, diagnostics } }
7656
+ }]));
7657
+ }
7658
+ if (debug) logTrace(`[EffortEnrichment] Unresolved effort: ${label}`);
7659
+ return;
7660
+ }
6692
7661
  const effortHints = resolved.effort.hints;
6693
7662
  if (effortHints && Object.keys(effortHints).length > 0) {
6694
7663
  block.mergeHints(effortHints);
@@ -6701,7 +7670,8 @@ function tryEnrichBlock(block, resolver, overwrite, debug) {
6701
7670
  image: resolved.label,
6702
7671
  value: resolved,
6703
7672
  origin: "compiler",
6704
- timestamp: /* @__PURE__ */ new Date()
7673
+ timestamp: /* @__PURE__ */ new Date(),
7674
+ ...equipmentRefs.length > 0 || diagnostics.length > 0 ? { metadata: { [REFERENCE_METADATA_KEY]: { matched: matchedKind, source: label, refs: [{ kind: "effort", slug: resolved.slug, label: resolved.label }, ...equipmentRefs], ...diagnostics.length > 0 ? { diagnostics } : {} } } } : {}
6705
7675
  };
6706
7676
  const privateMetrics = block.getMetricMemoryByVisibility("private");
6707
7677
  const existingLoc = privateMetrics.find(
@@ -6716,10 +7686,31 @@ function tryEnrichBlock(block, resolver, overwrite, debug) {
6716
7686
  const location = new MemoryLocation("metric:tracked", [effortDataMetric]);
6717
7687
  block.pushMemory(location);
6718
7688
  }
7689
+ if (equipmentRefs.length > 0) {
7690
+ pushPrivateMetrics(block, MetricType.Equipment, [
7691
+ new EquipmentMetric(equipmentRefs.map((ref) => ref.slug), {
7692
+ refs: equipmentRefs,
7693
+ source: label,
7694
+ origin: "compiler"
7695
+ })
7696
+ ], MetricType.Equipment, overwrite);
7697
+ }
6719
7698
  } catch (err) {
6720
7699
  console.error(`[EffortEnrichment] Failed to resolve effort "${label}":`, err);
6721
7700
  }
6722
7701
  }
7702
+ function pushPrivateMetrics(block, markerType, metrics, replaceType, overwrite) {
7703
+ const privateMetrics = block.getMetricMemoryByVisibility("private");
7704
+ const existingLoc = privateMetrics.find(
7705
+ (loc) => loc.metrics.toArray().some((m) => m.type === markerType)
7706
+ );
7707
+ if (existingLoc && overwrite) {
7708
+ const filtered = existingLoc.metrics.toArray().filter((m) => m.type !== replaceType);
7709
+ existingLoc.update([...filtered, ...metrics]);
7710
+ } else if (!existingLoc) {
7711
+ block.pushMemory(new MemoryLocation("metric:tracked", metrics));
7712
+ }
7713
+ }
6723
7714
 
6724
7715
  // src/runtime/actions/stack/CompileAndPushBlockAction.ts
6725
7716
  var CompileAndPushBlockAction = class {
@@ -6745,7 +7736,8 @@ var CompileAndPushBlockAction = class {
6745
7736
  }
6746
7737
  if (runtime.analyticsContext?.effortResolver) {
6747
7738
  applyEffortEnrichment(compiledBlock, {
6748
- resolver: runtime.analyticsContext.effortResolver
7739
+ resolver: runtime.analyticsContext.effortResolver,
7740
+ references: runtime.analyticsContext.references
6749
7741
  });
6750
7742
  }
6751
7743
  return [new PushBlockAction(compiledBlock, this.options)];
@@ -10771,7 +11763,8 @@ function createAnalyticsEngineForBlock(block, options) {
10771
11763
  effortResolver = new EffortResolver(registry);
10772
11764
  }
10773
11765
  const analyticsContext = {
10774
- effortResolver
11766
+ effortResolver,
11767
+ ...options?.references ? { references: options.references } : {}
10775
11768
  };
10776
11769
  const context = {
10777
11770
  dialect,
@@ -13671,6 +14664,6 @@ function compareStatement(expectations, actual, mode) {
13671
14664
  return diffs;
13672
14665
  }
13673
14666
 
13674
- export { ABSENT, AGGREGATE_BUILTINS, AUTHORITATIVE_CASTS, AbortSessionAction, AmrapLogicStrategy, AnalyticsEngine, AnalyticsTransformer, BUILTIN_CALCS, BUNDLED_EFFORT_COUNT, BehaviorContext, BlockBuilder, BlockContext, ButtonBehavior, CONSUMED_HINTS, CONSUMED_HINT_KEYS, CONTEXT_ATOMS, CalcEngine, CalcEvalError, CalcParseError, CalcRegistrationError, CalcUnitError, CalculateBlockProcessor, CalculationRegistry, CardioDialect, ChildSelectionBehavior, ChildrenStrategy, ChoiceGroupMetric, ClearChildrenAction, ClimbDialect, ClimbMetricType, CompileAndPushBlockAction, CompletionTimestampBehavior, CompositeEffortRegistry, CountdownTimerBehavior, CountupTimerBehavior, CrossFitDialect, DAY, DEFAULT_RESOLVER_OPTIONS, DEFAULT_RUNTIME_OPTIONS, DEFAULT_UNRESOLVED_EFFORT_MET, DIM_COUNT, DIM_ENERGY, DIM_LENGTH, DIM_MASS, DIM_TIME, DIM_ZERO, DISCIPLINE_FACTORS, DialectStack, EFFORT_DATA_METRIC_TYPE, EFFORT_DISCIPLINES, EFFORT_REGISTRY_ORIGINS, EffortBlock, EffortFallbackStrategy, EffortResolver, EmitEventAction, EmitSystemOutputAction, ErrorAction, EventBus, ExecutionContext, ExitBehavior, FixtureSyntaxError, GenericGroupStrategy, GenericLoopStrategy, GenericTimerStrategy, HabitsDialect, INTENSITY_TIERS, InMemoryEffortRegistry, IntervalLogicStrategy, JitCompiler, LabelingBehavior, LocalEventProvider, LocalRuntimeSubscription, LookupRegistry, MdTimerRuntime, MemoryLocation, MemoryTypeEnum, MetricPromotionBehavior, NAMED_COMPOUNDS, NextAction, NextEvent, NextEventHandler, OutputEmitter, PREDEFINED_SOUNDS, PRODUCTION_STRATEGIES, PlaySoundAction, PopBlockAction, PushBlockAction, PushRestBlockAction, REST_EXCLUSION, RPE_LABELS, RegisterEventHandlerAction, Registry, ReportOutputBehavior, ReportOutputStrategy, RestBlock, RoundsMetric, RuntimeAdapter, RuntimeBlock, RuntimeBuilder, RuntimeClock, RuntimeFactory, RuntimeLogger, RuntimeMemory, RuntimeObservers, RuntimeStack, STANDARD_UNITS, STORE_CALCS, STREAM_ATOMS, ScriptRuntime, SessionRootBlock, SessionRootStrategy, SnapshotClock, SoundCueBehavior, SoundStrategy, SpanTrackingBehavior, StandardAnalyticsProfile, StartSessionAction, StartWorkoutAction, SubscriptionManager, ThrowError, ThrowErrorAction, TimeSpan, TwoPassEffortResolutionProcess, TypedMemoryReference, UNITS, UnitRegistry, UnitSet, UnitsDialect, UnregisterEventHandlerAction, UpdateNextPreviewAction, VISIBILITY_ICONS, VISIBILITY_LABELS, WaitingToStartBlock, WaitingToStartInjectorBehavior, WhiteboardScript, WodDialect, YogaDialect, addAliasToEffort, addDim, buildStaticLookups, buildTooltip, bundledEfforts, calculateElapsed, canonicalizeType, checkCalcLine, classifyStatements, closeCurrentSpan, collapseUnresolvedChoices, commonFixtureSet, compareStatement, compileLineForm, composeDivUnit, composeMulUnit, compoundName, computeColumnLabel, computeLabel, computeWorkloadRollups, convertScalar, createAnalyticsEngineForBlock, createCalcEngine, createCalcStack, createCompiler, createDialectStack, createDisciplinesTable, createEffortTable, createLineLocator, createPolicy as createMetricPresentationPolicy, createMockClock, createParser, createProfileTable, createRpeLabelsTable, dailySessionLoads, dayBucket, dialectRegistry, dialectStack, diffStatement, dimEquals, disciplineFactorFor, evaluate, evaluateCalculateDefinitions, extractEffortData, extractMetrics, extractStatements, extractStatementsRaw, extractSyntaxFacts, filterTagsByVisibility, findBestFuzzyMatch, findUnresolvedChoices, fixtureBackSquat, fixtureBurpee, fixturePlank, fixtureRowing, fixtureRunning, fixtureUserCustom, fixtureUserOverride, formatDim, formatDuration, metricsToLabel as fragmentsToLabel, fullFixtureSet, fuseUnits, fuseUnitsInMetrics, getAnalyticsFromLogs, getAnalyticsFromRuntime, getHints, getMetricVisibility, getUnit, hasHint, hintMetric, hintsToContainer, inferDim, isCastSubscription, isChoiceResolved, isDimensionless, isEffortDiscipline, isFragmentTag, isTimeLikeMetric, isWithinThreshold, levenshteinDistance, metricPresentation, mutateTimerSpans, normalizeForFuzzy, num, openSpan, outputNodeId, outputNodeIds, parseCalcLine, parseCalculateBlock, parseExpression, parseMetricLine, parseScript, printCalculation, readTimer, realtimeProcessorRegistry, renderMetric, resolveDominantOrigin, sharedParser, startSpan, str, strategyRegistry, subDim, summaryProcessorRegistry, toStoredOutputStatement, truthy, validateOutputUnit, parser as whiteboardParser, whiteboardScript, whiteboardScriptLanguage, writeChoiceSelection };
14667
+ export { ABSENT, AGGREGATE_BUILTINS, AUTHORITATIVE_CASTS, AbortSessionAction, AmrapLogicStrategy, AnalyticsEngine, AnalyticsTransformer, BUILTIN_CALCS, BUNDLED_EFFORT_COUNT, BehaviorContext, BlockBuilder, BlockContext, ButtonBehavior, CONSUMED_HINTS, CONSUMED_HINT_KEYS, CONTEXT_ATOMS, CalcEngine, CalcEvalError, CalcParseError, CalcRegistrationError, CalcUnitError, CalculateBlockProcessor, CalculationRegistry, CardioDialect, ChildSelectionBehavior, ChildrenStrategy, ChoiceGroupMetric, ClearChildrenAction, ClimbDialect, ClimbMetricType, CompileAndPushBlockAction, CompletionTimestampBehavior, CompositeEffortRegistry, CountdownTimerBehavior, CountupTimerBehavior, CrossFitDialect, DAY, DEFAULT_RESOLVER_OPTIONS, DEFAULT_RUNTIME_OPTIONS, DEFAULT_UNRESOLVED_EFFORT_MET, DIM_COUNT, DIM_ENERGY, DIM_LENGTH, DIM_MASS, DIM_TIME, DIM_ZERO, DISCIPLINE_FACTORS, DialectStack, EFFORT_DATA_METRIC_TYPE, EFFORT_DISCIPLINES, EFFORT_REGISTRY_ORIGINS, EffortBlock, EffortFallbackStrategy, EffortResolver, EmitEventAction, EmitSystemOutputAction, ErrorAction, EventBus, ExecutionContext, ExitBehavior, FixtureSyntaxError, GenericGroupStrategy, GenericLoopStrategy, GenericTimerStrategy, HabitsDialect, INTENSITY_TIERS, InMemoryEffortRegistry, IntervalLogicStrategy, JitCompiler, LabelingBehavior, LocalEventProvider, LocalRuntimeSubscription, LookupRegistry, MdTimerRuntime, MemoryLocation, MemoryTypeEnum, MetricPromotionBehavior, NAMED_COMPOUNDS, NextAction, NextEvent, NextEventHandler, OutputEmitter, PREDEFINED_SOUNDS, PRODUCTION_STRATEGIES, PlaySoundAction, PopBlockAction, PushBlockAction, PushRestBlockAction, REST_EXCLUSION, RPE_LABELS, RegisterEventHandlerAction, Registry, ReportOutputBehavior, ReportOutputStrategy, RestBlock, RoundsMetric, RuntimeAdapter, RuntimeBlock, RuntimeBuilder, RuntimeClock, RuntimeFactory, RuntimeLogger, RuntimeMemory, RuntimeObservers, RuntimeStack, STANDARD_UNITS, STORE_CALCS, STREAM_ATOMS, ScriptRuntime, SessionRootBlock, SessionRootStrategy, SnapshotClock, SoundCueBehavior, SoundStrategy, SpanTrackingBehavior, StandardAnalyticsProfile, StartSessionAction, StartWorkoutAction, SubscriptionManager, ThrowError, ThrowErrorAction, TimeSpan, TwoPassEffortResolutionProcess, TypedMemoryReference, UNITS, UnitRegistry, UnitSet, UnitsDialect, UnregisterEventHandlerAction, UpdateNextPreviewAction, VISIBILITY_ICONS, VISIBILITY_LABELS, WaitingToStartBlock, WaitingToStartInjectorBehavior, WhiteboardScript, WodDialect, YogaDialect, addAliasToEffort, addDim, buildStaticLookups, buildTooltip, bundledEfforts, calculateElapsed, canonicalizeType, checkCalcLine, classifyStatements, closeCurrentSpan, collapseUnresolvedChoices, commonFixtureSet, compareStatement, compileLineForm, composeDivUnit, composeMulUnit, compoundName, computeColumnLabel, computeLabel, computeWorkloadRollups, convertScalar, createAnalyticsEngineForBlock, createCalcEngine, createCalcStack, createCompiler, createDialectStack, createDisciplinesTable, createEffortTable, createLineLocator, createPolicy as createMetricPresentationPolicy, createMockClock, createParser, createProfileTable, createReferenceIndex, createRpeLabelsTable, dailySessionLoads, dayBucket, dialectRegistry, dialectStack, diffStatement, dimEquals, disciplineFactorFor, documentToEffort, documentToEquipment, effortToDocument, equipmentToDocument, evaluate, evaluateCalculateDefinitions, extractEffortData, extractMetrics, extractStatementRefs, extractStatements, extractStatementsRaw, extractSyntaxFacts, filterTagsByVisibility, findBestFuzzyMatch, findUnresolvedChoices, fixtureBackSquat, fixtureBurpee, fixturePlank, fixtureRowing, fixtureRunning, fixtureUserCustom, fixtureUserOverride, formatDim, formatDuration, metricsToLabel as fragmentsToLabel, fullFixtureSet, fuseUnits, fuseUnitsInMetrics, getAnalyticsFromLogs, getAnalyticsFromRuntime, getHints, getMetricVisibility, getUnit, hasHint, hintMetric, hintsToContainer, inferDim, isCastSubscription, isChoiceResolved, isDimensionless, isEffortDiscipline, isFragmentTag, isTimeLikeMetric, isWithinThreshold, levenshteinDistance, metricPresentation, mutateTimerSpans, normalizeForFuzzy, num, openSpan, outputNodeId, outputNodeIds, parseCalcLine, parseCalculateBlock, parseEffortFile, parseEquipmentFile, parseExpression, parseMetricLine, parseScript, printCalculation, readNestedSection, readTimer, realtimeProcessorRegistry, renderMetric, resolveDominantOrigin, slugifyRefLabel, startSpan, str, strategyRegistry, subDim, summaryProcessorRegistry, toStoredOutputStatement, truthy, validateOutputUnit, parser as whiteboardParser, whiteboardScript, whiteboardScriptLanguage, writeChoiceSelection };
13675
14668
  //# sourceMappingURL=index.mjs.map
13676
14669
  //# sourceMappingURL=index.mjs.map