@bitcobblers/wod-wiki-tools 0.36.2136 → 0.37.2139

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/bin/wod.cjs +522 -246
  2. package/dist/bin/wod.cjs.map +1 -1
  3. package/dist/bin/wod.mjs +4 -3
  4. package/dist/bin/wod.mjs.map +1 -1
  5. package/dist/{chunk-FZKXOYMJ.mjs → chunk-CVFMFD7D.mjs} +153 -188
  6. package/dist/chunk-CVFMFD7D.mjs.map +1 -0
  7. package/dist/{chunk-6QNX4FED.mjs → chunk-HRFOKCYI.mjs} +139 -47
  8. package/dist/chunk-HRFOKCYI.mjs.map +1 -0
  9. package/dist/{chunk-CYKNTEGY.mjs → chunk-IWT7U3WX.mjs} +8 -3
  10. package/dist/chunk-IWT7U3WX.mjs.map +1 -0
  11. package/dist/chunk-JUBKCRNV.mjs +12 -0
  12. package/dist/chunk-JUBKCRNV.mjs.map +1 -0
  13. package/dist/{chunk-MA6BBTWC.mjs → chunk-RWNL6APX.mjs} +3 -3
  14. package/dist/{chunk-MA6BBTWC.mjs.map → chunk-RWNL6APX.mjs.map} +1 -1
  15. package/dist/compiler-OXEXGIZG.mjs +9 -0
  16. package/dist/{compiler-CTPLIZJK.mjs.map → compiler-OXEXGIZG.mjs.map} +1 -1
  17. package/dist/index.cjs +522 -246
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.mjs +5 -4
  22. package/dist/scaffoldCatalog-OIU4FEKJ.mjs +205 -0
  23. package/dist/scaffoldCatalog-OIU4FEKJ.mjs.map +1 -0
  24. package/dist/{server-JXECWS4A.mjs → server-2DVBA35J.mjs} +4 -4
  25. package/dist/{server-JXECWS4A.mjs.map → server-2DVBA35J.mjs.map} +1 -1
  26. package/dist/web/playground/assets/main-2qCz3_0z.js +194 -0
  27. package/dist/web/playground/assets/main-BUcjhK2w.css +2 -0
  28. package/dist/web/playground/assets/{statementBuilderFlow-ugwi0fu0.js → statementBuilderFlow-DKhL-YLz.js} +2 -2
  29. package/dist/web/playground/assets/{vendor-g-NJOjvC.js → vendor-DaJxNU_W.js} +3 -3
  30. package/dist/web/playground/assets/{vendor-codemirror-C41SFn-r.js → vendor-codemirror-BtDM22HA.js} +2 -2
  31. package/dist/web/playground/index.html +4 -4
  32. package/dist/web/playground/seed/core/chunks/canvas.a45c409e.json +1 -0
  33. package/dist/web/playground/seed/core/chunks/efforts.e68f2093.json +1 -0
  34. package/dist/web/playground/seed/core/chunks/equipment.c4fb1d64.json +1 -0
  35. package/dist/web/playground/seed/core/chunks/{template.e5e0e82d.json → template.0120276c.json} +1 -1
  36. package/dist/web/playground/seed/core/manifest.json +27 -10
  37. package/dist/web/playground/seed/core/report.html +15 -13
  38. package/dist/web/server/assets/main-BUcjhK2w.css +2 -0
  39. package/dist/web/server/assets/main-DxsZ-yOz.js +193 -0
  40. package/dist/web/server/assets/{statementBuilderFlow-40WaYjdq.js → statementBuilderFlow-CI5vrd60.js} +2 -2
  41. package/dist/web/server/assets/{vendor-g-NJOjvC.js → vendor-DaJxNU_W.js} +3 -3
  42. package/dist/web/server/assets/{vendor-codemirror-C41SFn-r.js → vendor-codemirror-BtDM22HA.js} +2 -2
  43. package/dist/web/server/index.html +4 -4
  44. package/dist/web/server/seed/core/chunks/canvas.a45c409e.json +1 -0
  45. package/dist/web/server/seed/core/chunks/efforts.e68f2093.json +1 -0
  46. package/dist/web/server/seed/core/chunks/equipment.c4fb1d64.json +1 -0
  47. package/dist/web/server/seed/core/chunks/{template.e5e0e82d.json → template.0120276c.json} +1 -1
  48. package/dist/web/server/seed/core/manifest.json +27 -10
  49. package/dist/web/server/seed/core/report.html +15 -13
  50. package/package.json +6 -6
  51. package/dist/chunk-6QNX4FED.mjs.map +0 -1
  52. package/dist/chunk-CYKNTEGY.mjs.map +0 -1
  53. package/dist/chunk-FZKXOYMJ.mjs.map +0 -1
  54. package/dist/compiler-CTPLIZJK.mjs +0 -9
  55. package/dist/web/playground/assets/main-B-bffxwE.css +0 -2
  56. package/dist/web/playground/assets/main-iLSeEl2E.js +0 -188
  57. package/dist/web/playground/seed/core/chunks/canvas.20048125.json +0 -1
  58. package/dist/web/server/assets/main-B-bffxwE.css +0 -2
  59. package/dist/web/server/assets/main-CRyLxlPN.js +0 -187
  60. package/dist/web/server/seed/core/chunks/canvas.20048125.json +0 -1
