@bitcobblers/wod-wiki-engine 0.11.43 → 0.35.2115

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/wod.cjs CHANGED
@@ -2,15 +2,858 @@
2
2
  'use strict';
3
3
 
4
4
  var wodWikiLang = require('@bitcobblers/wod-wiki-lang');
5
+ var child_process = require('child_process');
6
+ var fs = require('fs');
7
+ var path = require('path');
5
8
  var wodWikiCore = require('@bitcobblers/wod-wiki-core');
6
9
  var wodWikiWql = require('@bitcobblers/wod-wiki-wql');
10
+ var crypto = require('crypto');
7
11
 
12
+ var __defProp = Object.defineProperty;
13
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
14
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
9
15
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
10
16
  }) : x)(function(x) {
11
17
  if (typeof require !== "undefined") return require.apply(this, arguments);
12
18
  throw Error('Dynamic require of "' + x + '" is not supported');
13
19
  });
20
+ var __esm = (fn, res) => function __init() {
21
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
22
+ };
23
+ var __export = (target, all) => {
24
+ for (var name in all)
25
+ __defProp(target, name, { get: all[name], enumerable: true });
26
+ };
27
+
28
+ // src/cli/seedbuild/contract.ts
29
+ function canvasRouteSlug2(content) {
30
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
31
+ if (fm?.[1] && !/^template:\s*canvas\s*$/m.test(fm[1])) return null;
32
+ const route = fm?.[1]?.match(/^route:\s*(\S+)\s*$/m)?.[1];
33
+ if (!route) return null;
34
+ return route.replace(/^\//, "") || null;
35
+ }
36
+ var SEED_SCHEMA2, TEMPLATE_CHUNK_ID2, DASHBOARDS_CHUNK_ID2, BLOCK_INDEX_CHUNK_PREFIX, BLOCK_EFFORTS_CHUNK_PREFIX;
37
+ var init_contract = __esm({
38
+ "src/cli/seedbuild/contract.ts"() {
39
+ SEED_SCHEMA2 = 7;
40
+ TEMPLATE_CHUNK_ID2 = "template";
41
+ DASHBOARDS_CHUNK_ID2 = "dashboards";
42
+ BLOCK_INDEX_CHUNK_PREFIX = "block-index.";
43
+ BLOCK_EFFORTS_CHUNK_PREFIX = "block-efforts.";
44
+ }
45
+ });
46
+
47
+ // src/cli/seedbuild/effort-slugs.ts
48
+ function extractEffortSlugs2(statements) {
49
+ const slugs = /* @__PURE__ */ new Set();
50
+ for (const statement of statements) {
51
+ if (statement.exerciseId) {
52
+ slugs.add(statement.exerciseId);
53
+ } else {
54
+ const effortMetric = statement.metrics?.getMetric?.("effort");
55
+ const effort = effortMetric && typeof effortMetric.value === "string" ? effortMetric.value : void 0;
56
+ if (effort) {
57
+ const slug = effort.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
58
+ if (slug) slugs.add(slug);
59
+ }
60
+ }
61
+ }
62
+ return Array.from(slugs);
63
+ }
64
+ var init_effort_slugs = __esm({
65
+ "src/cli/seedbuild/effort-slugs.ts"() {
66
+ }
67
+ });
68
+
69
+ // src/cli/seedbuild/filename.ts
70
+ function fileToDisplayName(filename) {
71
+ const base = filename.replace(/\.md$/, "");
72
+ if (base.toUpperCase() === "README") return "Overview";
73
+ const cleaned = base.replace(/^day-\d+-/, "");
74
+ const tokens = cleaned.split(/[-_]/);
75
+ const titled = [];
76
+ for (let i = 0; i < tokens.length; i++) {
77
+ const token = tokens[i];
78
+ const next = tokens[i + 1];
79
+ const after = tokens[i + 2];
80
+ const prev = tokens[i - 1];
81
+ if (/^\d{1,2}$/.test(token) && /^\d{1,2}$/.test(next ?? "") && !/^\d+$/.test(prev ?? "") && !/^\d+$/.test(after ?? "")) {
82
+ titled.push(`${token}.${next}`);
83
+ i++;
84
+ continue;
85
+ }
86
+ if (titled.length > 0 && FILE_TITLE_STOP_WORDS[token.toLowerCase()]) {
87
+ titled.push(token.toLowerCase());
88
+ continue;
89
+ }
90
+ titled.push(token.charAt(0).toUpperCase() + token.slice(1));
91
+ }
92
+ return titled.join(" ");
93
+ }
94
+ var FILE_TITLE_STOP_WORDS;
95
+ var init_filename = __esm({
96
+ "src/cli/seedbuild/filename.ts"() {
97
+ FILE_TITLE_STOP_WORDS = {
98
+ and: true,
99
+ or: true,
100
+ of: true,
101
+ for: true,
102
+ to: true,
103
+ in: true,
104
+ on: true,
105
+ at: true,
106
+ by: true,
107
+ with: true,
108
+ the: true,
109
+ a: true,
110
+ an: true
111
+ };
112
+ }
113
+ });
114
+
115
+ // src/cli/seedbuild/frontmatter.ts
116
+ function unquote(value) {
117
+ return value.replace(
118
+ /^(['"])(.*)\1$/,
119
+ (_match, quote, inner) => quote === '"' ? inner.replace(/\\(["\\])/g, "$1") : inner
120
+ );
121
+ }
122
+ function parseFrontmatter(raw) {
123
+ const match = raw.match(FRONTMATTER_RE);
124
+ if (!match) return { meta: {}, body: raw };
125
+ return { meta: parseMetaLines(match[1].split(/\r?\n/)), body: raw.slice(match[0].length) };
126
+ }
127
+ function parseMetaLines(lines) {
128
+ const meta = {};
129
+ for (let i = 0; i < lines.length; i++) {
130
+ const keyMatch = lines[i].match(/^([A-Za-z][\w.-]*)\s*:(.*)$/);
131
+ if (!keyMatch) continue;
132
+ const key = keyMatch[1];
133
+ const rawVal = keyMatch[2].trim();
134
+ if (rawVal === "") {
135
+ const items = [];
136
+ while (i + 1 < lines.length) {
137
+ const itemMatch = lines[i + 1].match(/^\s+-\s+(.+)$/);
138
+ if (!itemMatch) break;
139
+ items.push(itemMatch[1].trim());
140
+ i++;
141
+ }
142
+ meta[key] = items.length > 0 ? items : "";
143
+ } else {
144
+ const wasQuoted = /^(['"])(.*)\1$/.test(rawVal);
145
+ const value = unquote(rawVal);
146
+ const num2 = Number(value);
147
+ meta[key] = !wasQuoted && value !== "" && !isNaN(num2) ? num2 : value;
148
+ }
149
+ }
150
+ return meta;
151
+ }
152
+ function getScalar(meta, key) {
153
+ const value = meta[key];
154
+ return typeof value === "string" || typeof value === "number" ? value : void 0;
155
+ }
156
+ function getList(meta, key) {
157
+ const value = meta[key];
158
+ return Array.isArray(value) ? value : [];
159
+ }
160
+ var FRONTMATTER_RE;
161
+ var init_frontmatter = __esm({
162
+ "src/cli/seedbuild/frontmatter.ts"() {
163
+ FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
164
+ }
165
+ });
166
+ function parseDiscipline(val) {
167
+ if (!val) return void 0;
168
+ const normalized = unquoteYaml(val.trim()).toLowerCase();
169
+ if (wodWikiLang.isEffortDiscipline(normalized)) return normalized;
170
+ console.warn(`[effort-file] unknown discipline '${val}' \u2014 treating as unset`);
171
+ return void 0;
172
+ }
173
+ function unquoteYaml(val) {
174
+ if (val.startsWith('"') && val.endsWith('"')) {
175
+ return val.slice(1, -1).replace(/\\(["\\])/g, "$1");
176
+ }
177
+ if (val.startsWith("'") && val.endsWith("'")) {
178
+ return val.slice(1, -1).replace(/\\'/g, "'");
179
+ }
180
+ return val;
181
+ }
182
+ function coerceNestedScalar(raw) {
183
+ if (raw === "true") return true;
184
+ if (raw === "false") return false;
185
+ if (raw !== "" && !Number.isNaN(Number(raw))) return Number(raw);
186
+ return raw;
187
+ }
188
+ function readNestedSection(lines, key) {
189
+ const start = lines.findIndex((l) => new RegExp(`^${key}:\\s*$`).test(l));
190
+ if (start === -1) return void 0;
191
+ const result = {};
192
+ let subKey = null;
193
+ for (let i = start + 1; i < lines.length; i++) {
194
+ const line = lines[i];
195
+ if (/^\S/.test(line)) break;
196
+ const trimmed = line.trim();
197
+ if (!trimmed || trimmed.startsWith("#")) continue;
198
+ const indent = line.length - line.trimStart().length;
199
+ const m = trimmed.match(/^([\w.-]+):\s*(.*)$/);
200
+ if (!m) continue;
201
+ const [, k, rawVal] = m;
202
+ if (indent === 2) {
203
+ if (rawVal === "") {
204
+ subKey = k;
205
+ result[k] = {};
206
+ } else {
207
+ subKey = null;
208
+ result[k] = coerceNestedScalar(rawVal);
209
+ }
210
+ } else if (indent >= 4 && subKey !== null) {
211
+ result[subKey][k] = coerceNestedScalar(rawVal);
212
+ }
213
+ }
214
+ return Object.keys(result).length > 0 ? result : void 0;
215
+ }
216
+ function parseEffortFile(raw) {
217
+ const blockMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
218
+ if (!blockMatch) return null;
219
+ const { meta, body } = parseFrontmatter(raw);
220
+ const innerLines = blockMatch[1].split(/\r?\n/);
221
+ const id = getScalar(meta, "id");
222
+ const slug = getScalar(meta, "slug");
223
+ const label = getScalar(meta, "label");
224
+ const aliases = getList(meta, "aliases");
225
+ const baseAttrs = readNestedSection(innerLines, "baseAttributes");
226
+ const hints = readNestedSection(innerLines, "hints");
227
+ const derivation = readNestedSection(innerLines, "derivation");
228
+ const metRaw = baseAttrs?.met;
229
+ const met = typeof metRaw === "number" ? metRaw : Number(metRaw);
230
+ if (!id || !slug || !label || metRaw === void 0 || Number.isNaN(met)) return null;
231
+ const discipline = parseDiscipline(
232
+ typeof baseAttrs?.discipline === "string" ? baseAttrs.discipline : void 0
233
+ );
234
+ const intensityTier = baseAttrs?.intensityTier;
235
+ const trimmedBody = body.trim();
236
+ return {
237
+ id: String(id),
238
+ slug: String(slug),
239
+ label: String(label),
240
+ aliases,
241
+ baseAttributes: {
242
+ met,
243
+ ...discipline ? { discipline } : {},
244
+ ...intensityTier ? { intensityTier } : {}
245
+ },
246
+ registrySource: "bundled",
247
+ ...trimmedBody ? { body: trimmedBody } : {},
248
+ ...hints ? { hints } : {},
249
+ ...derivation ? { derivation } : {}
250
+ };
251
+ }
252
+ var init_effort_file = __esm({
253
+ "src/cli/seedbuild/effort-file.ts"() {
254
+ init_frontmatter();
255
+ }
256
+ });
257
+ function resolveGitVersion(repoRoot) {
258
+ try {
259
+ const run = (args) => child_process.execSync(`git ${args}`, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
260
+ const count = Number(run("rev-list --count HEAD"));
261
+ return {
262
+ version: Number.isFinite(count) && count > 0 ? count : 0,
263
+ versionLabel: run("describe --tags --long --always --dirty"),
264
+ commit: run("rev-parse HEAD"),
265
+ dirty: run("status --porcelain") !== ""
266
+ };
267
+ } catch {
268
+ return UNKNOWN_GIT_VERSION;
269
+ }
270
+ }
271
+ var UNKNOWN_GIT_VERSION;
272
+ var init_git_version = __esm({
273
+ "src/cli/seedbuild/git-version.ts"() {
274
+ UNKNOWN_GIT_VERSION = {
275
+ version: 0,
276
+ versionLabel: "unknown",
277
+ commit: "",
278
+ dirty: false
279
+ };
280
+ }
281
+ });
282
+ function escapeHtml(s) {
283
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
284
+ }
285
+ function rowHtml(c) {
286
+ const kind = c.kind ?? "notes";
287
+ return `<tr><td>${escapeHtml(c.id)}</td><td>${kind}</td><td>${c.count}</td><td>${(c.bytes / 1024).toFixed(1)} KB</td><td><code>${c.sha256.slice(0, 8)}</code></td></tr>`;
288
+ }
289
+ function writeBuildReport(outDir, data) {
290
+ const { manifest, totalMs, passMs, fileCount } = data;
291
+ const totalBytes = manifest.chunks.reduce((s, c) => s + c.bytes, 0);
292
+ const dirtyBadge = manifest.dirty ? ' <strong style="color:#b45309">(dirty working tree \u2014 not reproducible from <code>' + escapeHtml(manifest.commit ?? "") + "</code>)</strong>" : "";
293
+ const html = `<!doctype html>
294
+ <html lang="en">
295
+ <head>
296
+ <meta charset="utf-8" />
297
+ <title>Seed build report</title>
298
+ <style>
299
+ body { font-family: system-ui, sans-serif; max-width: 960px; margin: 2rem auto; padding: 0 1rem; color: #1f2937; }
300
+ h1 { font-size: 1.4rem; } h2 { font-size: 1.1rem; margin-top: 2rem; }
301
+ table { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
302
+ th, td { text-align: left; padding: 0.3rem 0.6rem; border-bottom: 1px solid #e5e7eb; }
303
+ code { font-size: 0.85em; }
304
+ dl { display: grid; grid-template-columns: max-content 1fr; gap: 0.2rem 1rem; font-size: 0.95rem; }
305
+ dt { font-weight: 600; }
306
+ </style>
307
+ </head>
308
+ <body>
309
+ <h1>Seed build report</h1>
310
+ <h2>Version</h2>
311
+ <dl>
312
+ <dt>version</dt><dd>${manifest.version} <code>${escapeHtml(manifest.versionLabel ?? "")}</code>${dirtyBadge}</dd>
313
+ <dt>commit</dt><dd><code>${escapeHtml(manifest.commit ?? "unknown")}</code></dd>
314
+ <dt>built at</dt><dd>${escapeHtml(manifest.builtAt)}</dd>
315
+ <dt>schema</dt><dd>v${manifest.schema}</dd>
316
+ </dl>
317
+ <h2>Benchmarks</h2>
318
+ <dl>
319
+ <dt>total</dt><dd>${totalMs} ms</dd>
320
+ <dt>corpus scan + partition</dt><dd>${passMs.scan} ms (${fileCount} files)</dd>
321
+ <dt>block-index derivation</dt><dd>${passMs.blockIndex} ms</dd>
322
+ <dt>block-efforts derivation</dt><dd>${passMs.blockEfforts} ms</dd>
323
+ <dt>chunk writes</dt><dd>${passMs.write} ms</dd>
324
+ </dl>
325
+ <h2>Chunks (${manifest.chunks.length}, ${(totalBytes / 1024).toFixed(0)} KB total)</h2>
326
+ <table>
327
+ <thead><tr><th>id</th><th>kind</th><th>rows</th><th>size</th><th>sha256</th></tr></thead>
328
+ <tbody>
329
+ ${manifest.chunks.map(rowHtml).join("\n")}
330
+ </tbody>
331
+ </table>
332
+ </body>
333
+ </html>
334
+ `;
335
+ const file = path.join(outDir, "report.html");
336
+ fs.writeFileSync(file, html, "utf8");
337
+ return file;
338
+ }
339
+ var init_report = __esm({
340
+ "src/cli/seedbuild/report.ts"() {
341
+ }
342
+ });
343
+
344
+ // src/cli/seedbuild/compiler.ts
345
+ var compiler_exports = {};
346
+ __export(compiler_exports, {
347
+ buildBlockEffortRows: () => buildBlockEffortRows,
348
+ buildBlockIndexRows: () => buildBlockIndexRows,
349
+ buildStaticParentMap: () => buildStaticParentMap,
350
+ chunkIdFor: () => chunkIdFor,
351
+ collectCorpusMarkdown: () => collectCorpusMarkdown,
352
+ discoverUncatalogedEfforts: () => discoverUncatalogedEfforts,
353
+ feedDateToCreatedAt: () => feedDateToCreatedAt,
354
+ generateSeed: () => generateSeed,
355
+ loadExistingEffortMap: () => loadExistingEffortMap,
356
+ partitionIntoChunks: () => partitionIntoChunks,
357
+ scaffoldDiscoveredEfforts: () => scaffoldDiscoveredEfforts,
358
+ validateDashboardNote: () => validateDashboardNote
359
+ });
360
+ function listMarkdownFiles(dir, prefix = "") {
361
+ if (!fs.existsSync(dir)) return [];
362
+ const out = [];
363
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
364
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
365
+ if (entry.isDirectory()) out.push(...listMarkdownFiles(path.join(dir, entry.name), rel));
366
+ else if (entry.isFile() && entry.name.endsWith(".md")) out.push(rel);
367
+ }
368
+ return out.sort();
369
+ }
370
+ function chunkIdFor(relPath) {
371
+ const parts = relPath.split("/");
372
+ if (parts[0] !== "markdown" || parts.length < 2) return null;
373
+ const corpus = parts[1];
374
+ if (parts.length === 2) return null;
375
+ const slug = parts[2]?.replace(/\.md$/, "");
376
+ switch (corpus) {
377
+ case "canvas":
378
+ case "dashboards":
379
+ case "efforts":
380
+ return corpus;
381
+ case "collections":
382
+ return parts.length === 3 ? `collection._root` : `collection.${slug}`;
383
+ case "feeds":
384
+ return parts.length === 3 ? `feed._root` : `feed.${slug}`;
385
+ default:
386
+ return null;
387
+ }
388
+ }
389
+ function collectCorpusMarkdown(markdownDir) {
390
+ return listMarkdownFiles(markdownDir).map((p) => `markdown/${p}`);
391
+ }
392
+ function partitionIntoChunks(paths) {
393
+ const byId = /* @__PURE__ */ new Map();
394
+ for (const p of paths) {
395
+ const id = chunkIdFor(p);
396
+ if (id === null) {
397
+ throw new Error(
398
+ `[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.`
399
+ );
400
+ }
401
+ const list = byId.get(id);
402
+ if (list) list.push(p);
403
+ else byId.set(id, [p]);
404
+ }
405
+ return [...byId.entries()].sort(([a], [b]) => a < b ? -1 : 1).map(([id, files]) => ({ id, files }));
406
+ }
407
+ function validateDashboardNote(raw) {
408
+ const problems = [];
409
+ const { meta } = parseFrontmatter(raw);
410
+ const { sections } = wodWikiWql.parseDashboardNote(raw);
411
+ const doc = wodWikiWql.buildDashboardDocument(sections, meta);
412
+ if (!doc.isDashboard) return ["file is not a dashboard note (missing dashboard frontmatter)"];
413
+ if (doc.title === void 0) problems.push("dashboard has no title");
414
+ if (doc.widgets.length < 3) problems.push(`dashboard has only ${doc.widgets.length} widgets (the locked format has \u2265 3)`);
415
+ const declared = new Set(doc.tokens.map((t) => t.name));
416
+ for (const widget of doc.widgets) {
417
+ const where = `widget '${widget.title ?? widget.body.slice(0, 40)}'`;
418
+ if (widget.title === void 0) problems.push(`${where}: missing title`);
419
+ if (!wodWikiWql.isDashboardWidgetType(wodWikiWql.resolveWidgetType(widget.type))) {
420
+ problems.push(`${where}: unknown widget type '${widget.type}'`);
421
+ }
422
+ const parsed = wodWikiWql.parseQuery(widget.body);
423
+ if (parsed.error !== void 0) problems.push(`${where}: WQL parse error: ${parsed.error}`);
424
+ for (const ref of [
425
+ ...widget.body.matchAll(/\$([A-Za-z][\w-]*)/g),
426
+ ...Object.values(widget.attributes).flatMap((v) => [...String(v).matchAll(/\$([A-Za-z][\w-]*)/g)])
427
+ ]) {
428
+ if (!declared.has(ref[1])) problems.push(`${where}: undeclared token $${ref[1]}`);
429
+ }
430
+ }
431
+ return problems;
432
+ }
433
+ function gateDashboards(groups, corpusRoot) {
434
+ for (const group of groups) {
435
+ if (group.id !== DASHBOARDS_CHUNK_ID2) continue;
436
+ for (const p of group.files) {
437
+ const problems = validateDashboardNote(fs.readFileSync(path.join(corpusRoot, p), "utf8"));
438
+ if (problems.length > 0) {
439
+ throw new Error(`[generate-seed] Dashboard validation failed for '${p}':
440
+ - ${problems.join("\n - ")}`);
441
+ }
442
+ }
443
+ }
444
+ }
445
+ function toSegmentDataType2(section) {
446
+ switch (section.type) {
447
+ case "time":
448
+ case "log":
449
+ return "wod";
450
+ case "title": {
451
+ const l = section.level ?? 1;
452
+ return `h${Math.min(Math.max(l, 1), 6)}`;
453
+ }
454
+ case "frontmatter":
455
+ return "frontmatter";
456
+ case "markdown":
457
+ case "widget":
458
+ case "embed":
459
+ case "code":
460
+ return "markdown";
461
+ default:
462
+ return "markdown";
463
+ }
464
+ }
465
+ function getFileCreatedAt(file, repoRoot) {
466
+ try {
467
+ const out = child_process.execSync(`git log -1 --format=%ct -- '${file}'`, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
468
+ const ts = Number(out) * 1e3;
469
+ return Number.isFinite(ts) && ts > 0 ? ts : 0;
470
+ } catch {
471
+ return 0;
472
+ }
473
+ }
474
+ function feedDateToCreatedAt(dateKey) {
475
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(dateKey)) return 0;
476
+ const ts = Date.parse(`${dateKey}T00:00:00Z`);
477
+ return Number.isNaN(ts) ? 0 : ts;
478
+ }
479
+ function buildBlockIndexRows(markdownDir, corpusRoot) {
480
+ const index = [];
481
+ for (const file of listMarkdownFiles(path.join(markdownDir, "collections"))) {
482
+ const rel = `markdown/collections/${file}`;
483
+ const parts = rel.split("/");
484
+ if (parts.length < 4) continue;
485
+ const dirName = parts[parts.length - 2];
486
+ const fileNameExt = parts[parts.length - 1];
487
+ const isReadme = fileNameExt.toLowerCase() === "readme.md";
488
+ const fileName = fileNameExt.replace(/\.md$/, "");
489
+ const noteId = isReadme ? dirName : `${dirName}/${fileName}`;
490
+ const content = fs.readFileSync(path.join(corpusRoot, rel), "utf8");
491
+ const headingMatch = isReadme ? content.match(/^#\s+(.+)$/m) : null;
492
+ const noteTitle = headingMatch?.[1]?.trim() || fileToDisplayName(isReadme ? dirName : fileNameExt);
493
+ const createdAt = getFileCreatedAt(rel, corpusRoot);
494
+ const sections = wodWikiCore.parseDocumentSections(content);
495
+ let position = 0;
496
+ for (const section of sections) {
497
+ const blockContentId = section.scriptBlock?.contentId ?? void 0;
498
+ index.push({
499
+ id: `static:${noteId}:${section.id}:1`,
500
+ noteId,
501
+ segmentId: section.id,
502
+ segmentVersion: 1,
503
+ position: position++,
504
+ dataType: toSegmentDataType2(section),
505
+ blockContentId,
506
+ rawContent: section.displayContent,
507
+ noteTitle,
508
+ createdAt,
509
+ isStatic: true,
510
+ sourceId: isReadme ? `collection:${dirName}` : `collection:${noteId}`,
511
+ sourcePath: rel
512
+ });
513
+ }
514
+ }
515
+ for (const file of listMarkdownFiles(path.join(markdownDir, "feeds"))) {
516
+ const rel = `markdown/feeds/${file}`;
517
+ const parts = rel.split("/");
518
+ if (parts.length < 5) continue;
519
+ const dirName = parts[parts.length - 3];
520
+ const dateKey = parts[parts.length - 2];
521
+ const fileNameExt = parts[parts.length - 1];
522
+ if (fileNameExt.toLowerCase() === "readme.md") continue;
523
+ const fileName = fileNameExt.replace(/\.md$/, "");
524
+ const noteId = `feeds/${dirName}/${dateKey}/${fileName}`;
525
+ const noteTitle = fileToDisplayName(fileNameExt);
526
+ const content = fs.readFileSync(path.join(corpusRoot, rel), "utf8");
527
+ const sections = wodWikiCore.parseDocumentSections(content);
528
+ let position = 0;
529
+ for (const section of sections) {
530
+ const blockContentId = section.scriptBlock?.contentId ?? void 0;
531
+ index.push({
532
+ id: `static:${noteId}:${section.id}:1`,
533
+ noteId,
534
+ segmentId: section.id,
535
+ segmentVersion: 1,
536
+ position: position++,
537
+ dataType: toSegmentDataType2(section),
538
+ blockContentId,
539
+ rawContent: section.displayContent,
540
+ noteTitle,
541
+ createdAt: feedDateToCreatedAt(dateKey),
542
+ isStatic: true,
543
+ sourceId: `feed:${noteId}`,
544
+ sourcePath: rel
545
+ });
546
+ }
547
+ }
548
+ for (const file of listMarkdownFiles(path.join(markdownDir, "canvas"))) {
549
+ const rel = `markdown/canvas/${file}`;
550
+ const content = fs.readFileSync(path.join(corpusRoot, rel), "utf8");
551
+ const noteId = canvasRouteSlug2(content);
552
+ if (!noteId) continue;
553
+ const fileNameExt = file.split("/").pop() ?? file;
554
+ const noteTitle = fileToDisplayName(fileNameExt);
555
+ const createdAt = getFileCreatedAt(rel, corpusRoot);
556
+ const sections = wodWikiCore.parseDocumentSections(content);
557
+ let position = 0;
558
+ for (const section of sections) {
559
+ const blockContentId = section.scriptBlock?.contentId ?? void 0;
560
+ index.push({
561
+ id: `static:${noteId}:${section.id}:1`,
562
+ noteId,
563
+ segmentId: section.id,
564
+ segmentVersion: 1,
565
+ position: position++,
566
+ dataType: toSegmentDataType2(section),
567
+ blockContentId,
568
+ rawContent: section.displayContent,
569
+ noteTitle,
570
+ createdAt,
571
+ isStatic: true,
572
+ sourceId: `guides:${noteId}`,
573
+ sourcePath: rel
574
+ });
575
+ }
576
+ }
577
+ return index;
578
+ }
579
+ function formatDiscoveredLabel(raw) {
580
+ const clean = raw.replace(/[-_]+/g, " ").trim();
581
+ return clean.split(/\s+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(" ");
582
+ }
583
+ function loadExistingEffortMap(markdownDir) {
584
+ const slugs = /* @__PURE__ */ new Set();
585
+ const aliasToCanonical = /* @__PURE__ */ new Map();
586
+ const effortsDir = path.join(markdownDir, "efforts");
587
+ for (const file of listMarkdownFiles(effortsDir)) {
588
+ const raw = fs.readFileSync(path.join(effortsDir, file), "utf8");
589
+ const parsed = parseEffortFile(raw);
590
+ if (!parsed) continue;
591
+ slugs.add(parsed.slug);
592
+ aliasToCanonical.set(parsed.slug, parsed.slug);
593
+ aliasToCanonical.set(parsed.label.toLowerCase(), parsed.slug);
594
+ aliasToCanonical.set(parsed.slug.replace(/-/g, " "), parsed.slug);
595
+ for (const alias of parsed.aliases) {
596
+ aliasToCanonical.set(alias.toLowerCase(), parsed.slug);
597
+ aliasToCanonical.set(alias.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""), parsed.slug);
598
+ }
599
+ }
600
+ return { slugs, aliasToCanonical };
601
+ }
602
+ function discoverUncatalogedEfforts(markdownDir, corpusRoot) {
603
+ const { slugs, aliasToCanonical } = loadExistingEffortMap(markdownDir);
604
+ const blocks = buildBlockIndexRows(markdownDir, corpusRoot);
605
+ const discovered = /* @__PURE__ */ new Map();
606
+ for (const block of blocks) {
607
+ if (block.dataType !== "wod" || !block.rawContent) continue;
608
+ const script = wodWikiLang.parseScript(block.rawContent);
609
+ for (const statement of script.statements) {
610
+ const effortMetric = statement.metrics?.getMetric?.("effort");
611
+ const effortLabel = (effortMetric && typeof effortMetric.value === "string" ? effortMetric.value : void 0) ?? statement.exerciseId;
612
+ if (!effortLabel) continue;
613
+ const slugified = effortLabel.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
614
+ if (!slugified || NON_EFFORT_SLUGS[slugified]) continue;
615
+ const canonical = aliasToCanonical.get(slugified) ?? aliasToCanonical.get(effortLabel.toLowerCase());
616
+ if (canonical && slugs.has(canonical)) continue;
617
+ const existing = discovered.get(slugified);
618
+ if (existing) {
619
+ existing.rawLabels.add(effortLabel);
620
+ } else {
621
+ discovered.set(slugified, {
622
+ label: formatDiscoveredLabel(effortLabel),
623
+ rawLabels: /* @__PURE__ */ new Set([effortLabel])
624
+ });
625
+ }
626
+ }
627
+ }
628
+ return Array.from(discovered.entries()).map(([slug, data]) => ({
629
+ slug,
630
+ label: data.label,
631
+ rawLabels: Array.from(data.rawLabels)
632
+ }));
633
+ }
634
+ function scaffoldDiscoveredEfforts(discovered, markdownDir) {
635
+ const targetDir = path.join(markdownDir, "efforts", "discovered");
636
+ if (!fs.existsSync(targetDir)) {
637
+ fs.mkdirSync(targetDir, { recursive: true });
638
+ }
639
+ const createdPaths = [];
640
+ for (const effort of discovered) {
641
+ const filePath = path.join(targetDir, `${effort.slug}.md`);
642
+ if (fs.existsSync(filePath)) continue;
643
+ const aliases = effort.rawLabels.filter((l) => l.toLowerCase() !== effort.label.toLowerCase()).map((l) => ` - "${l.replace(/"/g, '\\"')}"`);
644
+ const content = [
645
+ "---",
646
+ `id: effort-bundled-${effort.slug}`,
647
+ `slug: ${effort.slug}`,
648
+ `label: "${effort.label.replace(/"/g, '\\"')}"`,
649
+ aliases.length > 0 ? `aliases:
650
+ ${aliases.join("\n")}` : "aliases: []",
651
+ "baseAttributes:",
652
+ " met: 5.0",
653
+ " discipline: general",
654
+ " intensityTier: moderate",
655
+ "registrySource: bundled",
656
+ "---",
657
+ ""
658
+ ].join("\n");
659
+ fs.writeFileSync(filePath, content, "utf8");
660
+ createdPaths.push(filePath);
661
+ }
662
+ return createdPaths;
663
+ }
664
+ function buildBlockEffortRows(markdownDir, corpusRoot) {
665
+ const efforts = [];
666
+ const { aliasToCanonical } = loadExistingEffortMap(markdownDir);
667
+ const blocks = buildBlockIndexRows(markdownDir, corpusRoot);
668
+ for (const block of blocks) {
669
+ if (block.dataType !== "wod" || !block.rawContent) continue;
670
+ const script = wodWikiLang.parseScript(block.rawContent);
671
+ const effortSlugs = extractEffortSlugs2(script.statements);
672
+ for (const rawSlug of effortSlugs) {
673
+ const norm = rawSlug.toLowerCase().trim();
674
+ const canonicalSlug = aliasToCanonical.get(norm) ?? aliasToCanonical.get(norm.replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "")) ?? norm.replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
675
+ if (!canonicalSlug || NON_EFFORT_SLUGS[canonicalSlug] || NON_EFFORT_SLUGS[norm]) {
676
+ continue;
677
+ }
678
+ efforts.push({
679
+ id: `${block.id}:${canonicalSlug}`,
680
+ noteId: block.noteId,
681
+ blockId: block.segmentId,
682
+ blockContentId: block.blockContentId,
683
+ effortSlug: canonicalSlug,
684
+ isStatic: block.isStatic,
685
+ sourcePath: block.sourcePath,
686
+ createdAt: block.createdAt
687
+ });
688
+ }
689
+ }
690
+ return efforts;
691
+ }
692
+ function buildStaticParentMap(blockRows) {
693
+ const map = {};
694
+ for (const row of blockRows) {
695
+ if (!row.sourcePath) continue;
696
+ const prev = map[row.noteId];
697
+ if (prev && prev.sourcePath !== row.sourcePath) {
698
+ throw new Error(
699
+ `[generate-seed] ambiguous static parent '${row.noteId}' maps to both '${prev.sourcePath}' and '${row.sourcePath}'`
700
+ );
701
+ }
702
+ map[row.noteId] = { sourcePath: row.sourcePath, sourceId: row.sourceId };
703
+ }
704
+ return map;
705
+ }
706
+ function sha256Hex(data) {
707
+ return crypto.createHash("sha256").update(data).digest("hex");
708
+ }
709
+ function generateSeed(options = {}) {
710
+ const t0 = Date.now();
711
+ const passMs = { scan: 0, blockIndex: 0, blockEfforts: 0, write: 0 };
712
+ const corpusRoot = options.corpusRoot ?? process.cwd();
713
+ const markdownDir = options.markdownDir ?? path.join(corpusRoot, "markdown");
714
+ const outDir = options.outDir ?? path.join(corpusRoot, "dist");
715
+ const gitVersion = options.version !== void 0 ? { ...resolveGitVersion(corpusRoot), version: options.version } : resolveGitVersion(corpusRoot);
716
+ if (options.scaffoldDiscoveredEfforts) {
717
+ const discovered = discoverUncatalogedEfforts(markdownDir, corpusRoot);
718
+ if (discovered.length > 0) {
719
+ scaffoldDiscoveredEfforts(discovered, markdownDir);
720
+ }
721
+ }
722
+ const tScan = Date.now();
723
+ const paths = collectCorpusMarkdown(markdownDir);
724
+ const groups = partitionIntoChunks(paths);
725
+ gateDashboards(groups, corpusRoot);
726
+ if (options.templateFile && fs.existsSync(options.templateFile)) {
727
+ const templateRowPath = options.templateRowPath ?? path.relative(corpusRoot, options.templateFile).split("\\").join("/");
728
+ groups.push({ id: TEMPLATE_CHUNK_ID2, files: [templateRowPath] });
729
+ }
730
+ passMs.scan = Date.now() - tScan;
731
+ const chunksDir = path.join(outDir, "chunks");
732
+ fs.rmSync(outDir, { recursive: true, force: true });
733
+ fs.mkdirSync(chunksDir, { recursive: true });
734
+ const manifestChunks = [];
735
+ let totalFiles = 0;
736
+ const tWrite = Date.now();
737
+ for (const { id, files } of groups) {
738
+ const templateSource = id === TEMPLATE_CHUNK_ID2 ? options.templateFile : void 0;
739
+ const rows = files.map((p) => ({
740
+ path: p,
741
+ content: fs.readFileSync(templateSource ?? path.join(corpusRoot, p), "utf8")
742
+ }));
743
+ const bytes = Buffer.from(JSON.stringify(rows), "utf8");
744
+ const sha = sha256Hex(bytes);
745
+ const file = `chunks/${id}.${sha.slice(0, 8)}.json`;
746
+ fs.writeFileSync(path.join(outDir, file), bytes);
747
+ manifestChunks.push({ id, path: file, sha256: sha, bytes: bytes.length, count: rows.length });
748
+ totalFiles += rows.length;
749
+ }
750
+ const tBlockIndex = Date.now();
751
+ const blockRows = buildBlockIndexRows(markdownDir, corpusRoot);
752
+ passMs.blockIndex = Date.now() - tBlockIndex;
753
+ fs.writeFileSync(
754
+ path.join(outDir, "block-parent-map.json"),
755
+ JSON.stringify(buildStaticParentMap(blockRows), null, 2) + "\n"
756
+ );
757
+ for (let i = 0; blockRows.length > 0 && i * BLOCK_INDEX_ROWS_PER_CHUNK < blockRows.length; i++) {
758
+ const part = blockRows.slice(i * BLOCK_INDEX_ROWS_PER_CHUNK, (i + 1) * BLOCK_INDEX_ROWS_PER_CHUNK);
759
+ if (part.length === 0) break;
760
+ const id = `${BLOCK_INDEX_CHUNK_PREFIX}${i}`;
761
+ const bytes = Buffer.from(JSON.stringify(part), "utf8");
762
+ const sha = sha256Hex(bytes);
763
+ const file = `chunks/${id}.${sha.slice(0, 8)}.json`;
764
+ fs.writeFileSync(path.join(outDir, file), bytes);
765
+ manifestChunks.push({ id, path: file, sha256: sha, bytes: bytes.length, count: part.length, kind: "block-index" });
766
+ }
767
+ const tBlockEfforts = Date.now();
768
+ const effortRows = buildBlockEffortRows(markdownDir, corpusRoot);
769
+ passMs.blockEfforts = Date.now() - tBlockEfforts;
770
+ for (let i = 0; effortRows.length > 0 && i * BLOCK_INDEX_ROWS_PER_CHUNK < effortRows.length; i++) {
771
+ const part = effortRows.slice(i * BLOCK_INDEX_ROWS_PER_CHUNK, (i + 1) * BLOCK_INDEX_ROWS_PER_CHUNK);
772
+ if (part.length === 0) break;
773
+ const id = `${BLOCK_EFFORTS_CHUNK_PREFIX}${i}`;
774
+ const bytes = Buffer.from(JSON.stringify(part), "utf8");
775
+ const sha = sha256Hex(bytes);
776
+ const file = `chunks/${id}.${sha.slice(0, 8)}.json`;
777
+ fs.writeFileSync(path.join(outDir, file), bytes);
778
+ manifestChunks.push({ id, path: file, sha256: sha, bytes: bytes.length, count: part.length, kind: "block-efforts" });
779
+ }
780
+ passMs.write = Date.now() - tWrite;
781
+ const manifest = {
782
+ schema: SEED_SCHEMA2,
783
+ version: gitVersion.version,
784
+ versionLabel: gitVersion.versionLabel,
785
+ commit: gitVersion.commit,
786
+ dirty: gitVersion.dirty,
787
+ builtAt: (/* @__PURE__ */ new Date()).toISOString(),
788
+ chunks: manifestChunks
789
+ };
790
+ fs.writeFileSync(path.join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
791
+ const reportPath = writeBuildReport(outDir, {
792
+ manifest,
793
+ totalMs: Date.now() - t0,
794
+ passMs,
795
+ fileCount: paths.length});
796
+ return { manifest, outDir, totalFiles, reportPath, passMs };
797
+ }
798
+ var BLOCK_INDEX_ROWS_PER_CHUNK, NON_EFFORT_SLUGS;
799
+ var init_compiler = __esm({
800
+ "src/cli/seedbuild/compiler.ts"() {
801
+ init_contract();
802
+ init_effort_slugs();
803
+ init_filename();
804
+ init_effort_file();
805
+ init_git_version();
806
+ init_report();
807
+ init_frontmatter();
808
+ BLOCK_INDEX_ROWS_PER_CHUNK = 4e3;
809
+ NON_EFFORT_SLUGS = {
810
+ "amrap": true,
811
+ "emom": true,
812
+ "tabata": true,
813
+ "for-time": true,
814
+ "rounds": true,
815
+ "sets": true,
816
+ "repetitions": true,
817
+ "count-down": true,
818
+ "total-reps": true,
819
+ "warmup": true,
820
+ "cooldown": true,
821
+ "cool-down": true,
822
+ "transition": true,
823
+ "rest": true,
824
+ "break": true,
825
+ "work": true,
826
+ "i-suck-at-this": true,
827
+ "isuckatthis": true,
828
+ "movement": true,
829
+ "movement-you": true,
830
+ "selected-exercise": true,
831
+ "training-setup": true,
832
+ "grade-system": true,
833
+ "hangs": true,
834
+ "warm-light-wind": true,
835
+ "basin": true,
836
+ "rock-canyon": true,
837
+ "b-onsight": true,
838
+ "a-redpoint": true,
839
+ "d-redpoint": true,
840
+ "v0-flash": true,
841
+ "v2-flash": true,
842
+ "v7-redpoint": true,
843
+ "avgrpe-mean": true,
844
+ "calculate": true,
845
+ "totalload-sum": true,
846
+ "query": true,
847
+ "by": true,
848
+ "": true,
849
+ "-": true,
850
+ "...": true,
851
+ "x": true,
852
+ "s": true,
853
+ "bw": true
854
+ };
855
+ }
856
+ });
14
857
  var activePacks = /* @__PURE__ */ new Map();
15
858
  function registerLanguagePack(pack) {
16
859
  const packId = pack.identity?.id ?? pack.identity?.name ?? "anonymous-pack";
@@ -35,11 +878,13 @@ function registerLanguagePack(pack) {
35
878
 
36
879
  // src/cli/loader.ts
37
880
  function getPathUtils() {
38
- if (typeof globalThis.process?.versions?.node === "undefined" && typeof globalThis.Bun === "undefined") {
881
+ if (typeof globalThis.process?.versions?.node === "undefined" && !("Bun" in globalThis)) {
39
882
  throw new Error("Path resolution is only supported in Node / Bun environments");
40
883
  }
41
884
  return {
885
+ // eslint-disable-next-line no-restricted-syntax -- lazy CJS require keeps node builtins out of browser bundles
42
886
  resolve: __require("path").resolve,
887
+ // eslint-disable-next-line no-restricted-syntax -- lazy CJS require keeps node builtins out of browser bundles
43
888
  pathToFileURL: __require("url").pathToFileURL
44
889
  };
45
890
  }
@@ -237,36 +1082,6 @@ async function runExecution(source, options = {}) {
237
1082
  }
238
1083
 
239
1084
  // src/store.ts
240
- function factRowsToEventRows(facts) {
241
- return facts.map((f, i) => {
242
- const metricKey = f.metricKey ?? f.type;
243
- return {
244
- id: `fact:${f.resultId}:${metricKey}:${i}`,
245
- resultId: f.resultId,
246
- noteId: f.noteId,
247
- blockContentId: f.blockContentId,
248
- pageId: f.pageId,
249
- origin: f.origin,
250
- timestamp: f.timestamp,
251
- grain: f.grain === "event" ? "event" : "summary",
252
- outputType: "analytics",
253
- effortSlug: f.effortSlug,
254
- metrics: [{
255
- type: metricKey,
256
- value: f.value,
257
- ...f.unit ? { unit: f.unit } : {},
258
- metadata: {
259
- canonicalKey: metricKey,
260
- ...f.effortSlug ? { effortSlug: f.effortSlug } : {},
261
- ...f.discipline ? { effortDiscipline: f.discipline } : {},
262
- ...f.intensityTier ? { effortIntensityTier: f.intensityTier } : {}
263
- }
264
- }],
265
- segmentId: f.segmentId,
266
- segmentVersion: f.segmentVersion
267
- };
268
- });
269
- }
270
1085
  function inMemoryEventStore(events) {
271
1086
  const rows = [...events];
272
1087
  return {
@@ -309,190 +1124,1456 @@ var WqlSyntaxError = class extends Error {
309
1124
  query;
310
1125
  syntaxError;
311
1126
  };
312
- function factsFromExecutionLog(log, resultId = "stdin-result-1") {
313
- const facts = [];
314
- for (const s of log.logs) {
315
- const started = s.timeSpan?.started ?? s.timestamp ?? Date.now();
316
- for (const m of s.metrics) {
317
- const mType = m.type || "metric";
318
- const numVal = typeof m.value === "number" ? m.value : Number(m.value);
319
- facts.push({
320
- id: `fact-${s.id ?? 0}-${mType}-${facts.length + 1}`,
321
- noteId: "stdin-note-1",
322
- blockContentId: s.sourceBlockKey || "stdin-block-1",
323
- segmentId: s.sourceBlockKey || "stdin-seg-1",
324
- segmentVersion: 1,
325
- resultId,
326
- grain: s.outputType === "analytics" ? "summary" : "event",
327
- type: mType,
328
- value: Number.isNaN(numVal) ? 0 : numVal,
329
- unit: m.unit,
330
- label: m.label || mType,
331
- metricKey: mType,
332
- metricLabel: m.label || mType,
333
- timestamp: started,
334
- createdAt: started
1127
+ function legacyLogsOf(result) {
1128
+ const legacy = result;
1129
+ return legacy.data?.logs ?? [];
1130
+ }
1131
+ function extractPayload(rawJson) {
1132
+ try {
1133
+ const parsed = JSON.parse(rawJson);
1134
+ if (isIRFile(parsed)) {
1135
+ return parsed.data;
1136
+ }
1137
+ return parsed;
1138
+ } catch (err) {
1139
+ throw new Error(`Failed to parse JSON input: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
1140
+ }
1141
+ }
1142
+ function buildStoresFromData(data) {
1143
+ const identity = (r) => ({
1144
+ noteId: r.noteId,
1145
+ resultId: r.id,
1146
+ segmentId: r.segmentId,
1147
+ segmentVersion: r.segmentVersion,
1148
+ blockContentId: r.blockContentId,
1149
+ origin: r.origin,
1150
+ pageId: r.pageId,
1151
+ workoutTimestamp: r.createdAt
1152
+ });
1153
+ const events = [
1154
+ ...data.eventRecords,
1155
+ ...data.facts.map((f, i) => ({
1156
+ id: `fact:${f.resultId ?? "stdin"}:${f.metricKey ?? f.type}:${i}`,
1157
+ resultId: f.resultId ?? "stdin-result-1",
1158
+ noteId: f.noteId ?? "stdin-note-1",
1159
+ blockContentId: f.blockContentId,
1160
+ pageId: f.pageId,
1161
+ origin: f.origin,
1162
+ timestamp: f.timestamp,
1163
+ grain: f.grain === "event" ? "event" : "summary",
1164
+ outputType: "analytics",
1165
+ effortSlug: f.effortSlug,
1166
+ metrics: [{
1167
+ type: f.metricKey ?? f.type,
1168
+ value: f.value,
1169
+ ...f.unit ? { unit: f.unit } : {},
1170
+ origin: "engine"
1171
+ }],
1172
+ segmentId: f.segmentId,
1173
+ segmentVersion: f.segmentVersion
1174
+ })),
1175
+ ...data.results.flatMap((r) => {
1176
+ const logs = legacyLogsOf(r);
1177
+ return [...wodWikiWql.toEventRows(logs, identity(r)), ...wodWikiWql.toSummaryEventRows(logs, identity(r))];
1178
+ })
1179
+ ];
1180
+ const eventStore = inMemoryEventStore(events);
1181
+ const noteStore = {
1182
+ getAllNotes: async () => [...data.notes],
1183
+ getNoteIdsForTag: async (tag) => {
1184
+ const matching = /* @__PURE__ */ new Set();
1185
+ for (const [noteId, tags] of Object.entries(data.noteTags)) {
1186
+ if (tags.includes(tag)) matching.add(noteId);
1187
+ }
1188
+ return matching;
1189
+ },
1190
+ getNoteTagLabels: async (noteId) => [...data.noteTags[noteId] ?? []]
1191
+ };
1192
+ const blockStore = {
1193
+ getAllBlocks: async () => [...data.blocks]
1194
+ };
1195
+ const effortStore = {
1196
+ getAllEfforts: async () => [...data.efforts]
1197
+ };
1198
+ return { eventStore, noteStore, blockStore, effortStore };
1199
+ }
1200
+ function loadQueryData(options) {
1201
+ const data = {
1202
+ eventRecords: [],
1203
+ facts: [],
1204
+ results: [],
1205
+ notes: [],
1206
+ blocks: [],
1207
+ efforts: [...wodWikiLang.bundledEfforts],
1208
+ noteTags: {}
1209
+ };
1210
+ if (options.corpusPath) {
1211
+ const fs = getFs();
1212
+ if (!fs.existsSync(options.corpusPath)) {
1213
+ throw new Error(`Corpus file not found at path: ${options.corpusPath}`);
1214
+ }
1215
+ const raw = fs.readFileSync(options.corpusPath, "utf-8");
1216
+ const payload = extractPayload(raw);
1217
+ if (Array.isArray(payload)) {
1218
+ data.facts = payload;
1219
+ } else if (payload && typeof payload === "object") {
1220
+ const obj = payload;
1221
+ if (obj.kind === "event-journal" || Array.isArray(obj.records)) {
1222
+ if (Array.isArray(obj.records)) {
1223
+ data.eventRecords = obj.records;
1224
+ }
1225
+ if (Array.isArray(obj.notes)) {
1226
+ data.notes = obj.notes;
1227
+ for (const n of obj.notes) {
1228
+ if (n && typeof n === "object" && n.id && Array.isArray(n.tags)) {
1229
+ data.noteTags[n.id] = n.tags;
1230
+ }
1231
+ }
1232
+ }
1233
+ } else if (obj.facts || obj.results || obj.notes || obj.blocks || obj.efforts || obj.tags || obj.logs) {
1234
+ const corpus = payload;
1235
+ if (corpus.facts) data.facts = corpus.facts;
1236
+ if (corpus.results) data.results = corpus.results;
1237
+ if (corpus.notes) data.notes = corpus.notes;
1238
+ if (corpus.blocks) data.blocks = corpus.blocks;
1239
+ if (corpus.efforts) data.efforts = corpus.efforts;
1240
+ if (corpus.tags) data.noteTags = corpus.tags;
1241
+ if (corpus.logs && (!corpus.results || corpus.results.length === 0)) {
1242
+ const syntheticResult = {
1243
+ id: "corpus-result-1",
1244
+ noteId: "corpus-note-1",
1245
+ blockContentId: "corpus-block-1",
1246
+ origin: "journal",
1247
+ createdAt: corpus.logs[0]?.timeSpan?.started ?? Date.now(),
1248
+ startTime: corpus.logs[0]?.timeSpan?.started ?? Date.now(),
1249
+ endTime: corpus.logs[corpus.logs.length - 1]?.timeSpan?.ended ?? Date.now(),
1250
+ duration: 0,
1251
+ completed: true
1252
+ };
1253
+ data.results = [syntheticResult];
1254
+ }
1255
+ } else {
1256
+ throw new Error(`Unrecognized corpus payload shape in "${options.corpusPath}" (expected event-journal, fact-set, or CorpusIRData)`);
1257
+ }
1258
+ }
1259
+ } else if (options.stdinLog) {
1260
+ const payload = extractPayload(options.stdinLog);
1261
+ let executionLog;
1262
+ const rawLogs = payload.logs ?? ("statements" in payload ? payload.statements : void 0) ?? [];
1263
+ if ("results" in payload) {
1264
+ executionLog = { results: payload.results, logs: rawLogs, statements: rawLogs };
1265
+ } else {
1266
+ executionLog = { results: payload, logs: rawLogs, statements: rawLogs };
1267
+ }
1268
+ const resultId = "stdin-result-1";
1269
+ const workoutResult = {
1270
+ id: resultId,
1271
+ noteId: "stdin-note-1",
1272
+ blockContentId: "stdin-block-1",
1273
+ origin: "journal",
1274
+ createdAt: executionLog.results.startTime,
1275
+ startTime: executionLog.results.startTime,
1276
+ endTime: executionLog.results.endTime,
1277
+ duration: executionLog.results.duration ?? 0,
1278
+ completed: executionLog.results.completed
1279
+ };
1280
+ data.results = [workoutResult];
1281
+ data.eventRecords = wodWikiWql.toEventRows(executionLog.logs, {
1282
+ noteId: workoutResult.noteId,
1283
+ resultId: workoutResult.id,
1284
+ segmentId: workoutResult.segmentId,
1285
+ segmentVersion: workoutResult.segmentVersion,
1286
+ blockContentId: workoutResult.blockContentId,
1287
+ origin: workoutResult.origin,
1288
+ pageId: workoutResult.pageId,
1289
+ workoutTimestamp: workoutResult.createdAt
1290
+ });
1291
+ } else if (options.stdinFacts) {
1292
+ const payload = extractPayload(options.stdinFacts);
1293
+ if (Array.isArray(payload)) {
1294
+ data.facts = payload;
1295
+ } else if (payload && typeof payload === "object" && "facts" in payload) {
1296
+ data.facts = payload.facts;
1297
+ }
1298
+ } else {
1299
+ throw new Error("No input dataset provided. Specify --corpus <path>, --stdin-log, or --stdin-facts.");
1300
+ }
1301
+ return data;
1302
+ }
1303
+ async function runQueryCli(wqlString, options = {}) {
1304
+ const parsed = wodWikiWql.parseQuery(wqlString);
1305
+ if (parsed.error) {
1306
+ throw new WqlSyntaxError(wqlString, parsed.error);
1307
+ }
1308
+ const stores = options.stores ?? buildStoresFromData(loadQueryData(options));
1309
+ const service = new wodWikiWql.QueryService(stores);
1310
+ if (wodWikiWql.isPipelineQuery(parsed)) {
1311
+ const result2 = await service.runPipeline(wqlString, {
1312
+ preferredUnit: options.preferredUnit
1313
+ });
1314
+ return createIRFile("pipeline-result", result2, { source: options.sourceLabel ?? "cli:wod query" });
1315
+ }
1316
+ if (wodWikiWql.isFindQuery(parsed)) {
1317
+ const result2 = await service.runFind(parsed);
1318
+ const kind = parsed.target === "session" ? "rows-result" : "find-result";
1319
+ return createIRFile(kind, result2, { source: options.sourceLabel ?? "cli:wod query" });
1320
+ }
1321
+ const result = await service.runQuery(wqlString, {
1322
+ preferredUnit: options.preferredUnit
1323
+ });
1324
+ return createIRFile("query-result", result, { source: options.sourceLabel ?? "cli:wod query" });
1325
+ }
1326
+ var nodePath = () => nodeRequire()("node:path");
1327
+ var nodeFs = () => nodeRequire()("node:fs");
1328
+ var nodeCrypto = () => nodeRequire()("node:crypto");
1329
+ function nodeRequire() {
1330
+ const g = globalThis;
1331
+ if (typeof g.process?.versions?.node === "undefined" && typeof g.Bun === "undefined") {
1332
+ throw new Error("SQLite operations are only supported in Node / Bun environments");
1333
+ }
1334
+ const req = g.require ?? (typeof __require === "function" ? __require : void 0);
1335
+ if (!req) throw new Error("SQLite operations require a CJS require shim (missing tsup banner?)");
1336
+ return req;
1337
+ }
1338
+ var sqlModulePromise;
1339
+ function loadSql() {
1340
+ sqlModulePromise ??= import(
1341
+ /* @vite-ignore */
1342
+ '@bitcobblers/wod-wiki-storage/sql'
1343
+ );
1344
+ return sqlModulePromise;
1345
+ }
1346
+ function resolveDbPath(dbPath) {
1347
+ const chosen = dbPath ?? process.env.WOD_WIKI_DB ?? "./wodwiki.db";
1348
+ return nodePath().resolve(process.cwd(), chosen);
1349
+ }
1350
+ async function openCliDb(dbPath) {
1351
+ const [{ createClient }, sql] = await Promise.all([
1352
+ import(
1353
+ /* @vite-ignore */
1354
+ '@libsql/client'
1355
+ ),
1356
+ loadSql()
1357
+ ]);
1358
+ const resolved = resolveDbPath(dbPath);
1359
+ nodeFs().mkdirSync(nodePath().dirname(resolved), { recursive: true });
1360
+ const db = sql.libsqlDatabase(createClient({ url: `file:${resolved}` }));
1361
+ await sql.initializeStore(db);
1362
+ await sql.initializeDomain(db);
1363
+ return { db, path: resolved, close: () => db.close() };
1364
+ }
1365
+ function formatV5Digest(digest) {
1366
+ const bytes = digest.slice(0, 16);
1367
+ bytes[6] = bytes[6] & 15 | 80;
1368
+ bytes[8] = bytes[8] & 63 | 128;
1369
+ const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0"));
1370
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
1371
+ }
1372
+ function uuid5(namespace, name) {
1373
+ return formatV5Digest(nodeCrypto().createHash("sha1").update(namespace).update(name).digest());
1374
+ }
1375
+ var CLI_UUID_NAMESPACE = "c0ffee00-8b1a-4e2c-9d3f-5a6b7c8d9e0f";
1376
+ function stableNoteId(sourceLabel, content) {
1377
+ if (sourceLabel && sourceLabel !== "-") {
1378
+ return uuid5(CLI_UUID_NAMESPACE, `wod-cli:${nodePath().resolve(process.cwd(), sourceLabel)}`);
1379
+ }
1380
+ return uuid5(CLI_UUID_NAMESPACE, `wod-cli:stdin:${content.length}:${content.slice(0, 256)}`);
1381
+ }
1382
+ function randomId() {
1383
+ return nodeCrypto().randomUUID();
1384
+ }
1385
+ async function allRows(sql, db, store) {
1386
+ return sql.listRows(db, sql.storeDefinition(store));
1387
+ }
1388
+ async function rowsByIndex(sql, db, store, index, only) {
1389
+ return sql.listRows(db, sql.storeDefinition(store), { only }, void 0, index);
1390
+ }
1391
+ function sqliteEventStore(db) {
1392
+ const all = async () => await allRows(await loadSql(), db, "events");
1393
+ return {
1394
+ getEventsByTimeRange: async (start, end) => (await all()).filter((r) => r.timestamp >= start && r.timestamp <= end),
1395
+ getEventsByResult: async (resultId) => (await all()).filter((r) => r.resultId === resultId),
1396
+ getEventsForNote: async (noteId) => (await all()).filter((r) => r.noteId === noteId),
1397
+ getEventsByContent: async (blockContentId) => (await all()).filter((r) => r.blockContentId === blockContentId),
1398
+ scanAll: all,
1399
+ appendEvents: async (appended) => {
1400
+ const sql = await loadSql();
1401
+ await db.transaction(async (tx) => {
1402
+ const eventsDef = sql.storeDefinition("events");
1403
+ for (const row of appended) await sql.putRow(tx, eventsDef, row);
1404
+ });
1405
+ },
1406
+ finalizeSummaries: async (resultId, finals) => {
1407
+ const sql = await loadSql();
1408
+ await db.transaction(async (tx) => {
1409
+ const eventsDef = sql.storeDefinition("events");
1410
+ const existing = (await sql.listRows(tx, eventsDef)).filter(
1411
+ (r) => r.resultId === resultId && r.grain === "summary" && r.origin !== "user"
1412
+ );
1413
+ for (const row of existing) await sql.deleteRows(tx, eventsDef, row.id);
1414
+ for (const row of finals) await sql.putRow(tx, eventsDef, row);
1415
+ });
1416
+ },
1417
+ deleteEvents: async (ids) => {
1418
+ const sql = await loadSql();
1419
+ await db.transaction(async (tx) => {
1420
+ const eventsDef = sql.storeDefinition("events");
1421
+ for (const id of ids) await sql.deleteRows(tx, eventsDef, id);
335
1422
  });
336
1423
  }
1424
+ };
1425
+ }
1426
+ function sqliteNoteStore(db) {
1427
+ return {
1428
+ getAllNotes: async () => await allRows(await loadSql(), db, "notes"),
1429
+ getNoteIdsForTag: async (label) => {
1430
+ const sql = await loadSql();
1431
+ const tags = (await allRows(sql, db, "tags")).filter((t) => t.label === label);
1432
+ const ids = {};
1433
+ for (const tag of tags) {
1434
+ for (const row of await rowsByIndex(sql, db, "note_tags", "by-tag", tag.id)) {
1435
+ ids[row.noteId] = true;
1436
+ }
1437
+ }
1438
+ return new Set(Object.keys(ids));
1439
+ },
1440
+ getNoteTagLabels: async (noteId) => {
1441
+ const sql = await loadSql();
1442
+ const tagRows = await allRows(sql, db, "tags");
1443
+ const labelById = {};
1444
+ for (const t of tagRows) labelById[t.id] = t.label;
1445
+ const links = await rowsByIndex(sql, db, "note_tags", "by-note", noteId);
1446
+ return links.map((l) => labelById[l.tagId]).filter((l) => typeof l === "string");
1447
+ },
1448
+ queryDomain: async (request) => {
1449
+ const result = await (await loadSql()).handleDomainQuery(db, request);
1450
+ return result;
1451
+ }
1452
+ };
1453
+ }
1454
+ function sqliteBlockStore(db) {
1455
+ return {
1456
+ getAllBlocks: async () => await allRows(await loadSql(), db, "block_index"),
1457
+ queryDomain: async (request) => {
1458
+ const result = await (await loadSql()).handleDomainQuery(db, { ...request, plan: "blocks" });
1459
+ return result;
1460
+ }
1461
+ };
1462
+ }
1463
+ function sqliteEffortStore(db) {
1464
+ return {
1465
+ getAllEfforts: async () => {
1466
+ const stored = await allRows(await loadSql(), db, "efforts");
1467
+ const bySlug = {};
1468
+ for (const effort of stored) bySlug[effort.slug] = effort;
1469
+ for (const effort of wodWikiLang.bundledEfforts) {
1470
+ bySlug[effort.slug] ??= effort;
1471
+ }
1472
+ return Object.values(bySlug);
1473
+ }
1474
+ };
1475
+ }
1476
+ function sqliteIndexStore(db, store) {
1477
+ return {
1478
+ getAllFromIndex: async (index, key) => rowsByIndex(await loadSql(), db, store, index, key)
1479
+ };
1480
+ }
1481
+ async function getNoteBundle(db, noteId) {
1482
+ const sql = await loadSql();
1483
+ const notes = await allRows(sql, db, "notes");
1484
+ const note = notes.find((n) => n.id === noteId);
1485
+ const segments = await rowsByIndex(sql, db, "segments", "by-note", noteId);
1486
+ return { note, segments };
1487
+ }
1488
+ async function getSessionBundle(db, resultId) {
1489
+ const sql = await loadSql();
1490
+ const sessions = await allRows(sql, db, "sessions");
1491
+ const session = sessions.find((s) => s.id === resultId);
1492
+ if (!session) return void 0;
1493
+ const events = (await allRows(sql, db, "events")).filter((e) => e.resultId === resultId);
1494
+ return { session, events };
1495
+ }
1496
+ async function getEffortBySlug(db, slug) {
1497
+ const sql = await loadSql();
1498
+ const efforts = await allRows(sql, db, "efforts");
1499
+ return efforts.find((e) => e.slug === slug);
1500
+ }
1501
+ async function listTagsByType(db, type) {
1502
+ const sql = await loadSql();
1503
+ const tags = await allRows(sql, db, "tags");
1504
+ return tags.filter((t) => t.type === type);
1505
+ }
1506
+ function toSegmentDataType(section) {
1507
+ switch (section.type) {
1508
+ case "time":
1509
+ case "log":
1510
+ return "wod";
1511
+ case "title": {
1512
+ const level = Math.min(6, Math.max(1, section.level ?? 1));
1513
+ return `h${level}`;
1514
+ }
1515
+ case "frontmatter":
1516
+ return "frontmatter";
1517
+ case "markdown":
1518
+ default:
1519
+ return "markdown";
1520
+ }
1521
+ }
1522
+ function sectionSourceFragments(content, sections) {
1523
+ const starts = [0];
1524
+ for (let i = 0; i < content.length; i++) {
1525
+ if (content[i] === "\n") starts.push(i + 1);
1526
+ }
1527
+ return sections.map((section, index) => content.slice(
1528
+ starts[section.startLine],
1529
+ index + 1 < sections.length ? starts[sections[index + 1].startLine] : content.length
1530
+ ));
1531
+ }
1532
+ function scriptBlockContentId(data) {
1533
+ if (data && typeof data === "object" && "contentId" in data && typeof data.contentId === "string") {
1534
+ return data.contentId;
1535
+ }
1536
+ return void 0;
1537
+ }
1538
+ function extractEffortSlugs(statements) {
1539
+ const slugs = /* @__PURE__ */ new Set();
1540
+ for (const statement of statements) {
1541
+ if (statement.exerciseId) {
1542
+ slugs.add(statement.exerciseId);
1543
+ } else {
1544
+ const effortMetric = statement.metrics?.getMetric?.("effort");
1545
+ const effort = effortMetric && "effort" in effortMetric ? effortMetric.effort : void 0;
1546
+ if (typeof effort === "string") {
1547
+ const slug = effort.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1548
+ if (slug) slugs.add(slug);
1549
+ }
1550
+ }
1551
+ }
1552
+ return [...slugs];
1553
+ }
1554
+ async function rowsByNote(sql, db, store, noteId) {
1555
+ return sql.listRows(db, sql.storeDefinition(store), { only: noteId }, void 0, "by-note");
1556
+ }
1557
+ function titleFromLabel(sourceLabel) {
1558
+ if (!sourceLabel || sourceLabel === "-") return "stdin";
1559
+ const stem = sourceLabel.split("/").pop() ?? sourceLabel;
1560
+ return stem.replace(/\.[^.]+$/, "") || sourceLabel;
1561
+ }
1562
+ async function persistNote(db, options) {
1563
+ const sql = await loadSql();
1564
+ const now = options.now ?? Date.now();
1565
+ const noteId = options.noteId ?? stableNoteId(options.sourceLabel, options.source);
1566
+ const title = options.title ?? titleFromLabel(options.sourceLabel);
1567
+ const sections = wodWikiCore.parseDocumentSections(options.source);
1568
+ const fragments = sectionSourceFragments(options.source, sections);
1569
+ const existingNote = await noteById(sql, db.db, noteId);
1570
+ const priorSegments = await rowsByNote(sql, db.db, "segments", noteId);
1571
+ const versionBySegmentId = {};
1572
+ for (const segment of priorSegments) {
1573
+ versionBySegmentId[segment.id] = Math.max(versionBySegmentId[segment.id] ?? 0, segment.version);
1574
+ }
1575
+ const createdAt = existingNote?.createdAt ?? now;
1576
+ const note = {
1577
+ id: noteId,
1578
+ title,
1579
+ type: existingNote?.type ?? "note",
1580
+ date: existingNote?.date ?? now,
1581
+ createdAt
1582
+ };
1583
+ const ops = [];
1584
+ const segments = sections.map((section, position) => {
1585
+ const id = `${noteId}:${section.id}`;
1586
+ const version = (versionBySegmentId[id] ?? 0) + 1;
1587
+ return {
1588
+ id,
1589
+ version,
1590
+ noteId,
1591
+ position,
1592
+ dataType: toSegmentDataType(section),
1593
+ data: section.scriptBlock || null,
1594
+ rawContent: section.displayContent,
1595
+ sourceContent: fragments[position],
1596
+ createdAt: version === 1 ? createdAt : now,
1597
+ updatedAt: now,
1598
+ isHistory: false
1599
+ };
1600
+ });
1601
+ for (const segment of priorSegments) {
1602
+ if (segment.isHistory !== true) {
1603
+ ops.push({ op: "put", store: "segments", value: { ...segment, isHistory: true, updatedAt: now } });
1604
+ }
1605
+ }
1606
+ ops.push({ op: "put", store: "notes", value: note });
1607
+ for (const segment of segments) ops.push({ op: "put", store: "segments", value: segment });
1608
+ const priorBlocks = await rowsByNote(sql, db.db, "block_index", noteId);
1609
+ const priorBlockEfforts = await rowsByNote(sql, db.db, "block_efforts", noteId);
1610
+ for (const row of priorBlocks) ops.push({ op: "delete", store: "block_index", key: row.id });
1611
+ for (const row of priorBlockEfforts) ops.push({ op: "delete", store: "block_efforts", key: row.id });
1612
+ let blockCount = 0;
1613
+ let effortCount = 0;
1614
+ for (const segment of segments) {
1615
+ const blockContentId = segment.dataType === "wod" ? scriptBlockContentId(segment.data) : void 0;
1616
+ ops.push({
1617
+ op: "put",
1618
+ store: "block_index",
1619
+ value: {
1620
+ id: `${noteId}:${segment.id}:${segment.version}`,
1621
+ noteId,
1622
+ segmentId: segment.id,
1623
+ segmentVersion: segment.version,
1624
+ position: segment.position,
1625
+ dataType: segment.dataType,
1626
+ blockContentId,
1627
+ rawContent: segment.rawContent,
1628
+ noteTitle: title,
1629
+ createdAt: segment.createdAt
1630
+ }
1631
+ });
1632
+ blockCount += 1;
1633
+ if (segment.dataType === "wod" && segment.rawContent) {
1634
+ const script = wodWikiLang.parseScript(segment.rawContent);
1635
+ for (const effortSlug of extractEffortSlugs(script.statements)) {
1636
+ ops.push({
1637
+ op: "put",
1638
+ store: "block_efforts",
1639
+ value: {
1640
+ id: `${noteId}:${segment.id}:${effortSlug}`,
1641
+ noteId,
1642
+ blockId: segment.id,
1643
+ blockContentId,
1644
+ effortSlug,
1645
+ createdAt: segment.createdAt
1646
+ }
1647
+ });
1648
+ effortCount += 1;
1649
+ }
1650
+ }
1651
+ }
1652
+ let tagCount = 0;
1653
+ if (options.tags) {
1654
+ const priorLinks = await rowsByNote(sql, db.db, "note_tags", noteId);
1655
+ for (const link of priorLinks) ops.push({ op: "delete", store: "note_tags", key: link.id });
1656
+ const tagByLabel = new Map(
1657
+ (await sql.listRows(db.db, sql.storeDefinition("tags"))).map((t) => [t.label, t])
1658
+ );
1659
+ for (const item of options.tags) {
1660
+ const label = item.label.trim();
1661
+ if (!label) continue;
1662
+ let tag = tagByLabel.get(label);
1663
+ if (!tag) {
1664
+ tag = { id: randomId(), label, type: item.type, createdAt: now };
1665
+ tagByLabel.set(label, tag);
1666
+ ops.push({ op: "put", store: "tags", value: tag });
1667
+ } else if (item.type && !tag.type) {
1668
+ tag = { ...tag, type: item.type };
1669
+ tagByLabel.set(label, tag);
1670
+ ops.push({ op: "put", store: "tags", value: tag });
1671
+ }
1672
+ tagCount += 1;
1673
+ ops.push({
1674
+ op: "put",
1675
+ store: "note_tags",
1676
+ // Deterministic junction id (unique [noteId, tagId] index).
1677
+ value: { id: uuid5(CLI_UUID_NAMESPACE, `note-tag:${noteId}:${tag.id}`), noteId, tagId: tag.id }
1678
+ });
1679
+ }
1680
+ }
1681
+ await db.db.transaction(async (tx) => {
1682
+ await sql.refreshDomain(tx, ops);
1683
+ });
1684
+ return {
1685
+ noteId,
1686
+ note,
1687
+ segments: segments.length,
1688
+ blocks: blockCount,
1689
+ efforts: effortCount,
1690
+ tags: tagCount,
1691
+ created: !existingNote
1692
+ };
1693
+ }
1694
+ async function noteById(sql, db, noteId) {
1695
+ const rows = await sql.listRows(db, sql.storeDefinition("notes"));
1696
+ return rows.find((row) => row.id === noteId);
1697
+ }
1698
+ async function persistRun(db, options) {
1699
+ const sql = await loadSql();
1700
+ const results = options.executionLog.results;
1701
+ const logs = options.executionLog.logs ?? [];
1702
+ const noteState = await persistNote(db, {
1703
+ source: options.source,
1704
+ sourceLabel: options.sourceLabel,
1705
+ title: options.title,
1706
+ now: options.now
1707
+ });
1708
+ const segments = (await rowsByNote(sql, db.db, "segments", noteState.noteId)).filter((s) => s.isHistory !== true && s.dataType === "wod").sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
1709
+ const runSegment = segments[0];
1710
+ const blockContentId = runSegment ? scriptBlockContentId(runSegment.data) : void 0;
1711
+ const resultId = randomId();
1712
+ const session = {
1713
+ id: resultId,
1714
+ segmentId: runSegment?.id,
1715
+ segmentVersion: runSegment?.version,
1716
+ noteId: noteState.noteId,
1717
+ blockId: runSegment?.id,
1718
+ blockContentId,
1719
+ origin: "journal",
1720
+ status: results.completed === false ? "in-progress" : "completed",
1721
+ startTime: results.startTime,
1722
+ endTime: results.endTime,
1723
+ duration: results.duration ?? 0,
1724
+ completed: results.completed,
1725
+ createdAt: results.endTime || Date.now()
1726
+ };
1727
+ const identity = {
1728
+ noteId: noteState.noteId,
1729
+ resultId,
1730
+ segmentId: session.segmentId,
1731
+ segmentVersion: session.segmentVersion,
1732
+ blockContentId,
1733
+ origin: session.origin,
1734
+ pageId: session.pageId,
1735
+ workoutTimestamp: session.createdAt
1736
+ };
1737
+ const eventRows = wodWikiWql.toEventRows(logs, identity);
1738
+ const summaryRows = wodWikiWql.toSummaryEventRows(logs, identity);
1739
+ await db.db.transaction(async (tx) => {
1740
+ await sql.putRow(tx, sql.storeDefinition("sessions"), session);
1741
+ const eventsDef = sql.storeDefinition("events");
1742
+ for (const row of [...eventRows, ...summaryRows]) await sql.putRow(tx, eventsDef, row);
1743
+ });
1744
+ return {
1745
+ noteId: noteState.noteId,
1746
+ resultId,
1747
+ session,
1748
+ events: eventRows.length,
1749
+ summaries: summaryRows.length,
1750
+ noteCreated: noteState.created
1751
+ };
1752
+ }
1753
+ async function persistSessionBundle(db, session, events) {
1754
+ const sql = await loadSql();
1755
+ const existing = await noteById(sql, db.db, session.noteId);
1756
+ let noteCreated = false;
1757
+ if (!existing) {
1758
+ noteCreated = true;
1759
+ const now = session.createdAt || Date.now();
1760
+ await db.db.transaction(async (tx) => {
1761
+ await sql.putRow(tx, sql.storeDefinition("notes"), {
1762
+ id: session.noteId,
1763
+ title: `imported-${session.noteId.slice(0, 8)}`,
1764
+ createdAt: now,
1765
+ type: "note"
1766
+ });
1767
+ });
1768
+ }
1769
+ await db.db.transaction(async (tx) => {
1770
+ await sql.putRow(tx, sql.storeDefinition("sessions"), session);
1771
+ const eventsDef = sql.storeDefinition("events");
1772
+ for (const row of events) await sql.putRow(tx, eventsDef, row);
1773
+ });
1774
+ return { events: events.length, noteCreated };
1775
+ }
1776
+ async function persistEffort(db, effort) {
1777
+ const sql = await loadSql();
1778
+ await db.db.transaction(async (tx) => {
1779
+ await sql.putRow(tx, sql.storeDefinition("efforts"), effort);
1780
+ });
1781
+ }
1782
+ async function persistTypedTags(db, labels, type) {
1783
+ const sql = await loadSql();
1784
+ const now = Date.now();
1785
+ const tagByLabel = new Map(
1786
+ (await sql.listRows(db.db, sql.storeDefinition("tags"))).map((t) => [t.label, t])
1787
+ );
1788
+ const ops = [];
1789
+ let added = 0;
1790
+ for (const raw of labels) {
1791
+ const label = raw.trim();
1792
+ if (!label) continue;
1793
+ const tag = tagByLabel.get(label);
1794
+ if (!tag) {
1795
+ ops.push({ op: "put", store: "tags", value: { id: randomId(), label, type, createdAt: now } });
1796
+ added += 1;
1797
+ } else if (!tag.type) {
1798
+ ops.push({ op: "put", store: "tags", value: { ...tag, type } });
1799
+ }
1800
+ }
1801
+ if (ops.length > 0) {
1802
+ await db.db.transaction(async (tx) => {
1803
+ await sql.refreshDomain(tx, ops);
1804
+ });
1805
+ }
1806
+ return added;
1807
+ }
1808
+ var NOTE_ID_FRONTMATTER_KEY = "wod-note-id";
1809
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1810
+ function extractNoteIdentity(markdown) {
1811
+ const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
1812
+ if (!block) return void 0;
1813
+ let noteId;
1814
+ let title;
1815
+ for (const line of block[1].split(/\r?\n/)) {
1816
+ const kv = line.match(/^([^:]+):\s*(.*)$/);
1817
+ if (!kv) continue;
1818
+ const key = kv[1].trim();
1819
+ const value = kv[2].trim();
1820
+ if (key === NOTE_ID_FRONTMATTER_KEY && UUID_RE.test(value)) noteId = value;
1821
+ if (key === "title" && value) title = value;
1822
+ }
1823
+ return noteId ? { noteId, title } : void 0;
1824
+ }
1825
+ var FRONTMATTER_SAFE = /^[^:#\[\]{}'"*&,!?|>%@`\s][^:#\[\]{}"']*$/;
1826
+ function injectIdentityFrontmatter(markdown, identity) {
1827
+ const stamp = (lines2) => {
1828
+ const replaceOrInsert = (key, value) => {
1829
+ if (value === void 0 || !FRONTMATTER_SAFE.test(value)) return;
1830
+ const idx = lines2.findIndex((l) => l.startsWith(`${key}:`));
1831
+ if (idx >= 0) lines2[idx] = `${key}: ${value}`;
1832
+ else lines2.push(`${key}: ${value}`);
1833
+ };
1834
+ replaceOrInsert("title", identity.title);
1835
+ replaceOrInsert(NOTE_ID_FRONTMATTER_KEY, identity.noteId);
1836
+ return lines2;
1837
+ };
1838
+ const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
1839
+ if (block) {
1840
+ const lines2 = stamp(block[1].split(/\r?\n/));
1841
+ return `---
1842
+ ${lines2.join("\n")}
1843
+ ---${markdown.slice(block[0].length)}`;
1844
+ }
1845
+ const lines = stamp([]);
1846
+ return `---
1847
+ ${lines.join("\n")}
1848
+ ---
1849
+
1850
+ ${markdown}`;
1851
+ }
1852
+ function segmentToFragment(segment) {
1853
+ if (segment.sourceContent !== void 0) return segment.sourceContent;
1854
+ if (segment.dataType === "wod") {
1855
+ const block = segment.data;
1856
+ const dialect = block?.dialect ?? "time";
1857
+ const fenceTag = block?.sport ? `${dialect}:${block.sport}` : dialect;
1858
+ return `\`\`\`${fenceTag}
1859
+ ${segment.rawContent}
1860
+ \`\`\``;
1861
+ }
1862
+ if (segment.dataType === "frontmatter") {
1863
+ return `---
1864
+ ${segment.rawContent}
1865
+ ---`;
1866
+ }
1867
+ return segment.rawContent;
1868
+ }
1869
+ function composeNoteMarkdown(note, segments, version) {
1870
+ const pool = version === void 0 ? segments.filter((s) => s.isHistory !== true) : segments;
1871
+ const cap = version ?? Number.MAX_SAFE_INTEGER;
1872
+ const bySegmentId = {};
1873
+ for (const segment of pool) {
1874
+ if (segment.version > cap) continue;
1875
+ const current = bySegmentId[segment.id];
1876
+ if (!current || segment.version > current.version) bySegmentId[segment.id] = segment;
1877
+ }
1878
+ const picked = Object.values(bySegmentId).sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
1879
+ const markdown = picked.map(segmentToFragment).join("");
1880
+ return injectIdentityFrontmatter(markdown, { noteId: note.id, title: note.title });
1881
+ }
1882
+ var FRONTMATTER_BLOCK_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
1883
+ function readFrontmatterMeta(markdown) {
1884
+ const match = FRONTMATTER_BLOCK_RE.exec(markdown);
1885
+ if (!match) return {};
1886
+ const meta = {};
1887
+ const lines = match[1].split(/\r?\n/);
1888
+ let currentListKey;
1889
+ for (const rawLine of lines) {
1890
+ const line = rawLine.trimEnd();
1891
+ const item = line.match(/^\s+-\s+(.*)$/);
1892
+ if (item && currentListKey) {
1893
+ meta[currentListKey].push(item[1].trim());
1894
+ continue;
1895
+ }
1896
+ const kv = line.match(/^([^:]+):\s*(.*)$/);
1897
+ if (!kv) continue;
1898
+ const key = kv[1].trim().toLowerCase();
1899
+ const value = kv[2].trim();
1900
+ if (value === "") {
1901
+ meta[key] = [];
1902
+ currentListKey = key;
1903
+ } else if (value.startsWith("[") && value.endsWith("]")) {
1904
+ meta[key] = value.slice(1, -1).split(",").map((v) => v.trim().replace(/^['"]|['"]$/g, "")).filter((v) => v.length > 0);
1905
+ currentListKey = void 0;
1906
+ } else {
1907
+ meta[key] = value;
1908
+ currentListKey = void 0;
1909
+ }
1910
+ }
1911
+ return meta;
1912
+ }
1913
+ var asList = (value) => Array.isArray(value) ? value : typeof value === "string" ? value.split(",").map((v) => v.trim()).filter((v) => v.length > 0) : [];
1914
+ function extractTypedTags(markdown, typedKeys = wodWikiWql.WQL_TYPED_TAG_KEYS) {
1915
+ const meta = readFrontmatterMeta(markdown);
1916
+ const results = asList(meta["tags"]).map((label) => ({ label }));
1917
+ const metaKeys = Object.keys(meta);
1918
+ for (const typeName of typedKeys) {
1919
+ if (typeName === "tags") continue;
1920
+ const matchedKey = metaKeys.find((k) => k === typeName.toLowerCase());
1921
+ if (!matchedKey) continue;
1922
+ for (const label of asList(meta[matchedKey])) {
1923
+ results.push({ label, type: typeName });
1924
+ }
1925
+ }
1926
+ return results.filter((t) => t.label.length > 0);
1927
+ }
1928
+ function markdownToEffort(raw) {
1929
+ const match = FRONTMATTER_BLOCK_RE.exec(raw);
1930
+ if (!match) return null;
1931
+ const meta = readFrontmatterMeta(raw);
1932
+ const body = raw.slice(match[0].length).trim();
1933
+ const id = scalarOf(meta["id"]);
1934
+ const slug = scalarOf(meta["slug"]);
1935
+ const label = scalarOf(meta["label"]);
1936
+ if (!id || !slug || !label) return null;
1937
+ const baseAttributes = {};
1938
+ const blockRe = /^\s{2,}([^:]+):\s*(.+)$/;
1939
+ let inAttrs = false;
1940
+ for (const line of match[1].split(/\r?\n/)) {
1941
+ if (/^baseAttributes:/.test(line)) {
1942
+ inAttrs = true;
1943
+ continue;
1944
+ }
1945
+ if (inAttrs) {
1946
+ const attr = blockRe.exec(line);
1947
+ if (attr && /^ {2,}/.test(line)) {
1948
+ const v = attr[2].trim();
1949
+ baseAttributes[attr[1].trim()] = /^\d+(\.\d+)?$/.test(v) ? Number(v) : v;
1950
+ } else if (/^\S/.test(line)) {
1951
+ inAttrs = false;
1952
+ }
1953
+ }
1954
+ }
1955
+ if (typeof baseAttributes["met"] !== "number") return null;
1956
+ return {
1957
+ id,
1958
+ slug,
1959
+ label,
1960
+ aliases: asList(meta["aliases"]),
1961
+ baseAttributes,
1962
+ ...body ? { body } : {}
1963
+ };
1964
+ }
1965
+ function effortToMarkdown(effort) {
1966
+ const lines = ["---", `id: ${effort.id}`, `slug: ${effort.slug}`, `label: ${effort.label}`];
1967
+ if (effort.aliases && effort.aliases.length > 0) {
1968
+ lines.push("aliases:");
1969
+ for (const alias of effort.aliases) lines.push(` - ${alias}`);
1970
+ }
1971
+ const attrs = Object.entries(effort.baseAttributes).filter(([, v]) => typeof v === "number" || typeof v === "string");
1972
+ if (attrs.length > 0) {
1973
+ lines.push("baseAttributes:");
1974
+ for (const [k, v] of attrs) lines.push(` ${k}: ${String(v)}`);
1975
+ }
1976
+ lines.push("---", "");
1977
+ return `${lines.join("\n")}
1978
+ ${(effort.body ?? "").trim()}`.trimEnd() + "\n";
1979
+ }
1980
+ function scalarOf(value) {
1981
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
1982
+ }
1983
+ function equipmentToCsv(labels) {
1984
+ return ["label", ...labels].map((l) => escapeCsvField(l)).join("\n") + "\n";
1985
+ }
1986
+ function csvToEquipmentLabels(csv) {
1987
+ const lines = splitCsvRecords(csv);
1988
+ if (lines.length === 0) return [];
1989
+ const header = parseCsvLine(lines[0]);
1990
+ const labelIdx = header[0] === "label" ? 0 : -1;
1991
+ const start = labelIdx >= 0 ? 1 : 0;
1992
+ const labels = [];
1993
+ for (const line of lines.slice(start)) {
1994
+ const cells = parseCsvLine(line);
1995
+ const label = (labelIdx >= 0 ? cells[0] : cells.join(",")).trim();
1996
+ if (label) labels.push(label);
1997
+ }
1998
+ return [...new Set(labels)];
1999
+ }
2000
+ var SESSION_HEADER = [
2001
+ "sessionId",
2002
+ "noteId",
2003
+ "segmentId",
2004
+ "segmentVersion",
2005
+ "blockId",
2006
+ "blockContentId",
2007
+ "origin",
2008
+ "status",
2009
+ "startTime",
2010
+ "endTime",
2011
+ "duration",
2012
+ "completed",
2013
+ "sessionCreatedAt"
2014
+ ];
2015
+ var EVENT_HEADER = [
2016
+ "eventId",
2017
+ "grain",
2018
+ "outputType",
2019
+ "timestamp",
2020
+ "pageId",
2021
+ "effortSlug",
2022
+ "eventSegmentId",
2023
+ "eventSegmentVersion",
2024
+ "eventBlockContentId",
2025
+ "eventOrigin",
2026
+ "timeSpan",
2027
+ "sourceBlockKey",
2028
+ "sourceStatementId",
2029
+ "line",
2030
+ "text",
2031
+ "dialect",
2032
+ "stackLevel",
2033
+ "completionReason",
2034
+ "representationKind",
2035
+ "metricType",
2036
+ "metricValue",
2037
+ "metricUnit",
2038
+ "metricImage",
2039
+ "metricOrigin",
2040
+ "metricMetadata",
2041
+ "metricTemporal",
2042
+ // Verbatim JSON backstops: make the machine round-trip lossless across
2043
+ // schema fields that have no dedicated spreadsheet column. Explicit cells
2044
+ // above still win on import, so hand-edited values are honored.
2045
+ "metricJson",
2046
+ "eventExtraJson"
2047
+ ];
2048
+ var SESSION_CSV_HEADER = [...SESSION_HEADER, ...EVENT_HEADER];
2049
+ function escapeCsvField(field) {
2050
+ if (field === void 0 || field === null) return "";
2051
+ const str = typeof field === "object" ? JSON.stringify(field) : String(field);
2052
+ if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
2053
+ return `"${str.replace(/"/g, '""')}"`;
2054
+ }
2055
+ return str;
2056
+ }
2057
+ var metricValueToCell = (value) => {
2058
+ if (value === void 0 || value === null) return "";
2059
+ if (typeof value === "object") return JSON.stringify(value);
2060
+ return String(value);
2061
+ };
2062
+ var metricValueFromCell = (cell) => {
2063
+ if (cell === "") return void 0;
2064
+ if (/^-?\d+(\.\d+)?$/.test(cell)) return Number(cell);
2065
+ if (cell === "true" || cell === "false") return cell === "true";
2066
+ if (cell.startsWith("{") || cell.startsWith("[")) {
2067
+ try {
2068
+ return JSON.parse(cell);
2069
+ } catch {
2070
+ return cell;
2071
+ }
2072
+ }
2073
+ return cell;
2074
+ };
2075
+ function sessionCells(session) {
2076
+ return [
2077
+ session.id,
2078
+ session.noteId,
2079
+ session.segmentId,
2080
+ session.segmentVersion,
2081
+ session.blockId,
2082
+ session.blockContentId,
2083
+ session.origin,
2084
+ session.status,
2085
+ session.startTime,
2086
+ session.endTime,
2087
+ session.duration,
2088
+ session.completed,
2089
+ session.createdAt
2090
+ ].map(escapeCsvField);
2091
+ }
2092
+ function metricCells(event, metric, index) {
2093
+ return [
2094
+ event.id,
2095
+ event.grain,
2096
+ event.outputType,
2097
+ event.timestamp,
2098
+ event.pageId,
2099
+ event.effortSlug,
2100
+ event.segmentId,
2101
+ event.segmentVersion,
2102
+ event.blockContentId,
2103
+ event.origin,
2104
+ event.timeSpan,
2105
+ event.sourceBlockKey,
2106
+ event.sourceStatementId,
2107
+ event.line,
2108
+ event.text,
2109
+ event.dialect,
2110
+ event.stackLevel,
2111
+ event.completionReason,
2112
+ event.representationKind,
2113
+ metric.type,
2114
+ metricValueToCell(metric.value),
2115
+ metric.unit,
2116
+ metric.image,
2117
+ metric.origin,
2118
+ metric.metadata === void 0 ? "" : JSON.stringify(metric.metadata),
2119
+ event.metricTemporal?.[index] === void 0 ? "" : JSON.stringify(event.metricTemporal[index]),
2120
+ JSON.stringify(metric)
2121
+ ].map(escapeCsvField);
2122
+ }
2123
+ function eventExtraCells(event) {
2124
+ const { metrics: _metrics, ...rest } = event;
2125
+ return escapeCsvField(JSON.stringify(rest));
2126
+ }
2127
+ function sessionToCsv(session, events) {
2128
+ const lines = [SESSION_CSV_HEADER.join(",")];
2129
+ lines.push([...sessionCells(session), ...EVENT_HEADER.map(() => "")].join(","));
2130
+ const ordered = [...events].sort((a, b) => a.id.localeCompare(b.id));
2131
+ for (const event of ordered) {
2132
+ const extra = eventExtraCells(event);
2133
+ const metrics = event.metrics ?? [];
2134
+ if (metrics.length === 0) {
2135
+ lines.push([...sessionCells(session), ...metricCells(event, { type: "" }, 0), extra].join(","));
2136
+ continue;
2137
+ }
2138
+ for (let i = 0; i < metrics.length; i++) {
2139
+ lines.push([...sessionCells(session), ...metricCells(event, metrics[i], i), extra].join(","));
2140
+ }
2141
+ }
2142
+ return lines.join("\n") + "\n";
2143
+ }
2144
+ function parseCsvLine(line) {
2145
+ const cells = [];
2146
+ let current = "";
2147
+ let quoted = false;
2148
+ for (let i = 0; i < line.length; i++) {
2149
+ const ch = line[i];
2150
+ if (quoted) {
2151
+ if (ch === '"') {
2152
+ if (line[i + 1] === '"') {
2153
+ current += '"';
2154
+ i += 1;
2155
+ } else {
2156
+ quoted = false;
2157
+ }
2158
+ } else {
2159
+ current += ch;
2160
+ }
2161
+ } else if (ch === '"' && current === "") {
2162
+ quoted = true;
2163
+ } else if (ch === ",") {
2164
+ cells.push(current);
2165
+ current = "";
2166
+ } else {
2167
+ current += ch;
2168
+ }
2169
+ }
2170
+ cells.push(current);
2171
+ return cells;
2172
+ }
2173
+ function splitCsvRecords(csv) {
2174
+ const records = [];
2175
+ let current = "";
2176
+ let quoted = false;
2177
+ for (let i = 0; i < csv.length; i++) {
2178
+ const ch = csv[i];
2179
+ if (ch === '"') {
2180
+ if (quoted && csv[i + 1] === '"') {
2181
+ current += '""';
2182
+ i += 1;
2183
+ continue;
2184
+ }
2185
+ quoted = !quoted;
2186
+ current += ch;
2187
+ } else if (!quoted && (ch === "\n" || ch === "\r")) {
2188
+ if (ch === "\r" && csv[i + 1] === "\n") i += 1;
2189
+ if (current.length > 0) records.push(current);
2190
+ current = "";
2191
+ } else {
2192
+ current += ch;
2193
+ }
337
2194
  }
338
- return facts;
2195
+ if (current.length > 0) records.push(current);
2196
+ return records;
339
2197
  }
340
- function extractPayload(rawJson) {
2198
+ var num = (cell) => cell === "" ? void 0 : Number(cell);
2199
+ var nonempty = (cell) => cell === "" ? void 0 : cell;
2200
+ function jsonCell(cell) {
2201
+ if (!cell) return void 0;
341
2202
  try {
342
- const parsed = JSON.parse(rawJson);
343
- if (isIRFile(parsed)) {
344
- return parsed.data;
2203
+ return JSON.parse(cell);
2204
+ } catch {
2205
+ return void 0;
2206
+ }
2207
+ }
2208
+ function csvToSessionRows(csv) {
2209
+ const lines = splitCsvRecords(csv);
2210
+ if (lines.length < 2) throw new Error("[wod import] Session CSV has no data rows");
2211
+ const header = parseCsvLine(lines[0]);
2212
+ const col = (name) => {
2213
+ const idx = header.indexOf(name);
2214
+ if (idx < 0) throw new Error(`[wod import] Session CSV missing column "${name}"`);
2215
+ return idx;
2216
+ };
2217
+ const at = (cells, name) => cells[col(name)] ?? "";
2218
+ const eventRows = /* @__PURE__ */ new Map();
2219
+ let session;
2220
+ const ensureSession = (cells) => {
2221
+ if (session) return session;
2222
+ session = {
2223
+ id: at(cells, "sessionId"),
2224
+ noteId: at(cells, "noteId"),
2225
+ segmentId: at(cells, "segmentId") || void 0,
2226
+ segmentVersion: num(at(cells, "segmentVersion")),
2227
+ blockId: at(cells, "blockId") || void 0,
2228
+ blockContentId: at(cells, "blockContentId") || void 0,
2229
+ origin: at(cells, "origin") || void 0,
2230
+ status: at(cells, "status") || void 0,
2231
+ startTime: Number(at(cells, "startTime") || 0),
2232
+ endTime: Number(at(cells, "endTime") || 0),
2233
+ duration: Number(at(cells, "duration") || 0),
2234
+ completed: at(cells, "completed") === "true",
2235
+ createdAt: Number(at(cells, "sessionCreatedAt") || 0)
2236
+ };
2237
+ return session;
2238
+ };
2239
+ for (const line of lines.slice(1)) {
2240
+ const cells = parseCsvLine(line);
2241
+ if (cells.length === 1 && cells[0] === "") continue;
2242
+ const currentSession = ensureSession(cells);
2243
+ const eventId = at(cells, "eventId");
2244
+ if (!eventId) continue;
2245
+ let event = eventRows.get(eventId);
2246
+ if (!event) {
2247
+ const extra = jsonCell(at(cells, "eventExtraJson")) ?? {};
2248
+ event = {
2249
+ ...extra,
2250
+ id: eventId,
2251
+ resultId: currentSession.id,
2252
+ noteId: nonempty(at(cells, "noteId")) ?? extra.noteId ?? currentSession.noteId,
2253
+ blockContentId: nonempty(at(cells, "eventBlockContentId")) ?? extra.blockContentId,
2254
+ pageId: nonempty(at(cells, "pageId")) ?? extra.pageId,
2255
+ origin: nonempty(at(cells, "eventOrigin")) ?? extra.origin,
2256
+ timestamp: Number(at(cells, "timestamp") || extra.timestamp || 0),
2257
+ grain: nonempty(at(cells, "grain")) ?? extra.grain ?? "event",
2258
+ effortSlug: nonempty(at(cells, "effortSlug")) ?? extra.effortSlug,
2259
+ outputType: nonempty(at(cells, "outputType")) ?? extra.outputType ?? "event",
2260
+ segmentId: nonempty(at(cells, "eventSegmentId")) ?? extra.segmentId,
2261
+ segmentVersion: num(at(cells, "eventSegmentVersion")) ?? extra.segmentVersion,
2262
+ timeSpan: jsonCell(at(cells, "timeSpan")) ?? extra.timeSpan,
2263
+ sourceBlockKey: nonempty(at(cells, "sourceBlockKey")) ?? extra.sourceBlockKey,
2264
+ sourceStatementId: num(at(cells, "sourceStatementId")) ?? extra.sourceStatementId,
2265
+ line: num(at(cells, "line")) ?? extra.line,
2266
+ text: nonempty(at(cells, "text")) ?? extra.text,
2267
+ dialect: nonempty(at(cells, "dialect")) ?? extra.dialect,
2268
+ stackLevel: num(at(cells, "stackLevel")) ?? extra.stackLevel,
2269
+ completionReason: nonempty(at(cells, "completionReason")) ?? extra.completionReason,
2270
+ representationKind: nonempty(at(cells, "representationKind")) ?? extra.representationKind,
2271
+ metrics: [],
2272
+ // Rebuilt per-row from the metricTemporal column below — never
2273
+ // inherited from the backstop, or rows would double-collect.
2274
+ metricTemporal: void 0
2275
+ };
2276
+ eventRows.set(eventId, event);
2277
+ }
2278
+ const metricType = at(cells, "metricType");
2279
+ if (!metricType) continue;
2280
+ const baseMetric = jsonCell(at(cells, "metricJson")) ?? {};
2281
+ const metric = {
2282
+ ...baseMetric,
2283
+ type: metricType,
2284
+ ...at(cells, "metricValue") !== "" ? { value: metricValueFromCell(at(cells, "metricValue")) } : {},
2285
+ ...nonempty(at(cells, "metricUnit")) ? { unit: at(cells, "metricUnit") } : {},
2286
+ ...nonempty(at(cells, "metricImage")) ? { image: at(cells, "metricImage") } : {},
2287
+ ...nonempty(at(cells, "metricOrigin")) ? { origin: at(cells, "metricOrigin") } : {},
2288
+ ...at(cells, "metricMetadata") !== "" ? { metadata: jsonCell(at(cells, "metricMetadata")) } : {}
2289
+ };
2290
+ event.metrics.push(metric);
2291
+ const temporal = jsonCell(at(cells, "metricTemporal"));
2292
+ if (temporal !== void 0) {
2293
+ (event.metricTemporal ??= []).push(temporal);
345
2294
  }
346
- return parsed;
347
- } catch (err) {
348
- throw new Error(`Failed to parse JSON input: ${err instanceof Error ? err.message : String(err)}`);
349
2295
  }
2296
+ if (!session) throw new Error("[wod import] Session CSV did not contain session columns");
2297
+ return { session, events: [...eventRows.values()] };
350
2298
  }
351
- function buildStoresFromData(data) {
352
- const identity = (r) => ({
353
- noteId: r.noteId,
354
- resultId: r.id,
355
- segmentId: r.segmentId,
356
- segmentVersion: r.segmentVersion,
357
- blockContentId: r.blockContentId,
358
- origin: r.origin,
359
- pageId: r.pageId,
360
- workoutTimestamp: r.createdAt
361
- });
362
- const events = [
363
- ...factRowsToEventRows(data.facts),
364
- ...data.results.flatMap((r) => {
365
- const logs = r.data.logs ?? [];
366
- return [...wodWikiWql.toEventRows(logs, identity(r)), ...wodWikiWql.toSummaryEventRows(logs, identity(r))];
367
- })
368
- ];
369
- const eventStore = inMemoryEventStore(events);
370
- const noteStore = {
371
- getAllNotes: async () => [...data.notes],
372
- getNoteIdsForTag: async (tag) => {
373
- const matching = /* @__PURE__ */ new Set();
374
- for (const [noteId, tags] of Object.entries(data.noteTags)) {
375
- if (tags.includes(tag)) matching.add(noteId);
376
- }
377
- return matching;
2299
+
2300
+ // src/cli/seed.ts
2301
+ var SEED_NAMESPACE = "1b671a64-40d5-491e-99b0-da01ff1f3341";
2302
+ var SEED_SCHEMA = 7;
2303
+ var CANVAS_CHUNK_ID = "canvas";
2304
+ var DASHBOARDS_CHUNK_ID = "dashboards";
2305
+ var EFFORTS_CHUNK_ID = "efforts";
2306
+ var TEMPLATE_CHUNK_ID = "template";
2307
+ var scalarOf2 = (value) => typeof value === "string" ? value : void 0;
2308
+ function seedNoteId(path) {
2309
+ return formatV5Digest(nodeCrypto().createHash("sha1").update(`${SEED_NAMESPACE}:${path}`).digest());
2310
+ }
2311
+ function titleFromPath(path) {
2312
+ const stem = path.split("/").pop() ?? path;
2313
+ return stem.replace(/\.md$/, "").replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2314
+ }
2315
+ function catalogForChunkId(chunkId) {
2316
+ const match = /^(?:collection|feed)\.(.+)$/.exec(chunkId);
2317
+ const value = match?.[1];
2318
+ return value && value !== "_root" ? value : void 0;
2319
+ }
2320
+ function canvasRouteSlug(content) {
2321
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
2322
+ if (fm?.[1] && !/^template:\s*canvas\s*$/m.test(fm[1])) return null;
2323
+ const route = fm?.[1]?.match(/^route:\s*(\S+)\s*$/m)?.[1];
2324
+ if (!route) return null;
2325
+ return route.replace(/^\//, "") || null;
2326
+ }
2327
+ function seedNoteSourceId(path, content, chunkId) {
2328
+ const parts = path.split("/");
2329
+ const stem = (parts[parts.length - 1] ?? "").replace(/\.md$/i, "");
2330
+ const under = parts.slice(2).map((p) => p.replace(/\.md$/i, "")).join("/");
2331
+ if (chunkId === CANVAS_CHUNK_ID) {
2332
+ const route = canvasRouteSlug(content);
2333
+ return route ? `guides:${route}` : `guides:${under}`;
2334
+ }
2335
+ if (chunkId === DASHBOARDS_CHUNK_ID) return "dashboards";
2336
+ if (chunkId === EFFORTS_CHUNK_ID) return "efforts";
2337
+ if (chunkId === TEMPLATE_CHUNK_ID) return "playground";
2338
+ if (chunkId.startsWith("collection.")) {
2339
+ const catalog = catalogForChunkId(chunkId);
2340
+ if (!catalog) return "collection:_root";
2341
+ return stem.toLowerCase() === "readme" ? `collection:${catalog}` : `collection:${catalog}/${stem}`;
2342
+ }
2343
+ if (chunkId.startsWith("feed.")) return `feed:feeds/${under}`;
2344
+ return void 0;
2345
+ }
2346
+ function buildNoteRecord(row, chunkId, seedVersion) {
2347
+ const id = seedNoteId(row.path);
2348
+ const createdAt = seedVersion;
2349
+ const meta = readFrontmatterMeta(row.content);
2350
+ const tags = extractTypedTags(row.content);
2351
+ let date;
2352
+ let page;
2353
+ let pageNote;
2354
+ const rawDate = scalarOf2(meta["date"]);
2355
+ if (rawDate && /^\d{4}-\d{2}-\d{2}$/.test(rawDate)) {
2356
+ date = (/* @__PURE__ */ new Date(`${rawDate}T12:00:00Z`)).getTime();
2357
+ }
2358
+ const slug = scalarOf2(meta["slug"]);
2359
+ if (slug) {
2360
+ const pageId = seedNoteId(`page:${slug}`);
2361
+ page = { id: pageId, slug, title: titleFromPath(row.path), createdAt };
2362
+ pageNote = { id: seedNoteId(`pagenote:${slug}:${row.path}`), pageId, noteId: id, createdAt };
2363
+ }
2364
+ const sourceId = seedNoteSourceId(row.path, row.content, chunkId);
2365
+ if (!sourceId) {
2366
+ throw new Error(`[wod seed] chunk "${chunkId}" row "${row.path}" has no source family \u2014 refusing to import it as journal`);
2367
+ }
2368
+ const metaTitle = scalarOf2(meta["title"]);
2369
+ return {
2370
+ note: {
2371
+ id,
2372
+ title: metaTitle?.trim() || titleFromPath(row.path),
2373
+ date,
2374
+ createdAt,
2375
+ type: chunkId === DASHBOARDS_CHUNK_ID ? "dashboard" : "note",
2376
+ sourceId,
2377
+ catalog: catalogForChunkId(chunkId),
2378
+ seedOrigin: "seed",
2379
+ seedVersion,
2380
+ seedChunkId: chunkId,
2381
+ sourcePath: row.path
378
2382
  },
379
- getNoteTagLabels: async (noteId) => [...data.noteTags[noteId] ?? []]
380
- };
381
- const blockStore = {
382
- getAllBlocks: async () => [...data.blocks]
2383
+ segment: {
2384
+ id: `seed:${id}`,
2385
+ version: 1,
2386
+ noteId: id,
2387
+ position: 0,
2388
+ dataType: "markdown",
2389
+ data: null,
2390
+ rawContent: row.content,
2391
+ createdAt
2392
+ },
2393
+ tags,
2394
+ page,
2395
+ pageNote,
2396
+ rawContent: row.content,
2397
+ sourcePath: row.path
383
2398
  };
384
- const effortStore = {
385
- getAllEfforts: async () => [...data.efforts]
2399
+ }
2400
+ async function metaRecord(sql, db, sourceId) {
2401
+ const rows = await sql.listRows(db, sql.storeDefinition("meta"));
2402
+ const existing = rows.find((r) => r.key === `seed:${sourceId}`);
2403
+ return existing ?? {
2404
+ key: `seed:${sourceId}`,
2405
+ schema: SEED_SCHEMA,
2406
+ seedVersion: 0,
2407
+ builtAt: "",
2408
+ importedAt: 0,
2409
+ chunks: {},
2410
+ claim: null
386
2411
  };
387
- return { eventStore, noteStore, blockStore, effortStore };
388
2412
  }
389
- function loadQueryData(options) {
390
- const data = {
391
- facts: [],
392
- results: [],
393
- notes: [],
394
- blocks: [],
395
- efforts: [...wodWikiLang.bundledEfforts],
396
- noteTags: {}
2413
+ async function applyOps(db, ops) {
2414
+ if (ops.length === 0) return;
2415
+ const sql = await loadSql();
2416
+ await db.db.transaction(async (tx) => {
2417
+ await sql.refreshDomain(tx, ops);
2418
+ });
2419
+ }
2420
+ async function importSeedDir(db, dir, sourceId) {
2421
+ const sql = await loadSql();
2422
+ const fs = nodeFs();
2423
+ const source = sourceId || nodePath().basename(dir);
2424
+ const manifest = JSON.parse(fs.readFileSync(nodePath().resolve(dir, "manifest.json"), "utf-8"));
2425
+ const prior = await metaRecord(sql, db.db, source);
2426
+ const notesById = new Map(
2427
+ (await sql.listRows(db.db, sql.storeDefinition("notes"))).map((row) => [row.id, row])
2428
+ );
2429
+ const tagRowByLabel = new Map(
2430
+ (await sql.listRows(db.db, sql.storeDefinition("tags"))).map((t) => [t.label, t])
2431
+ );
2432
+ const summary = {
2433
+ sourceId: source,
2434
+ version: manifest.version,
2435
+ notes: 0,
2436
+ efforts: 0,
2437
+ blocks: 0,
2438
+ blockEfforts: 0,
2439
+ deleted: 0,
2440
+ skippedUserOwned: 0
397
2441
  };
398
- if (options.corpusPath) {
399
- const fs = getFs();
400
- if (!fs.existsSync(options.corpusPath)) {
401
- throw new Error(`Corpus file not found at path: ${options.corpusPath}`);
402
- }
403
- const raw = fs.readFileSync(options.corpusPath, "utf-8");
404
- const payload = extractPayload(raw);
405
- if (Array.isArray(payload)) {
406
- data.facts = payload;
407
- } else if (payload && typeof payload === "object") {
408
- const corpus = payload;
409
- if (corpus.facts) data.facts = corpus.facts;
410
- if (corpus.results) data.results = corpus.results;
411
- if (corpus.notes) data.notes = corpus.notes;
412
- if (corpus.blocks) data.blocks = corpus.blocks;
413
- if (corpus.efforts) data.efforts = corpus.efforts;
414
- if (corpus.tags) data.noteTags = corpus.tags;
415
- if (corpus.logs && (!corpus.results || corpus.results.length === 0)) {
416
- const syntheticResult = {
417
- id: "corpus-result-1",
418
- noteId: "corpus-note-1",
419
- blockContentId: "corpus-block-1",
420
- origin: "journal",
421
- createdAt: corpus.logs[0]?.timeSpan?.started ?? Date.now(),
422
- data: {
423
- startTime: corpus.logs[0]?.timeSpan?.started ?? Date.now(),
424
- endTime: corpus.logs[corpus.logs.length - 1]?.timeSpan?.ended ?? Date.now(),
425
- duration: 0,
426
- completed: true,
427
- logs: corpus.logs
2442
+ const nextMeta = {
2443
+ ...prior,
2444
+ schema: SEED_SCHEMA,
2445
+ seedVersion: manifest.version,
2446
+ builtAt: manifest.builtAt,
2447
+ importedAt: Date.now(),
2448
+ chunks: {}
2449
+ };
2450
+ const ordered = [
2451
+ ...manifest.chunks.filter((c) => (c.kind ?? "notes") === "notes"),
2452
+ ...manifest.chunks.filter((c) => c.kind === "block-index"),
2453
+ ...manifest.chunks.filter((c) => c.kind === "block-efforts")
2454
+ ];
2455
+ const bySourcePath = /* @__PURE__ */ new Map();
2456
+ const noteIdsByChunk = {};
2457
+ const blockIdsByChunk = {};
2458
+ for (const chunk of ordered) {
2459
+ const payload = JSON.parse(fs.readFileSync(nodePath().resolve(dir, chunk.path), "utf-8"));
2460
+ const ops = [];
2461
+ if ((chunk.kind ?? "notes") === "notes") {
2462
+ const rows2 = payload;
2463
+ const built = rows2.map((row) => buildNoteRecord(row, chunk.id, manifest.version));
2464
+ const noteIds = [];
2465
+ for (const record of built) {
2466
+ bySourcePath.set(record.sourcePath, record.note.id);
2467
+ const existing = notesById.get(record.note.id);
2468
+ if (existing && existing.seedOrigin !== "seed") {
2469
+ summary.skippedUserOwned += 1;
2470
+ continue;
2471
+ }
2472
+ noteIds.push(record.note.id);
2473
+ ops.push({ op: "put", store: "notes", value: record.note });
2474
+ ops.push({ op: "put", store: "segments", value: record.segment });
2475
+ if (record.page) ops.push({ op: "put", store: "page", value: record.page });
2476
+ if (record.pageNote) ops.push({ op: "put", store: "page_notes", value: record.pageNote });
2477
+ for (const item of record.tags) {
2478
+ const label = item.label;
2479
+ let tag = tagRowByLabel.get(label);
2480
+ if (!tag) {
2481
+ tag = { id: randomId(), label, type: item.type, createdAt: Date.now() };
2482
+ tagRowByLabel.set(label, tag);
2483
+ ops.push({ op: "put", store: "tags", value: tag });
2484
+ } else if (item.type && !tag.type) {
2485
+ tag = { ...tag, type: item.type };
2486
+ tagRowByLabel.set(label, tag);
2487
+ ops.push({ op: "put", store: "tags", value: tag });
2488
+ }
2489
+ const tagId = tag.id;
2490
+ ops.push({
2491
+ op: "put",
2492
+ store: "note_tags",
2493
+ // Deterministic junction id — re-seeds must upsert the same row
2494
+ // (note_tags has a unique [noteId, tagId] index).
2495
+ value: { id: uuid5(CLI_UUID_NAMESPACE, `note-tag:${record.note.id}:${tagId}`), noteId: record.note.id, tagId }
2496
+ });
2497
+ }
2498
+ if (chunk.id === EFFORTS_CHUNK_ID) {
2499
+ const effort = markdownToEffort(record.rawContent);
2500
+ if (effort) {
2501
+ ops.push({ op: "put", store: "efforts", value: { ...effort, registrySource: "bundled" } });
2502
+ summary.efforts += 1;
428
2503
  }
429
- };
430
- data.results = [syntheticResult];
2504
+ }
431
2505
  }
2506
+ const priorIds = prior.chunks[chunk.id]?.noteIds ?? [];
2507
+ const goneIds = priorIds.filter((id) => !noteIds.includes(id) && notesById.get(id)?.seedOrigin === "seed");
2508
+ for (const id of goneIds) {
2509
+ ops.push(...await noteCascadeOps(sql, db, id));
2510
+ summary.deleted += 1;
2511
+ }
2512
+ noteIdsByChunk[chunk.id] = rows2.map((row) => seedNoteId(row.path));
2513
+ nextMeta.chunks[chunk.id] = { sha256: chunk.sha256, noteIds: noteIdsByChunk[chunk.id] };
2514
+ summary.notes += noteIds.length;
2515
+ await applyOps(db, ops);
2516
+ continue;
432
2517
  }
433
- } else if (options.stdinLog) {
434
- const payload = extractPayload(options.stdinLog);
435
- let executionLog;
436
- const rawLogs = payload.logs ?? payload.statements ?? [];
437
- if ("results" in payload) {
438
- executionLog = { results: payload.results, logs: rawLogs, statements: rawLogs };
439
- } else {
440
- executionLog = { results: payload, logs: rawLogs, statements: rawLogs };
441
- }
442
- const resultId = "stdin-result-1";
443
- const workoutResult = {
444
- id: resultId,
445
- noteId: "stdin-note-1",
446
- blockContentId: "stdin-block-1",
447
- origin: "journal",
448
- createdAt: executionLog.results.startTime,
449
- data: {
450
- ...executionLog.results,
451
- logs: executionLog.logs
2518
+ const rows = payload.filter((row) => row.isStatic === true);
2519
+ const rekeyed = rows.map((row) => {
2520
+ const noteId = row.sourcePath ? bySourcePath.get(row.sourcePath) : void 0;
2521
+ if (!noteId) {
2522
+ throw new Error(
2523
+ `[wod seed] static row ${row.id}: sourcePath "${row.sourcePath ?? ""}" has no imported note \u2014 chunk aborted`
2524
+ );
2525
+ }
2526
+ return chunk.kind === "block-efforts" ? { ...row, noteId } : { ...row, noteId, routeId: row.noteId };
2527
+ });
2528
+ const store = chunk.kind === "block-index" ? "block_index" : "block_efforts";
2529
+ for (const row of rekeyed) ops.push({ op: "put", store, value: row });
2530
+ const priorBlockIds = prior.chunks[chunk.id]?.blockIds ?? [];
2531
+ const writtenIds = new Set(rekeyed.map((row) => row.id));
2532
+ const goneBlocks = priorBlockIds.filter((id) => !writtenIds.has(id));
2533
+ for (const id of goneBlocks) ops.push({ op: "delete", store, key: id });
2534
+ summary.deleted += goneBlocks.length;
2535
+ blockIdsByChunk[chunk.id] = payload.map((row) => row.id);
2536
+ nextMeta.chunks[chunk.id] = { sha256: chunk.sha256, blockIds: blockIdsByChunk[chunk.id] };
2537
+ if (chunk.kind === "block-index") summary.blocks += rekeyed.length;
2538
+ else summary.blockEfforts += rekeyed.length;
2539
+ await applyOps(db, ops);
2540
+ }
2541
+ const manifestIds = new Set(manifest.chunks.map((c) => c.id));
2542
+ for (const [chunkId, checkpoint] of Object.entries(prior.chunks)) {
2543
+ if (manifestIds.has(chunkId)) continue;
2544
+ const sweep = [];
2545
+ for (const id of checkpoint.noteIds ?? []) {
2546
+ if (notesById.get(id)?.seedOrigin === "seed") {
2547
+ sweep.push(...await noteCascadeOps(sql, db, id));
2548
+ summary.deleted += 1;
452
2549
  }
453
- };
454
- data.results = [workoutResult];
455
- data.facts = factsFromExecutionLog(executionLog, resultId);
456
- } else if (options.stdinFacts) {
457
- const payload = extractPayload(options.stdinFacts);
458
- if (Array.isArray(payload)) {
459
- data.facts = payload;
460
- } else if (payload && typeof payload === "object" && "facts" in payload) {
461
- data.facts = payload.facts;
462
2550
  }
463
- } else {
464
- throw new Error("No input dataset provided. Specify --corpus <path>, --stdin-log, or --stdin-facts.");
2551
+ for (const id of checkpoint.blockIds ?? []) {
2552
+ sweep.push({ op: "delete", store: "block_index", key: id });
2553
+ summary.deleted += 1;
2554
+ }
2555
+ await applyOps(db, sweep);
465
2556
  }
466
- return data;
2557
+ await applyOps(db, [{ op: "put", store: "meta", value: nextMeta }]);
2558
+ return summary;
467
2559
  }
468
- async function runQueryCli(wqlString, options = {}) {
469
- const parsed = wodWikiWql.parseQuery(wqlString);
470
- if (parsed.error) {
471
- throw new WqlSyntaxError(wqlString, parsed.error);
472
- }
473
- const stores = buildStoresFromData(loadQueryData(options));
474
- const service = new wodWikiWql.QueryService(
475
- stores.eventStore,
476
- stores.noteStore,
477
- stores.blockStore,
478
- stores.effortStore
479
- );
480
- if (wodWikiWql.isFindQuery(parsed)) {
481
- const result2 = await service.runFind(parsed);
482
- return createIRFile("find-result", result2, { source: options.sourceLabel ?? "cli:wod query" });
483
- }
484
- if (wodWikiWql.isRowsQuery(parsed)) {
485
- const result2 = await service.runRows(parsed);
486
- return createIRFile("rows-result", result2, { source: options.sourceLabel ?? "cli:wod query" });
2560
+ async function noteCascadeOps(sql, db, noteId) {
2561
+ const ops = [{ op: "delete", store: "notes", key: noteId }];
2562
+ for (const store of ["segments", "block_index", "block_efforts", "note_tags"]) {
2563
+ const rows = await sql.listRows(db.db, sql.storeDefinition(store), { only: noteId }, void 0, "by-note");
2564
+ for (const row of rows) {
2565
+ ops.push({ op: "delete", store, key: store === "segments" ? [row.id, row.version ?? 1] : row.id });
2566
+ }
487
2567
  }
488
- const result = await service.runQuery(wqlString, {
489
- preferredUnit: options.preferredUnit
490
- });
491
- return createIRFile("query-result", result, { source: options.sourceLabel ?? "cli:wod query" });
2568
+ return ops;
2569
+ }
2570
+ function defaultSeedDirs(cwd) {
2571
+ const base = nodePath().resolve(cwd, "apps", "playground", "public", "seed");
2572
+ return ["core", "community"].map((id) => nodePath().resolve(base, id)).filter((dir) => nodeFs().existsSync(nodePath().resolve(dir, "manifest.json")));
492
2573
  }
493
2574
 
494
2575
  // src/cli/formatters.ts
495
- function escapeCsvField(field) {
2576
+ function escapeCsvField2(field) {
496
2577
  if (field === void 0 || field === null) return "";
497
2578
  const str = String(field);
498
2579
  if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
@@ -538,9 +2619,9 @@ function formatParseOutput(ir, format, pretty = true) {
538
2619
  n.type,
539
2620
  n.from,
540
2621
  n.to,
541
- escapeCsvField(n.raw),
542
- escapeCsvField(n.metrics.map(formatMetric).join("; ")),
543
- escapeCsvField(n.hints?.join("; ") ?? "")
2622
+ escapeCsvField2(n.raw),
2623
+ escapeCsvField2(n.metrics.map(formatMetric).join("; ")),
2624
+ escapeCsvField2(n.hints?.join("; ") ?? "")
544
2625
  ].join(",")
545
2626
  );
546
2627
  return [header, ...rows].join("\n");
@@ -581,11 +2662,11 @@ function formatExecutionOutput(ir, format, pretty = true) {
581
2662
  s.outputType,
582
2663
  s.timeSpan?.started ?? s.timestamp ?? 0,
583
2664
  s.timeSpan?.ended ?? "",
584
- escapeCsvField(s.sourceBlockKey),
2665
+ escapeCsvField2(s.sourceBlockKey),
585
2666
  s.stackLevel,
586
- escapeCsvField(s.completionReason ?? ""),
587
- escapeCsvField(s.metrics.map(formatMetric).join("; ")),
588
- escapeCsvField(s.hints?.join("; ") ?? "")
2667
+ escapeCsvField2(s.completionReason ?? ""),
2668
+ escapeCsvField2(s.metrics.map(formatMetric).join("; ")),
2669
+ escapeCsvField2(s.hints?.join("; ") ?? "")
589
2670
  ].join(",")
590
2671
  );
591
2672
  return [header, ...rows].join("\n");
@@ -634,7 +2715,7 @@ function formatQueryOutput(ir, format, pretty = true) {
634
2715
  run.timestamp,
635
2716
  event.outputType,
636
2717
  event.id,
637
- escapeCsvField(event.metrics.map(formatMetric).join("; "))
2718
+ escapeCsvField2(event.metrics.map(formatMetric).join("; "))
638
2719
  ].join(",")
639
2720
  );
640
2721
  }
@@ -666,13 +2747,13 @@ function formatQueryOutput(ir, format, pretty = true) {
666
2747
  const header = "target,id,title_or_label,sourceId";
667
2748
  const rows = [];
668
2749
  for (const n of data2.notes) {
669
- rows.push(["note", n.id, escapeCsvField(n.title), escapeCsvField(n.sourceId ?? "")].join(","));
2750
+ rows.push(["note", n.id, escapeCsvField2(n.title), escapeCsvField2(n.sourceId ?? "")].join(","));
670
2751
  }
671
2752
  for (const b of data2.blocks) {
672
- rows.push(["block", b.id, escapeCsvField(b.noteTitle), escapeCsvField(b.sourceId ?? "")].join(","));
2753
+ rows.push(["block", b.id, escapeCsvField2(b.noteTitle), escapeCsvField2(b.sourceId ?? "")].join(","));
673
2754
  }
674
2755
  for (const e of data2.efforts ?? []) {
675
- rows.push(["effort", e.id, escapeCsvField(e.label), escapeCsvField(e.slug)].join(","));
2756
+ rows.push(["effort", e.id, escapeCsvField2(e.label), escapeCsvField2(e.slug)].join(","));
676
2757
  }
677
2758
  return [header, ...rows].join("\n");
678
2759
  }
@@ -694,11 +2775,48 @@ function formatQueryOutput(ir, format, pretty = true) {
694
2775
  }
695
2776
  return lines2.join("\n");
696
2777
  }
2778
+ if (kind === "pipeline-result") {
2779
+ const data2 = ir.data;
2780
+ const sink = data2.chart ? `:${data2.chart.head}` : "data";
2781
+ if (format === "csv") {
2782
+ const header = "series,timestamp,date,value,unit";
2783
+ const rows = [];
2784
+ for (const s of data2.series ?? []) {
2785
+ for (const pt of s.points) {
2786
+ rows.push(
2787
+ [
2788
+ escapeCsvField2(s.label || s.key),
2789
+ pt.ts,
2790
+ formatDate(pt.ts),
2791
+ pt.value,
2792
+ escapeCsvField2(s.unit ?? data2.unit ?? "")
2793
+ ].join(",")
2794
+ );
2795
+ }
2796
+ }
2797
+ return [header, ...rows].join("\n");
2798
+ }
2799
+ const lines2 = [];
2800
+ if (data2.error) lines2.push(`Pipeline Error: ${data2.error}`);
2801
+ lines2.push(`Pipeline Result (\u2192 ${sink}):`);
2802
+ lines2.push("\u2500".repeat(80));
2803
+ if (data2.table) {
2804
+ lines2.push(`Table: ${data2.table.rows.length} of ${data2.table.totalCount} rows`);
2805
+ }
2806
+ for (const s of data2.series ?? []) {
2807
+ lines2.push(`${s.label || s.key}: ${s.points.length} points${s.unit ? ` (${s.unit})` : ""}`);
2808
+ }
2809
+ if (data2.notes?.length) lines2.push(`notes: ${data2.notes.length}`);
2810
+ if (data2.blocks?.length) lines2.push(`blocks: ${data2.blocks.length}`);
2811
+ if (data2.efforts?.length) lines2.push(`efforts: ${data2.efforts.length}`);
2812
+ if (data2.runs?.length) lines2.push(`runs: ${data2.runs.length}`);
2813
+ return lines2.join("\n");
2814
+ }
697
2815
  const data = ir.data;
698
2816
  if (data.scalar !== void 0 && data.series.length === 0) {
699
2817
  if (format === "csv") {
700
2818
  return `metric,scalar,unit
701
- ${escapeCsvField(data.parsed.metric)},${data.scalar},${escapeCsvField(data.unit ?? "")}`;
2819
+ ${escapeCsvField2(data.parsed.metric)},${data.scalar},${escapeCsvField2(data.unit ?? "")}`;
702
2820
  }
703
2821
  return `Scalar Result: ${data.scalar}${data.unit ? " " + data.unit : ""}`;
704
2822
  }
@@ -709,11 +2827,11 @@ ${escapeCsvField(data.parsed.metric)},${data.scalar},${escapeCsvField(data.unit
709
2827
  for (const pt of s.points) {
710
2828
  rows.push(
711
2829
  [
712
- escapeCsvField(s.label || s.key),
2830
+ escapeCsvField2(s.label || s.key),
713
2831
  pt.ts,
714
2832
  formatDate(pt.ts),
715
2833
  pt.value,
716
- escapeCsvField(s.unit ?? data.unit ?? "")
2834
+ escapeCsvField2(s.unit ?? data.unit ?? "")
717
2835
  ].join(",")
718
2836
  );
719
2837
  }
@@ -754,7 +2872,8 @@ var VERSION = "0.6.0";
754
2872
  function parseCliArgs(args) {
755
2873
  const result = {
756
2874
  format: "json",
757
- packs: []
2875
+ packs: [],
2876
+ sources: []
758
2877
  };
759
2878
  let i = 0;
760
2879
  while (i < args.length) {
@@ -805,8 +2924,54 @@ function parseCliArgs(args) {
805
2924
  } else if (arg === "--unit") {
806
2925
  result.unit = args[++i];
807
2926
  i++;
2927
+ } else if (arg === "--db") {
2928
+ result.db = args[++i];
2929
+ i++;
2930
+ } else if (arg.startsWith("--db=")) {
2931
+ result.db = arg.slice("--db=".length);
2932
+ i++;
2933
+ } else if (arg === "--debug") {
2934
+ result.debug = "";
2935
+ i++;
2936
+ } else if (arg.startsWith("--debug=")) {
2937
+ result.debug = arg.slice("--debug=".length);
2938
+ i++;
2939
+ } else if (arg === "--at") {
2940
+ result.at = args[++i];
2941
+ i++;
2942
+ } else if (arg.startsWith("--at=")) {
2943
+ result.at = arg.slice("--at=".length);
2944
+ i++;
2945
+ } else if (arg === "--source") {
2946
+ const source = args[++i];
2947
+ if (source) result.sources.push(source);
2948
+ i++;
2949
+ } else if (arg.startsWith("--source=")) {
2950
+ result.sources.push(arg.slice("--source=".length));
2951
+ i++;
2952
+ } else if (arg === "--scaffold") {
2953
+ result.scaffold = true;
2954
+ i++;
2955
+ } else if (arg === "--template") {
2956
+ result.template = args[++i];
2957
+ i++;
2958
+ } else if (arg.startsWith("--template=")) {
2959
+ result.template = arg.slice("--template=".length);
2960
+ i++;
2961
+ } else if (arg === "--out") {
2962
+ result.output = args[++i];
2963
+ i++;
2964
+ } else if (arg.startsWith("--out=")) {
2965
+ result.output = arg.slice("--out=".length);
2966
+ i++;
2967
+ } else if (arg === "--dir") {
2968
+ result.dir = args[++i];
2969
+ i++;
2970
+ } else if (arg.startsWith("--dir=")) {
2971
+ result.dir = arg.slice("--dir=".length);
2972
+ i++;
808
2973
  } else if (!result.subcommand) {
809
- if (arg === "parse" || arg === "run" || arg === "query" || arg === "help" || arg === "version") {
2974
+ if (arg === "parse" || arg === "run" || arg === "query" || arg === "download" || arg === "import" || arg === "build" || arg === "seed" || arg === "reset" || arg === "help" || arg === "version") {
810
2975
  result.subcommand = arg;
811
2976
  } else {
812
2977
  result.target = arg;
@@ -815,16 +2980,32 @@ function parseCliArgs(args) {
815
2980
  } else if (!result.target) {
816
2981
  result.target = arg;
817
2982
  i++;
2983
+ } else if (!result.target2) {
2984
+ result.target2 = arg;
2985
+ i++;
818
2986
  } else {
819
2987
  i++;
820
2988
  }
821
2989
  }
822
2990
  return result;
823
2991
  }
824
- async function readStdin() {
825
- if (typeof globalThis.Bun?.stdin?.text === "function") {
826
- return globalThis.Bun.stdin.text();
2992
+ function readBunStdinText() {
2993
+ const g = globalThis;
2994
+ if (g && typeof g === "object" && "Bun" in g) {
2995
+ const bun = g.Bun;
2996
+ if (bun && typeof bun === "object" && "stdin" in bun) {
2997
+ const stdin = bun.stdin;
2998
+ if (stdin && typeof stdin === "object" && "text" in stdin) {
2999
+ const text = stdin.text;
3000
+ if (typeof text === "function") return text;
3001
+ }
3002
+ }
827
3003
  }
3004
+ return void 0;
3005
+ }
3006
+ async function readStdin() {
3007
+ const bunText = readBunStdinText();
3008
+ if (bunText) return bunText();
828
3009
  return new Promise((resolve, reject) => {
829
3010
  const chunks = [];
830
3011
  process.stdin.on("data", (chunk) => {
@@ -846,20 +3027,84 @@ USAGE:
846
3027
  wod parse <file | -> [options]
847
3028
  wod run <file | -> [options]
848
3029
  wod query '<wql-string>' [options]
3030
+ wod download <note|session|effort|equipment> <id> [options]
3031
+ wod import <note|session|effort|equipment> <file> [options]
3032
+ wod build [dir] [options]
3033
+ wod seed [options]
3034
+ wod reset [options]
849
3035
 
850
3036
  SUBCOMMANDS:
851
- parse <file | -> Parse Whiteboard Script into AST StatementNode IR (kind: 'parse-tree')
852
- run <file | -> Execute Whiteboard Script state machine and emit ExecutionLog IR (kind: 'execution-log')
853
- query '<wql>' Evaluate WQL query and emit QueryResult / RowsQueryResult / FindQueryResult IR
3037
+ parse <file | -> Parse Whiteboard Script, create a note in the database
3038
+ (segments + block index), optionally emit parse-tree IR.
3039
+ Markdown carrying a 'wod-note-id' frontmatter stamp
3040
+ (see download) imports back into that same note.
3041
+ run <file | -> Execute the script state machine, record a session +
3042
+ event/summary rows in the database, optionally emit
3043
+ execution-log IR
3044
+ query '<wql>' Evaluate WQL against the database (or an explicit
3045
+ dataset) and emit QueryResult / RowsQueryResult IR
3046
+ download note <id> Compose the note's markdown from its segment rows and
3047
+ write it to <id>.md. Use --at <version> to compose
3048
+ as-of a segment version (default: latest). The stamp
3049
+ in the frontmatter makes 'wod parse' re-import it.
3050
+ Frontmatter tags (general + typed equipment/quality/\u2026)
3051
+ are materialized into the tag graph on parse/import.
3052
+ download session <id>
3053
+ Write the session's generated metrics (event + summary
3054
+ rows, one CSV row per metric) to <id>.csv. Import back
3055
+ with 'wod import session'.
3056
+ download effort <slug>
3057
+ Write the effort registry row as corpus-style markdown
3058
+ (frontmatter id/slug/label/aliases/baseAttributes + body)
3059
+ to <slug>.md. Import back with 'wod import effort'.
3060
+ download equipment
3061
+ Write every equipment tag as a one-column CSV to
3062
+ equipment.csv. Import back with 'wod import equipment'.
3063
+ import note <file> Re-import exported note markdown (same as parse with
3064
+ the stamped identity).
3065
+ import session <file>
3066
+ Recreate a session + its events from a downloaded CSV
3067
+ into the same or a clean database (id-keyed upsert).
3068
+ import effort <file>
3069
+ Upsert an effort registry row from corpus-style markdown.
3070
+ import equipment <file>
3071
+ Create/upgrade equipment tags (CSV, one label per row).
3072
+ build [dir] Compile a seed corpus: validate every .md under
3073
+ <dir>/markdown (parity gate + dashboard format gate) and
3074
+ write manifest.json + content-hashed chunks/ +
3075
+ block-parent-map.json + report.html to <dir>/dist
3076
+ (override with -o/--output). Version stamps derive from
3077
+ <dir>'s git history (full checkout required).
3078
+
3079
+ BUILD OPTIONS (for 'build'):
3080
+ --scaffold Also scaffold markdown/efforts/discovered/*.md stubs for
3081
+ workout effort labels missing from the registry.
3082
+ --template <file> Compile an extra markdown file as the 'template' chunk
3083
+ (the host's playground intake template).
854
3084
 
855
- INPUT SOURCING (for 'query'):
3085
+ INPUT SOURCING (for 'query'; without these, the database is queried):
856
3086
  -c, --corpus <path> Load historical corpus dataset from a versioned JSON file
857
3087
  --stdin-log Read kind: 'execution-log' JSON payload from stdin
858
3088
  --stdin-facts Read raw kind: 'fact-set' JSON payload from stdin
859
3089
 
3090
+ DATABASE (parse / run / query / download / import / seed / reset):
3091
+ --db <path> SQLite database file (default: $WOD_WIKI_DB or
3092
+ ./wodwiki.db). Created on demand by parse/run/seed.
3093
+
3094
+ SEED SOURCING (for 'seed'):
3095
+ --source <dir> Extra compiled seed directory (manifest.json + chunks);
3096
+ repeatable. Defaults to the bundled core + community
3097
+ artifacts under apps/playground/public/seed.
3098
+
860
3099
  OPTIONS:
861
3100
  --format <fmt> Output format: json (default), table, csv
862
- -o, --output <path> Write output to a file instead of stdout
3101
+ -o, --output <path> Write the IR payload (or download) to a path instead of
3102
+ the default; '-' writes to stdout
3103
+ --at <version> With 'download note': compose the markdown as-of the
3104
+ given segment version (default: latest)
3105
+ --debug[=name] Also write the IR payload to <name> (default
3106
+ wod-<subcommand>-debug.json); without it, parse/run
3107
+ print a one-line summary of what was persisted
863
3108
  --pack <module> Dynamically load and register a Language Pack (repeatable)
864
3109
  --sport <sport> Target sport fence dialect (e.g. climbing, crossfit)
865
3110
  --unit <unit> Preferred display unit for queries (e.g. lb, kg)
@@ -878,7 +3123,7 @@ function printUsage(writer) {
878
3123
  if (writer) {
879
3124
  writer(text);
880
3125
  } else {
881
- console.log(text);
3126
+ process.stdout.write(text.endsWith("\n") ? text : text + "\n");
882
3127
  }
883
3128
  }
884
3129
  async function cliMain(argv = process.argv.slice(2), io = {}) {
@@ -917,6 +3162,13 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
917
3162
  writeOut(formattedText);
918
3163
  }
919
3164
  };
3165
+ const debugTarget = (subcommand) => parsed.debug === void 0 ? void 0 : parsed.debug || `wod-${subcommand}-debug.json`;
3166
+ const writeDebugFile = (subcommand, ir) => {
3167
+ const target = debugTarget(subcommand);
3168
+ if (target !== void 0) {
3169
+ getFs2().writeFileSync(target, JSON.stringify(ir, null, 2) + "\n", "utf-8");
3170
+ }
3171
+ };
920
3172
  try {
921
3173
  if (parsed.subcommand === "parse") {
922
3174
  if (!parsed.target) {
@@ -935,9 +3187,28 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
935
3187
  source = fs.readFileSync(parsed.target, "utf-8");
936
3188
  }
937
3189
  const ir = runParse(source, { sport: parsed.sport, sourceLabel: `cli:wod parse ${parsed.target}` });
938
- const pretty = !parsed.output && Boolean(process.stdout.isTTY);
939
- const formatted = formatParseOutput(ir, parsed.format, pretty);
940
- deliverOutput(formatted);
3190
+ const identity = extractNoteIdentity(source);
3191
+ const tags = extractTypedTags(source);
3192
+ const db = await openCliDb(parsed.db);
3193
+ try {
3194
+ const persisted = await persistNote(db, {
3195
+ source,
3196
+ sourceLabel: parsed.target,
3197
+ noteId: identity?.noteId,
3198
+ title: identity?.title,
3199
+ tags
3200
+ });
3201
+ writeOut(
3202
+ `[wod parse] note ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.segments} segments, ${persisted.blocks} blocks, ${persisted.efforts} efforts${persisted.tags > 0 ? `, ${persisted.tags} tags` : ""} \u2192 ${db.path}`
3203
+ );
3204
+ } finally {
3205
+ await db.close();
3206
+ }
3207
+ if (parsed.output) {
3208
+ const pretty = Boolean(process.stdout.isTTY);
3209
+ deliverOutput(formatParseOutput(ir, parsed.format, pretty));
3210
+ }
3211
+ writeDebugFile("parse", ir);
941
3212
  return 0;
942
3213
  }
943
3214
  if (parsed.subcommand === "run") {
@@ -957,9 +3228,24 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
957
3228
  source = fs.readFileSync(parsed.target, "utf-8");
958
3229
  }
959
3230
  const ir = await runExecution(source, { sport: parsed.sport, sourceLabel: `cli:wod run ${parsed.target}` });
960
- const pretty = !parsed.output && Boolean(process.stdout.isTTY);
961
- const formatted = formatExecutionOutput(ir, parsed.format, pretty);
962
- deliverOutput(formatted);
3231
+ const db = await openCliDb(parsed.db);
3232
+ try {
3233
+ const persisted = await persistRun(db, {
3234
+ source,
3235
+ sourceLabel: parsed.target,
3236
+ executionLog: ir.data
3237
+ });
3238
+ writeOut(
3239
+ `[wod run] session ${persisted.resultId} \u2014 ${persisted.events} events, ${persisted.summaries} summaries on note ${persisted.noteId} \u2192 ${db.path}`
3240
+ );
3241
+ } finally {
3242
+ await db.close();
3243
+ }
3244
+ if (parsed.output) {
3245
+ const pretty = Boolean(process.stdout.isTTY);
3246
+ deliverOutput(formatExecutionOutput(ir, parsed.format, pretty));
3247
+ }
3248
+ writeDebugFile("run", ir);
963
3249
  return 0;
964
3250
  }
965
3251
  if (parsed.subcommand === "query") {
@@ -967,26 +3253,269 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
967
3253
  writeErr("[wod query] Error: Missing WQL query argument");
968
3254
  return 1;
969
3255
  }
3256
+ const useDb = !parsed.corpus && !parsed.stdinLog && !parsed.stdinFacts;
970
3257
  let stdinLogContent;
971
3258
  let stdinFactsContent;
972
3259
  if (parsed.stdinLog) {
973
3260
  stdinLogContent = await getStdin();
974
3261
  } else if (parsed.stdinFacts) {
975
3262
  stdinFactsContent = await getStdin();
976
- } else if (!parsed.corpus) {
977
- writeErr("[wod query] Error: Query requires input data. Specify --corpus <file>, --stdin-log, or --stdin-facts.");
978
- return 1;
979
3263
  }
980
- const ir = await runQueryCli(parsed.target, {
981
- corpusPath: parsed.corpus,
982
- stdinLog: stdinLogContent,
983
- stdinFacts: stdinFactsContent,
984
- preferredUnit: parsed.unit,
985
- sourceLabel: `cli:wod query "${parsed.target}"`
986
- });
3264
+ let ir;
3265
+ if (useDb) {
3266
+ const dbPath = resolveDbPath(parsed.db);
3267
+ if (!nodeFs().existsSync(dbPath)) {
3268
+ writeErr(
3269
+ `[wod query] No input dataset and no database at ${dbPath}. Specify --corpus <file>, --stdin-log, --stdin-facts, or run \`wod seed\` first.`
3270
+ );
3271
+ return 1;
3272
+ }
3273
+ const db = await openCliDb(parsed.db);
3274
+ try {
3275
+ ir = await runQueryCli(parsed.target, {
3276
+ preferredUnit: parsed.unit,
3277
+ sourceLabel: `cli:wod query "${parsed.target}"`,
3278
+ stores: {
3279
+ eventStore: sqliteEventStore(db.db),
3280
+ noteStore: sqliteNoteStore(db.db),
3281
+ blockStore: sqliteBlockStore(db.db),
3282
+ effortStore: sqliteEffortStore(db.db),
3283
+ blockEffortsStore: sqliteIndexStore(db.db, "block_efforts"),
3284
+ tagsStore: sqliteIndexStore(db.db, "tags"),
3285
+ noteTagsStore: sqliteIndexStore(db.db, "note_tags")
3286
+ }
3287
+ });
3288
+ } finally {
3289
+ await db.close();
3290
+ }
3291
+ } else {
3292
+ if (!parsed.corpus && !stdinLogContent && !stdinFactsContent) {
3293
+ writeErr("[wod query] Error: Query requires input data. Specify --corpus <file>, --stdin-log, or --stdin-facts.");
3294
+ return 1;
3295
+ }
3296
+ ir = await runQueryCli(parsed.target, {
3297
+ corpusPath: parsed.corpus,
3298
+ stdinLog: stdinLogContent,
3299
+ stdinFacts: stdinFactsContent,
3300
+ preferredUnit: parsed.unit,
3301
+ sourceLabel: `cli:wod query "${parsed.target}"`
3302
+ });
3303
+ }
987
3304
  const pretty = !parsed.output && Boolean(process.stdout.isTTY);
988
3305
  const formatted = formatQueryOutput(ir, parsed.format, pretty);
989
3306
  deliverOutput(formatted);
3307
+ writeDebugFile("query", ir);
3308
+ return 0;
3309
+ }
3310
+ if (parsed.subcommand === "download") {
3311
+ const kind = parsed.target;
3312
+ const id = parsed.target2;
3313
+ const DOWNLOAD_KINDS = ["note", "session", "effort", "equipment"];
3314
+ if (!id && kind !== "equipment" || !DOWNLOAD_KINDS.includes(kind ?? "")) {
3315
+ writeErr("[wod download] Usage: wod download <note|session|effort|equipment> <id> [--at <version>] [-o <path>]");
3316
+ return 1;
3317
+ }
3318
+ const targetId = id;
3319
+ const db = await openCliDb(parsed.db);
3320
+ try {
3321
+ if (kind === "note") {
3322
+ const bundle = await getNoteBundle(db.db, targetId);
3323
+ if (!bundle.note) {
3324
+ writeErr(`[wod download] Note not found in ${db.path}: ${id}`);
3325
+ return 1;
3326
+ }
3327
+ const at = parsed.at === void 0 ? void 0 : Number(parsed.at);
3328
+ if (parsed.at !== void 0 && (!Number.isInteger(at) || at < 1)) {
3329
+ writeErr(`[wod download] Invalid --at version: ${parsed.at}`);
3330
+ return 1;
3331
+ }
3332
+ const markdown = composeNoteMarkdown(bundle.note, bundle.segments, at);
3333
+ const target = parsed.output && parsed.output !== "-" ? parsed.output : `${id}${at ? `.v${at}` : ""}.md`;
3334
+ if (parsed.output === "-") {
3335
+ writeOut(markdown);
3336
+ } else {
3337
+ nodeFs().writeFileSync(target, markdown, "utf-8");
3338
+ }
3339
+ writeOut(
3340
+ `[wod download] note ${id}${at ? ` v${at}` : ""} \u2192 ${target} (${bundle.segments.length} segment rows)`
3341
+ );
3342
+ } else if (kind === "session") {
3343
+ const bundle = await getSessionBundle(db.db, targetId);
3344
+ if (!bundle) {
3345
+ writeErr(`[wod download] Session not found in ${db.path}: ${id}`);
3346
+ return 1;
3347
+ }
3348
+ const csv = sessionToCsv(bundle.session, bundle.events);
3349
+ const target = parsed.output && parsed.output !== "-" ? parsed.output : `${id}.csv`;
3350
+ if (parsed.output === "-") {
3351
+ writeOut(csv);
3352
+ } else {
3353
+ nodeFs().writeFileSync(target, csv, "utf-8");
3354
+ }
3355
+ writeOut(`[wod download] session ${id} \u2192 ${target} (${bundle.events.length} events)`);
3356
+ } else if (kind === "effort") {
3357
+ const effort = await getEffortBySlug(db.db, targetId);
3358
+ if (!effort) {
3359
+ writeErr(`[wod download] Effort not found in ${db.path}: ${id}`);
3360
+ return 1;
3361
+ }
3362
+ const markdown = effortToMarkdown(effort);
3363
+ const target = parsed.output && parsed.output !== "-" ? parsed.output : `${id}.md`;
3364
+ if (parsed.output === "-") {
3365
+ writeOut(markdown);
3366
+ } else {
3367
+ nodeFs().writeFileSync(target, markdown, "utf-8");
3368
+ }
3369
+ writeOut(`[wod download] effort ${id} \u2192 ${target}`);
3370
+ } else {
3371
+ const labels = (await listTagsByType(db.db, "equipment")).map((t) => t.label).sort();
3372
+ const csv = equipmentToCsv(labels);
3373
+ const target = parsed.output && parsed.output !== "-" ? parsed.output : "equipment.csv";
3374
+ if (parsed.output === "-") {
3375
+ writeOut(csv);
3376
+ } else {
3377
+ nodeFs().writeFileSync(target, csv, "utf-8");
3378
+ }
3379
+ writeOut(`[wod download] equipment \u2192 ${target} (${labels.length} labels)`);
3380
+ }
3381
+ } finally {
3382
+ await db.close();
3383
+ }
3384
+ return 0;
3385
+ }
3386
+ if (parsed.subcommand === "import") {
3387
+ const kind = parsed.target;
3388
+ const file = parsed.target2;
3389
+ const IMPORT_KINDS = ["note", "session", "effort", "equipment"];
3390
+ if (!file || !IMPORT_KINDS.includes(kind ?? "")) {
3391
+ writeErr("[wod import] Usage: wod import <note|session|effort|equipment> <file>");
3392
+ return 1;
3393
+ }
3394
+ const fs = nodeFs();
3395
+ if (!fs.existsSync(file)) {
3396
+ writeErr(`[wod import] File not found: ${file}`);
3397
+ return 1;
3398
+ }
3399
+ if (kind === "note") {
3400
+ const source = fs.readFileSync(file, "utf-8");
3401
+ const identity = extractNoteIdentity(source);
3402
+ const tags = extractTypedTags(source);
3403
+ const db2 = await openCliDb(parsed.db);
3404
+ try {
3405
+ const persisted = await persistNote(db2, {
3406
+ source,
3407
+ sourceLabel: file,
3408
+ noteId: identity?.noteId,
3409
+ title: identity?.title,
3410
+ tags
3411
+ });
3412
+ writeOut(
3413
+ `[wod import] note ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.segments} segments${persisted.tags > 0 ? `, ${persisted.tags} tags` : ""} \u2192 ${db2.path}`
3414
+ );
3415
+ } finally {
3416
+ await db2.close();
3417
+ }
3418
+ return 0;
3419
+ }
3420
+ if (kind === "effort") {
3421
+ const effort = markdownToEffort(fs.readFileSync(file, "utf-8"));
3422
+ if (!effort) {
3423
+ writeErr(`[wod import] Unparseable effort file (need frontmatter id/slug/label + numeric baseAttributes.met): ${file}`);
3424
+ return 1;
3425
+ }
3426
+ const db2 = await openCliDb(parsed.db);
3427
+ try {
3428
+ await persistEffort(db2, { ...effort, registrySource: "user" });
3429
+ writeOut(`[wod import] effort ${effort.slug} \u2014 ${effort.label} \u2192 ${db2.path}`);
3430
+ } finally {
3431
+ await db2.close();
3432
+ }
3433
+ return 0;
3434
+ }
3435
+ if (kind === "equipment") {
3436
+ const labels = csvToEquipmentLabels(fs.readFileSync(file, "utf-8"));
3437
+ if (labels.length === 0) {
3438
+ writeErr(`[wod import] No equipment labels found in ${file}`);
3439
+ return 1;
3440
+ }
3441
+ const db2 = await openCliDb(parsed.db);
3442
+ try {
3443
+ const added = await persistTypedTags(db2, labels, "equipment");
3444
+ writeOut(`[wod import] equipment \u2014 ${labels.length} labels (${added} new) \u2192 ${db2.path}`);
3445
+ } finally {
3446
+ await db2.close();
3447
+ }
3448
+ return 0;
3449
+ }
3450
+ const { session, events } = csvToSessionRows(fs.readFileSync(file, "utf-8"));
3451
+ const db = await openCliDb(parsed.db);
3452
+ try {
3453
+ const result = await persistSessionBundle(db, session, events);
3454
+ writeOut(
3455
+ `[wod import] session ${session.id} \u2014 ${result.events} events${result.noteCreated ? " (stub note created)" : ""} \u2192 ${db.path}`
3456
+ );
3457
+ } finally {
3458
+ await db.close();
3459
+ }
3460
+ return 0;
3461
+ }
3462
+ if (parsed.subcommand === "build") {
3463
+ const cwd = process.cwd();
3464
+ const root = nodePath().resolve(cwd, parsed.dir ?? parsed.target ?? ".");
3465
+ if (!nodeFs().existsSync(nodePath().resolve(root, "markdown"))) {
3466
+ writeErr(`[wod build] No markdown/ corpus under ${root} \u2014 run from the content repo root (or pass its directory).`);
3467
+ return 1;
3468
+ }
3469
+ const { generateSeed: generateSeed2 } = await Promise.resolve().then(() => (init_compiler(), compiler_exports));
3470
+ try {
3471
+ const result = generateSeed2({
3472
+ corpusRoot: root,
3473
+ ...parsed.output ? { outDir: nodePath().resolve(cwd, parsed.output) } : {},
3474
+ ...parsed.template ? { templateFile: nodePath().resolve(cwd, parsed.template) } : {},
3475
+ scaffoldDiscoveredEfforts: parsed.scaffold === true
3476
+ });
3477
+ const kb = (n) => `${(n / 1024).toFixed(0)} KB`;
3478
+ const totalBytes = result.manifest.chunks.reduce((s, c) => s + c.bytes, 0);
3479
+ writeOut(`[wod build] schema v${result.manifest.schema} version ${result.manifest.version} (${result.manifest.versionLabel}) \u2192 ${result.outDir}`);
3480
+ for (const c of result.manifest.chunks) {
3481
+ writeOut(` ${c.id.padEnd(48)} ${String(c.count).padStart(4)} ${c.kind === "block-index" ? "rows " : "files"} ${kb(c.bytes).padStart(8)}`);
3482
+ }
3483
+ writeOut(`[wod build] ${result.manifest.chunks.length} chunks, ${result.totalFiles} files, ${kb(totalBytes)} total \u2014 scan ${result.passMs.scan}ms, block-index ${result.passMs.blockIndex}ms, block-efforts ${result.passMs.blockEfforts}ms, write ${result.passMs.write}ms`);
3484
+ return 0;
3485
+ } catch (error) {
3486
+ writeErr(`[wod build] ${error.message}`);
3487
+ return 1;
3488
+ }
3489
+ }
3490
+ if (parsed.subcommand === "seed") {
3491
+ const dirs = parsed.sources.length > 0 ? parsed.sources.map((s) => nodePath().resolve(process.cwd(), s)) : defaultSeedDirs(process.cwd());
3492
+ if (dirs.length === 0) {
3493
+ writeErr(
3494
+ "[wod seed] No seed sources found. Run `bun scripts/sync-seed.ts` to build the bundled artifacts, or pass --source <dir> pointing at a compiled seed directory (manifest.json + chunks)."
3495
+ );
3496
+ return 1;
3497
+ }
3498
+ const db = await openCliDb(parsed.db);
3499
+ try {
3500
+ for (const dir of dirs) {
3501
+ const summary = await importSeedDir(db, dir, "");
3502
+ writeOut(
3503
+ `[wod seed] ${summary.sourceId}: v${summary.version} \u2014 ${summary.notes} notes, ${summary.efforts} efforts, ${summary.blocks} blocks, ${summary.blockEfforts} block-efforts (${summary.deleted} deleted, ${summary.skippedUserOwned} user-owned skipped) \u2192 ${db.path}`
3504
+ );
3505
+ }
3506
+ } finally {
3507
+ await db.close();
3508
+ }
3509
+ return 0;
3510
+ }
3511
+ if (parsed.subcommand === "reset") {
3512
+ const target = resolveDbPath(parsed.db);
3513
+ if (nodeFs().existsSync(target)) {
3514
+ nodeFs().unlinkSync(target);
3515
+ writeOut(`[wod reset] removed ${target}`);
3516
+ } else {
3517
+ writeOut(`[wod reset] nothing to reset \u2014 ${target} does not exist`);
3518
+ }
990
3519
  return 0;
991
3520
  }
992
3521
  writeErr(`[wod] Unknown subcommand "${parsed.subcommand}"`);