@bitcobblers/wod-wiki-lang 0.50.2187 → 0.50.2189
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 +797 -698
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.mjs +798 -700
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -551,6 +551,9 @@ var EffortMetric = class {
|
|
|
551
551
|
this.effort = effort;
|
|
552
552
|
this.value = options?.resolvedSlug ?? effort;
|
|
553
553
|
this.image = effort;
|
|
554
|
+
if (options?.canonicalLabel) {
|
|
555
|
+
this.name = options.canonicalLabel;
|
|
556
|
+
}
|
|
554
557
|
if (options?.references) {
|
|
555
558
|
this.metadata = { [wodWikiCore.REFERENCE_METADATA_KEY]: options.references };
|
|
556
559
|
}
|
|
@@ -558,6 +561,7 @@ var EffortMetric = class {
|
|
|
558
561
|
effort;
|
|
559
562
|
value;
|
|
560
563
|
image;
|
|
564
|
+
name;
|
|
561
565
|
origin = "parser";
|
|
562
566
|
metadata;
|
|
563
567
|
type = wodWikiCore.MetricType.Effort;
|
|
@@ -1571,11 +1575,12 @@ var EquipmentMetric = class {
|
|
|
1571
1575
|
/** Canonical slugs, ordered and deduplicated. */
|
|
1572
1576
|
value;
|
|
1573
1577
|
image;
|
|
1574
|
-
name
|
|
1578
|
+
name;
|
|
1575
1579
|
metadata;
|
|
1576
1580
|
constructor(slugs, options) {
|
|
1577
1581
|
this.value = [...new Set(slugs)];
|
|
1578
1582
|
this.image = options?.source ?? this.value.join(" ");
|
|
1583
|
+
this.name = options?.refs?.length ? options.refs.map((r) => r.label).join(", ") : "equipment";
|
|
1579
1584
|
this.origin = options?.origin ?? "parser";
|
|
1580
1585
|
const reference = {
|
|
1581
1586
|
matched: options?.matched ?? "whole",
|
|
@@ -1719,9 +1724,14 @@ function classifyStatements(facts, references) {
|
|
|
1719
1724
|
metricPairs.push(slot.pair);
|
|
1720
1725
|
}
|
|
1721
1726
|
const mergedPairs = mergeFragments(metricPairs);
|
|
1722
|
-
const finalPairs = references ? applyReferenceResolution(mergedPairs, fact, references) : mergedPairs;
|
|
1727
|
+
const finalPairs = references?.index ? applyReferenceResolution(mergedPairs, fact, references) : mergedPairs;
|
|
1723
1728
|
statement.metrics = finalPairs.map((pair) => pair.metrics);
|
|
1724
1729
|
statement.metricMeta = new Map(finalPairs.map((pair) => [pair.metrics, pair.meta]));
|
|
1730
|
+
const effortMetric = finalPairs.find((p) => p.metrics.type === wodWikiCore.MetricType.Effort)?.metrics;
|
|
1731
|
+
const effortRef = wodWikiCore.getReferenceMetadata(effortMetric?.metadata)?.refs?.find((r) => r.kind === "effort");
|
|
1732
|
+
if (effortRef?.slug) {
|
|
1733
|
+
statement.exerciseId = effortRef.slug;
|
|
1734
|
+
}
|
|
1725
1735
|
statement.id = fact.id;
|
|
1726
1736
|
statement.line = fact.line;
|
|
1727
1737
|
statement.text = fact.meta.raw;
|
|
@@ -1749,33 +1759,34 @@ function equipmentMeta(resolution, authored, meta2) {
|
|
|
1749
1759
|
};
|
|
1750
1760
|
}
|
|
1751
1761
|
function applyReferenceResolution(pairs, fact, references) {
|
|
1752
|
-
const
|
|
1762
|
+
const index = references.index;
|
|
1763
|
+
const { defaults } = references;
|
|
1753
1764
|
const result = [];
|
|
1754
1765
|
const emitResolution = (resolution, authored, meta2) => {
|
|
1755
1766
|
if (resolution.effortRef || resolution.matched !== "none" || resolution.diagnostics.length > 0) {
|
|
1756
1767
|
result.push({
|
|
1757
1768
|
metrics: new EffortMetric(authored, {
|
|
1758
1769
|
resolvedSlug: resolution.effortRef?.slug,
|
|
1770
|
+
canonicalLabel: resolution.effortRef?.label,
|
|
1759
1771
|
references: referenceMetadata(resolution)
|
|
1760
1772
|
}),
|
|
1761
1773
|
meta: meta2
|
|
1762
1774
|
});
|
|
1763
1775
|
}
|
|
1764
1776
|
if (resolution.equipment.length > 0) {
|
|
1777
|
+
const eqMeta = equipmentMeta(resolution, authored, meta2);
|
|
1778
|
+
const eqSource = resolution.matched === "split" ? eqMeta.raw : resolution.matched === "explicit" ? resolution.equipment.map((ref) => ref.label).join(", ") : authored;
|
|
1765
1779
|
result.push({
|
|
1766
1780
|
metrics: new EquipmentMetric(
|
|
1767
1781
|
resolution.equipment.map((ref) => ref.slug),
|
|
1768
1782
|
{
|
|
1769
1783
|
refs: resolution.equipment,
|
|
1770
|
-
source:
|
|
1784
|
+
source: eqSource,
|
|
1771
1785
|
matched: resolution.matched,
|
|
1772
1786
|
diagnostics: resolution.diagnostics
|
|
1773
1787
|
}
|
|
1774
1788
|
),
|
|
1775
|
-
|
|
1776
|
-
// what follows. Marking the whole phrase for both would let one mark
|
|
1777
|
-
// hide the other.
|
|
1778
|
-
meta: equipmentMeta(resolution, authored, meta2)
|
|
1789
|
+
meta: eqMeta
|
|
1779
1790
|
});
|
|
1780
1791
|
}
|
|
1781
1792
|
};
|
|
@@ -1850,6 +1861,64 @@ function applyReferenceResolution(pairs, fact, references) {
|
|
|
1850
1861
|
result.push(...malformedHints);
|
|
1851
1862
|
return result;
|
|
1852
1863
|
}
|
|
1864
|
+
function resolveFusedEffortReferences(statements, references) {
|
|
1865
|
+
if (!references?.index) return;
|
|
1866
|
+
const { index, defaults } = references;
|
|
1867
|
+
for (const statement of statements) {
|
|
1868
|
+
const current = statement.metrics.toArray();
|
|
1869
|
+
if (!current.some((m) => m.type === wodWikiCore.MetricType.Effort && !m.metadata)) continue;
|
|
1870
|
+
const nextMetrics = [];
|
|
1871
|
+
const nextMeta = new Map(statement.metricMeta);
|
|
1872
|
+
for (const metric of current) {
|
|
1873
|
+
if (metric.type !== wodWikiCore.MetricType.Effort || metric.metadata) {
|
|
1874
|
+
nextMetrics.push(metric);
|
|
1875
|
+
continue;
|
|
1876
|
+
}
|
|
1877
|
+
const authored = metric instanceof EffortMetric ? metric.effort : String(metric.value ?? "");
|
|
1878
|
+
const normalized = authored.trim().toLowerCase();
|
|
1879
|
+
if (STRUCTURAL_EFFORT_WORDS.has(normalized) || STANDARD_UNITS2.has(normalized)) {
|
|
1880
|
+
nextMetrics.push(metric);
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
const meta2 = statement.metricMeta?.get(metric);
|
|
1884
|
+
const resolution = index.resolve(authored, defaults);
|
|
1885
|
+
if (resolution.matched === "none" && resolution.diagnostics.length === 0) {
|
|
1886
|
+
nextMetrics.push(metric);
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
nextMeta.delete(metric);
|
|
1890
|
+
if (resolution.effortRef || resolution.matched !== "none" || resolution.diagnostics.length > 0) {
|
|
1891
|
+
const resolvedEffort = new EffortMetric(authored, {
|
|
1892
|
+
resolvedSlug: resolution.effortRef?.slug,
|
|
1893
|
+
canonicalLabel: resolution.effortRef?.label,
|
|
1894
|
+
references: referenceMetadata(resolution)
|
|
1895
|
+
});
|
|
1896
|
+
nextMetrics.push(resolvedEffort);
|
|
1897
|
+
if (meta2) nextMeta.set(resolvedEffort, meta2);
|
|
1898
|
+
if (resolution.effortRef?.slug) {
|
|
1899
|
+
statement.exerciseId = resolution.effortRef.slug;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
if (resolution.equipment.length > 0) {
|
|
1903
|
+
const eqMeta = meta2 ? equipmentMeta(resolution, authored, meta2) : void 0;
|
|
1904
|
+
const eqSource = resolution.matched === "split" && eqMeta ? eqMeta.raw : authored;
|
|
1905
|
+
const eqMetric = new EquipmentMetric(
|
|
1906
|
+
resolution.equipment.map((ref) => ref.slug),
|
|
1907
|
+
{
|
|
1908
|
+
refs: resolution.equipment,
|
|
1909
|
+
source: eqSource,
|
|
1910
|
+
matched: resolution.matched,
|
|
1911
|
+
diagnostics: resolution.diagnostics
|
|
1912
|
+
}
|
|
1913
|
+
);
|
|
1914
|
+
nextMetrics.push(eqMetric);
|
|
1915
|
+
if (eqMeta) nextMeta.set(eqMetric, eqMeta);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
statement.metrics.clear().add(...nextMetrics);
|
|
1919
|
+
statement.metricMeta = nextMeta;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1853
1922
|
function classifyPrimitive(primitive) {
|
|
1854
1923
|
switch (primitive.kind) {
|
|
1855
1924
|
case "lap":
|
|
@@ -2467,6 +2536,7 @@ function extractStatements(sourceOrTree, sportOrTree, sportArg, references) {
|
|
|
2467
2536
|
const facts = extractSyntaxFacts(resolved.source, resolved.tree);
|
|
2468
2537
|
const statements = classifyStatements(facts, references);
|
|
2469
2538
|
dialectStack.processAll(statements, sport);
|
|
2539
|
+
resolveFusedEffortReferences(statements, references);
|
|
2470
2540
|
return normalizeInlineRounds(statements);
|
|
2471
2541
|
}
|
|
2472
2542
|
function isInlineRoundsPrescription(stmt) {
|
|
@@ -2588,557 +2658,87 @@ var WhiteboardScript = class {
|
|
|
2588
2658
|
}
|
|
2589
2659
|
};
|
|
2590
2660
|
|
|
2591
|
-
// src/
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
}
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
}
|
|
2611
|
-
function resolveEntityTier(entity) {
|
|
2612
|
-
if (entity.registrySource === "user") return "user";
|
|
2613
|
-
return resolveSourceTier(entity.sourceId);
|
|
2614
|
-
}
|
|
2615
|
-
function resolveEntityRank(entity) {
|
|
2616
|
-
return SEED_PRECEDENCE_RANK[resolveEntityTier(entity)];
|
|
2617
|
-
}
|
|
2618
|
-
function dedupeEntitiesByPrecedence(entities) {
|
|
2619
|
-
const bySlug = /* @__PURE__ */ new Map();
|
|
2620
|
-
for (const entity of entities) {
|
|
2621
|
-
const key = entity.slug.toLowerCase();
|
|
2622
|
-
const prior = bySlug.get(key);
|
|
2623
|
-
if (!prior || resolveEntityRank(entity) >= resolveEntityRank(prior)) {
|
|
2624
|
-
bySlug.set(key, entity);
|
|
2625
|
-
}
|
|
2661
|
+
// src/parser/parseScript.ts
|
|
2662
|
+
function parseScript(inputText, options = {}) {
|
|
2663
|
+
if (!inputText || !inputText.trim()) {
|
|
2664
|
+
return new WhiteboardScript(inputText, [], []);
|
|
2665
|
+
}
|
|
2666
|
+
try {
|
|
2667
|
+
const doc = inputText.endsWith("\n") ? inputText : inputText + "\n";
|
|
2668
|
+
const tree = parser.parse(doc);
|
|
2669
|
+
const sport = options.sport ?? options.dialect;
|
|
2670
|
+
const references = options.references || options.tags ? {
|
|
2671
|
+
...options.references ? { index: options.references } : {},
|
|
2672
|
+
defaults: options.defaults,
|
|
2673
|
+
tags: options.tags
|
|
2674
|
+
} : void 0;
|
|
2675
|
+
const statements = options.withoutDialects ? extractStatementsRaw(inputText, tree, references) : extractStatements(inputText, tree, sport, references);
|
|
2676
|
+
return new WhiteboardScript(inputText, statements, []);
|
|
2677
|
+
} catch (error) {
|
|
2678
|
+
const msg = error instanceof Error ? error.message : "Unknown parse error";
|
|
2679
|
+
return new WhiteboardScript(inputText, [], [{ message: msg }]);
|
|
2626
2680
|
}
|
|
2627
|
-
return Array.from(bySlug.values());
|
|
2628
2681
|
}
|
|
2629
|
-
var INTENSITY_TIERS = ["low", "moderate", "high"];
|
|
2630
|
-
var DEFAULT_RESOLVER_OPTIONS = {
|
|
2631
|
-
defaultMet: 5,
|
|
2632
|
-
defaultThreshold: 2
|
|
2633
|
-
};
|
|
2634
2682
|
|
|
2635
|
-
// src/
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
if (a.length > b.length) {
|
|
2641
|
-
const tmp = a;
|
|
2642
|
-
a = b;
|
|
2643
|
-
b = tmp;
|
|
2683
|
+
// src/parser/md-timer.ts
|
|
2684
|
+
var MdTimerRuntime = class {
|
|
2685
|
+
config;
|
|
2686
|
+
constructor(config) {
|
|
2687
|
+
this.config = config;
|
|
2644
2688
|
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2689
|
+
/**
|
|
2690
|
+
* Parse WhiteboardScript source into a {@link WhiteboardScript}, running the
|
|
2691
|
+
* Dialect Stack on every statement.
|
|
2692
|
+
*
|
|
2693
|
+
* @param sport - The block's `:sport` fence suffix (` ```log:climbing `).
|
|
2694
|
+
*/
|
|
2695
|
+
read(inputText, sport) {
|
|
2696
|
+
return parseScript(inputText, { sport, ...this.config });
|
|
2651
2697
|
}
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
prev[i] + 1,
|
|
2659
|
-
// deletion
|
|
2660
|
-
curr[i - 1] + 1,
|
|
2661
|
-
// insertion
|
|
2662
|
-
prev[i - 1] + cost
|
|
2663
|
-
// substitution
|
|
2664
|
-
);
|
|
2665
|
-
}
|
|
2666
|
-
const swap = prev;
|
|
2667
|
-
prev = curr;
|
|
2668
|
-
curr = swap;
|
|
2698
|
+
/**
|
|
2699
|
+
* Parse without running the Dialect Stack. Used by the parser test harness
|
|
2700
|
+
* which applies its own Dialect set. Production consumers should use {@link read}.
|
|
2701
|
+
*/
|
|
2702
|
+
readWithoutDialects(inputText) {
|
|
2703
|
+
return parseScript(inputText, { withoutDialects: true, ...this.config });
|
|
2669
2704
|
}
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
function isWithinThreshold(a, b, threshold) {
|
|
2676
|
-
const na = normalizeForFuzzy(a);
|
|
2677
|
-
const nb = normalizeForFuzzy(b);
|
|
2678
|
-
if (na === nb) return true;
|
|
2679
|
-
if (Math.abs(na.length - nb.length) > threshold) return false;
|
|
2680
|
-
return levenshteinDistance(na, nb) <= threshold;
|
|
2705
|
+
};
|
|
2706
|
+
|
|
2707
|
+
// src/parser/parserInstance.ts
|
|
2708
|
+
function createParser(config) {
|
|
2709
|
+
return new MdTimerRuntime(config);
|
|
2681
2710
|
}
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
}
|
|
2711
|
+
var nextTagID = 0;
|
|
2712
|
+
var Tag = class _Tag {
|
|
2713
|
+
/**
|
|
2714
|
+
@internal
|
|
2715
|
+
*/
|
|
2716
|
+
constructor(name2, set, base, modified) {
|
|
2717
|
+
this.name = name2;
|
|
2718
|
+
this.set = set;
|
|
2719
|
+
this.base = base;
|
|
2720
|
+
this.modified = modified;
|
|
2721
|
+
this.id = nextTagID++;
|
|
2694
2722
|
}
|
|
2695
|
-
|
|
2696
|
-
}
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
2702
|
-
const add = (key, entity) => {
|
|
2703
|
-
if (!key) return;
|
|
2704
|
-
const existing = byKey.get(key);
|
|
2705
|
-
if (existing) {
|
|
2706
|
-
if (!existing.includes(entity)) existing.push(entity);
|
|
2707
|
-
} else {
|
|
2708
|
-
byKey.set(key, [entity]);
|
|
2709
|
-
}
|
|
2710
|
-
};
|
|
2711
|
-
for (const entity of deduped) {
|
|
2712
|
-
add(entity.slug.toLowerCase(), entity);
|
|
2713
|
-
add(normalizeForFuzzy(entity.label), entity);
|
|
2714
|
-
for (const alias of entity.aliases) add(normalizeForFuzzy(alias), entity);
|
|
2723
|
+
toString() {
|
|
2724
|
+
let { name: name2 } = this;
|
|
2725
|
+
for (let mod of this.modified)
|
|
2726
|
+
if (mod.name)
|
|
2727
|
+
name2 = `${mod.name}(${name2})`;
|
|
2728
|
+
return name2;
|
|
2715
2729
|
}
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
if (topTier.length === 1) return { entity: topTier[0], ambiguous: [] };
|
|
2729
|
-
return { entity: null, ambiguous: topTier };
|
|
2730
|
-
}
|
|
2731
|
-
};
|
|
2732
|
-
}
|
|
2733
|
-
function equipmentRef(entity) {
|
|
2734
|
-
return { kind: "equipment", slug: entity.slug, label: entity.label };
|
|
2735
|
-
}
|
|
2736
|
-
function dedupeRefs(refs) {
|
|
2737
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2738
|
-
const result = [];
|
|
2739
|
-
for (const ref of refs) {
|
|
2740
|
-
const key = `${ref.kind}:${ref.slug}`;
|
|
2741
|
-
if (seen.has(key)) continue;
|
|
2742
|
-
seen.add(key);
|
|
2743
|
-
result.push(ref);
|
|
2744
|
-
}
|
|
2745
|
-
return result;
|
|
2746
|
-
}
|
|
2747
|
-
function createReferenceIndex(catalog, materialize) {
|
|
2748
|
-
const dedupedEquipment = dedupeEntitiesByPrecedence(catalog.equipment ?? []);
|
|
2749
|
-
const efforts = buildExactIndex(catalog.efforts ?? []);
|
|
2750
|
-
const equipment = buildExactIndex(dedupedEquipment);
|
|
2751
|
-
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);
|
|
2752
|
-
const materializer = materialize ?? ((effort) => ({
|
|
2753
|
-
effort,
|
|
2754
|
-
definition: effort,
|
|
2755
|
-
slug: effort.slug,
|
|
2756
|
-
label: effort.label,
|
|
2757
|
-
met: effort.baseAttributes.met,
|
|
2758
|
-
baseAttributes: effort.baseAttributes,
|
|
2759
|
-
discipline: effort.baseAttributes.discipline,
|
|
2760
|
-
disciplineFactor: effort.baseAttributes.disciplineFactor ?? 1,
|
|
2761
|
-
intensityTier: effort.baseAttributes.intensityTier,
|
|
2762
|
-
modifiers: {},
|
|
2763
|
-
registrySource: effort.registrySource,
|
|
2764
|
-
resolvedFrom: effort.registrySource === "user" ? "user" : effort.registrySource === "bundled" ? "bundled" : "default",
|
|
2765
|
-
isEstimated: false
|
|
2766
|
-
}));
|
|
2767
|
-
const resolveEffortExact = (key) => {
|
|
2768
|
-
const found = efforts.lookup(key.trim());
|
|
2769
|
-
if (found.entity) return { entity: found.entity };
|
|
2770
|
-
if (found.ambiguous.length > 0) {
|
|
2771
|
-
return {
|
|
2772
|
-
entity: null,
|
|
2773
|
-
diagnostic: {
|
|
2774
|
-
reason: "ambiguous-effort",
|
|
2775
|
-
message: `"${key}" matches multiple efforts.`,
|
|
2776
|
-
token: key,
|
|
2777
|
-
candidates: found.ambiguous.map((e) => e.slug)
|
|
2778
|
-
}
|
|
2779
|
-
};
|
|
2780
|
-
}
|
|
2781
|
-
return {
|
|
2782
|
-
entity: null,
|
|
2783
|
-
diagnostic: { reason: "unknown-effort", message: `Unknown effort "${key}".`, token: key }
|
|
2784
|
-
};
|
|
2785
|
-
};
|
|
2786
|
-
const resolveEquipmentValues = (values) => {
|
|
2787
|
-
const refs = [];
|
|
2788
|
-
const diagnostics = [];
|
|
2789
|
-
for (const value of values) {
|
|
2790
|
-
if (typeof value !== "string" || !value.trim()) {
|
|
2791
|
-
diagnostics.push({
|
|
2792
|
-
reason: "invalid-explicit",
|
|
2793
|
-
message: `Equipment reference must be a slug string, got ${JSON.stringify(value) ?? "null"}.`,
|
|
2794
|
-
token: String(value)
|
|
2795
|
-
});
|
|
2796
|
-
continue;
|
|
2797
|
-
}
|
|
2798
|
-
const found = equipment.lookup(value.trim());
|
|
2799
|
-
if (found.entity) {
|
|
2800
|
-
refs.push(equipmentRef(found.entity));
|
|
2801
|
-
} else if (found.ambiguous.length > 0) {
|
|
2802
|
-
diagnostics.push({
|
|
2803
|
-
reason: "ambiguous-equipment",
|
|
2804
|
-
message: `"${value}" matches multiple equipment entries.`,
|
|
2805
|
-
token: value,
|
|
2806
|
-
candidates: found.ambiguous.map((e) => e.slug)
|
|
2807
|
-
});
|
|
2808
|
-
} else {
|
|
2809
|
-
diagnostics.push({
|
|
2810
|
-
reason: "invalid-explicit",
|
|
2811
|
-
message: `Unknown equipment "${value}".`,
|
|
2812
|
-
token: value
|
|
2813
|
-
});
|
|
2814
|
-
}
|
|
2815
|
-
}
|
|
2816
|
-
return { refs: dedupeRefs(refs), diagnostics };
|
|
2817
|
-
};
|
|
2818
|
-
const index = {
|
|
2819
|
-
resolveEffort(slugOrAlias) {
|
|
2820
|
-
return efforts.lookup(slugOrAlias).entity;
|
|
2821
|
-
},
|
|
2822
|
-
resolveEquipment(slugOrAlias) {
|
|
2823
|
-
return equipment.lookup(slugOrAlias).entity;
|
|
2824
|
-
},
|
|
2825
|
-
materialize(effort) {
|
|
2826
|
-
return materializer(effort);
|
|
2827
|
-
},
|
|
2828
|
-
resolve(label, defaults) {
|
|
2829
|
-
const source = (label ?? "").trim();
|
|
2830
|
-
const diagnostics = [];
|
|
2831
|
-
const push = (diagnostic) => {
|
|
2832
|
-
if (diagnostic) diagnostics.push(diagnostic);
|
|
2833
|
-
};
|
|
2834
|
-
let matched = "none";
|
|
2835
|
-
let effortEntity = null;
|
|
2836
|
-
let remaining = "";
|
|
2837
|
-
let equipmentRefs = [];
|
|
2838
|
-
let equipmentAuthored = false;
|
|
2839
|
-
const fillEquipment = () => {
|
|
2840
|
-
if (equipmentRefs.length > 0 || equipmentAuthored) return;
|
|
2841
|
-
if (effortEntity === null) return;
|
|
2842
|
-
if (defaults?.equipment?.length) {
|
|
2843
|
-
const resolvedDefaults = resolveEquipmentValues(defaults.equipment);
|
|
2844
|
-
equipmentRefs = resolvedDefaults.refs;
|
|
2845
|
-
for (const diagnostic of resolvedDefaults.diagnostics) {
|
|
2846
|
-
push({ ...diagnostic, reason: "unknown-equipment" });
|
|
2847
|
-
}
|
|
2848
|
-
if (equipmentRefs.length > 0) return;
|
|
2849
|
-
}
|
|
2850
|
-
if (effortEntity.equipment?.length) {
|
|
2851
|
-
const resolvedDefEquipment = resolveEquipmentValues(effortEntity.equipment);
|
|
2852
|
-
equipmentRefs = resolvedDefEquipment.refs;
|
|
2853
|
-
for (const diagnostic of resolvedDefEquipment.diagnostics) {
|
|
2854
|
-
push({ ...diagnostic, reason: "unknown-equipment" });
|
|
2855
|
-
}
|
|
2856
|
-
}
|
|
2857
|
-
};
|
|
2858
|
-
if (!source) {
|
|
2859
|
-
if (defaults?.effort) {
|
|
2860
|
-
const found = resolveEffortExact(defaults.effort);
|
|
2861
|
-
if (found.entity) {
|
|
2862
|
-
effortEntity = found.entity;
|
|
2863
|
-
matched = "default";
|
|
2864
|
-
} else {
|
|
2865
|
-
push({
|
|
2866
|
-
reason: found.diagnostic?.reason === "ambiguous-effort" ? "ambiguous-effort" : "unknown-effort",
|
|
2867
|
-
message: `Default effort "${defaults.effort}" is not in the catalog.`,
|
|
2868
|
-
token: defaults.effort,
|
|
2869
|
-
candidates: found.diagnostic?.candidates
|
|
2870
|
-
});
|
|
2871
|
-
}
|
|
2872
|
-
}
|
|
2873
|
-
fillEquipment();
|
|
2874
|
-
return {
|
|
2875
|
-
matched,
|
|
2876
|
-
label: source,
|
|
2877
|
-
effort: effortEntity ? materializer(effortEntity) : null,
|
|
2878
|
-
effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
|
|
2879
|
-
equipment: equipmentRefs,
|
|
2880
|
-
remaining: "",
|
|
2881
|
-
diagnostics
|
|
2882
|
-
};
|
|
2883
|
-
}
|
|
2884
|
-
if (source.startsWith("{")) {
|
|
2885
|
-
let parsed;
|
|
2886
|
-
try {
|
|
2887
|
-
parsed = JSON.parse(source);
|
|
2888
|
-
} catch {
|
|
2889
|
-
return {
|
|
2890
|
-
matched: "none",
|
|
2891
|
-
label: source,
|
|
2892
|
-
effort: null,
|
|
2893
|
-
effortRef: null,
|
|
2894
|
-
equipment: [],
|
|
2895
|
-
remaining: "",
|
|
2896
|
-
diagnostics: [{
|
|
2897
|
-
reason: "malformed-json",
|
|
2898
|
-
message: "Reference object is not valid JSON.",
|
|
2899
|
-
token: source
|
|
2900
|
-
}]
|
|
2901
|
-
};
|
|
2902
|
-
}
|
|
2903
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && ("effort" in parsed || "equipment" in parsed)) {
|
|
2904
|
-
matched = "explicit";
|
|
2905
|
-
const obj = parsed;
|
|
2906
|
-
if (obj.effort !== void 0) {
|
|
2907
|
-
if (typeof obj.effort === "string" && obj.effort.trim()) {
|
|
2908
|
-
const found = resolveEffortExact(obj.effort);
|
|
2909
|
-
effortEntity = found.entity;
|
|
2910
|
-
if (found.diagnostic) {
|
|
2911
|
-
push(found.diagnostic.reason === "ambiguous-effort" ? found.diagnostic : { ...found.diagnostic, reason: "invalid-explicit", message: `Explicit effort "${obj.effort}" is not in the catalog.` });
|
|
2912
|
-
}
|
|
2913
|
-
} else {
|
|
2914
|
-
push({
|
|
2915
|
-
reason: "invalid-explicit",
|
|
2916
|
-
message: 'Explicit "effort" must be a slug string.',
|
|
2917
|
-
token: String(obj.effort)
|
|
2918
|
-
});
|
|
2919
|
-
}
|
|
2920
|
-
}
|
|
2921
|
-
if (obj.equipment !== void 0) {
|
|
2922
|
-
equipmentAuthored = true;
|
|
2923
|
-
const values = Array.isArray(obj.equipment) ? obj.equipment : [obj.equipment];
|
|
2924
|
-
const resolvedEquipment = resolveEquipmentValues(values);
|
|
2925
|
-
equipmentRefs = resolvedEquipment.refs;
|
|
2926
|
-
diagnostics.push(...resolvedEquipment.diagnostics);
|
|
2927
|
-
if (effortEntity?.equipment?.length) {
|
|
2928
|
-
const definitionSlugs = new Set(effortEntity.equipment);
|
|
2929
|
-
const authoredSlugs = equipmentRefs.map((ref) => ref.slug);
|
|
2930
|
-
if (authoredSlugs.length > 0 && authoredSlugs.some((slug) => !definitionSlugs.has(slug))) {
|
|
2931
|
-
push({
|
|
2932
|
-
reason: "conflict",
|
|
2933
|
-
message: `Explicit equipment [${authoredSlugs.join(", ")}] contradicts effort "${effortEntity.slug}" definition equipment [${effortEntity.equipment.join(", ")}].`,
|
|
2934
|
-
token: source
|
|
2935
|
-
});
|
|
2936
|
-
}
|
|
2937
|
-
}
|
|
2938
|
-
}
|
|
2939
|
-
fillEquipment();
|
|
2940
|
-
return {
|
|
2941
|
-
matched,
|
|
2942
|
-
label: source,
|
|
2943
|
-
effort: effortEntity ? materializer(effortEntity) : null,
|
|
2944
|
-
effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
|
|
2945
|
-
equipment: equipmentRefs,
|
|
2946
|
-
remaining: "",
|
|
2947
|
-
diagnostics
|
|
2948
|
-
};
|
|
2949
|
-
}
|
|
2950
|
-
}
|
|
2951
|
-
const lowerSource = source.toLowerCase();
|
|
2952
|
-
for (const candidate of prefixCandidates) {
|
|
2953
|
-
if (!lowerSource.startsWith(candidate.lower)) continue;
|
|
2954
|
-
const after = source.slice(candidate.lower.length);
|
|
2955
|
-
if (after && !/^\s/.test(after)) continue;
|
|
2956
|
-
const candidateRemaining = after.trim();
|
|
2957
|
-
if (!candidateRemaining) continue;
|
|
2958
|
-
const equipmentFound = equipment.lookup(candidate.lower);
|
|
2959
|
-
if (equipmentFound.ambiguous.length > 0) {
|
|
2960
|
-
push({
|
|
2961
|
-
reason: "ambiguous-equipment",
|
|
2962
|
-
message: `"${candidate.lower}" matches multiple equipment entries.`,
|
|
2963
|
-
token: source,
|
|
2964
|
-
candidates: equipmentFound.ambiguous.map((e) => e.slug)
|
|
2965
|
-
});
|
|
2966
|
-
return {
|
|
2967
|
-
matched: "none",
|
|
2968
|
-
label: source,
|
|
2969
|
-
effort: null,
|
|
2970
|
-
effortRef: null,
|
|
2971
|
-
equipment: [],
|
|
2972
|
-
remaining: "",
|
|
2973
|
-
diagnostics
|
|
2974
|
-
};
|
|
2975
|
-
}
|
|
2976
|
-
if (!equipmentFound.entity) continue;
|
|
2977
|
-
const effortFound = resolveEffortExact(candidateRemaining);
|
|
2978
|
-
if (effortFound.entity) {
|
|
2979
|
-
matched = "split";
|
|
2980
|
-
effortEntity = effortFound.entity;
|
|
2981
|
-
equipmentRefs = [equipmentRef(equipmentFound.entity)];
|
|
2982
|
-
equipmentAuthored = true;
|
|
2983
|
-
remaining = candidateRemaining;
|
|
2984
|
-
break;
|
|
2985
|
-
}
|
|
2986
|
-
if (effortFound.diagnostic?.reason === "ambiguous-effort") {
|
|
2987
|
-
push(effortFound.diagnostic);
|
|
2988
|
-
return {
|
|
2989
|
-
matched: "none",
|
|
2990
|
-
label: source,
|
|
2991
|
-
effort: null,
|
|
2992
|
-
effortRef: null,
|
|
2993
|
-
equipment: [],
|
|
2994
|
-
remaining: "",
|
|
2995
|
-
diagnostics
|
|
2996
|
-
};
|
|
2997
|
-
}
|
|
2998
|
-
}
|
|
2999
|
-
if (matched === "none") {
|
|
3000
|
-
const found = resolveEffortExact(source);
|
|
3001
|
-
if (found.entity) {
|
|
3002
|
-
effortEntity = found.entity;
|
|
3003
|
-
matched = "whole";
|
|
3004
|
-
} else if (found.diagnostic?.reason === "ambiguous-effort") {
|
|
3005
|
-
push(found.diagnostic);
|
|
3006
|
-
return {
|
|
3007
|
-
matched: "none",
|
|
3008
|
-
label: source,
|
|
3009
|
-
effort: null,
|
|
3010
|
-
effortRef: null,
|
|
3011
|
-
equipment: [],
|
|
3012
|
-
remaining: "",
|
|
3013
|
-
diagnostics
|
|
3014
|
-
};
|
|
3015
|
-
} else if (found.diagnostic) {
|
|
3016
|
-
push(found.diagnostic);
|
|
3017
|
-
}
|
|
3018
|
-
}
|
|
3019
|
-
fillEquipment();
|
|
3020
|
-
return {
|
|
3021
|
-
matched,
|
|
3022
|
-
label: source,
|
|
3023
|
-
effort: effortEntity ? materializer(effortEntity) : null,
|
|
3024
|
-
effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
|
|
3025
|
-
equipment: equipmentRefs,
|
|
3026
|
-
remaining,
|
|
3027
|
-
diagnostics
|
|
3028
|
-
};
|
|
3029
|
-
}
|
|
3030
|
-
};
|
|
3031
|
-
return index;
|
|
3032
|
-
}
|
|
3033
|
-
function extractStatementRefs(statements) {
|
|
3034
|
-
const efforts = [];
|
|
3035
|
-
const equipment = [];
|
|
3036
|
-
const seenEffort = /* @__PURE__ */ new Set();
|
|
3037
|
-
const seenEquipment = /* @__PURE__ */ new Set();
|
|
3038
|
-
const pushUnique = (list, seen, value) => {
|
|
3039
|
-
if (!value) return;
|
|
3040
|
-
if (seen.has(value)) return;
|
|
3041
|
-
seen.add(value);
|
|
3042
|
-
list.push(value);
|
|
3043
|
-
};
|
|
3044
|
-
for (const statement of statements ?? []) {
|
|
3045
|
-
const metrics = Array.isArray(statement.metrics) ? statement.metrics : statement.metrics?.toArray?.() ?? [];
|
|
3046
|
-
for (const metric of metrics) {
|
|
3047
|
-
const refs = wodWikiCore.getReferenceMetadata(metric.metadata)?.refs;
|
|
3048
|
-
if (!refs) continue;
|
|
3049
|
-
for (const ref of refs) {
|
|
3050
|
-
if (ref.kind === "effort") pushUnique(efforts, seenEffort, ref.slug);
|
|
3051
|
-
if (ref.kind === "equipment") pushUnique(equipment, seenEquipment, ref.slug);
|
|
3052
|
-
}
|
|
3053
|
-
}
|
|
3054
|
-
}
|
|
3055
|
-
return { efforts, equipment };
|
|
3056
|
-
}
|
|
3057
|
-
function slugifyRefLabel(label) {
|
|
3058
|
-
return normalizeForFuzzy(label).replace(/\s+/g, "-");
|
|
3059
|
-
}
|
|
3060
|
-
|
|
3061
|
-
// src/parser/parseScript.ts
|
|
3062
|
-
function parseScript(inputText, options = {}) {
|
|
3063
|
-
if (!inputText || !inputText.trim()) {
|
|
3064
|
-
return new WhiteboardScript(inputText, [], []);
|
|
3065
|
-
}
|
|
3066
|
-
try {
|
|
3067
|
-
const doc = inputText.endsWith("\n") ? inputText : inputText + "\n";
|
|
3068
|
-
const tree = parser.parse(doc);
|
|
3069
|
-
const sport = options.sport ?? options.dialect;
|
|
3070
|
-
const references = options.references || options.tags ? {
|
|
3071
|
-
index: options.references ?? createReferenceIndex({ efforts: [], equipment: [] }),
|
|
3072
|
-
defaults: options.defaults,
|
|
3073
|
-
tags: options.tags
|
|
3074
|
-
} : void 0;
|
|
3075
|
-
const statements = options.withoutDialects ? extractStatementsRaw(inputText, tree, references) : extractStatements(inputText, tree, sport, references);
|
|
3076
|
-
return new WhiteboardScript(inputText, statements, []);
|
|
3077
|
-
} catch (error) {
|
|
3078
|
-
const msg = error instanceof Error ? error.message : "Unknown parse error";
|
|
3079
|
-
return new WhiteboardScript(inputText, [], [{ message: msg }]);
|
|
3080
|
-
}
|
|
3081
|
-
}
|
|
3082
|
-
|
|
3083
|
-
// src/parser/md-timer.ts
|
|
3084
|
-
var MdTimerRuntime = class {
|
|
3085
|
-
config;
|
|
3086
|
-
constructor(config) {
|
|
3087
|
-
this.config = config;
|
|
3088
|
-
}
|
|
3089
|
-
/**
|
|
3090
|
-
* Parse WhiteboardScript source into a {@link WhiteboardScript}, running the
|
|
3091
|
-
* Dialect Stack on every statement.
|
|
3092
|
-
*
|
|
3093
|
-
* @param sport - The block's `:sport` fence suffix (` ```log:climbing `).
|
|
3094
|
-
*/
|
|
3095
|
-
read(inputText, sport) {
|
|
3096
|
-
return parseScript(inputText, { sport, ...this.config });
|
|
3097
|
-
}
|
|
3098
|
-
/**
|
|
3099
|
-
* Parse without running the Dialect Stack. Used by the parser test harness
|
|
3100
|
-
* which applies its own Dialect set. Production consumers should use {@link read}.
|
|
3101
|
-
*/
|
|
3102
|
-
readWithoutDialects(inputText) {
|
|
3103
|
-
return parseScript(inputText, { withoutDialects: true, ...this.config });
|
|
3104
|
-
}
|
|
3105
|
-
};
|
|
3106
|
-
|
|
3107
|
-
// src/parser/parserInstance.ts
|
|
3108
|
-
function createParser(config) {
|
|
3109
|
-
return new MdTimerRuntime(config);
|
|
3110
|
-
}
|
|
3111
|
-
var nextTagID = 0;
|
|
3112
|
-
var Tag = class _Tag {
|
|
3113
|
-
/**
|
|
3114
|
-
@internal
|
|
3115
|
-
*/
|
|
3116
|
-
constructor(name2, set, base, modified) {
|
|
3117
|
-
this.name = name2;
|
|
3118
|
-
this.set = set;
|
|
3119
|
-
this.base = base;
|
|
3120
|
-
this.modified = modified;
|
|
3121
|
-
this.id = nextTagID++;
|
|
3122
|
-
}
|
|
3123
|
-
toString() {
|
|
3124
|
-
let { name: name2 } = this;
|
|
3125
|
-
for (let mod of this.modified)
|
|
3126
|
-
if (mod.name)
|
|
3127
|
-
name2 = `${mod.name}(${name2})`;
|
|
3128
|
-
return name2;
|
|
3129
|
-
}
|
|
3130
|
-
static define(nameOrParent, parent) {
|
|
3131
|
-
let name2 = typeof nameOrParent == "string" ? nameOrParent : "?";
|
|
3132
|
-
if (nameOrParent instanceof _Tag)
|
|
3133
|
-
parent = nameOrParent;
|
|
3134
|
-
if (parent === null || parent === void 0 ? void 0 : parent.base)
|
|
3135
|
-
throw new Error("Can not derive from a modified tag");
|
|
3136
|
-
let tag = new _Tag(name2, [], null, []);
|
|
3137
|
-
tag.set.push(tag);
|
|
3138
|
-
if (parent)
|
|
3139
|
-
for (let t2 of parent.set)
|
|
3140
|
-
tag.set.push(t2);
|
|
3141
|
-
return tag;
|
|
2730
|
+
static define(nameOrParent, parent) {
|
|
2731
|
+
let name2 = typeof nameOrParent == "string" ? nameOrParent : "?";
|
|
2732
|
+
if (nameOrParent instanceof _Tag)
|
|
2733
|
+
parent = nameOrParent;
|
|
2734
|
+
if (parent === null || parent === void 0 ? void 0 : parent.base)
|
|
2735
|
+
throw new Error("Can not derive from a modified tag");
|
|
2736
|
+
let tag = new _Tag(name2, [], null, []);
|
|
2737
|
+
tag.set.push(tag);
|
|
2738
|
+
if (parent)
|
|
2739
|
+
for (let t2 of parent.set)
|
|
2740
|
+
tag.set.push(t2);
|
|
2741
|
+
return tag;
|
|
3142
2742
|
}
|
|
3143
2743
|
/**
|
|
3144
2744
|
Define a tag _modifier_, which is a function that, given a tag,
|
|
@@ -3851,17 +3451,16 @@ function structureKeywords() {
|
|
|
3851
3451
|
}));
|
|
3852
3452
|
}
|
|
3853
3453
|
function toCompletions(values, detail, quoteValues = false) {
|
|
3854
|
-
return values.map((value) =>
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
}));
|
|
3454
|
+
return values.map((value) => {
|
|
3455
|
+
const insertText = quoteValues ? !/^[A-Za-z][A-Za-z0-9]*$/.test(value.value) ? JSON.stringify(value.value) : value.value : value.label ?? value.value;
|
|
3456
|
+
return {
|
|
3457
|
+
label: value.label ?? value.value,
|
|
3458
|
+
apply: insertText,
|
|
3459
|
+
detail: value.detail ?? (value.label && value.label !== value.value ? value.value : detail),
|
|
3460
|
+
type: "text",
|
|
3461
|
+
info: value.info
|
|
3462
|
+
};
|
|
3463
|
+
});
|
|
3865
3464
|
}
|
|
3866
3465
|
function dialectWords(sport) {
|
|
3867
3466
|
if (!sport) return [];
|
|
@@ -3944,7 +3543,13 @@ function whiteboardScriptCompletionSource(options = {}) {
|
|
|
3944
3543
|
return null;
|
|
3945
3544
|
}
|
|
3946
3545
|
const efforts = options.efforts ? toCompletions(await options.efforts(), "effort") : [];
|
|
3947
|
-
const
|
|
3546
|
+
const equipmentPrefixes = options.equipment ? (await options.equipment()).map((eq) => ({
|
|
3547
|
+
label: eq.label ?? eq.value,
|
|
3548
|
+
apply: `${eq.label ?? eq.value} `,
|
|
3549
|
+
detail: eq.detail ?? "equipment",
|
|
3550
|
+
type: "text"
|
|
3551
|
+
})) : [];
|
|
3552
|
+
const effortOptions = [...keywords, ...efforts, ...equipmentPrefixes, ...sportWords];
|
|
3948
3553
|
if (/^[ \t]*[A-Za-z][A-Za-z0-9]*$/.test(before)) {
|
|
3949
3554
|
const supplied = suppliedPropertyKeys(context);
|
|
3950
3555
|
return {
|
|
@@ -3987,7 +3592,7 @@ function whiteboardScriptCompletionSource(options = {}) {
|
|
|
3987
3592
|
const typed = context.state.doc.sliceString(first.from, pos).toLowerCase();
|
|
3988
3593
|
const compatible = effortOptions.filter((option) => {
|
|
3989
3594
|
const inserted = typeof option.apply === "string" ? option.apply : option.label;
|
|
3990
|
-
return inserted.toLowerCase().startsWith(typed);
|
|
3595
|
+
return inserted.toLowerCase().startsWith(typed) || option.label.toLowerCase().startsWith(typed);
|
|
3991
3596
|
});
|
|
3992
3597
|
return { from: first.from, options: compatible, validFor: /^[A-Za-z ]*$/ };
|
|
3993
3598
|
}
|
|
@@ -4036,6 +3641,50 @@ function whiteboardScript(options) {
|
|
|
4036
3641
|
return new language.LanguageSupport(whiteboardScriptLanguage, whiteboardScriptCompletion(options ?? {}));
|
|
4037
3642
|
}
|
|
4038
3643
|
|
|
3644
|
+
// src/effort-registry/types.ts
|
|
3645
|
+
var EFFORT_REGISTRY_ORIGINS = ["bundled", "user"];
|
|
3646
|
+
var SEED_PRECEDENCE_RANK = {
|
|
3647
|
+
core: 0,
|
|
3648
|
+
community: 1,
|
|
3649
|
+
imported: 2,
|
|
3650
|
+
user: 3
|
|
3651
|
+
};
|
|
3652
|
+
function resolveSourceTier(sourceIdOrMetaKey, explicitTier) {
|
|
3653
|
+
if (explicitTier) return explicitTier;
|
|
3654
|
+
if (!sourceIdOrMetaKey) return "core";
|
|
3655
|
+
if (sourceIdOrMetaKey === "user") return "user";
|
|
3656
|
+
const id = sourceIdOrMetaKey.startsWith("seed:") ? sourceIdOrMetaKey.slice(5) : sourceIdOrMetaKey;
|
|
3657
|
+
if (id === "core" || id === "efforts" || id === "equipment") return "core";
|
|
3658
|
+
if (id === "community" || id.startsWith("community-") || id === "common") return "community";
|
|
3659
|
+
return "imported";
|
|
3660
|
+
}
|
|
3661
|
+
function resolveSourceRank(sourceIdOrMetaKey, explicitTier) {
|
|
3662
|
+
return SEED_PRECEDENCE_RANK[resolveSourceTier(sourceIdOrMetaKey, explicitTier)];
|
|
3663
|
+
}
|
|
3664
|
+
function resolveEntityTier(entity) {
|
|
3665
|
+
if (entity.registrySource === "user") return "user";
|
|
3666
|
+
return resolveSourceTier(entity.sourceId);
|
|
3667
|
+
}
|
|
3668
|
+
function resolveEntityRank(entity) {
|
|
3669
|
+
return SEED_PRECEDENCE_RANK[resolveEntityTier(entity)];
|
|
3670
|
+
}
|
|
3671
|
+
function dedupeEntitiesByPrecedence(entities) {
|
|
3672
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
3673
|
+
for (const entity of entities) {
|
|
3674
|
+
const key = entity.slug.toLowerCase();
|
|
3675
|
+
const prior = bySlug.get(key);
|
|
3676
|
+
if (!prior || resolveEntityRank(entity) >= resolveEntityRank(prior)) {
|
|
3677
|
+
bySlug.set(key, entity);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
return Array.from(bySlug.values());
|
|
3681
|
+
}
|
|
3682
|
+
var INTENSITY_TIERS = ["low", "moderate", "high"];
|
|
3683
|
+
var DEFAULT_RESOLVER_OPTIONS = {
|
|
3684
|
+
defaultMet: 5,
|
|
3685
|
+
defaultThreshold: 2
|
|
3686
|
+
};
|
|
3687
|
+
|
|
4039
3688
|
// src/effort-registry/disciplines.ts
|
|
4040
3689
|
var EFFORT_DISCIPLINES = [
|
|
4041
3690
|
"bodyweight",
|
|
@@ -4641,6 +4290,71 @@ function tagCategoryToDocument(category) {
|
|
|
4641
4290
|
}
|
|
4642
4291
|
return lines.join("\n");
|
|
4643
4292
|
}
|
|
4293
|
+
|
|
4294
|
+
// src/effort-registry/fuzzyMatch.ts
|
|
4295
|
+
function levenshteinDistance(a, b) {
|
|
4296
|
+
if (a === b) return 0;
|
|
4297
|
+
if (a.length === 0) return b.length;
|
|
4298
|
+
if (b.length === 0) return a.length;
|
|
4299
|
+
if (a.length > b.length) {
|
|
4300
|
+
const tmp = a;
|
|
4301
|
+
a = b;
|
|
4302
|
+
b = tmp;
|
|
4303
|
+
}
|
|
4304
|
+
const m = a.length;
|
|
4305
|
+
const n = b.length;
|
|
4306
|
+
let prev = new Uint32Array(m + 1);
|
|
4307
|
+
let curr = new Uint32Array(m + 1);
|
|
4308
|
+
for (let i = 0; i <= m; i++) {
|
|
4309
|
+
prev[i] = i;
|
|
4310
|
+
}
|
|
4311
|
+
for (let j = 1; j <= n; j++) {
|
|
4312
|
+
curr[0] = j;
|
|
4313
|
+
const bj = b.charCodeAt(j - 1);
|
|
4314
|
+
for (let i = 1; i <= m; i++) {
|
|
4315
|
+
const cost = a.charCodeAt(i - 1) === bj ? 0 : 1;
|
|
4316
|
+
curr[i] = Math.min(
|
|
4317
|
+
prev[i] + 1,
|
|
4318
|
+
// deletion
|
|
4319
|
+
curr[i - 1] + 1,
|
|
4320
|
+
// insertion
|
|
4321
|
+
prev[i - 1] + cost
|
|
4322
|
+
// substitution
|
|
4323
|
+
);
|
|
4324
|
+
}
|
|
4325
|
+
const swap = prev;
|
|
4326
|
+
prev = curr;
|
|
4327
|
+
curr = swap;
|
|
4328
|
+
}
|
|
4329
|
+
return prev[m];
|
|
4330
|
+
}
|
|
4331
|
+
function normalizeForFuzzy(input) {
|
|
4332
|
+
return input.toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, " ").replace(/[-_]+/g, " ").trim().replace(/\s+/g, " ");
|
|
4333
|
+
}
|
|
4334
|
+
function isWithinThreshold(a, b, threshold) {
|
|
4335
|
+
const na = normalizeForFuzzy(a);
|
|
4336
|
+
const nb = normalizeForFuzzy(b);
|
|
4337
|
+
if (na === nb) return true;
|
|
4338
|
+
if (Math.abs(na.length - nb.length) > threshold) return false;
|
|
4339
|
+
return levenshteinDistance(na, nb) <= threshold;
|
|
4340
|
+
}
|
|
4341
|
+
function findBestFuzzyMatch(query, candidates, threshold) {
|
|
4342
|
+
const nq = normalizeForFuzzy(query);
|
|
4343
|
+
let best = null;
|
|
4344
|
+
for (const candidate of candidates) {
|
|
4345
|
+
const nc = normalizeForFuzzy(candidate);
|
|
4346
|
+
if (Math.abs(nq.length - nc.length) > threshold) continue;
|
|
4347
|
+
const distance = levenshteinDistance(nq, nc);
|
|
4348
|
+
if (distance > threshold) continue;
|
|
4349
|
+
if (!best || distance < best.distance) {
|
|
4350
|
+
best = { target: candidate, distance };
|
|
4351
|
+
if (distance === 0) break;
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
return best;
|
|
4355
|
+
}
|
|
4356
|
+
|
|
4357
|
+
// src/effort-registry/EffortResolver.ts
|
|
4644
4358
|
function normalizeSlug(label) {
|
|
4645
4359
|
return normalizeForFuzzy(label).replace(/\s+/g, "-");
|
|
4646
4360
|
}
|
|
@@ -5002,148 +4716,519 @@ var CompositePropertyRegistry = class {
|
|
|
5002
4716
|
for (const item of this.userItems.values()) merged.set(item.slug, item);
|
|
5003
4717
|
return [...merged.values()];
|
|
5004
4718
|
}
|
|
5005
|
-
listByOrigin(origin) {
|
|
5006
|
-
return origin === "user" ? [...this.userItems.values()] : [...this.bundledItems.values()];
|
|
4719
|
+
listByOrigin(origin) {
|
|
4720
|
+
return origin === "user" ? [...this.userItems.values()] : [...this.bundledItems.values()];
|
|
4721
|
+
}
|
|
4722
|
+
async upsert(item) {
|
|
4723
|
+
if (item.registrySource !== "user") {
|
|
4724
|
+
throw new Error(`Cannot upsert property with registrySource '${item.registrySource}'; clone it as 'user' first`);
|
|
4725
|
+
}
|
|
4726
|
+
this.userItems.set(item.slug, item);
|
|
4727
|
+
if (this.storage) await this.storage.save(item);
|
|
4728
|
+
}
|
|
4729
|
+
async delete(slug) {
|
|
4730
|
+
const item = this.userItems.get(slug);
|
|
4731
|
+
if (!item) throw new Error(`Cannot delete '${slug}': not a user-owned record`);
|
|
4732
|
+
this.userItems.delete(slug);
|
|
4733
|
+
if (this.storage) await this.storage.delete(slug);
|
|
4734
|
+
}
|
|
4735
|
+
};
|
|
4736
|
+
var CompositeEquipmentRegistry = class extends CompositePropertyRegistry {
|
|
4737
|
+
constructor(options) {
|
|
4738
|
+
super({ storage: options?.storage });
|
|
4739
|
+
}
|
|
4740
|
+
};
|
|
4741
|
+
|
|
4742
|
+
// src/effort-registry/CompositeEffortRegistry.ts
|
|
4743
|
+
var CompositeEffortRegistry = class extends CompositePropertyRegistry {
|
|
4744
|
+
constructor(options) {
|
|
4745
|
+
super({
|
|
4746
|
+
bundled: options?.bundled,
|
|
4747
|
+
storage: options?.storage
|
|
4748
|
+
});
|
|
4749
|
+
}
|
|
4750
|
+
async loadBundled(customBundled) {
|
|
4751
|
+
this.bundledItems.clear();
|
|
4752
|
+
const source = customBundled ?? this.initialBundled ?? bundledEfforts;
|
|
4753
|
+
for (const effort of source) {
|
|
4754
|
+
const prior = this.bundledItems.get(effort.slug);
|
|
4755
|
+
if (!prior || resolveEntityRank(effort) >= resolveEntityRank(prior)) {
|
|
4756
|
+
this.bundledItems.set(effort.slug, effort);
|
|
4757
|
+
}
|
|
4758
|
+
}
|
|
4759
|
+
this.userItems.clear();
|
|
4760
|
+
if (this.storage) {
|
|
4761
|
+
try {
|
|
4762
|
+
const userEfforts = await this.storage.load();
|
|
4763
|
+
for (const effort of userEfforts) {
|
|
4764
|
+
if (effort.registrySource === "user") {
|
|
4765
|
+
this.userItems.set(effort.slug, effort);
|
|
4766
|
+
}
|
|
4767
|
+
}
|
|
4768
|
+
} catch (err) {
|
|
4769
|
+
console.warn("[CompositeEffortRegistry] Failed to load user efforts from storage adapter:", err);
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
this.initialized = true;
|
|
4773
|
+
}
|
|
4774
|
+
/** True after loadBundled() has resolved */
|
|
4775
|
+
isInitialized() {
|
|
4776
|
+
return this.initialized;
|
|
4777
|
+
}
|
|
4778
|
+
listByOrigin(origin) {
|
|
4779
|
+
if (origin === "user") {
|
|
4780
|
+
return Array.from(this.userItems.values());
|
|
4781
|
+
}
|
|
4782
|
+
if (origin === "bundled") {
|
|
4783
|
+
return Array.from(this.bundledItems.values());
|
|
4784
|
+
}
|
|
4785
|
+
return this.list().filter((e) => e.registrySource === origin);
|
|
4786
|
+
}
|
|
4787
|
+
async upsert(effort) {
|
|
4788
|
+
if (effort.registrySource !== "user") {
|
|
4789
|
+
throw new Error(
|
|
4790
|
+
`CompositeEffortRegistry.upsert: only user efforts can be written. Received registrySource="${effort.registrySource}"`
|
|
4791
|
+
);
|
|
4792
|
+
}
|
|
4793
|
+
this.userItems.set(effort.slug, effort);
|
|
4794
|
+
if (this.storage) {
|
|
4795
|
+
try {
|
|
4796
|
+
await this.storage.save(effort);
|
|
4797
|
+
} catch (err) {
|
|
4798
|
+
console.warn("[CompositeEffortRegistry] Failed to persist effort to storage adapter:", err);
|
|
4799
|
+
}
|
|
4800
|
+
}
|
|
4801
|
+
}
|
|
4802
|
+
async delete(slug) {
|
|
4803
|
+
const existing = this.resolve(slug);
|
|
4804
|
+
if (existing && existing.registrySource !== "user") {
|
|
4805
|
+
throw new Error(
|
|
4806
|
+
`CompositeEffortRegistry.delete: cannot delete non-user effort "${slug}"`
|
|
4807
|
+
);
|
|
4808
|
+
}
|
|
4809
|
+
this.userItems.delete(slug);
|
|
4810
|
+
if (this.storage) {
|
|
4811
|
+
try {
|
|
4812
|
+
await this.storage.delete(slug);
|
|
4813
|
+
} catch (err) {
|
|
4814
|
+
console.warn("[CompositeEffortRegistry] Failed to delete effort from storage adapter:", err);
|
|
4815
|
+
}
|
|
4816
|
+
}
|
|
4817
|
+
}
|
|
4818
|
+
};
|
|
4819
|
+
|
|
4820
|
+
// src/effort-registry/aliasManagement.ts
|
|
4821
|
+
async function addAliasToEffort(registry, targetSlug, newAlias) {
|
|
4822
|
+
const trimmed = newAlias.trim();
|
|
4823
|
+
if (!trimmed) {
|
|
4824
|
+
throw new Error("Alias cannot be empty");
|
|
4825
|
+
}
|
|
4826
|
+
const existing = registry.resolve(targetSlug);
|
|
4827
|
+
if (!existing) {
|
|
4828
|
+
throw new Error(`Effort "${targetSlug}" not found in registry`);
|
|
5007
4829
|
}
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
this.userItems.set(item.slug, item);
|
|
5013
|
-
if (this.storage) await this.storage.save(item);
|
|
4830
|
+
const normalizedNew = normalizeForFuzzy(trimmed);
|
|
4831
|
+
const alreadyHas = normalizeForFuzzy(existing.label) === normalizedNew || existing.aliases.some((a) => normalizeForFuzzy(a) === normalizedNew);
|
|
4832
|
+
if (alreadyHas) {
|
|
4833
|
+
return existing;
|
|
5014
4834
|
}
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
4835
|
+
const updatedAliases = [...existing.aliases, trimmed];
|
|
4836
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4837
|
+
if (existing.registrySource === "bundled") {
|
|
4838
|
+
const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? `effort-user-${crypto.randomUUID()}` : `effort-user-${Date.now()}`;
|
|
4839
|
+
const userEffort = {
|
|
4840
|
+
...existing,
|
|
4841
|
+
id,
|
|
4842
|
+
registrySource: "user",
|
|
4843
|
+
aliases: updatedAliases,
|
|
4844
|
+
updatedAt: now,
|
|
4845
|
+
derivation: existing.derivation ?? {
|
|
4846
|
+
parentSlug: existing.slug,
|
|
4847
|
+
coefficients: {},
|
|
4848
|
+
hardOverrides: {}
|
|
4849
|
+
}
|
|
4850
|
+
};
|
|
4851
|
+
await registry.upsert(userEffort);
|
|
4852
|
+
return userEffort;
|
|
5020
4853
|
}
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
4854
|
+
const updatedUserEffort = {
|
|
4855
|
+
...existing,
|
|
4856
|
+
aliases: updatedAliases,
|
|
4857
|
+
updatedAt: now
|
|
4858
|
+
};
|
|
4859
|
+
await registry.upsert(updatedUserEffort);
|
|
4860
|
+
return updatedUserEffort;
|
|
4861
|
+
}
|
|
4862
|
+
function buildExactIndex(entities) {
|
|
4863
|
+
const deduped = dedupeEntitiesByPrecedence(entities);
|
|
4864
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
4865
|
+
const add = (key, entity) => {
|
|
4866
|
+
if (!key) return;
|
|
4867
|
+
const existing = byKey.get(key);
|
|
4868
|
+
if (existing) {
|
|
4869
|
+
if (!existing.includes(entity)) existing.push(entity);
|
|
4870
|
+
} else {
|
|
4871
|
+
byKey.set(key, [entity]);
|
|
4872
|
+
}
|
|
4873
|
+
};
|
|
4874
|
+
for (const entity of deduped) {
|
|
4875
|
+
add(entity.slug.toLowerCase(), entity);
|
|
4876
|
+
add(normalizeForFuzzy(entity.label), entity);
|
|
4877
|
+
for (const alias of entity.aliases) add(normalizeForFuzzy(alias), entity);
|
|
5025
4878
|
}
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
4879
|
+
return {
|
|
4880
|
+
byKey,
|
|
4881
|
+
lookup(key) {
|
|
4882
|
+
const direct = byKey.get(key) ?? byKey.get(normalizeForFuzzy(key));
|
|
4883
|
+
if (!direct || direct.length === 0) return { entity: null, ambiguous: [] };
|
|
4884
|
+
if (direct.length === 1) return { entity: direct[0], ambiguous: [] };
|
|
4885
|
+
let maxRank = -1;
|
|
4886
|
+
for (const candidate of direct) {
|
|
4887
|
+
const rank = resolveEntityRank(candidate);
|
|
4888
|
+
if (rank > maxRank) maxRank = rank;
|
|
4889
|
+
}
|
|
4890
|
+
const topTier = direct.filter((candidate) => resolveEntityRank(candidate) === maxRank);
|
|
4891
|
+
if (topTier.length === 1) return { entity: topTier[0], ambiguous: [] };
|
|
4892
|
+
return { entity: null, ambiguous: topTier };
|
|
4893
|
+
}
|
|
4894
|
+
};
|
|
4895
|
+
}
|
|
4896
|
+
function equipmentRef(entity) {
|
|
4897
|
+
return { kind: "equipment", slug: entity.slug, label: entity.label };
|
|
4898
|
+
}
|
|
4899
|
+
function dedupeRefs(refs) {
|
|
4900
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4901
|
+
const result = [];
|
|
4902
|
+
for (const ref of refs) {
|
|
4903
|
+
const key = `${ref.kind}:${ref.slug}`;
|
|
4904
|
+
if (seen.has(key)) continue;
|
|
4905
|
+
seen.add(key);
|
|
4906
|
+
result.push(ref);
|
|
5035
4907
|
}
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
4908
|
+
return result;
|
|
4909
|
+
}
|
|
4910
|
+
function createReferenceIndex(catalog, materialize) {
|
|
4911
|
+
const dedupedEquipment = dedupeEntitiesByPrecedence(catalog.equipment ?? []);
|
|
4912
|
+
const efforts = buildExactIndex(catalog.efforts ?? []);
|
|
4913
|
+
const equipment = buildExactIndex(dedupedEquipment);
|
|
4914
|
+
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);
|
|
4915
|
+
const materializer = materialize ?? ((effort) => ({
|
|
4916
|
+
effort,
|
|
4917
|
+
definition: effort,
|
|
4918
|
+
slug: effort.slug,
|
|
4919
|
+
label: effort.label,
|
|
4920
|
+
met: effort.baseAttributes.met,
|
|
4921
|
+
baseAttributes: effort.baseAttributes,
|
|
4922
|
+
discipline: effort.baseAttributes.discipline,
|
|
4923
|
+
disciplineFactor: effort.baseAttributes.disciplineFactor ?? 1,
|
|
4924
|
+
intensityTier: effort.baseAttributes.intensityTier,
|
|
4925
|
+
modifiers: {},
|
|
4926
|
+
registrySource: effort.registrySource,
|
|
4927
|
+
resolvedFrom: effort.registrySource === "user" ? "user" : effort.registrySource === "bundled" ? "bundled" : "default",
|
|
4928
|
+
isEstimated: false
|
|
4929
|
+
}));
|
|
4930
|
+
const resolveEffortExact = (key) => {
|
|
4931
|
+
const found = efforts.lookup(key.trim());
|
|
4932
|
+
if (found.entity) return { entity: found.entity };
|
|
4933
|
+
if (found.ambiguous.length > 0) {
|
|
4934
|
+
return {
|
|
4935
|
+
entity: null,
|
|
4936
|
+
diagnostic: {
|
|
4937
|
+
reason: "ambiguous-effort",
|
|
4938
|
+
message: `"${key}" matches multiple efforts.`,
|
|
4939
|
+
token: key,
|
|
4940
|
+
candidates: found.ambiguous.map((e) => e.slug)
|
|
4941
|
+
}
|
|
4942
|
+
};
|
|
4943
|
+
}
|
|
4944
|
+
return {
|
|
4945
|
+
entity: null,
|
|
4946
|
+
diagnostic: { reason: "unknown-effort", message: `Unknown effort "${key}".`, token: key }
|
|
4947
|
+
};
|
|
4948
|
+
};
|
|
4949
|
+
const resolveEquipmentValues = (values) => {
|
|
4950
|
+
const refs = [];
|
|
4951
|
+
const diagnostics = [];
|
|
4952
|
+
for (const value of values) {
|
|
4953
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
4954
|
+
diagnostics.push({
|
|
4955
|
+
reason: "invalid-explicit",
|
|
4956
|
+
message: `Equipment reference must be a slug string, got ${JSON.stringify(value) ?? "null"}.`,
|
|
4957
|
+
token: String(value)
|
|
4958
|
+
});
|
|
4959
|
+
continue;
|
|
4960
|
+
}
|
|
4961
|
+
const found = equipment.lookup(value.trim());
|
|
4962
|
+
if (found.entity) {
|
|
4963
|
+
refs.push(equipmentRef(found.entity));
|
|
4964
|
+
} else if (found.ambiguous.length > 0) {
|
|
4965
|
+
diagnostics.push({
|
|
4966
|
+
reason: "ambiguous-equipment",
|
|
4967
|
+
message: `"${value}" matches multiple equipment entries.`,
|
|
4968
|
+
token: value,
|
|
4969
|
+
candidates: found.ambiguous.map((e) => e.slug)
|
|
4970
|
+
});
|
|
4971
|
+
} else {
|
|
4972
|
+
diagnostics.push({
|
|
4973
|
+
reason: "invalid-explicit",
|
|
4974
|
+
message: `Unknown equipment "${value}".`,
|
|
4975
|
+
token: value
|
|
4976
|
+
});
|
|
5043
4977
|
}
|
|
5044
4978
|
}
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
4979
|
+
return { refs: dedupeRefs(refs), diagnostics };
|
|
4980
|
+
};
|
|
4981
|
+
const index = {
|
|
4982
|
+
resolveEffort(slugOrAlias) {
|
|
4983
|
+
return efforts.lookup(slugOrAlias).entity;
|
|
4984
|
+
},
|
|
4985
|
+
resolveEquipment(slugOrAlias) {
|
|
4986
|
+
return equipment.lookup(slugOrAlias).entity;
|
|
4987
|
+
},
|
|
4988
|
+
materialize(effort) {
|
|
4989
|
+
return materializer(effort);
|
|
4990
|
+
},
|
|
4991
|
+
resolve(label, defaults) {
|
|
4992
|
+
const source = (label ?? "").trim();
|
|
4993
|
+
const diagnostics = [];
|
|
4994
|
+
const push = (diagnostic) => {
|
|
4995
|
+
if (diagnostic) diagnostics.push(diagnostic);
|
|
4996
|
+
};
|
|
4997
|
+
let matched = "none";
|
|
4998
|
+
let effortEntity = null;
|
|
4999
|
+
let remaining = "";
|
|
5000
|
+
let equipmentRefs = [];
|
|
5001
|
+
let equipmentAuthored = false;
|
|
5002
|
+
const fillEquipment = () => {
|
|
5003
|
+
if (equipmentRefs.length > 0 || equipmentAuthored) return;
|
|
5004
|
+
if (effortEntity === null) return;
|
|
5005
|
+
if (defaults?.equipment?.length) {
|
|
5006
|
+
const resolvedDefaults = resolveEquipmentValues(defaults.equipment);
|
|
5007
|
+
equipmentRefs = resolvedDefaults.refs;
|
|
5008
|
+
for (const diagnostic of resolvedDefaults.diagnostics) {
|
|
5009
|
+
push({ ...diagnostic, reason: "unknown-equipment" });
|
|
5010
|
+
}
|
|
5011
|
+
if (equipmentRefs.length > 0) return;
|
|
5012
|
+
}
|
|
5013
|
+
if (effortEntity.equipment?.length) {
|
|
5014
|
+
const resolvedDefEquipment = resolveEquipmentValues(effortEntity.equipment);
|
|
5015
|
+
equipmentRefs = resolvedDefEquipment.refs;
|
|
5016
|
+
for (const diagnostic of resolvedDefEquipment.diagnostics) {
|
|
5017
|
+
push({ ...diagnostic, reason: "unknown-equipment" });
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
};
|
|
5021
|
+
if (!source) {
|
|
5022
|
+
if (defaults?.effort) {
|
|
5023
|
+
const found = resolveEffortExact(defaults.effort);
|
|
5024
|
+
if (found.entity) {
|
|
5025
|
+
effortEntity = found.entity;
|
|
5026
|
+
matched = "default";
|
|
5027
|
+
} else {
|
|
5028
|
+
push({
|
|
5029
|
+
reason: found.diagnostic?.reason === "ambiguous-effort" ? "ambiguous-effort" : "unknown-effort",
|
|
5030
|
+
message: `Default effort "${defaults.effort}" is not in the catalog.`,
|
|
5031
|
+
token: defaults.effort,
|
|
5032
|
+
candidates: found.diagnostic?.candidates
|
|
5033
|
+
});
|
|
5034
|
+
}
|
|
5035
|
+
}
|
|
5036
|
+
fillEquipment();
|
|
5037
|
+
return {
|
|
5038
|
+
matched,
|
|
5039
|
+
label: source,
|
|
5040
|
+
effort: effortEntity ? materializer(effortEntity) : null,
|
|
5041
|
+
effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
|
|
5042
|
+
equipment: equipmentRefs,
|
|
5043
|
+
remaining: "",
|
|
5044
|
+
diagnostics
|
|
5045
|
+
};
|
|
5046
|
+
}
|
|
5047
|
+
if (source.startsWith("{")) {
|
|
5048
|
+
let parsed;
|
|
5049
|
+
try {
|
|
5050
|
+
parsed = JSON.parse(source);
|
|
5051
|
+
} catch {
|
|
5052
|
+
return {
|
|
5053
|
+
matched: "none",
|
|
5054
|
+
label: source,
|
|
5055
|
+
effort: null,
|
|
5056
|
+
effortRef: null,
|
|
5057
|
+
equipment: [],
|
|
5058
|
+
remaining: "",
|
|
5059
|
+
diagnostics: [{
|
|
5060
|
+
reason: "malformed-json",
|
|
5061
|
+
message: "Reference object is not valid JSON.",
|
|
5062
|
+
token: source
|
|
5063
|
+
}]
|
|
5064
|
+
};
|
|
5065
|
+
}
|
|
5066
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && ("effort" in parsed || "equipment" in parsed)) {
|
|
5067
|
+
matched = "explicit";
|
|
5068
|
+
const obj = parsed;
|
|
5069
|
+
if (obj.effort !== void 0) {
|
|
5070
|
+
if (typeof obj.effort === "string" && obj.effort.trim()) {
|
|
5071
|
+
const found = resolveEffortExact(obj.effort);
|
|
5072
|
+
effortEntity = found.entity;
|
|
5073
|
+
if (found.diagnostic) {
|
|
5074
|
+
push(found.diagnostic.reason === "ambiguous-effort" ? found.diagnostic : { ...found.diagnostic, reason: "invalid-explicit", message: `Explicit effort "${obj.effort}" is not in the catalog.` });
|
|
5075
|
+
}
|
|
5076
|
+
} else {
|
|
5077
|
+
push({
|
|
5078
|
+
reason: "invalid-explicit",
|
|
5079
|
+
message: 'Explicit "effort" must be a slug string.',
|
|
5080
|
+
token: String(obj.effort)
|
|
5081
|
+
});
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
if (obj.equipment !== void 0) {
|
|
5085
|
+
equipmentAuthored = true;
|
|
5086
|
+
const values = Array.isArray(obj.equipment) ? obj.equipment : [obj.equipment];
|
|
5087
|
+
const resolvedEquipment = resolveEquipmentValues(values);
|
|
5088
|
+
equipmentRefs = resolvedEquipment.refs;
|
|
5089
|
+
diagnostics.push(...resolvedEquipment.diagnostics);
|
|
5090
|
+
if (effortEntity?.equipment?.length) {
|
|
5091
|
+
const definitionSlugs = new Set(effortEntity.equipment);
|
|
5092
|
+
const authoredSlugs = equipmentRefs.map((ref) => ref.slug);
|
|
5093
|
+
if (authoredSlugs.length > 0 && authoredSlugs.some((slug) => !definitionSlugs.has(slug))) {
|
|
5094
|
+
push({
|
|
5095
|
+
reason: "conflict",
|
|
5096
|
+
message: `Explicit equipment [${authoredSlugs.join(", ")}] contradicts effort "${effortEntity.slug}" definition equipment [${effortEntity.equipment.join(", ")}].`,
|
|
5097
|
+
token: source
|
|
5098
|
+
});
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5052
5101
|
}
|
|
5102
|
+
fillEquipment();
|
|
5103
|
+
return {
|
|
5104
|
+
matched,
|
|
5105
|
+
label: source,
|
|
5106
|
+
effort: effortEntity ? materializer(effortEntity) : null,
|
|
5107
|
+
effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
|
|
5108
|
+
equipment: equipmentRefs,
|
|
5109
|
+
remaining: "",
|
|
5110
|
+
diagnostics
|
|
5111
|
+
};
|
|
5053
5112
|
}
|
|
5054
|
-
} catch (err) {
|
|
5055
|
-
console.warn("[CompositeEffortRegistry] Failed to load user efforts from storage adapter:", err);
|
|
5056
5113
|
}
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5114
|
+
const lowerSource = source.toLowerCase();
|
|
5115
|
+
for (const candidate of prefixCandidates) {
|
|
5116
|
+
if (!lowerSource.startsWith(candidate.lower)) continue;
|
|
5117
|
+
const after = source.slice(candidate.lower.length);
|
|
5118
|
+
if (after && !/^\s/.test(after)) continue;
|
|
5119
|
+
const candidateRemaining = after.trim();
|
|
5120
|
+
if (!candidateRemaining) continue;
|
|
5121
|
+
const equipmentFound = equipment.lookup(candidate.lower);
|
|
5122
|
+
if (equipmentFound.ambiguous.length > 0) {
|
|
5123
|
+
push({
|
|
5124
|
+
reason: "ambiguous-equipment",
|
|
5125
|
+
message: `"${candidate.lower}" matches multiple equipment entries.`,
|
|
5126
|
+
token: source,
|
|
5127
|
+
candidates: equipmentFound.ambiguous.map((e) => e.slug)
|
|
5128
|
+
});
|
|
5129
|
+
return {
|
|
5130
|
+
matched: "none",
|
|
5131
|
+
label: source,
|
|
5132
|
+
effort: null,
|
|
5133
|
+
effortRef: null,
|
|
5134
|
+
equipment: [],
|
|
5135
|
+
remaining: "",
|
|
5136
|
+
diagnostics
|
|
5137
|
+
};
|
|
5138
|
+
}
|
|
5139
|
+
if (!equipmentFound.entity) continue;
|
|
5140
|
+
const effortFound = resolveEffortExact(candidateRemaining);
|
|
5141
|
+
if (effortFound.entity) {
|
|
5142
|
+
matched = "split";
|
|
5143
|
+
effortEntity = effortFound.entity;
|
|
5144
|
+
const rawPrefix = source.slice(0, candidate.lower.length).trim();
|
|
5145
|
+
equipmentRefs = [{
|
|
5146
|
+
...equipmentRef(equipmentFound.entity),
|
|
5147
|
+
...rawPrefix.toLowerCase() !== equipmentFound.entity.label.toLowerCase() ? { matchedAlias: rawPrefix } : {}
|
|
5148
|
+
}];
|
|
5149
|
+
equipmentAuthored = true;
|
|
5150
|
+
remaining = candidateRemaining;
|
|
5151
|
+
break;
|
|
5152
|
+
}
|
|
5153
|
+
if (effortFound.diagnostic?.reason === "ambiguous-effort") {
|
|
5154
|
+
push(effortFound.diagnostic);
|
|
5155
|
+
return {
|
|
5156
|
+
matched: "none",
|
|
5157
|
+
label: source,
|
|
5158
|
+
effort: null,
|
|
5159
|
+
effortRef: null,
|
|
5160
|
+
equipment: [],
|
|
5161
|
+
remaining: "",
|
|
5162
|
+
diagnostics
|
|
5163
|
+
};
|
|
5164
|
+
}
|
|
5085
5165
|
}
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5166
|
+
if (matched === "none") {
|
|
5167
|
+
const found = resolveEffortExact(source);
|
|
5168
|
+
if (found.entity) {
|
|
5169
|
+
effortEntity = found.entity;
|
|
5170
|
+
matched = "whole";
|
|
5171
|
+
} else if (found.diagnostic?.reason === "ambiguous-effort") {
|
|
5172
|
+
push(found.diagnostic);
|
|
5173
|
+
return {
|
|
5174
|
+
matched: "none",
|
|
5175
|
+
label: source,
|
|
5176
|
+
effort: null,
|
|
5177
|
+
effortRef: null,
|
|
5178
|
+
equipment: [],
|
|
5179
|
+
remaining: "",
|
|
5180
|
+
diagnostics
|
|
5181
|
+
};
|
|
5182
|
+
} else if (found.diagnostic) {
|
|
5183
|
+
push(found.diagnostic);
|
|
5184
|
+
}
|
|
5101
5185
|
}
|
|
5186
|
+
fillEquipment();
|
|
5187
|
+
const effortMatchText = matched === "split" ? remaining : source;
|
|
5188
|
+
return {
|
|
5189
|
+
matched,
|
|
5190
|
+
label: source,
|
|
5191
|
+
effort: effortEntity ? materializer(effortEntity) : null,
|
|
5192
|
+
effortRef: effortEntity ? {
|
|
5193
|
+
kind: "effort",
|
|
5194
|
+
slug: effortEntity.slug,
|
|
5195
|
+
label: effortEntity.label,
|
|
5196
|
+
...effortMatchText && effortMatchText.toLowerCase() !== effortEntity.label.toLowerCase() ? { matchedAlias: effortMatchText } : {}
|
|
5197
|
+
} : null,
|
|
5198
|
+
equipment: equipmentRefs,
|
|
5199
|
+
remaining,
|
|
5200
|
+
diagnostics
|
|
5201
|
+
};
|
|
5102
5202
|
}
|
|
5103
|
-
}
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
const
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
const userEffort = {
|
|
5126
|
-
...existing,
|
|
5127
|
-
id,
|
|
5128
|
-
registrySource: "user",
|
|
5129
|
-
aliases: updatedAliases,
|
|
5130
|
-
updatedAt: now,
|
|
5131
|
-
derivation: existing.derivation ?? {
|
|
5132
|
-
parentSlug: existing.slug,
|
|
5133
|
-
coefficients: {},
|
|
5134
|
-
hardOverrides: {}
|
|
5203
|
+
};
|
|
5204
|
+
return index;
|
|
5205
|
+
}
|
|
5206
|
+
function extractStatementRefs(statements) {
|
|
5207
|
+
const efforts = [];
|
|
5208
|
+
const equipment = [];
|
|
5209
|
+
const seenEffort = /* @__PURE__ */ new Set();
|
|
5210
|
+
const seenEquipment = /* @__PURE__ */ new Set();
|
|
5211
|
+
const pushUnique = (list, seen, value) => {
|
|
5212
|
+
if (!value) return;
|
|
5213
|
+
if (seen.has(value)) return;
|
|
5214
|
+
seen.add(value);
|
|
5215
|
+
list.push(value);
|
|
5216
|
+
};
|
|
5217
|
+
for (const statement of statements ?? []) {
|
|
5218
|
+
const metrics = Array.isArray(statement.metrics) ? statement.metrics : statement.metrics?.toArray?.() ?? [];
|
|
5219
|
+
for (const metric of metrics) {
|
|
5220
|
+
const refs = wodWikiCore.getReferenceMetadata(metric.metadata)?.refs;
|
|
5221
|
+
if (!refs) continue;
|
|
5222
|
+
for (const ref of refs) {
|
|
5223
|
+
if (ref.kind === "effort") pushUnique(efforts, seenEffort, ref.slug);
|
|
5224
|
+
if (ref.kind === "equipment") pushUnique(equipment, seenEquipment, ref.slug);
|
|
5135
5225
|
}
|
|
5136
|
-
}
|
|
5137
|
-
await registry.upsert(userEffort);
|
|
5138
|
-
return userEffort;
|
|
5226
|
+
}
|
|
5139
5227
|
}
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
};
|
|
5145
|
-
await registry.upsert(updatedUserEffort);
|
|
5146
|
-
return updatedUserEffort;
|
|
5228
|
+
return { efforts, equipment };
|
|
5229
|
+
}
|
|
5230
|
+
function slugifyRefLabel(label) {
|
|
5231
|
+
return normalizeForFuzzy(label).replace(/\s+/g, "-");
|
|
5147
5232
|
}
|
|
5148
5233
|
|
|
5149
5234
|
// src/runtime/contracts/IRuntimeOptions.ts
|
|
@@ -10177,8 +10262,11 @@ function tryEnrichBlock(block, resolver, references, overwrite, debug, propertyS
|
|
|
10177
10262
|
if (normalizedBlockType !== "effort" && normalizedBlockType !== "exercise") {
|
|
10178
10263
|
return;
|
|
10179
10264
|
}
|
|
10180
|
-
const
|
|
10181
|
-
|
|
10265
|
+
const statementEffortMetric = block.getAllMemory().flatMap((loc) => loc.metrics.toArray()).find((m) => m.type === wodWikiCore.MetricType.Effort);
|
|
10266
|
+
const preResolvedMeta = wodWikiCore.getReferenceMetadata(statementEffortMetric?.metadata);
|
|
10267
|
+
const preResolvedEffortRef = preResolvedMeta?.refs?.find((r) => r.kind === "effort");
|
|
10268
|
+
const label = block.label || preResolvedMeta?.source || (typeof statementEffortMetric?.image === "string" ? statementEffortMetric.image : "");
|
|
10269
|
+
if (!label && !preResolvedEffortRef && !block.context?.exerciseId) {
|
|
10182
10270
|
if (debug) logTrace(`[EffortEnrichment] Skipping block with no label: ${block.key}`);
|
|
10183
10271
|
return;
|
|
10184
10272
|
}
|
|
@@ -10195,13 +10283,23 @@ function tryEnrichBlock(block, resolver, references, overwrite, debug, propertyS
|
|
|
10195
10283
|
let equipmentRefs = [];
|
|
10196
10284
|
let diagnostics = [];
|
|
10197
10285
|
if (references) {
|
|
10198
|
-
const
|
|
10199
|
-
|
|
10200
|
-
|
|
10201
|
-
|
|
10202
|
-
|
|
10286
|
+
const directSlug = preResolvedEffortRef?.slug || block.context?.exerciseId;
|
|
10287
|
+
const directEntity = directSlug ? references.resolveEffort(directSlug) : null;
|
|
10288
|
+
if (directEntity && preResolvedMeta) {
|
|
10289
|
+
resolved = references.materialize(directEntity);
|
|
10290
|
+
matchedKind = preResolvedMeta.matched;
|
|
10291
|
+
equipmentRefs = preResolvedMeta.refs?.filter((r) => r.kind === "equipment") ?? [];
|
|
10292
|
+
diagnostics = [...preResolvedMeta.diagnostics ?? []];
|
|
10293
|
+
} else {
|
|
10294
|
+
const resolution = references.resolve(label);
|
|
10295
|
+
resolved = resolution.effort ?? void 0;
|
|
10296
|
+
matchedKind = resolution.matched;
|
|
10297
|
+
equipmentRefs = resolution.equipment;
|
|
10298
|
+
diagnostics = resolution.diagnostics;
|
|
10299
|
+
}
|
|
10203
10300
|
} else {
|
|
10204
|
-
|
|
10301
|
+
const lookupKey = preResolvedEffortRef?.slug || block.context?.exerciseId || label;
|
|
10302
|
+
resolved = resolver.resolveEffort(lookupKey) ?? (lookupKey !== label ? resolver.resolveEffort(label) : void 0);
|
|
10205
10303
|
}
|
|
10206
10304
|
if (!resolved) {
|
|
10207
10305
|
if (diagnostics.length > 0) {
|
|
@@ -16209,6 +16307,7 @@ exports.renderMetric = renderMetric;
|
|
|
16209
16307
|
exports.resolveDominantOrigin = resolveDominantOrigin;
|
|
16210
16308
|
exports.resolveEntityRank = resolveEntityRank;
|
|
16211
16309
|
exports.resolveEntityTier = resolveEntityTier;
|
|
16310
|
+
exports.resolveFusedEffortReferences = resolveFusedEffortReferences;
|
|
16212
16311
|
exports.resolveProfileRules = resolveProfileRules;
|
|
16213
16312
|
exports.resolveSoundProfile = resolveSoundProfile;
|
|
16214
16313
|
exports.resolveSourceRank = resolveSourceRank;
|