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