@bitcobblers/wod-wiki-lang 0.37.2137 → 0.37.2140
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{browserRuntimeNow-CoR7dCNU.d.cts → browserRuntimeNow-M4hQ_DTl.d.cts} +205 -47
- package/dist/{browserRuntimeNow-CoR7dCNU.d.ts → browserRuntimeNow-M4hQ_DTl.d.ts} +205 -47
- package/dist/index.cjs +1083 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -10
- package/dist/index.d.ts +120 -10
- package/dist/index.mjs +1069 -34
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.cts +2 -2
- package/dist/react.d.ts +2 -2
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -418,15 +418,19 @@ function createPolicy() {
|
|
|
418
418
|
}
|
|
419
419
|
var metricPresentation = createPolicy();
|
|
420
420
|
var EffortMetric = class {
|
|
421
|
-
constructor(effort) {
|
|
421
|
+
constructor(effort, options) {
|
|
422
422
|
this.effort = effort;
|
|
423
|
-
this.value = effort;
|
|
423
|
+
this.value = options?.resolvedSlug ?? effort;
|
|
424
424
|
this.image = effort;
|
|
425
|
+
if (options?.references) {
|
|
426
|
+
this.metadata = { [wodWikiCore.REFERENCE_METADATA_KEY]: options.references };
|
|
427
|
+
}
|
|
425
428
|
}
|
|
426
429
|
effort;
|
|
427
430
|
value;
|
|
428
431
|
image;
|
|
429
432
|
origin = "parser";
|
|
433
|
+
metadata;
|
|
430
434
|
type = wodWikiCore.MetricType.Effort;
|
|
431
435
|
};
|
|
432
436
|
var DistanceMetric = class {
|
|
@@ -1394,6 +1398,27 @@ var DurationMetric = class {
|
|
|
1394
1398
|
return this.value > 0 ? "down" : "up";
|
|
1395
1399
|
}
|
|
1396
1400
|
};
|
|
1401
|
+
var EquipmentMetric = class {
|
|
1402
|
+
type = wodWikiCore.MetricType.Equipment;
|
|
1403
|
+
origin;
|
|
1404
|
+
/** Canonical slugs, ordered and deduplicated. */
|
|
1405
|
+
value;
|
|
1406
|
+
image;
|
|
1407
|
+
name = "equipment";
|
|
1408
|
+
metadata;
|
|
1409
|
+
constructor(slugs, options) {
|
|
1410
|
+
this.value = [...new Set(slugs)];
|
|
1411
|
+
this.image = options?.source ?? this.value.join(" ");
|
|
1412
|
+
this.origin = options?.origin ?? "parser";
|
|
1413
|
+
const reference = {
|
|
1414
|
+
matched: options?.matched ?? "whole",
|
|
1415
|
+
source: options?.source ?? this.image,
|
|
1416
|
+
...options?.refs ? { refs: options.refs } : {},
|
|
1417
|
+
...options?.diagnostics?.length ? { diagnostics: options.diagnostics } : {}
|
|
1418
|
+
};
|
|
1419
|
+
this.metadata = { [wodWikiCore.REFERENCE_METADATA_KEY]: reference };
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1397
1422
|
var GroupMetric = class {
|
|
1398
1423
|
constructor(group, image) {
|
|
1399
1424
|
this.group = group;
|
|
@@ -1490,16 +1515,29 @@ var PROPERTY_KEY_TO_METRIC_TYPE = {
|
|
|
1490
1515
|
volume: wodWikiCore.MetricType.Volume,
|
|
1491
1516
|
work: wodWikiCore.MetricType.Work
|
|
1492
1517
|
};
|
|
1493
|
-
|
|
1518
|
+
var STRUCTURAL_EFFORT_WORDS = /* @__PURE__ */ new Set(["rest", "amrap", "emom", "tabata", "for time"]);
|
|
1519
|
+
var STANDARD_UNITS2 = UnitRegistry.standard();
|
|
1520
|
+
var REFERENCE_PAIR_KEYS = /* @__PURE__ */ new Set(["effort", "equipment"]);
|
|
1521
|
+
function referenceMetadata(resolution) {
|
|
1522
|
+
return {
|
|
1523
|
+
matched: resolution.matched,
|
|
1524
|
+
source: resolution.label,
|
|
1525
|
+
...resolution.effortRef || resolution.equipment.length > 0 ? { refs: [...resolution.effortRef ? [resolution.effortRef] : [], ...resolution.equipment] } : {},
|
|
1526
|
+
...resolution.remaining ? { remaining: resolution.remaining } : {},
|
|
1527
|
+
...resolution.diagnostics.length > 0 ? { diagnostics: resolution.diagnostics } : {}
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
function classifyStatements(facts, references) {
|
|
1494
1531
|
return facts.statements.map((fact) => {
|
|
1495
1532
|
const statement = new wodWikiCore.ParsedCodeStatement();
|
|
1496
|
-
|
|
1533
|
+
let metricPairs = [];
|
|
1497
1534
|
for (const primitive of fact.primitives) {
|
|
1498
1535
|
metricPairs.push(...classifyPrimitive(primitive));
|
|
1499
1536
|
}
|
|
1500
1537
|
const mergedPairs = mergeFragments(metricPairs);
|
|
1501
|
-
|
|
1502
|
-
statement.
|
|
1538
|
+
const finalPairs = references ? applyReferenceResolution(mergedPairs, fact, references) : mergedPairs;
|
|
1539
|
+
statement.metrics = finalPairs.map((pair) => pair.metrics);
|
|
1540
|
+
statement.metricMeta = new Map(finalPairs.map((pair) => [pair.metrics, pair.meta]));
|
|
1503
1541
|
statement.id = fact.id;
|
|
1504
1542
|
statement.line = fact.line;
|
|
1505
1543
|
statement.text = fact.meta.raw;
|
|
@@ -1511,6 +1549,105 @@ function classifyStatements(facts) {
|
|
|
1511
1549
|
return statement;
|
|
1512
1550
|
});
|
|
1513
1551
|
}
|
|
1552
|
+
function applyReferenceResolution(pairs, fact, references) {
|
|
1553
|
+
const { index, defaults } = references;
|
|
1554
|
+
const result = [];
|
|
1555
|
+
const emitResolution = (resolution, authored, meta2) => {
|
|
1556
|
+
if (resolution.effortRef || resolution.matched !== "none" || resolution.diagnostics.length > 0) {
|
|
1557
|
+
result.push({
|
|
1558
|
+
metrics: new EffortMetric(authored, {
|
|
1559
|
+
resolvedSlug: resolution.effortRef?.slug,
|
|
1560
|
+
references: referenceMetadata(resolution)
|
|
1561
|
+
}),
|
|
1562
|
+
meta: meta2
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
if (resolution.equipment.length > 0) {
|
|
1566
|
+
result.push({
|
|
1567
|
+
metrics: new EquipmentMetric(
|
|
1568
|
+
resolution.equipment.map((ref) => ref.slug),
|
|
1569
|
+
{
|
|
1570
|
+
refs: resolution.equipment,
|
|
1571
|
+
source: authored,
|
|
1572
|
+
matched: resolution.matched,
|
|
1573
|
+
diagnostics: resolution.diagnostics
|
|
1574
|
+
}
|
|
1575
|
+
),
|
|
1576
|
+
meta: meta2
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
const handledRawObjects = /* @__PURE__ */ new Set();
|
|
1581
|
+
const malformedHints = [];
|
|
1582
|
+
for (const primitive of fact.primitives) {
|
|
1583
|
+
if (primitive.kind !== "metric_object") continue;
|
|
1584
|
+
if (handledRawObjects.has(primitive.raw)) continue;
|
|
1585
|
+
handledRawObjects.add(primitive.raw);
|
|
1586
|
+
if (primitive.pairs.length === 0) {
|
|
1587
|
+
malformedHints.push({
|
|
1588
|
+
metrics: {
|
|
1589
|
+
...hintMetric("reference.malformed_json", "parser"),
|
|
1590
|
+
metadata: referenceMetadata(index.resolve(primitive.raw))
|
|
1591
|
+
},
|
|
1592
|
+
meta: primitive.meta
|
|
1593
|
+
});
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
const hasReferenceKeys = primitive.pairs.some((pair) => REFERENCE_PAIR_KEYS.has(pair.key.toLowerCase()));
|
|
1597
|
+
if (!hasReferenceKeys) continue;
|
|
1598
|
+
const resolution = index.resolve(primitive.raw);
|
|
1599
|
+
if (resolution.matched === "explicit") {
|
|
1600
|
+
emitResolution(resolution, primitive.raw, primitive.meta);
|
|
1601
|
+
}
|
|
1602
|
+
for (const pair of primitive.pairs) {
|
|
1603
|
+
if (REFERENCE_PAIR_KEYS.has(pair.key.toLowerCase())) continue;
|
|
1604
|
+
const metricType = PROPERTY_KEY_TO_METRIC_TYPE[pair.key.toLowerCase()] ?? wodWikiCore.MetricType.Custom;
|
|
1605
|
+
const rawValue = Array.isArray(pair.value) ? pair.value.join(", ") : pair.value;
|
|
1606
|
+
result.push({
|
|
1607
|
+
metrics: new PropertyMetric(pair.key, rawValue, { type: metricType, image: `${pair.key}: ${rawValue}` }),
|
|
1608
|
+
meta: primitive.meta
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
let sawEffortMetric = false;
|
|
1613
|
+
let sawDurationMetric = false;
|
|
1614
|
+
let sawMovementQuantity = false;
|
|
1615
|
+
let statementMeta;
|
|
1616
|
+
for (const pair of pairs) {
|
|
1617
|
+
const metric = pair.metrics;
|
|
1618
|
+
if (metric.type === wodWikiCore.MetricType.Custom && handledRawObjects.has(pair.meta.raw)) continue;
|
|
1619
|
+
if (metric.type === wodWikiCore.MetricType.Effort) {
|
|
1620
|
+
const authored = metric instanceof EffortMetric ? metric.effort : String(metric.value ?? "");
|
|
1621
|
+
const normalized = authored.trim().toLowerCase();
|
|
1622
|
+
if (STRUCTURAL_EFFORT_WORDS.has(normalized) || STANDARD_UNITS2.has(normalized)) {
|
|
1623
|
+
result.push(pair);
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
sawEffortMetric = true;
|
|
1627
|
+
const resolution = index.resolve(authored, defaults);
|
|
1628
|
+
if (resolution.matched === "none" && resolution.diagnostics.length === 0) {
|
|
1629
|
+
result.push(pair);
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
emitResolution(resolution, authored, pair.meta);
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
if (metric.type === wodWikiCore.MetricType.Duration) sawDurationMetric = true;
|
|
1636
|
+
if (metric.type === wodWikiCore.MetricType.Rep || metric.type === wodWikiCore.MetricType.Rounds || metric.type === wodWikiCore.MetricType.Resistance || metric.type === wodWikiCore.MetricType.Distance) {
|
|
1637
|
+
sawMovementQuantity = true;
|
|
1638
|
+
}
|
|
1639
|
+
result.push(pair);
|
|
1640
|
+
statementMeta ??= pair.meta;
|
|
1641
|
+
}
|
|
1642
|
+
if (defaults && !sawEffortMetric && fact.isLeaf && sawMovementQuantity && !sawDurationMetric) {
|
|
1643
|
+
const resolution = index.resolve("", defaults);
|
|
1644
|
+
if (resolution.effortRef) {
|
|
1645
|
+
emitResolution(resolution, resolution.effortRef.label, statementMeta ?? fact.meta);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
result.push(...malformedHints);
|
|
1649
|
+
return result;
|
|
1650
|
+
}
|
|
1514
1651
|
function classifyPrimitive(primitive) {
|
|
1515
1652
|
switch (primitive.kind) {
|
|
1516
1653
|
case "lap":
|
|
@@ -1579,10 +1716,11 @@ function classifyPrimitive(primitive) {
|
|
|
1579
1716
|
const pairs = primitive.pairs;
|
|
1580
1717
|
return pairs.map((pair) => {
|
|
1581
1718
|
const metricType = PROPERTY_KEY_TO_METRIC_TYPE[pair.key.toLowerCase()] ?? wodWikiCore.MetricType.Custom;
|
|
1719
|
+
const rawValue = Array.isArray(pair.value) ? pair.value.join(", ") : pair.value;
|
|
1582
1720
|
return {
|
|
1583
|
-
metrics: new PropertyMetric(pair.key,
|
|
1721
|
+
metrics: new PropertyMetric(pair.key, rawValue, {
|
|
1584
1722
|
type: metricType,
|
|
1585
|
-
image: `${pair.key}: ${
|
|
1723
|
+
image: `${pair.key}: ${rawValue}`
|
|
1586
1724
|
}),
|
|
1587
1725
|
meta: primitive.meta
|
|
1588
1726
|
};
|
|
@@ -1620,6 +1758,23 @@ function mergeFragments(pairs) {
|
|
|
1620
1758
|
continue;
|
|
1621
1759
|
}
|
|
1622
1760
|
}
|
|
1761
|
+
if (current.metrics instanceof RepMetric && next.metrics instanceof EffortMetric && /^(reps?)$/i.test(next.metrics.effort)) {
|
|
1762
|
+
const gap = next.meta.startOffset - current.meta.endOffset;
|
|
1763
|
+
if (gap <= 1) {
|
|
1764
|
+
const mergedMeta = {
|
|
1765
|
+
...current.meta,
|
|
1766
|
+
endOffset: next.meta.endOffset,
|
|
1767
|
+
columnEnd: next.meta.columnEnd,
|
|
1768
|
+
length: next.meta.endOffset - current.meta.startOffset,
|
|
1769
|
+
raw: current.meta.raw + (gap === 1 ? " " : "") + next.meta.raw
|
|
1770
|
+
};
|
|
1771
|
+
current = {
|
|
1772
|
+
metrics: current.metrics,
|
|
1773
|
+
meta: mergedMeta
|
|
1774
|
+
};
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1623
1778
|
if (current.metrics instanceof RepMetric && (next.metrics instanceof ResistanceMetric || next.metrics instanceof DistanceMetric)) {
|
|
1624
1779
|
const gap = next.meta.startOffset - current.meta.endOffset;
|
|
1625
1780
|
if (gap <= 1) {
|
|
@@ -1995,12 +2150,12 @@ function resolveSourceInput(sourceOrTree, treeArg) {
|
|
|
1995
2150
|
}
|
|
1996
2151
|
return { source: "" };
|
|
1997
2152
|
}
|
|
1998
|
-
function extractStatementsRaw(sourceOrTree, treeArg) {
|
|
2153
|
+
function extractStatementsRaw(sourceOrTree, treeArg, references) {
|
|
1999
2154
|
const { source, tree } = resolveSourceInput(sourceOrTree, treeArg);
|
|
2000
2155
|
const facts = extractSyntaxFacts(source, tree);
|
|
2001
|
-
return normalizeInlineRounds(classifyStatements(facts));
|
|
2156
|
+
return normalizeInlineRounds(classifyStatements(facts, references));
|
|
2002
2157
|
}
|
|
2003
|
-
function extractStatements(sourceOrTree, sportOrTree, sportArg) {
|
|
2158
|
+
function extractStatements(sourceOrTree, sportOrTree, sportArg, references) {
|
|
2004
2159
|
let sport;
|
|
2005
2160
|
let tree;
|
|
2006
2161
|
if (typeof sportOrTree === "string") {
|
|
@@ -2011,7 +2166,7 @@ function extractStatements(sourceOrTree, sportOrTree, sportArg) {
|
|
|
2011
2166
|
}
|
|
2012
2167
|
const resolved = resolveSourceInput(sourceOrTree, tree);
|
|
2013
2168
|
const facts = extractSyntaxFacts(resolved.source, resolved.tree);
|
|
2014
|
-
const statements = classifyStatements(facts);
|
|
2169
|
+
const statements = classifyStatements(facts, references);
|
|
2015
2170
|
dialectStack.processAll(statements, sport);
|
|
2016
2171
|
return normalizeInlineRounds(statements);
|
|
2017
2172
|
}
|
|
@@ -2143,7 +2298,8 @@ function parseScript(inputText, options = {}) {
|
|
|
2143
2298
|
const doc = inputText.endsWith("\n") ? inputText : inputText + "\n";
|
|
2144
2299
|
const tree = parser.parse(doc);
|
|
2145
2300
|
const sport = options.sport ?? options.dialect;
|
|
2146
|
-
const
|
|
2301
|
+
const references = options.references ? { index: options.references, defaults: options.defaults } : void 0;
|
|
2302
|
+
const statements = options.withoutDialects ? extractStatementsRaw(inputText, tree, references) : extractStatements(inputText, tree, sport, references);
|
|
2147
2303
|
return new WhiteboardScript(inputText, statements, []);
|
|
2148
2304
|
} catch (error) {
|
|
2149
2305
|
const msg = error instanceof Error ? error.message : "Unknown parse error";
|
|
@@ -2153,6 +2309,10 @@ function parseScript(inputText, options = {}) {
|
|
|
2153
2309
|
|
|
2154
2310
|
// src/parser/md-timer.ts
|
|
2155
2311
|
var MdTimerRuntime = class {
|
|
2312
|
+
config;
|
|
2313
|
+
constructor(config) {
|
|
2314
|
+
this.config = config;
|
|
2315
|
+
}
|
|
2156
2316
|
/**
|
|
2157
2317
|
* Parse WhiteboardScript source into a {@link WhiteboardScript}, running the
|
|
2158
2318
|
* Dialect Stack on every statement.
|
|
@@ -2160,22 +2320,21 @@ var MdTimerRuntime = class {
|
|
|
2160
2320
|
* @param sport - The block's `:sport` fence suffix (` ```log:climbing `).
|
|
2161
2321
|
*/
|
|
2162
2322
|
read(inputText, sport) {
|
|
2163
|
-
return parseScript(inputText, { sport });
|
|
2323
|
+
return parseScript(inputText, { sport, ...this.config });
|
|
2164
2324
|
}
|
|
2165
2325
|
/**
|
|
2166
2326
|
* Parse without running the Dialect Stack. Used by the parser test harness
|
|
2167
2327
|
* which applies its own Dialect set. Production consumers should use {@link read}.
|
|
2168
2328
|
*/
|
|
2169
2329
|
readWithoutDialects(inputText) {
|
|
2170
|
-
return parseScript(inputText, { withoutDialects: true });
|
|
2330
|
+
return parseScript(inputText, { withoutDialects: true, ...this.config });
|
|
2171
2331
|
}
|
|
2172
2332
|
};
|
|
2173
2333
|
|
|
2174
2334
|
// src/parser/parserInstance.ts
|
|
2175
|
-
function createParser() {
|
|
2176
|
-
return new MdTimerRuntime();
|
|
2335
|
+
function createParser(config) {
|
|
2336
|
+
return new MdTimerRuntime(config);
|
|
2177
2337
|
}
|
|
2178
|
-
var sharedParser = /* @__PURE__ */ createParser();
|
|
2179
2338
|
var nextTagID = 0;
|
|
2180
2339
|
var Tag = class _Tag {
|
|
2181
2340
|
/**
|
|
@@ -2839,6 +2998,42 @@ function whiteboardScript() {
|
|
|
2839
2998
|
|
|
2840
2999
|
// src/effort-registry/types.ts
|
|
2841
3000
|
var EFFORT_REGISTRY_ORIGINS = ["bundled", "user"];
|
|
3001
|
+
var CATALOG_PRECEDENCE_RANK = {
|
|
3002
|
+
core: 0,
|
|
3003
|
+
community: 1,
|
|
3004
|
+
imported: 2,
|
|
3005
|
+
user: 3
|
|
3006
|
+
};
|
|
3007
|
+
function resolveSourceTier(sourceIdOrMetaKey, explicitTier) {
|
|
3008
|
+
if (explicitTier) return explicitTier;
|
|
3009
|
+
if (!sourceIdOrMetaKey) return "core";
|
|
3010
|
+
if (sourceIdOrMetaKey === "user") return "user";
|
|
3011
|
+
const id = sourceIdOrMetaKey.startsWith("seed:") ? sourceIdOrMetaKey.slice(5) : sourceIdOrMetaKey;
|
|
3012
|
+
if (id === "core" || id === "efforts" || id === "equipment") return "core";
|
|
3013
|
+
if (id === "community" || id.startsWith("community-") || id === "common") return "community";
|
|
3014
|
+
return "imported";
|
|
3015
|
+
}
|
|
3016
|
+
function resolveSourceRank(sourceIdOrMetaKey, explicitTier) {
|
|
3017
|
+
return CATALOG_PRECEDENCE_RANK[resolveSourceTier(sourceIdOrMetaKey, explicitTier)];
|
|
3018
|
+
}
|
|
3019
|
+
function resolveEntityTier(entity) {
|
|
3020
|
+
if (entity.registrySource === "user") return "user";
|
|
3021
|
+
return resolveSourceTier(entity.sourceId);
|
|
3022
|
+
}
|
|
3023
|
+
function resolveEntityRank(entity) {
|
|
3024
|
+
return CATALOG_PRECEDENCE_RANK[resolveEntityTier(entity)];
|
|
3025
|
+
}
|
|
3026
|
+
function dedupeEntitiesByPrecedence(entities) {
|
|
3027
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
3028
|
+
for (const entity of entities) {
|
|
3029
|
+
const key = entity.slug.toLowerCase();
|
|
3030
|
+
const prior = bySlug.get(key);
|
|
3031
|
+
if (!prior || resolveEntityRank(entity) >= resolveEntityRank(prior)) {
|
|
3032
|
+
bySlug.set(key, entity);
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
return Array.from(bySlug.values());
|
|
3036
|
+
}
|
|
2842
3037
|
var INTENSITY_TIERS = ["low", "moderate", "high"];
|
|
2843
3038
|
var DEFAULT_RESOLVER_OPTIONS = {
|
|
2844
3039
|
defaultMet: 5,
|
|
@@ -2878,6 +3073,426 @@ function disciplineFactorFor(discipline) {
|
|
|
2878
3073
|
const key = discipline.toLowerCase();
|
|
2879
3074
|
return isEffortDiscipline(key) ? DISCIPLINE_FACTORS[key] : 1;
|
|
2880
3075
|
}
|
|
3076
|
+
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
3077
|
+
var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
3078
|
+
function unquote(value) {
|
|
3079
|
+
return value.replace(
|
|
3080
|
+
/^(['"])(.*)\1$/,
|
|
3081
|
+
(_match, quote, inner) => quote === '"' ? inner.replace(
|
|
3082
|
+
/\\(["\\nrt])/g,
|
|
3083
|
+
(_all, c) => c === "n" ? "\n" : c === "r" ? "\r" : c === "t" ? " " : c
|
|
3084
|
+
) : inner
|
|
3085
|
+
);
|
|
3086
|
+
}
|
|
3087
|
+
function quoteYaml(val) {
|
|
3088
|
+
if (!val) return '""';
|
|
3089
|
+
if (/[:"'\n#{}[\],&*?|\-<>=%!@`]/.test(val) || val !== val.trim()) {
|
|
3090
|
+
return `"${val.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
|
|
3091
|
+
}
|
|
3092
|
+
return val;
|
|
3093
|
+
}
|
|
3094
|
+
function parseFrontmatter(raw) {
|
|
3095
|
+
const match = raw.match(FRONTMATTER_RE);
|
|
3096
|
+
if (!match) return { meta: {}, body: raw };
|
|
3097
|
+
const lines = match[1].split(/\r?\n/);
|
|
3098
|
+
const meta2 = {};
|
|
3099
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3100
|
+
const keyMatch = lines[i].match(/^([A-Za-z][\w.-]*)\s*:(.*)$/);
|
|
3101
|
+
if (!keyMatch) continue;
|
|
3102
|
+
const key = keyMatch[1];
|
|
3103
|
+
const rawVal = keyMatch[2].trim();
|
|
3104
|
+
if (rawVal === "") {
|
|
3105
|
+
const items = [];
|
|
3106
|
+
while (i + 1 < lines.length) {
|
|
3107
|
+
const itemMatch = lines[i + 1].match(/^\s+-\s+(.+)$/);
|
|
3108
|
+
if (!itemMatch) break;
|
|
3109
|
+
items.push(unquote(itemMatch[1].trim()));
|
|
3110
|
+
i++;
|
|
3111
|
+
}
|
|
3112
|
+
meta2[key] = items.length > 0 ? items : "";
|
|
3113
|
+
} else {
|
|
3114
|
+
const wasQuoted = /^(['"])(.*)\1$/.test(rawVal);
|
|
3115
|
+
const value = unquote(rawVal);
|
|
3116
|
+
const num2 = Number(value);
|
|
3117
|
+
meta2[key] = !wasQuoted && value !== "" && !isNaN(num2) ? num2 : value;
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
return { meta: meta2, body: raw.slice(match[0].length) };
|
|
3121
|
+
}
|
|
3122
|
+
function getScalar(meta2, key) {
|
|
3123
|
+
const value = meta2[key];
|
|
3124
|
+
return typeof value === "string" || typeof value === "number" ? value : void 0;
|
|
3125
|
+
}
|
|
3126
|
+
function getListStrict(meta2, key) {
|
|
3127
|
+
const value = meta2[key];
|
|
3128
|
+
if (value === void 0) return [];
|
|
3129
|
+
if (typeof value === "string" && value === "[]") return [];
|
|
3130
|
+
return Array.isArray(value) ? value : void 0;
|
|
3131
|
+
}
|
|
3132
|
+
function coerceNestedScalar(raw) {
|
|
3133
|
+
if (raw === "true") return true;
|
|
3134
|
+
if (raw === "false") return false;
|
|
3135
|
+
if (raw !== "" && !Number.isNaN(Number(raw))) return Number(raw);
|
|
3136
|
+
return unquote(raw);
|
|
3137
|
+
}
|
|
3138
|
+
function readNestedSection(lines, key) {
|
|
3139
|
+
const start = lines.findIndex((l) => new RegExp(`^${key}:\\s*$`).test(l));
|
|
3140
|
+
if (start === -1) return void 0;
|
|
3141
|
+
const result = {};
|
|
3142
|
+
let subKey = null;
|
|
3143
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
3144
|
+
const line = lines[i];
|
|
3145
|
+
if (/^\S/.test(line)) break;
|
|
3146
|
+
const trimmed = line.trim();
|
|
3147
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
3148
|
+
const indent = line.length - line.trimStart().length;
|
|
3149
|
+
const m = trimmed.match(/^([\w.-]+):\s*(.*)$/);
|
|
3150
|
+
if (!m) continue;
|
|
3151
|
+
const [, k, rawVal] = m;
|
|
3152
|
+
if (indent === 2) {
|
|
3153
|
+
if (rawVal === "") {
|
|
3154
|
+
subKey = k;
|
|
3155
|
+
result[k] = {};
|
|
3156
|
+
} else {
|
|
3157
|
+
subKey = null;
|
|
3158
|
+
result[k] = coerceNestedScalar(rawVal);
|
|
3159
|
+
}
|
|
3160
|
+
} else if (indent >= 4 && subKey !== null) {
|
|
3161
|
+
result[subKey][k] = coerceNestedScalar(rawVal);
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
3165
|
+
}
|
|
3166
|
+
function parseDiscipline(val) {
|
|
3167
|
+
if (!val) return void 0;
|
|
3168
|
+
const normalized = unquote(val.trim()).toLowerCase();
|
|
3169
|
+
if (isEffortDiscipline(normalized)) return normalized;
|
|
3170
|
+
console.warn(`[definition-markdown] unknown discipline '${val}' \u2014 expected one of the canonical values; treating as unset`);
|
|
3171
|
+
return void 0;
|
|
3172
|
+
}
|
|
3173
|
+
function identityProblems(slug, label) {
|
|
3174
|
+
const problems = [];
|
|
3175
|
+
const slugText = slug === void 0 ? "" : String(slug);
|
|
3176
|
+
if (!slugText) problems.push("Missing required field: slug");
|
|
3177
|
+
else if (!SLUG_RE.test(slugText)) problems.push("Invalid slug: must be lowercase letters, numbers, and hyphens only");
|
|
3178
|
+
if (label === void 0 || !String(label).trim()) problems.push("Missing required field: label");
|
|
3179
|
+
return problems;
|
|
3180
|
+
}
|
|
3181
|
+
function stringListProblems(values, kind) {
|
|
3182
|
+
const problems = [];
|
|
3183
|
+
for (const value of values) {
|
|
3184
|
+
if (!value.trim()) problems.push(`Invalid ${kind}: blank entries are not allowed`);
|
|
3185
|
+
else if (kind === "equipment" && !SLUG_RE.test(value)) {
|
|
3186
|
+
problems.push(`Invalid ${kind}: '${value}' must be a canonical slug (lowercase letters, numbers, hyphens)`);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
const duplicate = values.find((v, i) => values.indexOf(v) !== i);
|
|
3190
|
+
if (duplicate !== void 0) problems.push(`Invalid ${kind}: duplicate entry '${duplicate}'`);
|
|
3191
|
+
return problems;
|
|
3192
|
+
}
|
|
3193
|
+
function metProblem(value) {
|
|
3194
|
+
if (value === void 0) return "Missing required field: baseAttributes.met";
|
|
3195
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
3196
|
+
return "Invalid baseAttributes.met: must be a positive number";
|
|
3197
|
+
}
|
|
3198
|
+
return null;
|
|
3199
|
+
}
|
|
3200
|
+
function parseEffortFile(raw) {
|
|
3201
|
+
const blockMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
3202
|
+
if (!blockMatch) return null;
|
|
3203
|
+
const innerLines = blockMatch[1].split(/\r?\n/);
|
|
3204
|
+
const { meta: meta2, body } = parseFrontmatter(raw);
|
|
3205
|
+
const type = getScalar(meta2, "type");
|
|
3206
|
+
if (type !== void 0 && type !== "effort") return null;
|
|
3207
|
+
const id = getScalar(meta2, "id");
|
|
3208
|
+
const slug = getScalar(meta2, "slug");
|
|
3209
|
+
const label = getScalar(meta2, "label");
|
|
3210
|
+
const aliases = getListStrict(meta2, "aliases");
|
|
3211
|
+
const equipment = getListStrict(meta2, "equipment");
|
|
3212
|
+
if (aliases === void 0 || equipment === void 0) return null;
|
|
3213
|
+
if (identityProblems(slug, label).length > 0) return null;
|
|
3214
|
+
if (stringListProblems(aliases, "aliases").length > 0) return null;
|
|
3215
|
+
if (equipment.length > 0 && stringListProblems(equipment, "equipment").length > 0) return null;
|
|
3216
|
+
const baseAttrs = readNestedSection(innerLines, "baseAttributes") ?? {};
|
|
3217
|
+
if (metProblem(baseAttrs.met) !== null) return null;
|
|
3218
|
+
const met = baseAttrs.met;
|
|
3219
|
+
const discipline = parseDiscipline(
|
|
3220
|
+
typeof baseAttrs.discipline === "string" ? baseAttrs.discipline : void 0
|
|
3221
|
+
);
|
|
3222
|
+
const intensityTier = baseAttrs.intensityTier;
|
|
3223
|
+
const disciplineFactor = typeof baseAttrs.disciplineFactor === "number" && Number.isFinite(baseAttrs.disciplineFactor) ? baseAttrs.disciplineFactor : void 0;
|
|
3224
|
+
const hints = readNestedSection(innerLines, "hints");
|
|
3225
|
+
const derivation = readNestedSection(innerLines, "derivation");
|
|
3226
|
+
const trimmedBody = body.trim();
|
|
3227
|
+
return {
|
|
3228
|
+
id: String(id ?? ""),
|
|
3229
|
+
slug: String(slug),
|
|
3230
|
+
label: String(label),
|
|
3231
|
+
aliases,
|
|
3232
|
+
baseAttributes: {
|
|
3233
|
+
met,
|
|
3234
|
+
...discipline ? { discipline } : {},
|
|
3235
|
+
...disciplineFactor !== void 0 ? { disciplineFactor } : {},
|
|
3236
|
+
...intensityTier ? { intensityTier } : {}
|
|
3237
|
+
},
|
|
3238
|
+
registrySource: "bundled",
|
|
3239
|
+
...equipment.length > 0 ? { equipment } : {},
|
|
3240
|
+
...trimmedBody ? { body: trimmedBody } : {},
|
|
3241
|
+
...hints ? { hints } : {},
|
|
3242
|
+
...derivation ? { derivation } : {}
|
|
3243
|
+
};
|
|
3244
|
+
}
|
|
3245
|
+
function parseEquipmentFile(raw) {
|
|
3246
|
+
const { meta: meta2, body } = parseFrontmatter(raw);
|
|
3247
|
+
if (getScalar(meta2, "type") !== "equipment") return null;
|
|
3248
|
+
const id = getScalar(meta2, "id");
|
|
3249
|
+
const slug = getScalar(meta2, "slug");
|
|
3250
|
+
const label = getScalar(meta2, "label");
|
|
3251
|
+
const aliases = getListStrict(meta2, "aliases");
|
|
3252
|
+
if (id === void 0 || aliases === void 0) return null;
|
|
3253
|
+
if (identityProblems(slug, label).length > 0) return null;
|
|
3254
|
+
if (stringListProblems(aliases, "aliases").length > 0) return null;
|
|
3255
|
+
const trimmedBody = body.trim();
|
|
3256
|
+
return {
|
|
3257
|
+
id: String(id),
|
|
3258
|
+
slug: String(slug),
|
|
3259
|
+
label: String(label),
|
|
3260
|
+
aliases,
|
|
3261
|
+
registrySource: "bundled",
|
|
3262
|
+
...trimmedBody ? { body: trimmedBody } : {}
|
|
3263
|
+
};
|
|
3264
|
+
}
|
|
3265
|
+
function createDefaultEffort() {
|
|
3266
|
+
return {
|
|
3267
|
+
id: `effort-user-${uuid.v7()}`,
|
|
3268
|
+
slug: "",
|
|
3269
|
+
label: "",
|
|
3270
|
+
aliases: [],
|
|
3271
|
+
baseAttributes: { met: 5 },
|
|
3272
|
+
registrySource: "user"
|
|
3273
|
+
};
|
|
3274
|
+
}
|
|
3275
|
+
function documentToEffort(doc, baseEffort) {
|
|
3276
|
+
const errors = [];
|
|
3277
|
+
const blockMatch = doc.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
3278
|
+
if (!blockMatch) {
|
|
3279
|
+
errors.push("Missing YAML frontmatter delimiters (---)");
|
|
3280
|
+
return { effort: baseEffort ?? createDefaultEffort(), errors };
|
|
3281
|
+
}
|
|
3282
|
+
const innerLines = blockMatch[1].split(/\r?\n/);
|
|
3283
|
+
const { meta: meta2, body } = parseFrontmatter(doc);
|
|
3284
|
+
const bodyText = body.replace(/^\r?\n+/, "") || void 0;
|
|
3285
|
+
const id = getScalar(meta2, "id");
|
|
3286
|
+
const slug = getScalar(meta2, "slug");
|
|
3287
|
+
const label = getScalar(meta2, "label");
|
|
3288
|
+
const registrySource = getScalar(meta2, "registrySource");
|
|
3289
|
+
const createdAt = getScalar(meta2, "createdAt");
|
|
3290
|
+
const updatedAt = getScalar(meta2, "updatedAt");
|
|
3291
|
+
const aliases = getListStrict(meta2, "aliases");
|
|
3292
|
+
const equipmentRaw = meta2["equipment"];
|
|
3293
|
+
const equipment = getListStrict(meta2, "equipment");
|
|
3294
|
+
if (aliases === void 0) {
|
|
3295
|
+
errors.push("Invalid aliases: must be a list (`aliases:` followed by `- item` lines)");
|
|
3296
|
+
}
|
|
3297
|
+
if (equipment === void 0) {
|
|
3298
|
+
errors.push("Invalid equipment: must be a list (`equipment:` followed by `- slug` lines)");
|
|
3299
|
+
}
|
|
3300
|
+
const baseAttrs = readNestedSection(innerLines, "baseAttributes") ?? {};
|
|
3301
|
+
const baseAttributes = {};
|
|
3302
|
+
const metRaw = baseAttrs.met;
|
|
3303
|
+
if (metRaw === void 0) {
|
|
3304
|
+
const fallbackMet = baseEffort?.baseAttributes.met;
|
|
3305
|
+
const fallbackProblem = fallbackMet === void 0 ? null : metProblem(fallbackMet);
|
|
3306
|
+
if (fallbackProblem) errors.push(fallbackProblem);
|
|
3307
|
+
else if (fallbackMet !== void 0) baseAttributes.met = fallbackMet;
|
|
3308
|
+
else errors.push("Missing required field: baseAttributes.met");
|
|
3309
|
+
} else {
|
|
3310
|
+
const problem = metProblem(metRaw);
|
|
3311
|
+
if (problem) errors.push(problem);
|
|
3312
|
+
else baseAttributes.met = Number(metRaw);
|
|
3313
|
+
}
|
|
3314
|
+
if (getScalar(meta2, "met") !== void 0) {
|
|
3315
|
+
errors.push(
|
|
3316
|
+
"Invalid met placement: 'met' must be nested under baseAttributes (missing baseAttributes.met)"
|
|
3317
|
+
);
|
|
3318
|
+
}
|
|
3319
|
+
const discipline = parseDiscipline(
|
|
3320
|
+
typeof baseAttrs.discipline === "string" ? baseAttrs.discipline : void 0
|
|
3321
|
+
);
|
|
3322
|
+
if (discipline) baseAttributes.discipline = discipline;
|
|
3323
|
+
if (typeof baseAttrs.disciplineFactor === "number" && Number.isFinite(baseAttrs.disciplineFactor)) {
|
|
3324
|
+
baseAttributes.disciplineFactor = baseAttrs.disciplineFactor;
|
|
3325
|
+
}
|
|
3326
|
+
if (baseAttrs.intensityTier) baseAttributes.intensityTier = baseAttrs.intensityTier;
|
|
3327
|
+
const hints = readNestedSection(innerLines, "hints") ?? baseEffort?.hints;
|
|
3328
|
+
const derivation = readNestedSection(innerLines, "derivation");
|
|
3329
|
+
errors.push(...identityProblems(slug, label));
|
|
3330
|
+
if (aliases !== void 0) errors.push(...stringListProblems(aliases, "aliases"));
|
|
3331
|
+
if (equipment !== void 0 && equipment.length > 0) {
|
|
3332
|
+
errors.push(...stringListProblems(equipment, "equipment"));
|
|
3333
|
+
}
|
|
3334
|
+
const resolvedEquipment = equipmentRaw !== void 0 ? equipment ?? [] : baseEffort?.equipment?.length ? baseEffort.equipment : void 0;
|
|
3335
|
+
const effort = {
|
|
3336
|
+
id: String(id ?? "") || baseEffort?.id || `effort-user-${uuid.v7()}`,
|
|
3337
|
+
slug: String(slug ?? ""),
|
|
3338
|
+
label: String(label ?? ""),
|
|
3339
|
+
aliases: aliases ?? [],
|
|
3340
|
+
baseAttributes,
|
|
3341
|
+
registrySource: registrySource || baseEffort?.registrySource || "user",
|
|
3342
|
+
...resolvedEquipment ? { equipment: resolvedEquipment } : {},
|
|
3343
|
+
...createdAt ? { createdAt: String(createdAt) } : {},
|
|
3344
|
+
...updatedAt ? { updatedAt: String(updatedAt) } : {},
|
|
3345
|
+
...bodyText ? { body: bodyText } : {},
|
|
3346
|
+
...hints && Object.keys(hints).length > 0 ? { hints } : {},
|
|
3347
|
+
...derivation ? { derivation } : {}
|
|
3348
|
+
};
|
|
3349
|
+
return { effort, errors };
|
|
3350
|
+
}
|
|
3351
|
+
function effortToDocument(effort) {
|
|
3352
|
+
const lines = ["---"];
|
|
3353
|
+
lines.push(`id: ${effort.id}`);
|
|
3354
|
+
lines.push(`slug: ${effort.slug}`);
|
|
3355
|
+
lines.push(`label: ${quoteYaml(effort.label)}`);
|
|
3356
|
+
if (effort.aliases.length > 0) {
|
|
3357
|
+
lines.push("aliases:");
|
|
3358
|
+
for (const alias of effort.aliases) {
|
|
3359
|
+
lines.push(` - ${quoteYaml(alias)}`);
|
|
3360
|
+
}
|
|
3361
|
+
} else {
|
|
3362
|
+
lines.push("aliases: []");
|
|
3363
|
+
}
|
|
3364
|
+
lines.push("baseAttributes:");
|
|
3365
|
+
lines.push(` met: ${effort.baseAttributes.met}`);
|
|
3366
|
+
if (effort.baseAttributes.discipline) {
|
|
3367
|
+
lines.push(` discipline: ${effort.baseAttributes.discipline}`);
|
|
3368
|
+
}
|
|
3369
|
+
if (effort.baseAttributes.disciplineFactor !== void 0) {
|
|
3370
|
+
lines.push(` disciplineFactor: ${effort.baseAttributes.disciplineFactor}`);
|
|
3371
|
+
}
|
|
3372
|
+
if (effort.baseAttributes.intensityTier) {
|
|
3373
|
+
lines.push(` intensityTier: ${effort.baseAttributes.intensityTier}`);
|
|
3374
|
+
}
|
|
3375
|
+
lines.push(`registrySource: ${effort.registrySource}`);
|
|
3376
|
+
if (effort.equipment && effort.equipment.length > 0) {
|
|
3377
|
+
lines.push("equipment:");
|
|
3378
|
+
for (const slug of effort.equipment) {
|
|
3379
|
+
lines.push(` - ${quoteYaml(slug)}`);
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
if (effort.derivation) {
|
|
3383
|
+
lines.push("derivation:");
|
|
3384
|
+
if (effort.derivation.parentSlug) {
|
|
3385
|
+
lines.push(` parentSlug: ${effort.derivation.parentSlug}`);
|
|
3386
|
+
}
|
|
3387
|
+
if (effort.derivation.coefficients && Object.keys(effort.derivation.coefficients).length > 0) {
|
|
3388
|
+
lines.push(" coefficients:");
|
|
3389
|
+
for (const [k, v] of Object.entries(effort.derivation.coefficients)) {
|
|
3390
|
+
lines.push(` ${k}: ${v}`);
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
if (effort.derivation.hardOverrides && Object.keys(effort.derivation.hardOverrides).length > 0) {
|
|
3394
|
+
lines.push(" hardOverrides:");
|
|
3395
|
+
for (const [k, v] of Object.entries(effort.derivation.hardOverrides)) {
|
|
3396
|
+
lines.push(` ${k}: ${JSON.stringify(v)}`);
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
if (effort.createdAt) {
|
|
3401
|
+
lines.push(`createdAt: ${effort.createdAt}`);
|
|
3402
|
+
}
|
|
3403
|
+
if (effort.updatedAt) {
|
|
3404
|
+
lines.push(`updatedAt: ${effort.updatedAt}`);
|
|
3405
|
+
}
|
|
3406
|
+
if (effort.hints && Object.keys(effort.hints).length > 0) {
|
|
3407
|
+
lines.push("hints:");
|
|
3408
|
+
for (const [k, v] of Object.entries(effort.hints)) {
|
|
3409
|
+
lines.push(` ${k}: ${v !== null && typeof v === "object" ? JSON.stringify(v) : String(v)}`);
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
lines.push("---");
|
|
3413
|
+
if (effort.body) {
|
|
3414
|
+
lines.push("");
|
|
3415
|
+
lines.push(effort.body);
|
|
3416
|
+
}
|
|
3417
|
+
return lines.join("\n");
|
|
3418
|
+
}
|
|
3419
|
+
function createDefaultEquipment() {
|
|
3420
|
+
return {
|
|
3421
|
+
id: `equipment-user-${uuid.v7()}`,
|
|
3422
|
+
slug: "",
|
|
3423
|
+
label: "",
|
|
3424
|
+
aliases: [],
|
|
3425
|
+
registrySource: "user"
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
function documentToEquipment(doc, baseEquipment) {
|
|
3429
|
+
const errors = [];
|
|
3430
|
+
const blockMatch = doc.match(FRONTMATTER_RE);
|
|
3431
|
+
if (!blockMatch) {
|
|
3432
|
+
errors.push("Missing YAML frontmatter delimiters (---)");
|
|
3433
|
+
return { equipment: baseEquipment ?? createDefaultEquipment(), errors };
|
|
3434
|
+
}
|
|
3435
|
+
const { meta: meta2, body } = parseFrontmatter(doc);
|
|
3436
|
+
const bodyText = body.replace(/^\r?\n+/, "") || void 0;
|
|
3437
|
+
const type = getScalar(meta2, "type");
|
|
3438
|
+
const id = getScalar(meta2, "id");
|
|
3439
|
+
const slug = getScalar(meta2, "slug");
|
|
3440
|
+
const label = getScalar(meta2, "label");
|
|
3441
|
+
const registrySource = getScalar(meta2, "registrySource");
|
|
3442
|
+
const createdAt = getScalar(meta2, "createdAt");
|
|
3443
|
+
const updatedAt = getScalar(meta2, "updatedAt");
|
|
3444
|
+
const aliases = getListStrict(meta2, "aliases");
|
|
3445
|
+
if (type !== "equipment") errors.push("Missing or invalid discriminator: frontmatter must declare `type: equipment`");
|
|
3446
|
+
if (!slug || !SLUG_RE.test(String(slug))) errors.push("Invalid slug: must be a canonical slug (lowercase letters, numbers, hyphens)");
|
|
3447
|
+
if (!label) errors.push("Missing required field: label");
|
|
3448
|
+
if (aliases === void 0) {
|
|
3449
|
+
errors.push("Invalid aliases: must be a list (`aliases:` followed by `- item` lines)");
|
|
3450
|
+
} else {
|
|
3451
|
+
errors.push(...stringListProblems(aliases, "aliases"));
|
|
3452
|
+
}
|
|
3453
|
+
if (registrySource !== void 0 && registrySource !== "bundled" && registrySource !== "user") {
|
|
3454
|
+
errors.push("Invalid registrySource: must be 'bundled' or 'user'");
|
|
3455
|
+
}
|
|
3456
|
+
const equipment = {
|
|
3457
|
+
id: String(id ?? "") || baseEquipment?.id || `equipment-user-${uuid.v7()}`,
|
|
3458
|
+
slug: String(slug ?? ""),
|
|
3459
|
+
label: String(label ?? ""),
|
|
3460
|
+
aliases: aliases ?? [],
|
|
3461
|
+
registrySource: registrySource || baseEquipment?.registrySource || "user",
|
|
3462
|
+
...createdAt ? { createdAt: String(createdAt) } : {},
|
|
3463
|
+
...updatedAt ? { updatedAt: String(updatedAt) } : {},
|
|
3464
|
+
...bodyText ? { body: bodyText } : {}
|
|
3465
|
+
};
|
|
3466
|
+
return { equipment, errors };
|
|
3467
|
+
}
|
|
3468
|
+
function equipmentToDocument(equipment) {
|
|
3469
|
+
const lines = ["---"];
|
|
3470
|
+
lines.push("type: equipment");
|
|
3471
|
+
lines.push(`id: ${equipment.id}`);
|
|
3472
|
+
lines.push(`slug: ${equipment.slug}`);
|
|
3473
|
+
lines.push(`label: ${quoteYaml(equipment.label)}`);
|
|
3474
|
+
if (equipment.aliases.length > 0) {
|
|
3475
|
+
lines.push("aliases:");
|
|
3476
|
+
for (const alias of equipment.aliases) {
|
|
3477
|
+
lines.push(` - ${quoteYaml(alias)}`);
|
|
3478
|
+
}
|
|
3479
|
+
} else {
|
|
3480
|
+
lines.push("aliases: []");
|
|
3481
|
+
}
|
|
3482
|
+
lines.push(`registrySource: ${equipment.registrySource}`);
|
|
3483
|
+
if (equipment.createdAt) {
|
|
3484
|
+
lines.push(`createdAt: ${equipment.createdAt}`);
|
|
3485
|
+
}
|
|
3486
|
+
if (equipment.updatedAt) {
|
|
3487
|
+
lines.push(`updatedAt: ${equipment.updatedAt}`);
|
|
3488
|
+
}
|
|
3489
|
+
lines.push("---");
|
|
3490
|
+
if (equipment.body) {
|
|
3491
|
+
lines.push("");
|
|
3492
|
+
lines.push(equipment.body);
|
|
3493
|
+
}
|
|
3494
|
+
return lines.join("\n");
|
|
3495
|
+
}
|
|
2881
3496
|
|
|
2882
3497
|
// src/effort-registry/fuzzyMatch.ts
|
|
2883
3498
|
function levenshteinDistance(a, b) {
|
|
@@ -3262,12 +3877,24 @@ var CompositeEffortRegistry = class {
|
|
|
3262
3877
|
* Initialize the registry:
|
|
3263
3878
|
* 1. Load bundled efforts into memory
|
|
3264
3879
|
* 2. Load user efforts from storage adapter into memory
|
|
3880
|
+
*
|
|
3881
|
+
* An explicit `customBundled` REPLACES the bundled tier — passing an empty
|
|
3882
|
+
* set clears it — so a seed source removal never leaves stale bundled
|
|
3883
|
+
* efforts cached. The no-argument call keeps the constructor/default set.
|
|
3884
|
+
* Both tiers rebuild only AFTER a successful adapter load: a failed load
|
|
3885
|
+
* propagates (readiness must never report over stale data), and deleted
|
|
3886
|
+
* custom rows disappear because the user map is cleared too.
|
|
3265
3887
|
*/
|
|
3266
3888
|
async loadBundled(customBundled) {
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3889
|
+
this.bundledEfforts.clear();
|
|
3890
|
+
const source = customBundled ?? this.initialBundled ?? bundledEfforts;
|
|
3891
|
+
for (const effort of source) {
|
|
3892
|
+
const prior = this.bundledEfforts.get(effort.slug);
|
|
3893
|
+
if (!prior || resolveEntityRank(effort) >= resolveEntityRank(prior)) {
|
|
3894
|
+
this.bundledEfforts.set(effort.slug, effort);
|
|
3895
|
+
}
|
|
3270
3896
|
}
|
|
3897
|
+
this.userEfforts.clear();
|
|
3271
3898
|
if (this.storage) {
|
|
3272
3899
|
try {
|
|
3273
3900
|
const userEfforts = await this.storage.load();
|
|
@@ -3297,8 +3924,8 @@ var CompositeEffortRegistry = class {
|
|
|
3297
3924
|
if (normalizeForFuzzy(alias) === normalized) return effort;
|
|
3298
3925
|
}
|
|
3299
3926
|
}
|
|
3300
|
-
|
|
3301
|
-
|
|
3927
|
+
const bundled = Array.from(this.bundledEfforts.values()).filter((effort) => !this.userEfforts.has(effort.slug)).sort((a, b) => resolveEntityRank(b) - resolveEntityRank(a));
|
|
3928
|
+
for (const effort of bundled) {
|
|
3302
3929
|
if (normalizeForFuzzy(effort.label) === normalized) return effort;
|
|
3303
3930
|
for (const alias of effort.aliases) {
|
|
3304
3931
|
if (normalizeForFuzzy(alias) === normalized) return effort;
|
|
@@ -3397,6 +4024,367 @@ async function addAliasToEffort(registry, targetSlug, newAlias) {
|
|
|
3397
4024
|
await registry.upsert(updatedUserEffort);
|
|
3398
4025
|
return updatedUserEffort;
|
|
3399
4026
|
}
|
|
4027
|
+
function buildExactIndex(entities) {
|
|
4028
|
+
const deduped = dedupeEntitiesByPrecedence(entities);
|
|
4029
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
4030
|
+
const add = (key, entity) => {
|
|
4031
|
+
if (!key) return;
|
|
4032
|
+
const existing = byKey.get(key);
|
|
4033
|
+
if (existing) {
|
|
4034
|
+
if (!existing.includes(entity)) existing.push(entity);
|
|
4035
|
+
} else {
|
|
4036
|
+
byKey.set(key, [entity]);
|
|
4037
|
+
}
|
|
4038
|
+
};
|
|
4039
|
+
for (const entity of deduped) {
|
|
4040
|
+
add(entity.slug.toLowerCase(), entity);
|
|
4041
|
+
add(normalizeForFuzzy(entity.label), entity);
|
|
4042
|
+
for (const alias of entity.aliases) add(normalizeForFuzzy(alias), entity);
|
|
4043
|
+
}
|
|
4044
|
+
return {
|
|
4045
|
+
byKey,
|
|
4046
|
+
lookup(key) {
|
|
4047
|
+
const direct = byKey.get(key) ?? byKey.get(normalizeForFuzzy(key));
|
|
4048
|
+
if (!direct || direct.length === 0) return { entity: null, ambiguous: [] };
|
|
4049
|
+
if (direct.length === 1) return { entity: direct[0], ambiguous: [] };
|
|
4050
|
+
let maxRank = -1;
|
|
4051
|
+
for (const candidate of direct) {
|
|
4052
|
+
const rank = resolveEntityRank(candidate);
|
|
4053
|
+
if (rank > maxRank) maxRank = rank;
|
|
4054
|
+
}
|
|
4055
|
+
const topTier = direct.filter((candidate) => resolveEntityRank(candidate) === maxRank);
|
|
4056
|
+
if (topTier.length === 1) return { entity: topTier[0], ambiguous: [] };
|
|
4057
|
+
return { entity: null, ambiguous: topTier };
|
|
4058
|
+
}
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
4061
|
+
function equipmentRef(entity) {
|
|
4062
|
+
return { kind: "equipment", slug: entity.slug, label: entity.label };
|
|
4063
|
+
}
|
|
4064
|
+
function dedupeRefs(refs) {
|
|
4065
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4066
|
+
const result = [];
|
|
4067
|
+
for (const ref of refs) {
|
|
4068
|
+
const key = `${ref.kind}:${ref.slug}`;
|
|
4069
|
+
if (seen.has(key)) continue;
|
|
4070
|
+
seen.add(key);
|
|
4071
|
+
result.push(ref);
|
|
4072
|
+
}
|
|
4073
|
+
return result;
|
|
4074
|
+
}
|
|
4075
|
+
function createReferenceIndex(catalog, materialize) {
|
|
4076
|
+
const dedupedEquipment = dedupeEntitiesByPrecedence(catalog.equipment ?? []);
|
|
4077
|
+
const efforts = buildExactIndex(catalog.efforts ?? []);
|
|
4078
|
+
const equipment = buildExactIndex(dedupedEquipment);
|
|
4079
|
+
const prefixCandidates = dedupedEquipment.flatMap((entity) => [entity.label, ...entity.aliases].filter((name2) => !!name2 && !!name2.trim()).map((name2) => ({ lower: name2.trim().toLowerCase(), entity }))).sort((a, b) => b.lower.length - a.lower.length);
|
|
4080
|
+
const materializer = materialize ?? ((effort) => ({
|
|
4081
|
+
effort,
|
|
4082
|
+
definition: effort,
|
|
4083
|
+
slug: effort.slug,
|
|
4084
|
+
label: effort.label,
|
|
4085
|
+
met: effort.baseAttributes.met,
|
|
4086
|
+
baseAttributes: effort.baseAttributes,
|
|
4087
|
+
discipline: effort.baseAttributes.discipline,
|
|
4088
|
+
disciplineFactor: effort.baseAttributes.disciplineFactor ?? 1,
|
|
4089
|
+
intensityTier: effort.baseAttributes.intensityTier,
|
|
4090
|
+
modifiers: {},
|
|
4091
|
+
registrySource: effort.registrySource,
|
|
4092
|
+
resolvedFrom: effort.registrySource === "user" ? "user" : effort.registrySource === "bundled" ? "bundled" : "default",
|
|
4093
|
+
isEstimated: false
|
|
4094
|
+
}));
|
|
4095
|
+
const resolveEffortExact = (key) => {
|
|
4096
|
+
const found = efforts.lookup(key.trim());
|
|
4097
|
+
if (found.entity) return { entity: found.entity };
|
|
4098
|
+
if (found.ambiguous.length > 0) {
|
|
4099
|
+
return {
|
|
4100
|
+
entity: null,
|
|
4101
|
+
diagnostic: {
|
|
4102
|
+
reason: "ambiguous-effort",
|
|
4103
|
+
message: `"${key}" matches multiple efforts.`,
|
|
4104
|
+
token: key,
|
|
4105
|
+
candidates: found.ambiguous.map((e) => e.slug)
|
|
4106
|
+
}
|
|
4107
|
+
};
|
|
4108
|
+
}
|
|
4109
|
+
return {
|
|
4110
|
+
entity: null,
|
|
4111
|
+
diagnostic: { reason: "unknown-effort", message: `Unknown effort "${key}".`, token: key }
|
|
4112
|
+
};
|
|
4113
|
+
};
|
|
4114
|
+
const resolveEquipmentValues = (values) => {
|
|
4115
|
+
const refs = [];
|
|
4116
|
+
const diagnostics = [];
|
|
4117
|
+
for (const value of values) {
|
|
4118
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
4119
|
+
diagnostics.push({
|
|
4120
|
+
reason: "invalid-explicit",
|
|
4121
|
+
message: `Equipment reference must be a slug string, got ${JSON.stringify(value) ?? "null"}.`,
|
|
4122
|
+
token: String(value)
|
|
4123
|
+
});
|
|
4124
|
+
continue;
|
|
4125
|
+
}
|
|
4126
|
+
const found = equipment.lookup(value.trim());
|
|
4127
|
+
if (found.entity) {
|
|
4128
|
+
refs.push(equipmentRef(found.entity));
|
|
4129
|
+
} else if (found.ambiguous.length > 0) {
|
|
4130
|
+
diagnostics.push({
|
|
4131
|
+
reason: "ambiguous-equipment",
|
|
4132
|
+
message: `"${value}" matches multiple equipment entries.`,
|
|
4133
|
+
token: value,
|
|
4134
|
+
candidates: found.ambiguous.map((e) => e.slug)
|
|
4135
|
+
});
|
|
4136
|
+
} else {
|
|
4137
|
+
diagnostics.push({
|
|
4138
|
+
reason: "invalid-explicit",
|
|
4139
|
+
message: `Unknown equipment "${value}".`,
|
|
4140
|
+
token: value
|
|
4141
|
+
});
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
return { refs: dedupeRefs(refs), diagnostics };
|
|
4145
|
+
};
|
|
4146
|
+
const index = {
|
|
4147
|
+
resolveEffort(slugOrAlias) {
|
|
4148
|
+
return efforts.lookup(slugOrAlias).entity;
|
|
4149
|
+
},
|
|
4150
|
+
resolveEquipment(slugOrAlias) {
|
|
4151
|
+
return equipment.lookup(slugOrAlias).entity;
|
|
4152
|
+
},
|
|
4153
|
+
materialize(effort) {
|
|
4154
|
+
return materializer(effort);
|
|
4155
|
+
},
|
|
4156
|
+
resolve(label, defaults) {
|
|
4157
|
+
const source = (label ?? "").trim();
|
|
4158
|
+
const diagnostics = [];
|
|
4159
|
+
const push = (diagnostic) => {
|
|
4160
|
+
if (diagnostic) diagnostics.push(diagnostic);
|
|
4161
|
+
};
|
|
4162
|
+
let matched = "none";
|
|
4163
|
+
let effortEntity = null;
|
|
4164
|
+
let remaining = "";
|
|
4165
|
+
let equipmentRefs = [];
|
|
4166
|
+
let equipmentAuthored = false;
|
|
4167
|
+
const fillEquipment = () => {
|
|
4168
|
+
if (equipmentRefs.length > 0 || equipmentAuthored) return;
|
|
4169
|
+
if (effortEntity === null) return;
|
|
4170
|
+
if (defaults?.equipment?.length) {
|
|
4171
|
+
const resolvedDefaults = resolveEquipmentValues(defaults.equipment);
|
|
4172
|
+
equipmentRefs = resolvedDefaults.refs;
|
|
4173
|
+
for (const diagnostic of resolvedDefaults.diagnostics) {
|
|
4174
|
+
push({ ...diagnostic, reason: "unknown-equipment" });
|
|
4175
|
+
}
|
|
4176
|
+
if (equipmentRefs.length > 0) return;
|
|
4177
|
+
}
|
|
4178
|
+
if (effortEntity.equipment?.length) {
|
|
4179
|
+
const resolvedDefEquipment = resolveEquipmentValues(effortEntity.equipment);
|
|
4180
|
+
equipmentRefs = resolvedDefEquipment.refs;
|
|
4181
|
+
for (const diagnostic of resolvedDefEquipment.diagnostics) {
|
|
4182
|
+
push({ ...diagnostic, reason: "unknown-equipment" });
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
};
|
|
4186
|
+
if (!source) {
|
|
4187
|
+
if (defaults?.effort) {
|
|
4188
|
+
const found = resolveEffortExact(defaults.effort);
|
|
4189
|
+
if (found.entity) {
|
|
4190
|
+
effortEntity = found.entity;
|
|
4191
|
+
matched = "default";
|
|
4192
|
+
} else {
|
|
4193
|
+
push({
|
|
4194
|
+
reason: found.diagnostic?.reason === "ambiguous-effort" ? "ambiguous-effort" : "unknown-effort",
|
|
4195
|
+
message: `Default effort "${defaults.effort}" is not in the catalog.`,
|
|
4196
|
+
token: defaults.effort,
|
|
4197
|
+
candidates: found.diagnostic?.candidates
|
|
4198
|
+
});
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
fillEquipment();
|
|
4202
|
+
return {
|
|
4203
|
+
matched,
|
|
4204
|
+
label: source,
|
|
4205
|
+
effort: effortEntity ? materializer(effortEntity) : null,
|
|
4206
|
+
effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
|
|
4207
|
+
equipment: equipmentRefs,
|
|
4208
|
+
remaining: "",
|
|
4209
|
+
diagnostics
|
|
4210
|
+
};
|
|
4211
|
+
}
|
|
4212
|
+
if (source.startsWith("{")) {
|
|
4213
|
+
let parsed;
|
|
4214
|
+
try {
|
|
4215
|
+
parsed = JSON.parse(source);
|
|
4216
|
+
} catch {
|
|
4217
|
+
return {
|
|
4218
|
+
matched: "none",
|
|
4219
|
+
label: source,
|
|
4220
|
+
effort: null,
|
|
4221
|
+
effortRef: null,
|
|
4222
|
+
equipment: [],
|
|
4223
|
+
remaining: "",
|
|
4224
|
+
diagnostics: [{
|
|
4225
|
+
reason: "malformed-json",
|
|
4226
|
+
message: "Reference object is not valid JSON.",
|
|
4227
|
+
token: source
|
|
4228
|
+
}]
|
|
4229
|
+
};
|
|
4230
|
+
}
|
|
4231
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && ("effort" in parsed || "equipment" in parsed)) {
|
|
4232
|
+
matched = "explicit";
|
|
4233
|
+
const obj = parsed;
|
|
4234
|
+
if (obj.effort !== void 0) {
|
|
4235
|
+
if (typeof obj.effort === "string" && obj.effort.trim()) {
|
|
4236
|
+
const found = resolveEffortExact(obj.effort);
|
|
4237
|
+
effortEntity = found.entity;
|
|
4238
|
+
if (found.diagnostic) {
|
|
4239
|
+
push(found.diagnostic.reason === "ambiguous-effort" ? found.diagnostic : { ...found.diagnostic, reason: "invalid-explicit", message: `Explicit effort "${obj.effort}" is not in the catalog.` });
|
|
4240
|
+
}
|
|
4241
|
+
} else {
|
|
4242
|
+
push({
|
|
4243
|
+
reason: "invalid-explicit",
|
|
4244
|
+
message: 'Explicit "effort" must be a slug string.',
|
|
4245
|
+
token: String(obj.effort)
|
|
4246
|
+
});
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
if (obj.equipment !== void 0) {
|
|
4250
|
+
equipmentAuthored = true;
|
|
4251
|
+
const values = Array.isArray(obj.equipment) ? obj.equipment : [obj.equipment];
|
|
4252
|
+
const resolvedEquipment = resolveEquipmentValues(values);
|
|
4253
|
+
equipmentRefs = resolvedEquipment.refs;
|
|
4254
|
+
diagnostics.push(...resolvedEquipment.diagnostics);
|
|
4255
|
+
if (effortEntity?.equipment?.length) {
|
|
4256
|
+
const definitionSlugs = new Set(effortEntity.equipment);
|
|
4257
|
+
const authoredSlugs = equipmentRefs.map((ref) => ref.slug);
|
|
4258
|
+
if (authoredSlugs.length > 0 && authoredSlugs.some((slug) => !definitionSlugs.has(slug))) {
|
|
4259
|
+
push({
|
|
4260
|
+
reason: "conflict",
|
|
4261
|
+
message: `Explicit equipment [${authoredSlugs.join(", ")}] contradicts effort "${effortEntity.slug}" definition equipment [${effortEntity.equipment.join(", ")}].`,
|
|
4262
|
+
token: source
|
|
4263
|
+
});
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
fillEquipment();
|
|
4268
|
+
return {
|
|
4269
|
+
matched,
|
|
4270
|
+
label: source,
|
|
4271
|
+
effort: effortEntity ? materializer(effortEntity) : null,
|
|
4272
|
+
effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
|
|
4273
|
+
equipment: equipmentRefs,
|
|
4274
|
+
remaining: "",
|
|
4275
|
+
diagnostics
|
|
4276
|
+
};
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
const lowerSource = source.toLowerCase();
|
|
4280
|
+
for (const candidate of prefixCandidates) {
|
|
4281
|
+
if (!lowerSource.startsWith(candidate.lower)) continue;
|
|
4282
|
+
const after = source.slice(candidate.lower.length);
|
|
4283
|
+
if (after && !/^\s/.test(after)) continue;
|
|
4284
|
+
const candidateRemaining = after.trim();
|
|
4285
|
+
if (!candidateRemaining) continue;
|
|
4286
|
+
const equipmentFound = equipment.lookup(candidate.lower);
|
|
4287
|
+
if (equipmentFound.ambiguous.length > 0) {
|
|
4288
|
+
push({
|
|
4289
|
+
reason: "ambiguous-equipment",
|
|
4290
|
+
message: `"${candidate.lower}" matches multiple equipment entries.`,
|
|
4291
|
+
token: source,
|
|
4292
|
+
candidates: equipmentFound.ambiguous.map((e) => e.slug)
|
|
4293
|
+
});
|
|
4294
|
+
return {
|
|
4295
|
+
matched: "none",
|
|
4296
|
+
label: source,
|
|
4297
|
+
effort: null,
|
|
4298
|
+
effortRef: null,
|
|
4299
|
+
equipment: [],
|
|
4300
|
+
remaining: "",
|
|
4301
|
+
diagnostics
|
|
4302
|
+
};
|
|
4303
|
+
}
|
|
4304
|
+
if (!equipmentFound.entity) continue;
|
|
4305
|
+
const effortFound = resolveEffortExact(candidateRemaining);
|
|
4306
|
+
if (effortFound.entity) {
|
|
4307
|
+
matched = "split";
|
|
4308
|
+
effortEntity = effortFound.entity;
|
|
4309
|
+
equipmentRefs = [equipmentRef(equipmentFound.entity)];
|
|
4310
|
+
equipmentAuthored = true;
|
|
4311
|
+
remaining = candidateRemaining;
|
|
4312
|
+
break;
|
|
4313
|
+
}
|
|
4314
|
+
if (effortFound.diagnostic?.reason === "ambiguous-effort") {
|
|
4315
|
+
push(effortFound.diagnostic);
|
|
4316
|
+
return {
|
|
4317
|
+
matched: "none",
|
|
4318
|
+
label: source,
|
|
4319
|
+
effort: null,
|
|
4320
|
+
effortRef: null,
|
|
4321
|
+
equipment: [],
|
|
4322
|
+
remaining: "",
|
|
4323
|
+
diagnostics
|
|
4324
|
+
};
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
if (matched === "none") {
|
|
4328
|
+
const found = resolveEffortExact(source);
|
|
4329
|
+
if (found.entity) {
|
|
4330
|
+
effortEntity = found.entity;
|
|
4331
|
+
matched = "whole";
|
|
4332
|
+
} else if (found.diagnostic?.reason === "ambiguous-effort") {
|
|
4333
|
+
push(found.diagnostic);
|
|
4334
|
+
return {
|
|
4335
|
+
matched: "none",
|
|
4336
|
+
label: source,
|
|
4337
|
+
effort: null,
|
|
4338
|
+
effortRef: null,
|
|
4339
|
+
equipment: [],
|
|
4340
|
+
remaining: "",
|
|
4341
|
+
diagnostics
|
|
4342
|
+
};
|
|
4343
|
+
} else if (found.diagnostic) {
|
|
4344
|
+
push(found.diagnostic);
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
fillEquipment();
|
|
4348
|
+
return {
|
|
4349
|
+
matched,
|
|
4350
|
+
label: source,
|
|
4351
|
+
effort: effortEntity ? materializer(effortEntity) : null,
|
|
4352
|
+
effortRef: effortEntity ? { kind: "effort", slug: effortEntity.slug, label: effortEntity.label } : null,
|
|
4353
|
+
equipment: equipmentRefs,
|
|
4354
|
+
remaining,
|
|
4355
|
+
diagnostics
|
|
4356
|
+
};
|
|
4357
|
+
}
|
|
4358
|
+
};
|
|
4359
|
+
return index;
|
|
4360
|
+
}
|
|
4361
|
+
function extractStatementRefs(statements) {
|
|
4362
|
+
const efforts = [];
|
|
4363
|
+
const equipment = [];
|
|
4364
|
+
const seenEffort = /* @__PURE__ */ new Set();
|
|
4365
|
+
const seenEquipment = /* @__PURE__ */ new Set();
|
|
4366
|
+
const pushUnique = (list, seen, value) => {
|
|
4367
|
+
if (!value) return;
|
|
4368
|
+
if (seen.has(value)) return;
|
|
4369
|
+
seen.add(value);
|
|
4370
|
+
list.push(value);
|
|
4371
|
+
};
|
|
4372
|
+
for (const statement of statements ?? []) {
|
|
4373
|
+
const metrics = Array.isArray(statement.metrics) ? statement.metrics : statement.metrics?.toArray?.() ?? [];
|
|
4374
|
+
for (const metric of metrics) {
|
|
4375
|
+
const refs = wodWikiCore.getReferenceMetadata(metric.metadata)?.refs;
|
|
4376
|
+
if (!refs) continue;
|
|
4377
|
+
for (const ref of refs) {
|
|
4378
|
+
if (ref.kind === "effort") pushUnique(efforts, seenEffort, ref.slug);
|
|
4379
|
+
if (ref.kind === "equipment") pushUnique(equipment, seenEquipment, ref.slug);
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4383
|
+
return { efforts, equipment };
|
|
4384
|
+
}
|
|
4385
|
+
function slugifyRefLabel(label) {
|
|
4386
|
+
return normalizeForFuzzy(label).replace(/\s+/g, "-");
|
|
4387
|
+
}
|
|
3400
4388
|
|
|
3401
4389
|
// src/runtime/contracts/IRuntimeOptions.ts
|
|
3402
4390
|
var DEFAULT_RUNTIME_OPTIONS = {
|
|
@@ -6735,14 +7723,12 @@ function resolveDominantOrigin(origins) {
|
|
|
6735
7723
|
if (origins.every((o) => o === "compiler")) return "compiler";
|
|
6736
7724
|
return "analyzed";
|
|
6737
7725
|
}
|
|
6738
|
-
|
|
6739
|
-
// src/runtime/compiler/EffortEnrichmentPass.ts
|
|
6740
7726
|
var logTrace = (message) => console.log(message);
|
|
6741
7727
|
function applyEffortEnrichment(block, options) {
|
|
6742
|
-
const { resolver, overwrite = false, debug = false } = options;
|
|
6743
|
-
tryEnrichBlock(block, resolver, overwrite, debug);
|
|
7728
|
+
const { resolver, references, overwrite = false, debug = false } = options;
|
|
7729
|
+
tryEnrichBlock(block, resolver, references, overwrite, debug);
|
|
6744
7730
|
}
|
|
6745
|
-
function tryEnrichBlock(block, resolver, overwrite, debug) {
|
|
7731
|
+
function tryEnrichBlock(block, resolver, references, overwrite, debug) {
|
|
6746
7732
|
const normalizedBlockType = block.blockType?.toLowerCase();
|
|
6747
7733
|
if (normalizedBlockType !== "effort" && normalizedBlockType !== "exercise") {
|
|
6748
7734
|
return;
|
|
@@ -6760,7 +7746,32 @@ function tryEnrichBlock(block, resolver, overwrite, debug) {
|
|
|
6760
7746
|
return;
|
|
6761
7747
|
}
|
|
6762
7748
|
try {
|
|
6763
|
-
|
|
7749
|
+
let resolved;
|
|
7750
|
+
let matchedKind = "none";
|
|
7751
|
+
let equipmentRefs = [];
|
|
7752
|
+
let diagnostics = [];
|
|
7753
|
+
if (references) {
|
|
7754
|
+
const resolution = references.resolve(label);
|
|
7755
|
+
resolved = resolution.effort ?? void 0;
|
|
7756
|
+
matchedKind = resolution.matched;
|
|
7757
|
+
equipmentRefs = resolution.equipment;
|
|
7758
|
+
diagnostics = resolution.diagnostics;
|
|
7759
|
+
} else {
|
|
7760
|
+
resolved = resolver.resolveEffort(label);
|
|
7761
|
+
}
|
|
7762
|
+
if (!resolved) {
|
|
7763
|
+
if (diagnostics.length > 0) {
|
|
7764
|
+
block.pushMemory(new MemoryLocation("metric:tracked", [{
|
|
7765
|
+
type: wodWikiCore.MetricType.Hint,
|
|
7766
|
+
value: "reference.unresolved",
|
|
7767
|
+
image: label,
|
|
7768
|
+
origin: "compiler",
|
|
7769
|
+
metadata: { [wodWikiCore.REFERENCE_METADATA_KEY]: { matched: "none", source: label, diagnostics } }
|
|
7770
|
+
}]));
|
|
7771
|
+
}
|
|
7772
|
+
if (debug) logTrace(`[EffortEnrichment] Unresolved effort: ${label}`);
|
|
7773
|
+
return;
|
|
7774
|
+
}
|
|
6764
7775
|
const effortHints = resolved.effort.hints;
|
|
6765
7776
|
if (effortHints && Object.keys(effortHints).length > 0) {
|
|
6766
7777
|
block.mergeHints(effortHints);
|
|
@@ -6773,7 +7784,8 @@ function tryEnrichBlock(block, resolver, overwrite, debug) {
|
|
|
6773
7784
|
image: resolved.label,
|
|
6774
7785
|
value: resolved,
|
|
6775
7786
|
origin: "compiler",
|
|
6776
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
7787
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
7788
|
+
...equipmentRefs.length > 0 || diagnostics.length > 0 ? { metadata: { [wodWikiCore.REFERENCE_METADATA_KEY]: { matched: matchedKind, source: label, refs: [{ kind: "effort", slug: resolved.slug, label: resolved.label }, ...equipmentRefs], ...diagnostics.length > 0 ? { diagnostics } : {} } } } : {}
|
|
6777
7789
|
};
|
|
6778
7790
|
const privateMetrics = block.getMetricMemoryByVisibility("private");
|
|
6779
7791
|
const existingLoc = privateMetrics.find(
|
|
@@ -6788,10 +7800,31 @@ function tryEnrichBlock(block, resolver, overwrite, debug) {
|
|
|
6788
7800
|
const location = new MemoryLocation("metric:tracked", [effortDataMetric]);
|
|
6789
7801
|
block.pushMemory(location);
|
|
6790
7802
|
}
|
|
7803
|
+
if (equipmentRefs.length > 0) {
|
|
7804
|
+
pushPrivateMetrics(block, wodWikiCore.MetricType.Equipment, [
|
|
7805
|
+
new EquipmentMetric(equipmentRefs.map((ref) => ref.slug), {
|
|
7806
|
+
refs: equipmentRefs,
|
|
7807
|
+
source: label,
|
|
7808
|
+
origin: "compiler"
|
|
7809
|
+
})
|
|
7810
|
+
], wodWikiCore.MetricType.Equipment, overwrite);
|
|
7811
|
+
}
|
|
6791
7812
|
} catch (err) {
|
|
6792
7813
|
console.error(`[EffortEnrichment] Failed to resolve effort "${label}":`, err);
|
|
6793
7814
|
}
|
|
6794
7815
|
}
|
|
7816
|
+
function pushPrivateMetrics(block, markerType, metrics, replaceType, overwrite) {
|
|
7817
|
+
const privateMetrics = block.getMetricMemoryByVisibility("private");
|
|
7818
|
+
const existingLoc = privateMetrics.find(
|
|
7819
|
+
(loc) => loc.metrics.toArray().some((m) => m.type === markerType)
|
|
7820
|
+
);
|
|
7821
|
+
if (existingLoc && overwrite) {
|
|
7822
|
+
const filtered = existingLoc.metrics.toArray().filter((m) => m.type !== replaceType);
|
|
7823
|
+
existingLoc.update([...filtered, ...metrics]);
|
|
7824
|
+
} else if (!existingLoc) {
|
|
7825
|
+
block.pushMemory(new MemoryLocation("metric:tracked", metrics));
|
|
7826
|
+
}
|
|
7827
|
+
}
|
|
6795
7828
|
|
|
6796
7829
|
// src/runtime/actions/stack/CompileAndPushBlockAction.ts
|
|
6797
7830
|
var CompileAndPushBlockAction = class {
|
|
@@ -6817,7 +7850,8 @@ var CompileAndPushBlockAction = class {
|
|
|
6817
7850
|
}
|
|
6818
7851
|
if (runtime.analyticsContext?.effortResolver) {
|
|
6819
7852
|
applyEffortEnrichment(compiledBlock, {
|
|
6820
|
-
resolver: runtime.analyticsContext.effortResolver
|
|
7853
|
+
resolver: runtime.analyticsContext.effortResolver,
|
|
7854
|
+
references: runtime.analyticsContext.references
|
|
6821
7855
|
});
|
|
6822
7856
|
}
|
|
6823
7857
|
return [new PushBlockAction(compiledBlock, this.options)];
|
|
@@ -10843,7 +11877,8 @@ function createAnalyticsEngineForBlock(block, options) {
|
|
|
10843
11877
|
effortResolver = new EffortResolver(registry);
|
|
10844
11878
|
}
|
|
10845
11879
|
const analyticsContext = {
|
|
10846
|
-
effortResolver
|
|
11880
|
+
effortResolver,
|
|
11881
|
+
...options?.references ? { references: options.references } : {}
|
|
10847
11882
|
};
|
|
10848
11883
|
const context = {
|
|
10849
11884
|
dialect,
|
|
@@ -13756,6 +14791,7 @@ exports.BehaviorContext = BehaviorContext;
|
|
|
13756
14791
|
exports.BlockBuilder = BlockBuilder;
|
|
13757
14792
|
exports.BlockContext = BlockContext;
|
|
13758
14793
|
exports.ButtonBehavior = ButtonBehavior;
|
|
14794
|
+
exports.CATALOG_PRECEDENCE_RANK = CATALOG_PRECEDENCE_RANK;
|
|
13759
14795
|
exports.CONSUMED_HINTS = CONSUMED_HINTS;
|
|
13760
14796
|
exports.CONSUMED_HINT_KEYS = CONSUMED_HINT_KEYS;
|
|
13761
14797
|
exports.CONTEXT_ATOMS = CONTEXT_ATOMS;
|
|
@@ -13914,18 +14950,25 @@ exports.createMetricPresentationPolicy = createPolicy;
|
|
|
13914
14950
|
exports.createMockClock = createMockClock;
|
|
13915
14951
|
exports.createParser = createParser;
|
|
13916
14952
|
exports.createProfileTable = createProfileTable;
|
|
14953
|
+
exports.createReferenceIndex = createReferenceIndex;
|
|
13917
14954
|
exports.createRpeLabelsTable = createRpeLabelsTable;
|
|
13918
14955
|
exports.dailySessionLoads = dailySessionLoads;
|
|
13919
14956
|
exports.dayBucket = dayBucket;
|
|
14957
|
+
exports.dedupeEntitiesByPrecedence = dedupeEntitiesByPrecedence;
|
|
13920
14958
|
exports.dialectRegistry = dialectRegistry;
|
|
13921
14959
|
exports.dialectStack = dialectStack;
|
|
13922
14960
|
exports.diffStatement = diffStatement;
|
|
13923
14961
|
exports.dimEquals = dimEquals;
|
|
13924
14962
|
exports.disciplineFactorFor = disciplineFactorFor;
|
|
14963
|
+
exports.documentToEffort = documentToEffort;
|
|
14964
|
+
exports.documentToEquipment = documentToEquipment;
|
|
14965
|
+
exports.effortToDocument = effortToDocument;
|
|
14966
|
+
exports.equipmentToDocument = equipmentToDocument;
|
|
13925
14967
|
exports.evaluate = evaluate;
|
|
13926
14968
|
exports.evaluateCalculateDefinitions = evaluateCalculateDefinitions;
|
|
13927
14969
|
exports.extractEffortData = extractEffortData;
|
|
13928
14970
|
exports.extractMetrics = extractMetrics;
|
|
14971
|
+
exports.extractStatementRefs = extractStatementRefs;
|
|
13929
14972
|
exports.extractStatements = extractStatements;
|
|
13930
14973
|
exports.extractStatementsRaw = extractStatementsRaw;
|
|
13931
14974
|
exports.extractSyntaxFacts = extractSyntaxFacts;
|
|
@@ -13973,15 +15016,22 @@ exports.outputNodeId = outputNodeId;
|
|
|
13973
15016
|
exports.outputNodeIds = outputNodeIds;
|
|
13974
15017
|
exports.parseCalcLine = parseCalcLine;
|
|
13975
15018
|
exports.parseCalculateBlock = parseCalculateBlock;
|
|
15019
|
+
exports.parseEffortFile = parseEffortFile;
|
|
15020
|
+
exports.parseEquipmentFile = parseEquipmentFile;
|
|
13976
15021
|
exports.parseExpression = parseExpression;
|
|
13977
15022
|
exports.parseMetricLine = parseMetricLine;
|
|
13978
15023
|
exports.parseScript = parseScript;
|
|
13979
15024
|
exports.printCalculation = printCalculation;
|
|
15025
|
+
exports.readNestedSection = readNestedSection;
|
|
13980
15026
|
exports.readTimer = readTimer;
|
|
13981
15027
|
exports.realtimeProcessorRegistry = realtimeProcessorRegistry;
|
|
13982
15028
|
exports.renderMetric = renderMetric;
|
|
13983
15029
|
exports.resolveDominantOrigin = resolveDominantOrigin;
|
|
13984
|
-
exports.
|
|
15030
|
+
exports.resolveEntityRank = resolveEntityRank;
|
|
15031
|
+
exports.resolveEntityTier = resolveEntityTier;
|
|
15032
|
+
exports.resolveSourceRank = resolveSourceRank;
|
|
15033
|
+
exports.resolveSourceTier = resolveSourceTier;
|
|
15034
|
+
exports.slugifyRefLabel = slugifyRefLabel;
|
|
13985
15035
|
exports.startSpan = startSpan;
|
|
13986
15036
|
exports.str = str;
|
|
13987
15037
|
exports.strategyRegistry = strategyRegistry;
|