package/dist/bin/wod.cjs CHANGED
@@ -161,7 +161,7 @@ function sqliteEventStore(db) {
161
161
  getEventsByTimeRange: async (start, end) => (await all()).filter((r) => r.timestamp >= start && r.timestamp <= end),
162
162
  getEventsByResult: async (resultId) => (await all()).filter((r) => r.resultId === resultId),
163
163
  getEventsForNote: async (noteId) => (await all()).filter((r) => r.noteId === noteId),
164
- getEventsByContent: async (blockContentId4) => (await all()).filter((r) => r.blockContentId === blockContentId4),
164
+ getEventsByContent: async (blockContentId5) => (await all()).filter((r) => r.blockContentId === blockContentId5),
165
165
  scanAll: all,
166
166
  appendEvents: async (appended) => {
167
167
  const sql = await loadSql();
@@ -267,6 +267,11 @@ async function getEffortBySlug(db, slug) {
267
267
  const efforts = await allRows(sql, db, "efforts");
268
268
  return efforts.find((e) => e.slug === slug);
269
269
  }
270
+ async function getEquipmentBySlug(db, slug) {
271
+ const sql = await loadSql();
272
+ const equipment = await allRows(sql, db, "equipment");
273
+ return equipment.find((e) => e.slug === slug);
274
+ }
270
275
  async function listTagsByType(db, type) {
271
276
  const sql = await loadSql();
272
277
  const tags = await allRows(sql, db, "tags");
@@ -297,6 +302,7 @@ function seedNoteSourceId(path, content, chunkId) {
297
302
  if (chunkId === CANVAS_CHUNK_ID2) return `guides:${canvasRouteSlug(content) ?? under}`;
298
303
  if (chunkId === DASHBOARDS_CHUNK_ID) return "dashboards";
299
304
  if (chunkId === EFFORTS_CHUNK_ID) return "efforts";
305
+ if (chunkId === EQUIPMENT_CHUNK_ID) return "equipment";
300
306
  if (chunkId === TEMPLATE_CHUNK_ID) return "playground";
301
307
  if (chunkId.startsWith("collection.")) {
302
308
  const catalog = catalogForChunkId(chunkId);
@@ -305,40 +311,19 @@ function seedNoteSourceId(path, content, chunkId) {
305
311
  if (chunkId.startsWith("feed.")) return `feed:feeds/${under}`;
306
312
  return void 0;
307
313
  }
308
- var SEED_SCHEMA2, SEED_REGISTERED_WIDGETS, EFFORTS_CHUNK_ID, CANVAS_CHUNK_ID2, DASHBOARDS_CHUNK_ID, TEMPLATE_CHUNK_ID;
314
+ var SEED_SCHEMA2, SEED_REGISTERED_WIDGETS, EFFORTS_CHUNK_ID, EQUIPMENT_CHUNK_ID, CANVAS_CHUNK_ID2, DASHBOARDS_CHUNK_ID, TEMPLATE_CHUNK_ID;
309
315
  var init_contract = __esm({
310
316
  "src/seedbuild/contract.ts"() {
311
- SEED_SCHEMA2 = 8;
317
+ SEED_SCHEMA2 = 9;
312
318
  SEED_REGISTERED_WIDGETS = wodWikiCore.KNOWN_SECTION_WIDGETS;
313
319
  EFFORTS_CHUNK_ID = "efforts";
320
+ EQUIPMENT_CHUNK_ID = "equipment";
314
321
  CANVAS_CHUNK_ID2 = "canvas";
315
322
  DASHBOARDS_CHUNK_ID = "dashboards";
316
323
  TEMPLATE_CHUNK_ID = "template";
317
324
  }
318
325
  });
319
326
 
320
- // src/seedbuild/effort-slugs.ts
321
- function extractEffortSlugs2(statements) {
322
- const slugs = /* @__PURE__ */ new Set();
323
- for (const statement of statements) {
324
- if (statement.exerciseId) {
325
- slugs.add(statement.exerciseId);
326
- } else {
327
- const effortMetric = statement.metrics?.getMetric?.("effort");
328
- const effort = effortMetric && typeof effortMetric.value === "string" ? effortMetric.value : void 0;
329
- if (effort) {
330
- const slug = effort.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
331
- if (slug) slugs.add(slug);
332
- }
333
- }
334
- }
335
- return Array.from(slugs);
336
- }
337
- var init_effort_slugs = __esm({
338
- "src/seedbuild/effort-slugs.ts"() {
339
- }
340
- });
341
-
342
327
  // src/seedbuild/filename.ts
343
328
  function fileToDisplayName(filename) {
344
329
  const base = filename.replace(/\.md$/, "");
@@ -384,149 +369,6 @@ var init_filename = __esm({
384
369
  };
385
370
  }
386
371
  });
387
-
388
- // src/seedbuild/frontmatter.ts
389
- function unquote(value) {
390
- return value.replace(
391
- /^(['"])(.*)\1$/,
392
- (_match, quote, inner) => quote === '"' ? inner.replace(/\\(["\\])/g, "$1") : inner
393
- );
394
- }
395
- function parseFrontmatter(raw) {
396
- const match = raw.match(FRONTMATTER_RE);
397
- if (!match) return { meta: {}, body: raw };
398
- return { meta: parseMetaLines(match[1].split(/\r?\n/)), body: raw.slice(match[0].length) };
399
- }
400
- function parseMetaLines(lines) {
401
- const meta = {};
402
- for (let i = 0; i < lines.length; i++) {
403
- const keyMatch = lines[i].match(/^([A-Za-z][\w.-]*)\s*:(.*)$/);
404
- if (!keyMatch) continue;
405
- const key = keyMatch[1];
406
- const rawVal = keyMatch[2].trim();
407
- if (rawVal === "") {
408
- const items = [];
409
- while (i + 1 < lines.length) {
410
- const itemMatch = lines[i + 1].match(/^\s+-\s+(.+)$/);
411
- if (!itemMatch) break;
412
- items.push(itemMatch[1].trim());
413
- i++;
414
- }
415
- meta[key] = items.length > 0 ? items : "";
416
- } else {
417
- const wasQuoted = /^(['"])(.*)\1$/.test(rawVal);
418
- const value = unquote(rawVal);
419
- const num = Number(value);
420
- meta[key] = !wasQuoted && value !== "" && !isNaN(num) ? num : value;
421
- }
422
- }
423
- return meta;
424
- }
425
- function getScalar(meta, key) {
426
- const value = meta[key];
427
- return typeof value === "string" || typeof value === "number" ? value : void 0;
428
- }
429
- function getList(meta, key) {
430
- const value = meta[key];
431
- return Array.isArray(value) ? value : [];
432
- }
433
- var FRONTMATTER_RE;
434
- var init_frontmatter = __esm({
435
- "src/seedbuild/frontmatter.ts"() {
436
- FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
437
- }
438
- });
439
- function parseDiscipline(val) {
440
- if (!val) return void 0;
441
- const normalized = unquoteYaml(val.trim()).toLowerCase();
442
- if (wodWikiLang.isEffortDiscipline(normalized)) return normalized;
443
- console.warn(`[effort-file] unknown discipline '${val}' \u2014 treating as unset`);
444
- return void 0;
445
- }
446
- function unquoteYaml(val) {
447
- if (val.startsWith('"') && val.endsWith('"')) {
448
- return val.slice(1, -1).replace(/\\(["\\])/g, "$1");
449
- }
450
- if (val.startsWith("'") && val.endsWith("'")) {
451
- return val.slice(1, -1).replace(/\\'/g, "'");
452
- }
453
- return val;
454
- }
455
- function coerceNestedScalar(raw) {
456
- if (raw === "true") return true;
457
- if (raw === "false") return false;
458
- if (raw !== "" && !Number.isNaN(Number(raw))) return Number(raw);
459
- return raw;
460
- }
461
- function readNestedSection(lines, key) {
462
- const start = lines.findIndex((l) => new RegExp(`^${key}:\\s*$`).test(l));
463
- if (start === -1) return void 0;
464
- const result = {};
465
- let subKey = null;
466
- for (let i = start + 1; i < lines.length; i++) {
467
- const line = lines[i];
468
- if (/^\S/.test(line)) break;
469
- const trimmed = line.trim();
470
- if (!trimmed || trimmed.startsWith("#")) continue;
471
- const indent = line.length - line.trimStart().length;
472
- const m = trimmed.match(/^([\w.-]+):\s*(.*)$/);
473
- if (!m) continue;
474
- const [, k, rawVal] = m;
475
- if (indent === 2) {
476
- if (rawVal === "") {
477
- subKey = k;
478
- result[k] = {};
479
- } else {
480
- subKey = null;
481
- result[k] = coerceNestedScalar(rawVal);
482
- }
483
- } else if (indent >= 4 && subKey !== null) {
484
- result[subKey][k] = coerceNestedScalar(rawVal);
485
- }
486
- }
487
- return Object.keys(result).length > 0 ? result : void 0;
488
- }
489
- function parseEffortFile(raw) {
490
- const blockMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
491
- if (!blockMatch) return null;
492
- const { meta, body } = parseFrontmatter(raw);
493
- const innerLines = blockMatch[1].split(/\r?\n/);
494
- const id = getScalar(meta, "id");
495
- const slug = getScalar(meta, "slug");
496
- const label = getScalar(meta, "label");
497
- const aliases = getList(meta, "aliases");
498
- const baseAttrs = readNestedSection(innerLines, "baseAttributes");
499
- const hints = readNestedSection(innerLines, "hints");
500
- const derivation = readNestedSection(innerLines, "derivation");
501
- const metRaw = baseAttrs?.met;
502
- const met = typeof metRaw === "number" ? metRaw : Number(metRaw);
503
- if (!id || !slug || !label || metRaw === void 0 || Number.isNaN(met)) return null;
504
- const discipline = parseDiscipline(
505
- typeof baseAttrs?.discipline === "string" ? baseAttrs.discipline : void 0
506
- );
507
- const intensityTier = baseAttrs?.intensityTier;
508
- const trimmedBody = body.trim();
509
- return {
510
- id: String(id),
511
- slug: String(slug),
512
- label: String(label),
513
- aliases,
514
- baseAttributes: {
515
- met,
516
- ...discipline ? { discipline } : {},
517
- ...intensityTier ? { intensityTier } : {}
518
- },
519
- registrySource: "bundled",
520
- ...trimmedBody ? { body: trimmedBody } : {},
521
- ...hints ? { hints } : {},
522
- ...derivation ? { derivation } : {}
523
- };
524
- }
525
- var init_effort_file = __esm({
526
- "src/seedbuild/effort-file.ts"() {
527
- init_frontmatter();
528
- }
529
- });
530
372
  function resolveGitVersion(repoRoot) {
531
373
  try {
532
374
  const run = (args) => child_process.execSync(`git ${args}`, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
@@ -615,18 +457,93 @@ var init_report = __esm({
615
457
  }
616
458
  });
617
459
 
460
+ // src/seedbuild/frontmatter.ts
461
+ function unquote(value) {
462
+ return value.replace(
463
+ /^(['"])(.*)\1$/,
464
+ (_match, quote, inner) => quote === '"' ? inner.replace(/\\(["\\])/g, "$1") : inner
465
+ );
466
+ }
467
+ function parseFrontmatter(raw) {
468
+ const match = raw.match(FRONTMATTER_RE);
469
+ if (!match) return { meta: {}, body: raw };
470
+ return { meta: parseMetaLines(match[1].split(/\r?\n/)), body: raw.slice(match[0].length) };
471
+ }
472
+ function parseMetaLines(lines) {
473
+ const meta = {};
474
+ for (let i = 0; i < lines.length; i++) {
475
+ const keyMatch = lines[i].match(/^([A-Za-z][\w.-]*)\s*:(.*)$/);
476
+ if (!keyMatch) continue;
477
+ const key = keyMatch[1];
478
+ const rawVal = keyMatch[2].trim();
479
+ if (rawVal === "") {
480
+ const items = [];
481
+ while (i + 1 < lines.length) {
482
+ const itemMatch = lines[i + 1].match(/^\s+-\s+(.+)$/);
483
+ if (!itemMatch) break;
484
+ items.push(itemMatch[1].trim());
485
+ i++;
486
+ }
487
+ meta[key] = items.length > 0 ? items : "";
488
+ } else {
489
+ const wasQuoted = /^(['"])(.*)\1$/.test(rawVal);
490
+ const value = unquote(rawVal);
491
+ const num = Number(value);
492
+ meta[key] = !wasQuoted && value !== "" && !isNaN(num) ? num : value;
493
+ }
494
+ }
495
+ return meta;
496
+ }
497
+ function getScalar(meta, key) {
498
+ const value = meta[key];
499
+ return typeof value === "string" || typeof value === "number" ? value : void 0;
500
+ }
501
+ var FRONTMATTER_RE;
502
+ var init_frontmatter = __esm({
503
+ "src/seedbuild/frontmatter.ts"() {
504
+ FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
505
+ }
506
+ });
507
+ function object(value) {
508
+ return typeof value === "object" && value !== null && !Array.isArray(value);
509
+ }
510
+ function nonnegative(value) {
511
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
512
+ }
513
+ function assertSeedManifest2(value) {
514
+ if (!object(value) || value.schema !== SEED_SCHEMA2) throw new Error("Incompatible seed schema; rebuild seed artifacts");
515
+ if (!nonnegative(value.version) || typeof value.builtAt !== "string" || !Number.isFinite(Date.parse(value.builtAt)) || !Array.isArray(value.chunks)) throw new Error("seed manifest failed shape validation");
516
+ for (const key of ["versionLabel", "commit", "name", "description"]) if (value[key] !== void 0 && typeof value[key] !== "string") throw new Error("invalid seed manifest metadata");
517
+ if (value.repositoryUrl !== void 0 && (typeof value.repositoryUrl !== "string" || !/^https:\/\//.test(value.repositoryUrl) || new URL(value.repositoryUrl).username || new URL(value.repositoryUrl).password)) throw new Error("invalid seed repository URL");
518
+ if (value.dirty !== void 0 && typeof value.dirty !== "boolean") throw new Error("invalid seed manifest metadata");
519
+ const ids = /* @__PURE__ */ new Set(), paths = /* @__PURE__ */ new Set();
520
+ for (const chunk of value.chunks) {
521
+ if (!object(chunk) || typeof chunk.id !== "string" || !chunk.id || typeof chunk.path !== "string" || !/^chunks\/[a-zA-Z0-9._-]+\.json$/.test(chunk.path) || typeof chunk.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(chunk.sha256) || !nonnegative(chunk.bytes) || !nonnegative(chunk.count) || chunk.kind !== void 0 && chunk.kind !== "notes" || ids.has(chunk.id) || paths.has(chunk.path)) throw new Error("seed manifest failed shape validation");
522
+ ids.add(chunk.id);
523
+ paths.add(chunk.path);
524
+ }
525
+ return value;
526
+ }
527
+ var init_validation = __esm({
528
+ "src/seedbuild/validation.ts"() {
529
+ init_contract();
530
+ }
531
+ });
532
+
618
533
  // src/seedbuild/compiler.ts
619
534
  var compiler_exports = {};
620
535
  __export(compiler_exports, {
621
536
  buildBlockEffortRows: () => buildBlockEffortRows,
537
+ buildBlockEquipmentRows: () => buildBlockEquipmentRows,
622
538
  buildBlockIndexRows: () => buildBlockIndexRows,
623
539
  chunkIdFor: () => chunkIdFor,
624
540
  collectCorpusMarkdown: () => collectCorpusMarkdown,
625
541
  compileSeedRow: () => compileSeedRow,
626
542
  discoverUncatalogedEfforts: () => discoverUncatalogedEfforts,
627
543
  feedDateToCreatedAt: () => feedDateToCreatedAt,
544
+ frontmatterDateToCreatedAt: () => frontmatterDateToCreatedAt,
628
545
  generateSeed: () => generateSeed,
629
- loadExistingEffortMap: () => loadExistingEffortMap,
546
+ loadCorpusReferenceCatalog: () => loadCorpusReferenceCatalog,
630
547
  partitionIntoChunks: () => partitionIntoChunks,
631
548
  scaffoldDiscoveredEfforts: () => scaffoldDiscoveredEfforts,
632
549
  validateDashboardNote: () => validateDashboardNote,
@@ -652,6 +569,7 @@ function chunkIdFor(relPath) {
652
569
  case "canvas":
653
570
  case "dashboards":
654
571
  case "efforts":
572
+ case "equipment":
655
573
  return corpus;
656
574
  case "collections":
657
575
  return parts.length === 3 ? `collection._root` : `collection.${slug}`;
@@ -670,7 +588,7 @@ function partitionIntoChunks(paths) {
670
588
  const id = chunkIdFor(p);
671
589
  if (id === null) {
672
590
  throw new Error(
673
- `[generate-seed] Parity failure: '${p}' is not part of any known corpus (canvas | dashboards | efforts | collections | feeds). Extend chunkIdFor() if this is a new corpus.`
591
+ `[generate-seed] Parity failure: '${p}' is not part of any known corpus (canvas | dashboards | efforts | equipment | collections | feeds). Extend chunkIdFor() if this is a new corpus.`
674
592
  );
675
593
  }
676
594
  const list = byId.get(id);
@@ -726,6 +644,18 @@ function getFileCreatedAt(file, repoRoot) {
726
644
  return 0;
727
645
  }
728
646
  }
647
+ function frontmatterDateToCreatedAt(raw, path) {
648
+ const value = getScalar(parseFrontmatter(raw).meta, "date");
649
+ if (value === void 0 || value === "") return void 0;
650
+ const text = String(value);
651
+ const parts = text.match(/^(\d{4})-(\d{2})-(\d{2})$/);
652
+ const ts = parts ? Date.parse(`${text}T12:00:00Z`) : NaN;
653
+ const valid = !!parts && Number.isFinite(ts) && new Date(ts).getUTCMonth() === Number(parts[2]) - 1 && new Date(ts).getUTCDate() === Number(parts[3]);
654
+ if (!valid) {
655
+ throw new Error(`[wod build] invalid frontmatter date '${text}' in '${path}' \u2014 expected a real calendar day (YYYY-MM-DD)`);
656
+ }
657
+ return ts;
658
+ }
729
659
  function feedDateToCreatedAt(dateKey) {
730
660
  if (!/^\d{4}-\d{2}-\d{2}$/.test(dateKey)) return 0;
731
661
  const ts = Date.parse(`${dateKey}T00:00:00Z`);
@@ -740,7 +670,7 @@ function compileSeedRow(path, content, metadata = {}) {
740
670
  const section = sections.find((section2) => section2.from === block.from && section2.to === block.to);
741
671
  return { id: `${noteId}:${block.id}`, noteId, blockId: block.id, blockVersion: 1, sourceRevision: 1, from: block.from, to: block.to, position, dataType: block.kind, ...block.kind === "wod" ? { blockContentId: wodWikiCore.blockContentId(section?.scriptBlock?.content ?? content.slice(block.from, block.to)) } : {}, rawContent: content.slice(block.from, block.to), noteTitle: metadata.noteTitle ?? fileToDisplayName(path.split("/").at(-1) ?? path), createdAt: metadata.createdAt ?? 0, isStatic: true, sourceId: metadata.sourceId, sourcePath: path, routeId: metadata.routeId };
742
672
  });
743
- return { path, content, blocks, blockIndex, blockEfforts: [] };
673
+ return { path, content, blocks, blockIndex, blockEfforts: [], blockEquipment: [] };
744
674
  }
745
675
  function buildBlockIndexRows(markdownDir, corpusRoot) {
746
676
  const rows = [];
@@ -753,7 +683,7 @@ function buildBlockIndexRows(markdownDir, corpusRoot) {
753
683
  const catalog = parts[2];
754
684
  const routeId = chunkId === "canvas" ? canvasRouteSlug(content) ?? void 0 : chunkId.startsWith("collection.") ? stem.toLowerCase() === "readme" ? catalog : `${catalog}/${stem}` : chunkId.startsWith("feed.") ? parts.slice(1).join("/").replace(/\.md$/, "") : void 0;
755
685
  const sourceId = seedNoteSourceId(path$1, content, chunkId);
756
- const createdAt = chunkId.startsWith("feed.") ? feedDateToCreatedAt(parts.at(-2) ?? "") : getFileCreatedAt(path$1, corpusRoot);
686
+ const createdAt = frontmatterDateToCreatedAt(content, path$1) ?? (chunkId.startsWith("feed.") ? feedDateToCreatedAt(parts.at(-2) ?? "") : getFileCreatedAt(path$1, corpusRoot));
757
687
  rows.push(...compileSeedRow(path$1, content, { routeId, sourceId, createdAt }).blockIndex);
758
688
  }
759
689
  return rows;
@@ -790,41 +720,37 @@ function formatDiscoveredLabel(raw) {
790
720
  const clean = raw.replace(/[-_]+/g, " ").trim();
791
721
  return clean.split(/\s+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(" ");
792
722
  }
793
- function loadExistingEffortMap(markdownDir) {
794
- const slugs = /* @__PURE__ */ new Set();
795
- const aliasToCanonical = /* @__PURE__ */ new Map();
723
+ function loadCorpusReferenceCatalog(markdownDir) {
724
+ const efforts = [];
725
+ const equipment = [];
796
726
  const effortsDir = path.join(markdownDir, "efforts");
797
727
  for (const file of listMarkdownFiles(effortsDir)) {
798
- const raw = fs.readFileSync(path.join(effortsDir, file), "utf8");
799
- const parsed = parseEffortFile(raw);
800
- if (!parsed) continue;
801
- slugs.add(parsed.slug);
802
- aliasToCanonical.set(parsed.slug, parsed.slug);
803
- aliasToCanonical.set(parsed.label.toLowerCase(), parsed.slug);
804
- aliasToCanonical.set(parsed.slug.replace(/-/g, " "), parsed.slug);
805
- for (const alias of parsed.aliases) {
806
- aliasToCanonical.set(alias.toLowerCase(), parsed.slug);
807
- aliasToCanonical.set(alias.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""), parsed.slug);
808
- }
728
+ const parsed = wodWikiLang.parseEffortFile(fs.readFileSync(path.join(effortsDir, file), "utf8"));
729
+ if (parsed) efforts.push(parsed);
730
+ }
731
+ const equipmentDir = path.join(markdownDir, "equipment");
732
+ for (const file of listMarkdownFiles(equipmentDir)) {
733
+ const parsed = wodWikiLang.parseEquipmentFile(fs.readFileSync(path.join(equipmentDir, file), "utf8"));
734
+ if (parsed) equipment.push(parsed);
809
735
  }
810
- return { slugs, aliasToCanonical };
736
+ return { efforts, equipment };
811
737
  }
812
738
  function discoverUncatalogedEfforts(markdownDir, corpusRoot) {
813
- const { slugs, aliasToCanonical } = loadExistingEffortMap(markdownDir);
739
+ const catalog = loadCorpusReferenceCatalog(markdownDir);
740
+ const index = wodWikiLang.createReferenceIndex(catalog);
814
741
  const blocks = buildBlockIndexRows(markdownDir, corpusRoot);
815
742
  const discovered = /* @__PURE__ */ new Map();
816
743
  for (const block of blocks) {
817
744
  if (block.dataType !== "wod" || !block.rawContent) continue;
818
745
  const section = wodWikiCore.parseDocumentSections(block.rawContent, { registeredWidgets: SEED_REGISTERED_WIDGETS }).find((section2) => section2.scriptBlock);
819
- const script = wodWikiLang.parseScript(section?.scriptBlock?.content ?? block.rawContent);
746
+ const script = wodWikiLang.parseScript(section?.scriptBlock?.content ?? block.rawContent, { references: index });
820
747
  for (const statement of script.statements) {
821
748
  const effortMetric = statement.metrics?.getMetric?.("effort");
822
749
  const effortLabel = (effortMetric && typeof effortMetric.value === "string" ? effortMetric.value : void 0) ?? statement.exerciseId;
823
750
  if (!effortLabel) continue;
824
- const slugified = effortLabel.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
751
+ const slugified = wodWikiLang.slugifyRefLabel(effortLabel);
825
752
  if (!slugified || NON_EFFORT_SLUGS[slugified]) continue;
826
- const canonical = aliasToCanonical.get(slugified) ?? aliasToCanonical.get(effortLabel.toLowerCase());
827
- if (canonical && slugs.has(canonical)) continue;
753
+ if (index.resolveEffort(slugified) || index.resolveEffort(effortLabel)) continue;
828
754
  const existing = discovered.get(slugified);
829
755
  if (existing) {
830
756
  existing.rawLabels.add(effortLabel);
@@ -874,17 +800,16 @@ ${aliases.join("\n")}` : "aliases: []",
874
800
  }
875
801
  function buildBlockEffortRows(markdownDir, corpusRoot, blocks = buildBlockIndexRows(markdownDir, corpusRoot)) {
876
802
  const efforts = [];
877
- const { aliasToCanonical } = loadExistingEffortMap(markdownDir);
803
+ const index = wodWikiLang.createReferenceIndex(loadCorpusReferenceCatalog(markdownDir));
878
804
  for (const block of blocks) {
879
805
  if (block.dataType !== "wod" || !block.rawContent) continue;
880
806
  const section = wodWikiCore.parseDocumentSections(block.rawContent, { registeredWidgets: SEED_REGISTERED_WIDGETS }).find((section2) => section2.scriptBlock);
881
- const script = wodWikiLang.parseScript(section?.scriptBlock?.content ?? block.rawContent);
882
- const effortSlugs = extractEffortSlugs2(script.statements);
807
+ const script = wodWikiLang.parseScript(section?.scriptBlock?.content ?? block.rawContent, { references: index });
883
808
  const linked = /* @__PURE__ */ new Set();
884
- for (const rawSlug of effortSlugs) {
885
- const norm = rawSlug.toLowerCase().trim();
886
- const canonicalSlug = aliasToCanonical.get(norm) ?? aliasToCanonical.get(norm.replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "")) ?? norm.replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
887
- if (!canonicalSlug || NON_EFFORT_SLUGS[canonicalSlug] || NON_EFFORT_SLUGS[norm]) {
809
+ for (const rawSlug of wodWikiLang.extractStatementRefs(script.statements).efforts) {
810
+ const canonical = index.resolveEffort(rawSlug);
811
+ const canonicalSlug = canonical?.slug;
812
+ if (!canonicalSlug || NON_EFFORT_SLUGS[canonicalSlug] || NON_EFFORT_SLUGS[rawSlug]) {
888
813
  continue;
889
814
  }
890
815
  if (linked.has(canonicalSlug)) continue;
@@ -903,6 +828,32 @@ function buildBlockEffortRows(markdownDir, corpusRoot, blocks = buildBlockIndexR
903
828
  }
904
829
  return efforts;
905
830
  }
831
+ function buildBlockEquipmentRows(markdownDir, corpusRoot, blocks = buildBlockIndexRows(markdownDir, corpusRoot)) {
832
+ const links = [];
833
+ const index = wodWikiLang.createReferenceIndex(loadCorpusReferenceCatalog(markdownDir));
834
+ for (const block of blocks) {
835
+ if (block.dataType !== "wod" || !block.rawContent) continue;
836
+ const section = wodWikiCore.parseDocumentSections(block.rawContent, { registeredWidgets: SEED_REGISTERED_WIDGETS }).find((s) => s.scriptBlock);
837
+ const script = wodWikiLang.parseScript(section?.scriptBlock?.content ?? block.rawContent, { references: index });
838
+ const linked = /* @__PURE__ */ new Set();
839
+ for (const rawSlug of wodWikiLang.extractStatementRefs(script.statements).equipment) {
840
+ const canonicalSlug = index.resolveEquipment(rawSlug)?.slug;
841
+ if (!canonicalSlug || linked.has(canonicalSlug)) continue;
842
+ linked.add(canonicalSlug);
843
+ links.push({
844
+ id: `${block.id}:${canonicalSlug}`,
845
+ noteId: block.noteId,
846
+ blockId: block.blockId,
847
+ blockContentId: block.blockContentId,
848
+ equipmentSlug: canonicalSlug,
849
+ isStatic: block.isStatic,
850
+ sourcePath: block.sourcePath,
851
+ createdAt: block.createdAt
852
+ });
853
+ }
854
+ }
855
+ return links;
856
+ }
906
857
  function sha256Hex(data) {
907
858
  return crypto.createHash("sha256").update(data).digest("hex");
908
859
  }
@@ -913,6 +864,13 @@ function generateSeed(options = {}) {
913
864
  const markdownDir = options.markdownDir ?? path.join(corpusRoot, "markdown");
914
865
  const outDir = options.outDir ?? path.join(corpusRoot, "dist");
915
866
  const gitVersion = options.version !== void 0 ? { ...resolveGitVersion(corpusRoot), version: options.version } : resolveGitVersion(corpusRoot);
867
+ const packageFile = path.join(corpusRoot, "package.json");
868
+ const packageMetadata = fs.existsSync(packageFile) ? JSON.parse(fs.readFileSync(packageFile, "utf8")) : {};
869
+ const metadata = packageMetadata && typeof packageMetadata === "object" ? packageMetadata : {};
870
+ const repository = "repository" in metadata ? metadata.repository : void 0;
871
+ const repositoryUrl = typeof repository === "string" ? repository : repository && typeof repository === "object" && "url" in repository && typeof repository.url === "string" ? repository.url : void 0;
872
+ const webRepositoryUrl = repositoryUrl?.replace(/^git\+/, "").replace(/\.git$/, "");
873
+ const repositoryLink = webRepositoryUrl && URL.canParse(webRepositoryUrl) ? new URL(webRepositoryUrl) : void 0;
916
874
  if (options.scaffoldDiscoveredEfforts) {
917
875
  const discovered = discoverUncatalogedEfforts(markdownDir, corpusRoot);
918
876
  if (discovered.length > 0) {
@@ -947,9 +905,11 @@ ${shown}${more}`);
947
905
  const manifestChunks = [];
948
906
  const tBlockEfforts = Date.now();
949
907
  const effortRows = buildBlockEffortRows(markdownDir, corpusRoot, blockRows);
908
+ const equipmentRows = buildBlockEquipmentRows(markdownDir, corpusRoot, blockRows);
950
909
  passMs.blockEfforts = Date.now() - tBlockEfforts;
951
910
  const blocksByPath = /* @__PURE__ */ new Map();
952
911
  const effortsByPath = /* @__PURE__ */ new Map();
912
+ const equipmentByPath = /* @__PURE__ */ new Map();
953
913
  for (const row of blockRows) {
954
914
  const rows = blocksByPath.get(row.sourcePath) ?? [];
955
915
  rows.push(row);
@@ -960,6 +920,11 @@ ${shown}${more}`);
960
920
  rows.push(row);
961
921
  effortsByPath.set(row.sourcePath, rows);
962
922
  }
923
+ for (const row of equipmentRows) {
924
+ const rows = equipmentByPath.get(row.sourcePath) ?? [];
925
+ rows.push(row);
926
+ equipmentByPath.set(row.sourcePath, rows);
927
+ }
963
928
  let totalFiles = 0;
964
929
  const tWrite = Date.now();
965
930
  for (const { id, files } of groups) {
@@ -970,6 +935,7 @@ ${shown}${more}`);
970
935
  const compiled = compileSeedRow(p, content);
971
936
  compiled.blockIndex = blocksByPath.get(p) ?? compiled.blockIndex;
972
937
  compiled.blockEfforts = effortsByPath.get(p) ?? [];
938
+ compiled.blockEquipment = equipmentByPath.get(p) ?? [];
973
939
  return compiled;
974
940
  });
975
941
  const bytes = Buffer.from(JSON.stringify(rows), "utf8");
@@ -981,6 +947,9 @@ ${shown}${more}`);
981
947
  }
982
948
  passMs.write = Date.now() - tWrite;
983
949
  const manifest = {
950
+ name: "name" in metadata && typeof metadata.name === "string" ? metadata.name : void 0,
951
+ description: "description" in metadata && typeof metadata.description === "string" ? metadata.description : void 0,
952
+ repositoryUrl: repositoryLink?.protocol === "https:" && !repositoryLink.username && !repositoryLink.password ? repositoryLink.href : void 0,
984
953
  schema: SEED_SCHEMA2,
985
954
  version: gitVersion.version,
986
955
  versionLabel: gitVersion.versionLabel,
@@ -989,6 +958,7 @@ ${shown}${more}`);
989
958
  builtAt: (/* @__PURE__ */ new Date()).toISOString(),
990
959
  chunks: manifestChunks
991
960
  };
961
+ assertSeedManifest2(manifest);
992
962
  fs.writeFileSync(path.join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
993
963
  const reportPath = writeBuildReport(outDir, {
994
964
  manifest,
@@ -1001,12 +971,11 @@ var NON_EFFORT_SLUGS;
1001
971
  var init_compiler = __esm({
1002
972
  "src/seedbuild/compiler.ts"() {
1003
973
  init_contract();
1004
- init_effort_slugs();
1005
974
  init_filename();
1006
- init_effort_file();
1007
975
  init_git_version();
1008
976
  init_report();
1009
977
  init_frontmatter();
978
+ init_validation();
1010
979
  NON_EFFORT_SLUGS = {
1011
980
  "amrap": true,
1012
981
  "emom": true,
@@ -1056,6 +1025,219 @@ var init_compiler = __esm({
1056
1025
  }
1057
1026
  });
1058
1027
 
1028
+ // src/version.ts
1029
+ var VERSION;
1030
+ var init_version = __esm({
1031
+ "src/version.ts"() {
1032
+ VERSION = "0.37.2139";
1033
+ }
1034
+ });
1035
+
1036
+ // src/cli/scaffoldCatalog.ts
1037
+ var scaffoldCatalog_exports = {};
1038
+ __export(scaffoldCatalog_exports, {
1039
+ scaffoldCatalog: () => scaffoldCatalog,
1040
+ validateCatalogName: () => validateCatalogName
1041
+ });
1042
+ function validateCatalogName(name) {
1043
+ if (name.length === 0 || name === "." || name === "..") {
1044
+ throw new Error("Catalog name must be a single non-empty directory name");
1045
+ }
1046
+ if (name.includes("/") || name.includes("\\") || name.includes("\0")) {
1047
+ throw new Error(`Invalid catalog name '${name}': must be a single directory name (no path separators or traversal)`);
1048
+ }
1049
+ if (/^(?:[A-Za-z]:)?[/\\]/.test(name)) {
1050
+ throw new Error(`Invalid catalog name '${name}': absolute paths are not allowed`);
1051
+ }
1052
+ if (name.length > 214 || !/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
1053
+ throw new Error(
1054
+ `Invalid catalog name '${name}': must be a valid npm package name (lowercase letters, digits, dots, hyphens, underscores; no leading dot/hyphen)`
1055
+ );
1056
+ }
1057
+ return name;
1058
+ }
1059
+ function scaffoldCatalog(name, opts = {}) {
1060
+ validateCatalogName(name);
1061
+ const dir = path.resolve(opts.cwd ?? process.cwd(), name);
1062
+ try {
1063
+ fs.mkdirSync(dir);
1064
+ } catch (error) {
1065
+ if (error.code === "EEXIST") {
1066
+ throw new Error(`Refusing to overwrite '${dir}': destination already exists`, { cause: error });
1067
+ }
1068
+ throw error;
1069
+ }
1070
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1071
+ const files = [
1072
+ ["package.json", `${JSON.stringify(
1073
+ {
1074
+ name,
1075
+ version: "0.1.0",
1076
+ private: true,
1077
+ type: "module",
1078
+ description: `WOD Wiki workout catalog: ${name}`,
1079
+ scripts: {
1080
+ build: "wod build . --output dist"
1081
+ },
1082
+ devDependencies: {
1083
+ "@bitcobblers/wod-wiki-tools": `^${VERSION}`
1084
+ }
1085
+ },
1086
+ null,
1087
+ 2
1088
+ )}
1089
+ `],
1090
+ [".gitignore", "node_modules/\ndist/\n"],
1091
+ [".github/workflows/publish-catalog.yml", publishWorkflow()],
1092
+ ["markdown/collections/example/README.md", collectionReadme(name)],
1093
+ [`markdown/collections/example/morning-session.md`, exampleWorkout(date)],
1094
+ ["README.md", catalogReadme(name)]
1095
+ ];
1096
+ try {
1097
+ for (const [rel, content] of files) {
1098
+ const target = path.join(dir, rel);
1099
+ fs.mkdirSync(path.dirname(target), { recursive: true });
1100
+ fs.writeFileSync(target, content, "utf-8");
1101
+ }
1102
+ } catch (error) {
1103
+ fs.rmSync(dir, { recursive: true, force: true });
1104
+ throw error;
1105
+ }
1106
+ return { dir, files: files.map(([rel]) => rel) };
1107
+ }
1108
+ var publishWorkflow, collectionReadme, exampleWorkout, catalogReadme;
1109
+ var init_scaffoldCatalog = __esm({
1110
+ "src/cli/scaffoldCatalog.ts"() {
1111
+ init_version();
1112
+ publishWorkflow = () => `name: Publish catalog
1113
+
1114
+ # Compile the catalog with the wod CLI and deploy dist/ to GitHub Pages.
1115
+ # Requires Pages enabled: repo Settings \u2192 Pages \u2192 Source: "GitHub Actions".
1116
+
1117
+ on:
1118
+ push:
1119
+ branches: [main]
1120
+ workflow_dispatch:
1121
+
1122
+ permissions:
1123
+ contents: read
1124
+ pages: write
1125
+ id-token: write
1126
+
1127
+ concurrency:
1128
+ group: pages
1129
+ cancel-in-progress: true
1130
+
1131
+ jobs:
1132
+ publish:
1133
+ runs-on: ubuntu-latest
1134
+ environment:
1135
+ name: github-pages
1136
+ url: \${{ steps.deployment.outputs.page_url }}
1137
+ steps:
1138
+ - uses: actions/checkout@v4
1139
+ with:
1140
+ # Full history: seed manifest version stamps derive from git commit count.
1141
+ fetch-depth: 0
1142
+
1143
+ - name: Set up Bun
1144
+ uses: oven-sh/setup-bun@v2
1145
+ with:
1146
+ bun-version: 1.4.x
1147
+
1148
+ - name: Install dependencies
1149
+ run: bun install
1150
+
1151
+ - name: Build seed
1152
+ run: bunx wod build . --output dist
1153
+
1154
+ - uses: actions/configure-pages@v5
1155
+
1156
+ - name: Upload dist
1157
+ uses: actions/upload-pages-artifact@v3
1158
+ with:
1159
+ path: dist
1160
+
1161
+ - id: deployment
1162
+ uses: actions/deploy-pages@v4
1163
+ `;
1164
+ collectionReadme = (name) => `# ${name}
1165
+
1166
+ Example benchmark collection. Each \`.md\` file in this directory becomes part of
1167
+ the \`example\` collection chunk; \`README.md\` is the collection's landing page.
1168
+
1169
+ Add one file per workout:
1170
+
1171
+ \`\`\`\`markdown
1172
+ ---
1173
+ title: My workout
1174
+ date: 2026-01-01
1175
+ ---
1176
+
1177
+ # My workout
1178
+
1179
+ \`\`\`time
1180
+ (21-15-9)
1181
+ Thrusters @95lb
1182
+ Pull-ups
1183
+ \`\`\`
1184
+ \`\`\`\`
1185
+
1186
+ The \`date:\` frontmatter field sets the workout's chronological placement
1187
+ (regardless of filename or directory).
1188
+ `;
1189
+ exampleWorkout = (date) => `---
1190
+ title: Morning session
1191
+ date: ${date}
1192
+ ---
1193
+
1194
+ # Morning session
1195
+
1196
+ \`\`\`time
1197
+ (21-15-9)
1198
+ Thrusters @95lb
1199
+ Pull-ups
1200
+ \`\`\`
1201
+ `;
1202
+ catalogReadme = (name) => `# ${name}
1203
+
1204
+ A [WOD Wiki](https://github.com/bitcobblers/wod-wiki) workout catalog: plain
1205
+ Markdown workouts compiled to static, version-stamped seed chunks and published
1206
+ via GitHub Pages.
1207
+
1208
+ ## Develop
1209
+
1210
+ \`\`\`sh
1211
+ bun install
1212
+ bunx wod build . --output dist
1213
+ \`\`\`
1214
+
1215
+ Workouts live under \`markdown/\` \u2014 this scaffold starts with one example
1216
+ collection. Validate everything with \`bunx wod build\`: it gates every file on
1217
+ the Whiteboard grammar and corpus layout before writing \`dist/\`.
1218
+
1219
+ ## Publish
1220
+
1221
+ Push to GitHub and enable Pages (**Settings \u2192 Pages \u2192 Source: GitHub
1222
+ Actions**). The bundled workflow (\`.github/workflows/publish-catalog.yml\`)
1223
+ builds the catalog on every push to \`main\` and deploys \`dist/\`.
1224
+
1225
+ ## Share
1226
+
1227
+ Once published, anyone can subscribe to this catalog from a WOD Wiki instance
1228
+ at:
1229
+
1230
+ \`\`\`
1231
+ <app-url>/subscribe?source=<url-encoded published catalog URL>
1232
+ \`\`\`
1233
+
1234
+ For \`owner\`/\`repo\` on GitHub Pages the catalog URL is the deployed
1235
+ directory, \`https://owner.github.io/repo/\`, e.g.
1236
+ \`/subscribe?source=https%3A%2F%2Fowner.github.io%2Frepo%2F\`.
1237
+ `;
1238
+ }
1239
+ });
1240
+
1059
1241
  // src/server/errors.ts
1060
1242
  var HttpError;
1061
1243
  var init_errors = __esm({
@@ -1359,13 +1541,13 @@ __export(server_exports, {
1359
1541
  runCliServer: () => runCliServer
1360
1542
  });
1361
1543
  function withResolvers() {
1362
- let resolve2;
1544
+ let resolve3;
1363
1545
  let reject;
1364
1546
  const promise = new Promise((res, rej) => {
1365
- resolve2 = res;
1547
+ resolve3 = res;
1366
1548
  reject = rej;
1367
1549
  });
1368
- return { promise, resolve: resolve2, reject };
1550
+ return { promise, resolve: resolve3, reject };
1369
1551
  }
1370
1552
  function startHttpServer(handler, host, port) {
1371
1553
  const b = globalThis.Bun;
@@ -1373,7 +1555,7 @@ function startHttpServer(handler, host, port) {
1373
1555
  const s = b.serve({ port, hostname: host, fetch: handler });
1374
1556
  return Promise.resolve({ stop: () => s.stop() });
1375
1557
  }
1376
- const { promise, resolve: resolve2, reject } = withResolvers();
1558
+ const { promise, resolve: resolve3, reject } = withResolvers();
1377
1559
  const server = http__default.default.createServer(async (req, res) => {
1378
1560
  try {
1379
1561
  const url = new URL(req.url || "/", `http://${req.headers.host || `${host}:${port}`}`);
@@ -1398,7 +1580,7 @@ function startHttpServer(handler, host, port) {
1398
1580
  });
1399
1581
  server.once("error", reject);
1400
1582
  server.listen(port, host, () => {
1401
- resolve2({
1583
+ resolve3({
1402
1584
  stop: () => {
1403
1585
  const close = withResolvers();
1404
1586
  server.close(() => close.resolve());
@@ -1542,8 +1724,8 @@ async function loadLanguagePack(moduleSpec) {
1542
1724
  try {
1543
1725
  let importPath = moduleSpec;
1544
1726
  if (moduleSpec.startsWith(".") || moduleSpec.startsWith("/") || moduleSpec.includes("\\") || moduleSpec.endsWith(".ts") || moduleSpec.endsWith(".js") || moduleSpec.endsWith(".mjs")) {
1545
- const { resolve: resolve2, pathToFileURL } = getPathUtils();
1546
- const resolved = resolve2(process.cwd(), moduleSpec);
1727
+ const { resolve: resolve3, pathToFileURL } = getPathUtils();
1728
+ const resolved = resolve3(process.cwd(), moduleSpec);
1547
1729
  importPath = pathToFileURL(resolved).href;
1548
1730
  }
1549
1731
  const imported = await import(
@@ -2145,23 +2327,7 @@ function resolveDatabaseConfig(options = {}, env) {
2145
2327
  // src/cli/persist.ts
2146
2328
  init_db();
2147
2329
  var registeredWidgets = wodWikiCore.KNOWN_SECTION_WIDGETS;
2148
- var saveStores = ["notes", "block_revisions", "block_index", "block_efforts", "tags", "note_tags", "sessions"];
2149
- function extractEffortSlugs(statements) {
2150
- const slugs = /* @__PURE__ */ new Set();
2151
- for (const statement of statements) {
2152
- if (statement.exerciseId) {
2153
- slugs.add(statement.exerciseId);
2154
- } else {
2155
- const effortMetric = statement.metrics?.getMetric?.("effort");
2156
- const effort = effortMetric && "effort" in effortMetric ? effortMetric.effort : void 0;
2157
- if (typeof effort === "string") {
2158
- const slug = effort.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
2159
- if (slug) slugs.add(slug);
2160
- }
2161
- }
2162
- }
2163
- return [...slugs];
2164
- }
2330
+ var saveStores = ["notes", "block_revisions", "block_index", "block_efforts", "block_equipment", "efforts", "equipment", "tags", "note_tags", "sessions"];
2165
2331
  function titleFromLabel(sourceLabel) {
2166
2332
  if (!sourceLabel || sourceLabel === "-") return "stdin";
2167
2333
  const stem = sourceLabel.split("/").pop() ?? sourceLabel;
@@ -2233,9 +2399,19 @@ async function saveSource(db, options, startTime) {
2233
2399
  };
2234
2400
  const priorRows = await tx.readonly("block_index").getAllFromIndex("by-note", noteId);
2235
2401
  const priorEfforts = await tx.readonly("block_efforts").getAllFromIndex("by-note", noteId);
2402
+ const priorEquipment = await tx.readonly("block_equipment").getAllFromIndex("by-note", noteId);
2403
+ const storedEfforts = await tx.readonly("efforts").getAll();
2404
+ const effortsBySlug = /* @__PURE__ */ new Map();
2405
+ for (const effort of wodWikiLang.bundledEfforts) effortsBySlug.set(effort.slug, effort);
2406
+ for (const effort of storedEfforts) effortsBySlug.set(effort.slug, effort);
2407
+ const referenceIndex = wodWikiLang.createReferenceIndex({
2408
+ efforts: [...effortsBySlug.values()],
2409
+ equipment: await tx.readonly("equipment").getAll()
2410
+ });
2236
2411
  const priorById = new Map(priorRows.map((row) => [row.id, row]));
2237
2412
  const nextRows = [];
2238
2413
  const nextEfforts = [];
2414
+ const nextEquipment = [];
2239
2415
  for (const [position, block] of Object.values(blocks).sort((a, b) => a.from - b.from).entries()) {
2240
2416
  const id = `${noteId}:${block.id}`;
2241
2417
  const prior = priorById.get(id);
@@ -2261,7 +2437,20 @@ async function saveSource(db, options, startTime) {
2261
2437
  ...note.sourceId ? { sourceId: note.sourceId } : {}
2262
2438
  });
2263
2439
  if (block.kind === "wod") {
2264
- for (const effortSlug of extractEffortSlugs(wodWikiLang.parseScript(body, { sport: block.sport, dialect: block.dialect }).statements)) {
2440
+ const statements = wodWikiLang.parseScript(body, { sport: block.sport, dialect: block.dialect, references: referenceIndex }).statements;
2441
+ const refs = wodWikiLang.extractStatementRefs(statements);
2442
+ const effortSlugs = new Set(refs.efforts);
2443
+ if (effortSlugs.size === 0) {
2444
+ for (const s of statements) {
2445
+ const m = s.metrics?.getMetric?.(wodWikiCore.MetricType.Effort);
2446
+ const val = typeof m?.value === "string" ? m.value : m && "effort" in m ? String(m.effort) : "";
2447
+ const slug = val.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
2448
+ if (slug && !["rest", "amrap", "emom", "tabata", "for-time"].includes(slug)) {
2449
+ effortSlugs.add(slug);
2450
+ }
2451
+ }
2452
+ }
2453
+ for (const effortSlug of effortSlugs) {
2265
2454
  const effortId = `${noteId}:${block.id}:${effortSlug}`;
2266
2455
  const previous = priorEfforts.find((row) => row.id === effortId);
2267
2456
  nextEfforts.push({
@@ -2274,12 +2463,26 @@ async function saveSource(db, options, startTime) {
2274
2463
  createdAt: previous?.createdAt ?? now
2275
2464
  });
2276
2465
  }
2466
+ for (const equipmentSlug of refs.equipment) {
2467
+ const equipmentId = `${noteId}:${block.id}:${equipmentSlug}`;
2468
+ const previous = priorEquipment.find((row) => row.id === equipmentId);
2469
+ nextEquipment.push({
2470
+ ...previous,
2471
+ id: equipmentId,
2472
+ noteId,
2473
+ blockId: block.id,
2474
+ blockContentId: fingerprint,
2475
+ equipmentSlug,
2476
+ createdAt: previous?.createdAt ?? now
2477
+ });
2478
+ }
2277
2479
  }
2278
2480
  }
2279
2481
  if (!wodWikiStorage.rowsEqual(existing, note)) await tx.readwrite("notes").put(note);
2280
2482
  if (revisions.length) await tx.readwrite("block_revisions").putMany(revisions);
2281
2483
  await diffProjection(tx, "block_index", priorRows, nextRows);
2282
2484
  await diffProjection(tx, "block_efforts", priorEfforts, nextEfforts);
2485
+ await diffProjection(tx, "block_equipment", priorEquipment, nextEquipment);
2283
2486
  const tags = await saveTags(tx, noteId, options.tags, now);
2284
2487
  let session;
2285
2488
  let revision;
@@ -2315,6 +2518,7 @@ async function diffProjection(tx, store, previous, next) {
2315
2518
  if (!wodWikiStorage.rowsEqual(old.get(row.id), row)) {
2316
2519
  if (store === "block_index" && "blockVersion" in row) await tx.readwrite("block_index").put(row);
2317
2520
  if (store === "block_efforts" && "effortSlug" in row) await tx.readwrite("block_efforts").put(row);
2521
+ if (store === "block_equipment" && "equipmentSlug" in row) await tx.readwrite("block_equipment").put(row);
2318
2522
  }
2319
2523
  }
2320
2524
  }
@@ -2406,6 +2610,12 @@ async function persistEffort(db, effort) {
2406
2610
  await sql.putRow(tx, sql.storeDefinition("efforts"), effort);
2407
2611
  });
2408
2612
  }
2613
+ async function persistEquipment(db, equipment) {
2614
+ const sql = await loadSql();
2615
+ await db.db.transaction(async (tx) => {
2616
+ await sql.putRow(tx, sql.storeDefinition("equipment"), equipment);
2617
+ });
2618
+ }
2409
2619
  async function persistTypedTags(db, labels, type) {
2410
2620
  return db.storage.transaction(["tags"], "readwrite", async (tx) => {
2411
2621
  let added = 0;
@@ -2431,17 +2641,18 @@ async function importSeedDir(db, dir, sourceId) {
2431
2641
  const source = sourceId || path.basename(dir);
2432
2642
  const manifest = wodWikiEngine.assertSeedManifest(JSON.parse(fs.readFileSync(path.resolve(dir, "manifest.json"), "utf-8")));
2433
2643
  const key = wodWikiEngine.seedMetaKey(source);
2434
- let expected = await db.storage.readonly("meta").get(key);
2435
- if (expected && expected.schema !== wodWikiEngine.SEED_SCHEMA) throw new Error("Incompatible seed checkpoint; recreate storage");
2644
+ const stored = await db.storage.readonly("meta").get(key);
2645
+ let expected = stored?.schema === wodWikiEngine.SEED_SCHEMA ? stored : void 0;
2436
2646
  let meta = expected ?? wodWikiEngine.emptySeedMeta(key);
2437
2647
  if (manifest.version < meta.seedVersion) throw new Error("Seed manifest is older than stored checkpoint");
2438
- const summary = { sourceId: source, version: manifest.version, notes: 0, efforts: 0, blocks: 0, blockEfforts: 0, deleted: 0, skippedUserOwned: 0 };
2648
+ const summary = { sourceId: source, version: manifest.version, notes: 0, efforts: 0, equipment: 0, blocks: 0, blockEfforts: 0, deleted: 0, skippedUserOwned: 0 };
2439
2649
  const metadataChanged = manifest.version !== meta.seedVersion || manifest.builtAt !== meta.builtAt;
2440
2650
  const plan = manifest.chunks.filter((c) => metadataChanged || meta.chunks[c.id]?.sha256 !== c.sha256);
2441
2651
  plan.sort((a, b) => Number(b.id === wodWikiEngine.CANVAS_CHUNK_ID) - Number(a.id === wodWikiEngine.CANVAS_CHUNK_ID));
2442
2652
  const accumulate = (result) => {
2443
2653
  summary.notes += result.totalNotes;
2444
2654
  summary.efforts += result.totalEfforts;
2655
+ summary.equipment += result.totalEquipment;
2445
2656
  summary.blocks += result.totalBlocks;
2446
2657
  summary.deleted += result.deleted;
2447
2658
  summary.skippedUserOwned += result.skippedUserOwned;
@@ -2517,6 +2728,7 @@ async function publishCatalogSeed(db, options) {
2517
2728
  }
2518
2729
  const allIndexRows = await db.storage.readonly("block_index").getAll();
2519
2730
  const allEffortRows = await db.storage.readonly("block_efforts").getAll();
2731
+ const allEquipmentRows = await db.storage.readonly("block_equipment").getAll();
2520
2732
  const indexByNote = /* @__PURE__ */ new Map();
2521
2733
  for (const row of allIndexRows) {
2522
2734
  const list = indexByNote.get(row.noteId) ?? [];
@@ -2529,6 +2741,12 @@ async function publishCatalogSeed(db, options) {
2529
2741
  list.push(row);
2530
2742
  effortsByNote.set(row.noteId, list);
2531
2743
  }
2744
+ const equipmentByNote = /* @__PURE__ */ new Map();
2745
+ for (const row of allEquipmentRows) {
2746
+ const list = equipmentByNote.get(row.noteId) ?? [];
2747
+ list.push(row);
2748
+ equipmentByNote.set(row.noteId, list);
2749
+ }
2532
2750
  const outDir = path.resolve(process.cwd(), options.outDir);
2533
2751
  const chunksDir = path.join(outDir, "chunks");
2534
2752
  fs.mkdirSync(chunksDir, { recursive: true });
@@ -2543,6 +2761,7 @@ async function publishCatalogSeed(db, options) {
2543
2761
  const notePath = note.sourcePath ?? `markdown/collections/${catalogName}/${stem}.md`;
2544
2762
  const noteBlockIndex = indexByNote.get(note.id) ?? [];
2545
2763
  const noteBlockEfforts = effortsByNote.get(note.id) ?? [];
2764
+ const noteBlockEquipment = equipmentByNote.get(note.id) ?? [];
2546
2765
  const blockIndex = noteBlockIndex.length > 0 ? noteBlockIndex.map((b) => ({
2547
2766
  ...b,
2548
2767
  isStatic: true,
@@ -2552,7 +2771,9 @@ async function publishCatalogSeed(db, options) {
2552
2771
  })) : compileSeedRow(notePath, note.rawContent, {
2553
2772
  routeId: `${catalogName}/${stem}`,
2554
2773
  sourceId: `collection:${catalogName}/${stem}`,
2555
- createdAt: note.createdAt,
2774
+ // Authored frontmatter date first — exports keep the post's own
2775
+ // day, never the import/publish time.
2776
+ createdAt: note.date ?? note.createdAt,
2556
2777
  noteTitle: note.title
2557
2778
  }).blockIndex;
2558
2779
  const blockEfforts = noteBlockEfforts.map((e) => ({
@@ -2560,12 +2781,18 @@ async function publishCatalogSeed(db, options) {
2560
2781
  isStatic: true,
2561
2782
  sourcePath: e.sourcePath ?? notePath
2562
2783
  }));
2784
+ const blockEquipment = noteBlockEquipment.map((e) => ({
2785
+ ...e,
2786
+ isStatic: true,
2787
+ sourcePath: e.sourcePath ?? notePath
2788
+ }));
2563
2789
  chunkRows.push({
2564
2790
  path: notePath,
2565
2791
  content: note.rawContent,
2566
2792
  blocks: note.blocks,
2567
2793
  blockIndex,
2568
- blockEfforts
2794
+ blockEfforts,
2795
+ blockEquipment
2569
2796
  });
2570
2797
  }
2571
2798
  const chunkId = `collection.${catalogName}`;
@@ -2894,10 +3121,8 @@ ${escapeCsvField(data.parsed.metric)},${data.scalar},${escapeCsvField(data.unit
2894
3121
  return lines.join("\n");
2895
3122
  }
2896
3123
 
2897
- // src/version.ts
2898
- var VERSION = "0.36.2136";
2899
-
2900
3124
  // src/cli/runner.ts
3125
+ init_version();
2901
3126
  function getFs2() {
2902
3127
  if (typeof globalThis.process?.versions?.node === "undefined" && typeof globalThis.Bun === "undefined") {
2903
3128
  throw new Error("File system operations are only supported in Node / Bun environments");
@@ -3049,7 +3274,7 @@ function parseCliArgs(args) {
3049
3274
  if (Number.isSafeInteger(v) && v >= 0) result.seedVersion = v;
3050
3275
  i++;
3051
3276
  } else if (!result.subcommand) {
3052
- if (arg === "parse" || arg === "run" || arg === "query" || arg === "download" || arg === "export" || arg === "import" || arg === "publish" || arg === "build" || arg === "seed" || arg === "reset" || arg === "init" || arg === "server" || arg === "serve" || arg === "playground" || arg === "help" || arg === "version") {
3277
+ if (arg === "parse" || arg === "run" || arg === "query" || arg === "download" || arg === "export" || arg === "import" || arg === "publish" || arg === "build" || arg === "seed" || arg === "reset" || arg === "init" || arg === "server" || arg === "serve" || arg === "playground" || arg === "init-catalog" || arg === "help" || arg === "version") {
3053
3278
  result.subcommand = arg;
3054
3279
  } else {
3055
3280
  result.target = arg;
@@ -3089,13 +3314,13 @@ function readBunStdinText() {
3089
3314
  async function readStdin() {
3090
3315
  const bunText = readBunStdinText();
3091
3316
  if (bunText) return bunText();
3092
- return new Promise((resolve2, reject) => {
3317
+ return new Promise((resolve3, reject) => {
3093
3318
  const chunks = [];
3094
3319
  process.stdin.on("data", (chunk) => {
3095
3320
  chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
3096
3321
  });
3097
3322
  process.stdin.on("end", () => {
3098
- resolve2(Buffer.concat(chunks).toString("utf-8"));
3323
+ resolve3(Buffer.concat(chunks).toString("utf-8"));
3099
3324
  });
3100
3325
  process.stdin.on("error", (err) => {
3101
3326
  reject(err);
@@ -3115,6 +3340,10 @@ USAGE:
3115
3340
  wod import <note|playground|session|effort|equipment> <file> [options]
3116
3341
  wod publish [catalog] [options]
3117
3342
  wod build [dir] [options]
3343
+ wod init-catalog <name>
3344
+ Scaffold a new catalog repository (package.json, example
3345
+ collection, GitHub Actions Pages workflow). Refuses an
3346
+ existing destination.
3118
3347
  wod seed [options]
3119
3348
  wod reset [options]
3120
3349
  wod init Initialize the configured database (idempotent)
@@ -3317,6 +3546,22 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3317
3546
  }
3318
3547
  };
3319
3548
  try {
3549
+ if (parsed.subcommand === "init-catalog") {
3550
+ if (!parsed.target) {
3551
+ writeErr("[wod init-catalog] Error: Missing catalog name (a single new directory name)");
3552
+ return 1;
3553
+ }
3554
+ const { scaffoldCatalog: scaffoldCatalog2 } = await Promise.resolve().then(() => (init_scaffoldCatalog(), scaffoldCatalog_exports));
3555
+ try {
3556
+ const result = scaffoldCatalog2(parsed.target);
3557
+ writeOut(`[wod init-catalog] Created ${result.dir} (${result.files.length} files)`);
3558
+ writeOut(`Next: cd ${parsed.target} && bun install && bunx wod build . --output dist`);
3559
+ return 0;
3560
+ } catch (error) {
3561
+ writeErr(`[wod init-catalog] ${error.message}`);
3562
+ return 1;
3563
+ }
3564
+ }
3320
3565
  if (parsed.subcommand === "parse") {
3321
3566
  if (!parsed.target) {
3322
3567
  writeErr('[wod parse] Error: Missing input file or "-" for stdin');
@@ -3517,7 +3762,9 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3517
3762
  writeErr(`[wod ${verb}] Effort not found in ${db.path}: ${id}`);
3518
3763
  return 1;
3519
3764
  }
3520
- const markdown = wodWikiEngine.effortToMarkdown(effort);
3765
+ const ownerNoteId = typeof effort.ownerNoteId === "string" ? effort.ownerNoteId : `effort/${effort.slug}`;
3766
+ const owner = await db.storage.readonly("notes").get(ownerNoteId);
3767
+ const markdown = owner?.rawContent ?? wodWikiEngine.effortToMarkdown(effort);
3521
3768
  const target = parsed.output && parsed.output !== "-" ? parsed.output : `${id}.md`;
3522
3769
  if (parsed.output === "-") {
3523
3770
  writeOut(markdown);
@@ -3526,15 +3773,29 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3526
3773
  }
3527
3774
  writeOut(`[wod ${verb}] effort ${id} \u2192 ${target}`);
3528
3775
  } else {
3529
- const labels = (await listTagsByType(db.db, "equipment")).map((t) => t.label).sort();
3530
- const csv = wodWikiEngine.equipmentToCsv(labels);
3531
- const target = parsed.output && parsed.output !== "-" ? parsed.output : "equipment.csv";
3532
- if (parsed.output === "-") {
3533
- writeOut(csv);
3776
+ const equipment = id ? await getEquipmentBySlug(db.db, targetId) : void 0;
3777
+ if (equipment) {
3778
+ const ownerNoteId = typeof equipment.ownerNoteId === "string" ? equipment.ownerNoteId : `equipment/${equipment.slug}`;
3779
+ const owner = await db.storage.readonly("notes").get(ownerNoteId);
3780
+ const markdown = owner?.rawContent ?? wodWikiEngine.equipmentToMarkdown(equipment);
3781
+ const target = parsed.output && parsed.output !== "-" ? parsed.output : `${id}.md`;
3782
+ if (parsed.output === "-") {
3783
+ writeOut(markdown);
3784
+ } else {
3785
+ nodeFs().writeFileSync(target, markdown, "utf-8");
3786
+ }
3787
+ writeOut(`[wod ${verb}] equipment ${id} \u2192 ${target}`);
3534
3788
  } else {
3535
- nodeFs().writeFileSync(target, csv, "utf-8");
3789
+ const labels = (await listTagsByType(db.db, "equipment")).map((t) => t.label).sort();
3790
+ const csv = wodWikiEngine.equipmentToCsv(labels);
3791
+ const target = parsed.output && parsed.output !== "-" ? parsed.output : "equipment.csv";
3792
+ if (parsed.output === "-") {
3793
+ writeOut(csv);
3794
+ } else {
3795
+ nodeFs().writeFileSync(target, csv, "utf-8");
3796
+ }
3797
+ writeOut(`[wod ${verb}] equipment \u2192 ${target} (${labels.length} labels)`);
3536
3798
  }
3537
- writeOut(`[wod ${verb}] equipment \u2192 ${target} (${labels.length} labels)`);
3538
3799
  }
3539
3800
  } finally {
3540
3801
  await db.close();
@@ -3576,7 +3837,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3576
3837
  return 0;
3577
3838
  }
3578
3839
  if (kind === "effort") {
3579
- const effort = wodWikiEngine.markdownToEffort(fs.readFileSync(file, "utf-8"));
3840
+ const effort = wodWikiLang.parseEffortFile(fs.readFileSync(file, "utf-8"));
3580
3841
  if (!effort) {
3581
3842
  writeErr(`[wod import] Unparseable effort file (need frontmatter id/slug/label + numeric baseAttributes.met): ${file}`);
3582
3843
  return 1;
@@ -3590,6 +3851,21 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
3590
3851
  }
3591
3852
  return 0;
3592
3853
  }
3854
+ if (kind === "equipment" && /\.md$/i.test(file)) {
3855
+ const equipment = wodWikiLang.parseEquipmentFile(fs.readFileSync(file, "utf-8"));
3856
+ if (!equipment) {
3857
+ writeErr(`[wod import] Unparseable equipment file (need frontmatter type: equipment + id/slug/label): ${file}`);
3858
+ return 1;
3859
+ }
3860
+ const db2 = await openCliDb(cliDb());
3861
+ try {
3862
+ await persistEquipment(db2, { ...equipment, registrySource: "user" });
3863
+ writeOut(`[wod import] equipment ${equipment.slug} \u2014 ${equipment.label} \u2192 ${db2.path}`);
3864
+ } finally {
3865
+ await db2.close();
3866
+ }
3867
+ return 0;
3868
+ }
3593
3869
  if (kind === "equipment") {
3594
3870
  const labels = wodWikiEngine.csvToEquipmentLabels(fs.readFileSync(file, "utf-8"));
3595
3871
  if (labels.length === 0) {