@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/index.cjs CHANGED
@@ -1,16 +1,859 @@
1
1
  'use strict';
2
2
 
3
- var wodWikiCore = require('@bitcobblers/wod-wiki-core');
4
3
  var wodWikiLang = require('@bitcobblers/wod-wiki-lang');
5
- var react = require('@bitcobblers/wod-wiki-lang/react');
4
+ var child_process = require('child_process');
5
+ var fs = require('fs');
6
+ var path = require('path');
7
+ var wodWikiCore = require('@bitcobblers/wod-wiki-core');
6
8
  var wodWikiWql = require('@bitcobblers/wod-wiki-wql');
9
+ var crypto = require('crypto');
10
+ var react = require('@bitcobblers/wod-wiki-lang/react');
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 defineLanguagePack(pack) {
16
859
  return pack;
@@ -65,36 +908,6 @@ function getRegisteredLanguagePacks() {
65
908
  }
66
909
 
67
910
  // src/store.ts
68
- function factRowsToEventRows(facts) {
69
- return facts.map((f, i) => {
70
- const metricKey = f.metricKey ?? f.type;
71
- return {
72
- id: `fact:${f.resultId}:${metricKey}:${i}`,
73
- resultId: f.resultId,
74
- noteId: f.noteId,
75
- blockContentId: f.blockContentId,
76
- pageId: f.pageId,
77
- origin: f.origin,
78
- timestamp: f.timestamp,
79
- grain: f.grain === "event" ? "event" : "summary",
80
- outputType: "analytics",
81
- effortSlug: f.effortSlug,
82
- metrics: [{
83
- type: metricKey,
84
- value: f.value,
85
- ...f.unit ? { unit: f.unit } : {},
86
- metadata: {
87
- canonicalKey: metricKey,
88
- ...f.effortSlug ? { effortSlug: f.effortSlug } : {},
89
- ...f.discipline ? { effortDiscipline: f.discipline } : {},
90
- ...f.intensityTier ? { effortIntensityTier: f.intensityTier } : {}
91
- }
92
- }],
93
- segmentId: f.segmentId,
94
- segmentVersion: f.segmentVersion
95
- };
96
- });
97
- }
98
911
  function inMemoryEventStore(events) {
99
912
  const rows = [...events];
100
913
  return {
@@ -121,10 +934,6 @@ function inMemoryEventStore(events) {
121
934
  }
122
935
  };
123
936
  }
124
- function inMemoryEventStoreFromFacts(facts) {
125
- return inMemoryEventStore(factRowsToEventRows(facts));
126
- }
127
- var inMemoryFactStore = inMemoryEventStoreFromFacts;
128
937
  function createIRFile(kind, data, optionsOrSource, generatedAt) {
129
938
  let source = "wod:headless";
130
939
  let genAt = Date.now();
@@ -297,33 +1106,9 @@ var WqlSyntaxError = class extends Error {
297
1106
  query;
298
1107
  syntaxError;
299
1108
  };
300
- function factsFromExecutionLog(log, resultId = "stdin-result-1") {
301
- const facts = [];
302
- for (const s of log.logs) {
303
- const started = s.timeSpan?.started ?? s.timestamp ?? Date.now();
304
- for (const m of s.metrics) {
305
- const mType = m.type || "metric";
306
- const numVal = typeof m.value === "number" ? m.value : Number(m.value);
307
- facts.push({
308
- id: `fact-${s.id ?? 0}-${mType}-${facts.length + 1}`,
309
- noteId: "stdin-note-1",
310
- blockContentId: s.sourceBlockKey || "stdin-block-1",
311
- segmentId: s.sourceBlockKey || "stdin-seg-1",
312
- segmentVersion: 1,
313
- resultId,
314
- grain: s.outputType === "analytics" ? "summary" : "event",
315
- type: mType,
316
- value: Number.isNaN(numVal) ? 0 : numVal,
317
- unit: m.unit,
318
- label: m.label || mType,
319
- metricKey: mType,
320
- metricLabel: m.label || mType,
321
- timestamp: started,
322
- createdAt: started
323
- });
324
- }
325
- }
326
- return facts;
1109
+ function legacyLogsOf(result) {
1110
+ const legacy = result;
1111
+ return legacy.data?.logs ?? [];
327
1112
  }
328
1113
  function extractPayload(rawJson) {
329
1114
  try {
@@ -333,7 +1118,7 @@ function extractPayload(rawJson) {
333
1118
  }
334
1119
  return parsed;
335
1120
  } catch (err) {
336
- throw new Error(`Failed to parse JSON input: ${err instanceof Error ? err.message : String(err)}`);
1121
+ throw new Error(`Failed to parse JSON input: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
337
1122
  }
338
1123
  }
339
1124
  function buildStoresFromData(data) {
@@ -348,9 +1133,29 @@ function buildStoresFromData(data) {
348
1133
  workoutTimestamp: r.createdAt
349
1134
  });
350
1135
  const events = [
351
- ...factRowsToEventRows(data.facts),
1136
+ ...data.eventRecords,
1137
+ ...data.facts.map((f, i) => ({
1138
+ id: `fact:${f.resultId ?? "stdin"}:${f.metricKey ?? f.type}:${i}`,
1139
+ resultId: f.resultId ?? "stdin-result-1",
1140
+ noteId: f.noteId ?? "stdin-note-1",
1141
+ blockContentId: f.blockContentId,
1142
+ pageId: f.pageId,
1143
+ origin: f.origin,
1144
+ timestamp: f.timestamp,
1145
+ grain: f.grain === "event" ? "event" : "summary",
1146
+ outputType: "analytics",
1147
+ effortSlug: f.effortSlug,
1148
+ metrics: [{
1149
+ type: f.metricKey ?? f.type,
1150
+ value: f.value,
1151
+ ...f.unit ? { unit: f.unit } : {},
1152
+ origin: "engine"
1153
+ }],
1154
+ segmentId: f.segmentId,
1155
+ segmentVersion: f.segmentVersion
1156
+ })),
352
1157
  ...data.results.flatMap((r) => {
353
- const logs = r.data.logs ?? [];
1158
+ const logs = legacyLogsOf(r);
354
1159
  return [...wodWikiWql.toEventRows(logs, identity(r)), ...wodWikiWql.toSummaryEventRows(logs, identity(r))];
355
1160
  })
356
1161
  ];
@@ -376,6 +1181,7 @@ function buildStoresFromData(data) {
376
1181
  }
377
1182
  function loadQueryData(options) {
378
1183
  const data = {
1184
+ eventRecords: [],
379
1185
  facts: [],
380
1186
  results: [],
381
1187
  notes: [],
@@ -393,35 +1199,49 @@ function loadQueryData(options) {
393
1199
  if (Array.isArray(payload)) {
394
1200
  data.facts = payload;
395
1201
  } else if (payload && typeof payload === "object") {
396
- const corpus = payload;
397
- if (corpus.facts) data.facts = corpus.facts;
398
- if (corpus.results) data.results = corpus.results;
399
- if (corpus.notes) data.notes = corpus.notes;
400
- if (corpus.blocks) data.blocks = corpus.blocks;
401
- if (corpus.efforts) data.efforts = corpus.efforts;
402
- if (corpus.tags) data.noteTags = corpus.tags;
403
- if (corpus.logs && (!corpus.results || corpus.results.length === 0)) {
404
- const syntheticResult = {
405
- id: "corpus-result-1",
406
- noteId: "corpus-note-1",
407
- blockContentId: "corpus-block-1",
408
- origin: "journal",
409
- createdAt: corpus.logs[0]?.timeSpan?.started ?? Date.now(),
410
- data: {
1202
+ const obj = payload;
1203
+ if (obj.kind === "event-journal" || Array.isArray(obj.records)) {
1204
+ if (Array.isArray(obj.records)) {
1205
+ data.eventRecords = obj.records;
1206
+ }
1207
+ if (Array.isArray(obj.notes)) {
1208
+ data.notes = obj.notes;
1209
+ for (const n of obj.notes) {
1210
+ if (n && typeof n === "object" && n.id && Array.isArray(n.tags)) {
1211
+ data.noteTags[n.id] = n.tags;
1212
+ }
1213
+ }
1214
+ }
1215
+ } else if (obj.facts || obj.results || obj.notes || obj.blocks || obj.efforts || obj.tags || obj.logs) {
1216
+ const corpus = payload;
1217
+ if (corpus.facts) data.facts = corpus.facts;
1218
+ if (corpus.results) data.results = corpus.results;
1219
+ if (corpus.notes) data.notes = corpus.notes;
1220
+ if (corpus.blocks) data.blocks = corpus.blocks;
1221
+ if (corpus.efforts) data.efforts = corpus.efforts;
1222
+ if (corpus.tags) data.noteTags = corpus.tags;
1223
+ if (corpus.logs && (!corpus.results || corpus.results.length === 0)) {
1224
+ const syntheticResult = {
1225
+ id: "corpus-result-1",
1226
+ noteId: "corpus-note-1",
1227
+ blockContentId: "corpus-block-1",
1228
+ origin: "journal",
1229
+ createdAt: corpus.logs[0]?.timeSpan?.started ?? Date.now(),
411
1230
  startTime: corpus.logs[0]?.timeSpan?.started ?? Date.now(),
412
1231
  endTime: corpus.logs[corpus.logs.length - 1]?.timeSpan?.ended ?? Date.now(),
413
1232
  duration: 0,
414
- completed: true,
415
- logs: corpus.logs
416
- }
417
- };
418
- data.results = [syntheticResult];
1233
+ completed: true
1234
+ };
1235
+ data.results = [syntheticResult];
1236
+ }
1237
+ } else {
1238
+ throw new Error(`Unrecognized corpus payload shape in "${options.corpusPath}" (expected event-journal, fact-set, or CorpusIRData)`);
419
1239
  }
420
1240
  }
421
1241
  } else if (options.stdinLog) {
422
1242
  const payload = extractPayload(options.stdinLog);
423
1243
  let executionLog;
424
- const rawLogs = payload.logs ?? payload.statements ?? [];
1244
+ const rawLogs = payload.logs ?? ("statements" in payload ? payload.statements : void 0) ?? [];
425
1245
  if ("results" in payload) {
426
1246
  executionLog = { results: payload.results, logs: rawLogs, statements: rawLogs };
427
1247
  } else {
@@ -434,13 +1254,22 @@ function loadQueryData(options) {
434
1254
  blockContentId: "stdin-block-1",
435
1255
  origin: "journal",
436
1256
  createdAt: executionLog.results.startTime,
437
- data: {
438
- ...executionLog.results,
439
- logs: executionLog.logs
440
- }
1257
+ startTime: executionLog.results.startTime,
1258
+ endTime: executionLog.results.endTime,
1259
+ duration: executionLog.results.duration ?? 0,
1260
+ completed: executionLog.results.completed
441
1261
  };
442
1262
  data.results = [workoutResult];
443
- data.facts = factsFromExecutionLog(executionLog, resultId);
1263
+ data.eventRecords = wodWikiWql.toEventRows(executionLog.logs, {
1264
+ noteId: workoutResult.noteId,
1265
+ resultId: workoutResult.id,
1266
+ segmentId: workoutResult.segmentId,
1267
+ segmentVersion: workoutResult.segmentVersion,
1268
+ blockContentId: workoutResult.blockContentId,
1269
+ origin: workoutResult.origin,
1270
+ pageId: workoutResult.pageId,
1271
+ workoutTimestamp: workoutResult.createdAt
1272
+ });
444
1273
  } else if (options.stdinFacts) {
445
1274
  const payload = extractPayload(options.stdinFacts);
446
1275
  if (Array.isArray(payload)) {
@@ -458,20 +1287,18 @@ async function runQueryCli(wqlString, options = {}) {
458
1287
  if (parsed.error) {
459
1288
  throw new WqlSyntaxError(wqlString, parsed.error);
460
1289
  }
461
- const stores = buildStoresFromData(loadQueryData(options));
462
- const service = new wodWikiWql.QueryService(
463
- stores.eventStore,
464
- stores.noteStore,
465
- stores.blockStore,
466
- stores.effortStore
467
- );
1290
+ const stores = options.stores ?? buildStoresFromData(loadQueryData(options));
1291
+ const service = new wodWikiWql.QueryService(stores);
1292
+ if (wodWikiWql.isPipelineQuery(parsed)) {
1293
+ const result2 = await service.runPipeline(wqlString, {
1294
+ preferredUnit: options.preferredUnit
1295
+ });
1296
+ return createIRFile("pipeline-result", result2, { source: options.sourceLabel ?? "cli:wod query" });
1297
+ }
468
1298
  if (wodWikiWql.isFindQuery(parsed)) {
469
1299
  const result2 = await service.runFind(parsed);
470
- return createIRFile("find-result", result2, { source: options.sourceLabel ?? "cli:wod query" });
471
- }
472
- if (wodWikiWql.isRowsQuery(parsed)) {
473
- const result2 = await service.runRows(parsed);
474
- return createIRFile("rows-result", result2, { source: options.sourceLabel ?? "cli:wod query" });
1300
+ const kind = parsed.target === "session" ? "rows-result" : "find-result";
1301
+ return createIRFile(kind, result2, { source: options.sourceLabel ?? "cli:wod query" });
475
1302
  }
476
1303
  const result = await service.runQuery(wqlString, {
477
1304
  preferredUnit: options.preferredUnit
@@ -481,11 +1308,13 @@ async function runQueryCli(wqlString, options = {}) {
481
1308
 
482
1309
  // src/cli/loader.ts
483
1310
  function getPathUtils() {
484
- if (typeof globalThis.process?.versions?.node === "undefined" && typeof globalThis.Bun === "undefined") {
1311
+ if (typeof globalThis.process?.versions?.node === "undefined" && !("Bun" in globalThis)) {
485
1312
  throw new Error("Path resolution is only supported in Node / Bun environments");
486
1313
  }
487
1314
  return {
1315
+ // eslint-disable-next-line no-restricted-syntax -- lazy CJS require keeps node builtins out of browser bundles
488
1316
  resolve: __require("path").resolve,
1317
+ // eslint-disable-next-line no-restricted-syntax -- lazy CJS require keeps node builtins out of browser bundles
489
1318
  pathToFileURL: __require("url").pathToFileURL
490
1319
  };
491
1320
  }
@@ -524,9 +1353,1257 @@ async function loadLanguagePack(moduleSpec) {
524
1353
  throw new PackLoadError(moduleSpec, error);
525
1354
  }
526
1355
  }
1356
+ var nodePath = () => nodeRequire()("node:path");
1357
+ var nodeFs = () => nodeRequire()("node:fs");
1358
+ var nodeCrypto = () => nodeRequire()("node:crypto");
1359
+ function nodeRequire() {
1360
+ const g = globalThis;
1361
+ if (typeof g.process?.versions?.node === "undefined" && typeof g.Bun === "undefined") {
1362
+ throw new Error("SQLite operations are only supported in Node / Bun environments");
1363
+ }
1364
+ const req = g.require ?? (typeof __require === "function" ? __require : void 0);
1365
+ if (!req) throw new Error("SQLite operations require a CJS require shim (missing tsup banner?)");
1366
+ return req;
1367
+ }
1368
+ var sqlModulePromise;
1369
+ function loadSql() {
1370
+ sqlModulePromise ??= import(
1371
+ /* @vite-ignore */
1372
+ '@bitcobblers/wod-wiki-storage/sql'
1373
+ );
1374
+ return sqlModulePromise;
1375
+ }
1376
+ function resolveDbPath(dbPath) {
1377
+ const chosen = dbPath ?? process.env.WOD_WIKI_DB ?? "./wodwiki.db";
1378
+ return nodePath().resolve(process.cwd(), chosen);
1379
+ }
1380
+ async function openCliDb(dbPath) {
1381
+ const [{ createClient }, sql] = await Promise.all([
1382
+ import(
1383
+ /* @vite-ignore */
1384
+ '@libsql/client'
1385
+ ),
1386
+ loadSql()
1387
+ ]);
1388
+ const resolved = resolveDbPath(dbPath);
1389
+ nodeFs().mkdirSync(nodePath().dirname(resolved), { recursive: true });
1390
+ const db = sql.libsqlDatabase(createClient({ url: `file:${resolved}` }));
1391
+ await sql.initializeStore(db);
1392
+ await sql.initializeDomain(db);
1393
+ return { db, path: resolved, close: () => db.close() };
1394
+ }
1395
+ function formatV5Digest(digest) {
1396
+ const bytes = digest.slice(0, 16);
1397
+ bytes[6] = bytes[6] & 15 | 80;
1398
+ bytes[8] = bytes[8] & 63 | 128;
1399
+ const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0"));
1400
+ 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("")}`;
1401
+ }
1402
+ function uuid5(namespace, name) {
1403
+ return formatV5Digest(nodeCrypto().createHash("sha1").update(namespace).update(name).digest());
1404
+ }
1405
+ var CLI_UUID_NAMESPACE = "c0ffee00-8b1a-4e2c-9d3f-5a6b7c8d9e0f";
1406
+ function stableNoteId(sourceLabel, content) {
1407
+ if (sourceLabel && sourceLabel !== "-") {
1408
+ return uuid5(CLI_UUID_NAMESPACE, `wod-cli:${nodePath().resolve(process.cwd(), sourceLabel)}`);
1409
+ }
1410
+ return uuid5(CLI_UUID_NAMESPACE, `wod-cli:stdin:${content.length}:${content.slice(0, 256)}`);
1411
+ }
1412
+ function randomId() {
1413
+ return nodeCrypto().randomUUID();
1414
+ }
1415
+ async function allRows(sql, db, store) {
1416
+ return sql.listRows(db, sql.storeDefinition(store));
1417
+ }
1418
+ async function rowsByIndex(sql, db, store, index, only) {
1419
+ return sql.listRows(db, sql.storeDefinition(store), { only }, void 0, index);
1420
+ }
1421
+ function sqliteEventStore(db) {
1422
+ const all = async () => await allRows(await loadSql(), db, "events");
1423
+ return {
1424
+ getEventsByTimeRange: async (start, end) => (await all()).filter((r) => r.timestamp >= start && r.timestamp <= end),
1425
+ getEventsByResult: async (resultId) => (await all()).filter((r) => r.resultId === resultId),
1426
+ getEventsForNote: async (noteId) => (await all()).filter((r) => r.noteId === noteId),
1427
+ getEventsByContent: async (blockContentId) => (await all()).filter((r) => r.blockContentId === blockContentId),
1428
+ scanAll: all,
1429
+ appendEvents: async (appended) => {
1430
+ const sql = await loadSql();
1431
+ await db.transaction(async (tx) => {
1432
+ const eventsDef = sql.storeDefinition("events");
1433
+ for (const row of appended) await sql.putRow(tx, eventsDef, row);
1434
+ });
1435
+ },
1436
+ finalizeSummaries: async (resultId, finals) => {
1437
+ const sql = await loadSql();
1438
+ await db.transaction(async (tx) => {
1439
+ const eventsDef = sql.storeDefinition("events");
1440
+ const existing = (await sql.listRows(tx, eventsDef)).filter(
1441
+ (r) => r.resultId === resultId && r.grain === "summary" && r.origin !== "user"
1442
+ );
1443
+ for (const row of existing) await sql.deleteRows(tx, eventsDef, row.id);
1444
+ for (const row of finals) await sql.putRow(tx, eventsDef, row);
1445
+ });
1446
+ },
1447
+ deleteEvents: async (ids) => {
1448
+ const sql = await loadSql();
1449
+ await db.transaction(async (tx) => {
1450
+ const eventsDef = sql.storeDefinition("events");
1451
+ for (const id of ids) await sql.deleteRows(tx, eventsDef, id);
1452
+ });
1453
+ }
1454
+ };
1455
+ }
1456
+ function sqliteNoteStore(db) {
1457
+ return {
1458
+ getAllNotes: async () => await allRows(await loadSql(), db, "notes"),
1459
+ getNoteIdsForTag: async (label) => {
1460
+ const sql = await loadSql();
1461
+ const tags = (await allRows(sql, db, "tags")).filter((t) => t.label === label);
1462
+ const ids = {};
1463
+ for (const tag of tags) {
1464
+ for (const row of await rowsByIndex(sql, db, "note_tags", "by-tag", tag.id)) {
1465
+ ids[row.noteId] = true;
1466
+ }
1467
+ }
1468
+ return new Set(Object.keys(ids));
1469
+ },
1470
+ getNoteTagLabels: async (noteId) => {
1471
+ const sql = await loadSql();
1472
+ const tagRows = await allRows(sql, db, "tags");
1473
+ const labelById = {};
1474
+ for (const t of tagRows) labelById[t.id] = t.label;
1475
+ const links = await rowsByIndex(sql, db, "note_tags", "by-note", noteId);
1476
+ return links.map((l) => labelById[l.tagId]).filter((l) => typeof l === "string");
1477
+ },
1478
+ queryDomain: async (request) => {
1479
+ const result = await (await loadSql()).handleDomainQuery(db, request);
1480
+ return result;
1481
+ }
1482
+ };
1483
+ }
1484
+ function sqliteBlockStore(db) {
1485
+ return {
1486
+ getAllBlocks: async () => await allRows(await loadSql(), db, "block_index"),
1487
+ queryDomain: async (request) => {
1488
+ const result = await (await loadSql()).handleDomainQuery(db, { ...request, plan: "blocks" });
1489
+ return result;
1490
+ }
1491
+ };
1492
+ }
1493
+ function sqliteEffortStore(db) {
1494
+ return {
1495
+ getAllEfforts: async () => {
1496
+ const stored = await allRows(await loadSql(), db, "efforts");
1497
+ const bySlug = {};
1498
+ for (const effort of stored) bySlug[effort.slug] = effort;
1499
+ for (const effort of wodWikiLang.bundledEfforts) {
1500
+ bySlug[effort.slug] ??= effort;
1501
+ }
1502
+ return Object.values(bySlug);
1503
+ }
1504
+ };
1505
+ }
1506
+ function sqliteIndexStore(db, store) {
1507
+ return {
1508
+ getAllFromIndex: async (index, key) => rowsByIndex(await loadSql(), db, store, index, key)
1509
+ };
1510
+ }
1511
+ async function getNoteBundle(db, noteId) {
1512
+ const sql = await loadSql();
1513
+ const notes = await allRows(sql, db, "notes");
1514
+ const note = notes.find((n) => n.id === noteId);
1515
+ const segments = await rowsByIndex(sql, db, "segments", "by-note", noteId);
1516
+ return { note, segments };
1517
+ }
1518
+ async function getSessionBundle(db, resultId) {
1519
+ const sql = await loadSql();
1520
+ const sessions = await allRows(sql, db, "sessions");
1521
+ const session = sessions.find((s) => s.id === resultId);
1522
+ if (!session) return void 0;
1523
+ const events = (await allRows(sql, db, "events")).filter((e) => e.resultId === resultId);
1524
+ return { session, events };
1525
+ }
1526
+ async function getEffortBySlug(db, slug) {
1527
+ const sql = await loadSql();
1528
+ const efforts = await allRows(sql, db, "efforts");
1529
+ return efforts.find((e) => e.slug === slug);
1530
+ }
1531
+ async function listTagsByType(db, type) {
1532
+ const sql = await loadSql();
1533
+ const tags = await allRows(sql, db, "tags");
1534
+ return tags.filter((t) => t.type === type);
1535
+ }
1536
+ function toSegmentDataType(section) {
1537
+ switch (section.type) {
1538
+ case "time":
1539
+ case "log":
1540
+ return "wod";
1541
+ case "title": {
1542
+ const level = Math.min(6, Math.max(1, section.level ?? 1));
1543
+ return `h${level}`;
1544
+ }
1545
+ case "frontmatter":
1546
+ return "frontmatter";
1547
+ case "markdown":
1548
+ default:
1549
+ return "markdown";
1550
+ }
1551
+ }
1552
+ function sectionSourceFragments(content, sections) {
1553
+ const starts = [0];
1554
+ for (let i = 0; i < content.length; i++) {
1555
+ if (content[i] === "\n") starts.push(i + 1);
1556
+ }
1557
+ return sections.map((section, index) => content.slice(
1558
+ starts[section.startLine],
1559
+ index + 1 < sections.length ? starts[sections[index + 1].startLine] : content.length
1560
+ ));
1561
+ }
1562
+ function scriptBlockContentId(data) {
1563
+ if (data && typeof data === "object" && "contentId" in data && typeof data.contentId === "string") {
1564
+ return data.contentId;
1565
+ }
1566
+ return void 0;
1567
+ }
1568
+ function extractEffortSlugs(statements) {
1569
+ const slugs = /* @__PURE__ */ new Set();
1570
+ for (const statement of statements) {
1571
+ if (statement.exerciseId) {
1572
+ slugs.add(statement.exerciseId);
1573
+ } else {
1574
+ const effortMetric = statement.metrics?.getMetric?.("effort");
1575
+ const effort = effortMetric && "effort" in effortMetric ? effortMetric.effort : void 0;
1576
+ if (typeof effort === "string") {
1577
+ const slug = effort.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1578
+ if (slug) slugs.add(slug);
1579
+ }
1580
+ }
1581
+ }
1582
+ return [...slugs];
1583
+ }
1584
+ async function rowsByNote(sql, db, store, noteId) {
1585
+ return sql.listRows(db, sql.storeDefinition(store), { only: noteId }, void 0, "by-note");
1586
+ }
1587
+ function titleFromLabel(sourceLabel) {
1588
+ if (!sourceLabel || sourceLabel === "-") return "stdin";
1589
+ const stem = sourceLabel.split("/").pop() ?? sourceLabel;
1590
+ return stem.replace(/\.[^.]+$/, "") || sourceLabel;
1591
+ }
1592
+ async function persistNote(db, options) {
1593
+ const sql = await loadSql();
1594
+ const now = options.now ?? Date.now();
1595
+ const noteId = options.noteId ?? stableNoteId(options.sourceLabel, options.source);
1596
+ const title = options.title ?? titleFromLabel(options.sourceLabel);
1597
+ const sections = wodWikiCore.parseDocumentSections(options.source);
1598
+ const fragments = sectionSourceFragments(options.source, sections);
1599
+ const existingNote = await noteById(sql, db.db, noteId);
1600
+ const priorSegments = await rowsByNote(sql, db.db, "segments", noteId);
1601
+ const versionBySegmentId = {};
1602
+ for (const segment of priorSegments) {
1603
+ versionBySegmentId[segment.id] = Math.max(versionBySegmentId[segment.id] ?? 0, segment.version);
1604
+ }
1605
+ const createdAt = existingNote?.createdAt ?? now;
1606
+ const note = {
1607
+ id: noteId,
1608
+ title,
1609
+ type: existingNote?.type ?? "note",
1610
+ date: existingNote?.date ?? now,
1611
+ createdAt
1612
+ };
1613
+ const ops = [];
1614
+ const segments = sections.map((section, position) => {
1615
+ const id = `${noteId}:${section.id}`;
1616
+ const version = (versionBySegmentId[id] ?? 0) + 1;
1617
+ return {
1618
+ id,
1619
+ version,
1620
+ noteId,
1621
+ position,
1622
+ dataType: toSegmentDataType(section),
1623
+ data: section.scriptBlock || null,
1624
+ rawContent: section.displayContent,
1625
+ sourceContent: fragments[position],
1626
+ createdAt: version === 1 ? createdAt : now,
1627
+ updatedAt: now,
1628
+ isHistory: false
1629
+ };
1630
+ });
1631
+ for (const segment of priorSegments) {
1632
+ if (segment.isHistory !== true) {
1633
+ ops.push({ op: "put", store: "segments", value: { ...segment, isHistory: true, updatedAt: now } });
1634
+ }
1635
+ }
1636
+ ops.push({ op: "put", store: "notes", value: note });
1637
+ for (const segment of segments) ops.push({ op: "put", store: "segments", value: segment });
1638
+ const priorBlocks = await rowsByNote(sql, db.db, "block_index", noteId);
1639
+ const priorBlockEfforts = await rowsByNote(sql, db.db, "block_efforts", noteId);
1640
+ for (const row of priorBlocks) ops.push({ op: "delete", store: "block_index", key: row.id });
1641
+ for (const row of priorBlockEfforts) ops.push({ op: "delete", store: "block_efforts", key: row.id });
1642
+ let blockCount = 0;
1643
+ let effortCount = 0;
1644
+ for (const segment of segments) {
1645
+ const blockContentId = segment.dataType === "wod" ? scriptBlockContentId(segment.data) : void 0;
1646
+ ops.push({
1647
+ op: "put",
1648
+ store: "block_index",
1649
+ value: {
1650
+ id: `${noteId}:${segment.id}:${segment.version}`,
1651
+ noteId,
1652
+ segmentId: segment.id,
1653
+ segmentVersion: segment.version,
1654
+ position: segment.position,
1655
+ dataType: segment.dataType,
1656
+ blockContentId,
1657
+ rawContent: segment.rawContent,
1658
+ noteTitle: title,
1659
+ createdAt: segment.createdAt
1660
+ }
1661
+ });
1662
+ blockCount += 1;
1663
+ if (segment.dataType === "wod" && segment.rawContent) {
1664
+ const script = wodWikiLang.parseScript(segment.rawContent);
1665
+ for (const effortSlug of extractEffortSlugs(script.statements)) {
1666
+ ops.push({
1667
+ op: "put",
1668
+ store: "block_efforts",
1669
+ value: {
1670
+ id: `${noteId}:${segment.id}:${effortSlug}`,
1671
+ noteId,
1672
+ blockId: segment.id,
1673
+ blockContentId,
1674
+ effortSlug,
1675
+ createdAt: segment.createdAt
1676
+ }
1677
+ });
1678
+ effortCount += 1;
1679
+ }
1680
+ }
1681
+ }
1682
+ let tagCount = 0;
1683
+ if (options.tags) {
1684
+ const priorLinks = await rowsByNote(sql, db.db, "note_tags", noteId);
1685
+ for (const link of priorLinks) ops.push({ op: "delete", store: "note_tags", key: link.id });
1686
+ const tagByLabel = new Map(
1687
+ (await sql.listRows(db.db, sql.storeDefinition("tags"))).map((t) => [t.label, t])
1688
+ );
1689
+ for (const item of options.tags) {
1690
+ const label = item.label.trim();
1691
+ if (!label) continue;
1692
+ let tag = tagByLabel.get(label);
1693
+ if (!tag) {
1694
+ tag = { id: randomId(), label, type: item.type, createdAt: now };
1695
+ tagByLabel.set(label, tag);
1696
+ ops.push({ op: "put", store: "tags", value: tag });
1697
+ } else if (item.type && !tag.type) {
1698
+ tag = { ...tag, type: item.type };
1699
+ tagByLabel.set(label, tag);
1700
+ ops.push({ op: "put", store: "tags", value: tag });
1701
+ }
1702
+ tagCount += 1;
1703
+ ops.push({
1704
+ op: "put",
1705
+ store: "note_tags",
1706
+ // Deterministic junction id (unique [noteId, tagId] index).
1707
+ value: { id: uuid5(CLI_UUID_NAMESPACE, `note-tag:${noteId}:${tag.id}`), noteId, tagId: tag.id }
1708
+ });
1709
+ }
1710
+ }
1711
+ await db.db.transaction(async (tx) => {
1712
+ await sql.refreshDomain(tx, ops);
1713
+ });
1714
+ return {
1715
+ noteId,
1716
+ note,
1717
+ segments: segments.length,
1718
+ blocks: blockCount,
1719
+ efforts: effortCount,
1720
+ tags: tagCount,
1721
+ created: !existingNote
1722
+ };
1723
+ }
1724
+ async function noteById(sql, db, noteId) {
1725
+ const rows = await sql.listRows(db, sql.storeDefinition("notes"));
1726
+ return rows.find((row) => row.id === noteId);
1727
+ }
1728
+ async function persistRun(db, options) {
1729
+ const sql = await loadSql();
1730
+ const results = options.executionLog.results;
1731
+ const logs = options.executionLog.logs ?? [];
1732
+ const noteState = await persistNote(db, {
1733
+ source: options.source,
1734
+ sourceLabel: options.sourceLabel,
1735
+ title: options.title,
1736
+ now: options.now
1737
+ });
1738
+ 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));
1739
+ const runSegment = segments[0];
1740
+ const blockContentId = runSegment ? scriptBlockContentId(runSegment.data) : void 0;
1741
+ const resultId = randomId();
1742
+ const session = {
1743
+ id: resultId,
1744
+ segmentId: runSegment?.id,
1745
+ segmentVersion: runSegment?.version,
1746
+ noteId: noteState.noteId,
1747
+ blockId: runSegment?.id,
1748
+ blockContentId,
1749
+ origin: "journal",
1750
+ status: results.completed === false ? "in-progress" : "completed",
1751
+ startTime: results.startTime,
1752
+ endTime: results.endTime,
1753
+ duration: results.duration ?? 0,
1754
+ completed: results.completed,
1755
+ createdAt: results.endTime || Date.now()
1756
+ };
1757
+ const identity = {
1758
+ noteId: noteState.noteId,
1759
+ resultId,
1760
+ segmentId: session.segmentId,
1761
+ segmentVersion: session.segmentVersion,
1762
+ blockContentId,
1763
+ origin: session.origin,
1764
+ pageId: session.pageId,
1765
+ workoutTimestamp: session.createdAt
1766
+ };
1767
+ const eventRows = wodWikiWql.toEventRows(logs, identity);
1768
+ const summaryRows = wodWikiWql.toSummaryEventRows(logs, identity);
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 [...eventRows, ...summaryRows]) await sql.putRow(tx, eventsDef, row);
1773
+ });
1774
+ return {
1775
+ noteId: noteState.noteId,
1776
+ resultId,
1777
+ session,
1778
+ events: eventRows.length,
1779
+ summaries: summaryRows.length,
1780
+ noteCreated: noteState.created
1781
+ };
1782
+ }
1783
+ async function persistSessionBundle(db, session, events) {
1784
+ const sql = await loadSql();
1785
+ const existing = await noteById(sql, db.db, session.noteId);
1786
+ let noteCreated = false;
1787
+ if (!existing) {
1788
+ noteCreated = true;
1789
+ const now = session.createdAt || Date.now();
1790
+ await db.db.transaction(async (tx) => {
1791
+ await sql.putRow(tx, sql.storeDefinition("notes"), {
1792
+ id: session.noteId,
1793
+ title: `imported-${session.noteId.slice(0, 8)}`,
1794
+ createdAt: now,
1795
+ type: "note"
1796
+ });
1797
+ });
1798
+ }
1799
+ await db.db.transaction(async (tx) => {
1800
+ await sql.putRow(tx, sql.storeDefinition("sessions"), session);
1801
+ const eventsDef = sql.storeDefinition("events");
1802
+ for (const row of events) await sql.putRow(tx, eventsDef, row);
1803
+ });
1804
+ return { events: events.length, noteCreated };
1805
+ }
1806
+ async function persistEffort(db, effort) {
1807
+ const sql = await loadSql();
1808
+ await db.db.transaction(async (tx) => {
1809
+ await sql.putRow(tx, sql.storeDefinition("efforts"), effort);
1810
+ });
1811
+ }
1812
+ async function persistTypedTags(db, labels, type) {
1813
+ const sql = await loadSql();
1814
+ const now = Date.now();
1815
+ const tagByLabel = new Map(
1816
+ (await sql.listRows(db.db, sql.storeDefinition("tags"))).map((t) => [t.label, t])
1817
+ );
1818
+ const ops = [];
1819
+ let added = 0;
1820
+ for (const raw of labels) {
1821
+ const label = raw.trim();
1822
+ if (!label) continue;
1823
+ const tag = tagByLabel.get(label);
1824
+ if (!tag) {
1825
+ ops.push({ op: "put", store: "tags", value: { id: randomId(), label, type, createdAt: now } });
1826
+ added += 1;
1827
+ } else if (!tag.type) {
1828
+ ops.push({ op: "put", store: "tags", value: { ...tag, type } });
1829
+ }
1830
+ }
1831
+ if (ops.length > 0) {
1832
+ await db.db.transaction(async (tx) => {
1833
+ await sql.refreshDomain(tx, ops);
1834
+ });
1835
+ }
1836
+ return added;
1837
+ }
1838
+ var NOTE_ID_FRONTMATTER_KEY = "wod-note-id";
1839
+ 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;
1840
+ function extractNoteIdentity(markdown) {
1841
+ const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
1842
+ if (!block) return void 0;
1843
+ let noteId;
1844
+ let title;
1845
+ for (const line of block[1].split(/\r?\n/)) {
1846
+ const kv = line.match(/^([^:]+):\s*(.*)$/);
1847
+ if (!kv) continue;
1848
+ const key = kv[1].trim();
1849
+ const value = kv[2].trim();
1850
+ if (key === NOTE_ID_FRONTMATTER_KEY && UUID_RE.test(value)) noteId = value;
1851
+ if (key === "title" && value) title = value;
1852
+ }
1853
+ return noteId ? { noteId, title } : void 0;
1854
+ }
1855
+ var FRONTMATTER_SAFE = /^[^:#\[\]{}'"*&,!?|>%@`\s][^:#\[\]{}"']*$/;
1856
+ function injectIdentityFrontmatter(markdown, identity) {
1857
+ const stamp = (lines2) => {
1858
+ const replaceOrInsert = (key, value) => {
1859
+ if (value === void 0 || !FRONTMATTER_SAFE.test(value)) return;
1860
+ const idx = lines2.findIndex((l) => l.startsWith(`${key}:`));
1861
+ if (idx >= 0) lines2[idx] = `${key}: ${value}`;
1862
+ else lines2.push(`${key}: ${value}`);
1863
+ };
1864
+ replaceOrInsert("title", identity.title);
1865
+ replaceOrInsert(NOTE_ID_FRONTMATTER_KEY, identity.noteId);
1866
+ return lines2;
1867
+ };
1868
+ const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
1869
+ if (block) {
1870
+ const lines2 = stamp(block[1].split(/\r?\n/));
1871
+ return `---
1872
+ ${lines2.join("\n")}
1873
+ ---${markdown.slice(block[0].length)}`;
1874
+ }
1875
+ const lines = stamp([]);
1876
+ return `---
1877
+ ${lines.join("\n")}
1878
+ ---
527
1879
 
528
- // src/cli/formatters.ts
1880
+ ${markdown}`;
1881
+ }
1882
+ function segmentToFragment(segment) {
1883
+ if (segment.sourceContent !== void 0) return segment.sourceContent;
1884
+ if (segment.dataType === "wod") {
1885
+ const block = segment.data;
1886
+ const dialect = block?.dialect ?? "time";
1887
+ const fenceTag = block?.sport ? `${dialect}:${block.sport}` : dialect;
1888
+ return `\`\`\`${fenceTag}
1889
+ ${segment.rawContent}
1890
+ \`\`\``;
1891
+ }
1892
+ if (segment.dataType === "frontmatter") {
1893
+ return `---
1894
+ ${segment.rawContent}
1895
+ ---`;
1896
+ }
1897
+ return segment.rawContent;
1898
+ }
1899
+ function composeNoteMarkdown(note, segments, version) {
1900
+ const pool = version === void 0 ? segments.filter((s) => s.isHistory !== true) : segments;
1901
+ const cap = version ?? Number.MAX_SAFE_INTEGER;
1902
+ const bySegmentId = {};
1903
+ for (const segment of pool) {
1904
+ if (segment.version > cap) continue;
1905
+ const current = bySegmentId[segment.id];
1906
+ if (!current || segment.version > current.version) bySegmentId[segment.id] = segment;
1907
+ }
1908
+ const picked = Object.values(bySegmentId).sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
1909
+ const markdown = picked.map(segmentToFragment).join("");
1910
+ return injectIdentityFrontmatter(markdown, { noteId: note.id, title: note.title });
1911
+ }
1912
+ var FRONTMATTER_BLOCK_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
1913
+ function readFrontmatterMeta(markdown) {
1914
+ const match = FRONTMATTER_BLOCK_RE.exec(markdown);
1915
+ if (!match) return {};
1916
+ const meta = {};
1917
+ const lines = match[1].split(/\r?\n/);
1918
+ let currentListKey;
1919
+ for (const rawLine of lines) {
1920
+ const line = rawLine.trimEnd();
1921
+ const item = line.match(/^\s+-\s+(.*)$/);
1922
+ if (item && currentListKey) {
1923
+ meta[currentListKey].push(item[1].trim());
1924
+ continue;
1925
+ }
1926
+ const kv = line.match(/^([^:]+):\s*(.*)$/);
1927
+ if (!kv) continue;
1928
+ const key = kv[1].trim().toLowerCase();
1929
+ const value = kv[2].trim();
1930
+ if (value === "") {
1931
+ meta[key] = [];
1932
+ currentListKey = key;
1933
+ } else if (value.startsWith("[") && value.endsWith("]")) {
1934
+ meta[key] = value.slice(1, -1).split(",").map((v) => v.trim().replace(/^['"]|['"]$/g, "")).filter((v) => v.length > 0);
1935
+ currentListKey = void 0;
1936
+ } else {
1937
+ meta[key] = value;
1938
+ currentListKey = void 0;
1939
+ }
1940
+ }
1941
+ return meta;
1942
+ }
1943
+ var asList = (value) => Array.isArray(value) ? value : typeof value === "string" ? value.split(",").map((v) => v.trim()).filter((v) => v.length > 0) : [];
1944
+ function extractTypedTags(markdown, typedKeys = wodWikiWql.WQL_TYPED_TAG_KEYS) {
1945
+ const meta = readFrontmatterMeta(markdown);
1946
+ const results = asList(meta["tags"]).map((label) => ({ label }));
1947
+ const metaKeys = Object.keys(meta);
1948
+ for (const typeName of typedKeys) {
1949
+ if (typeName === "tags") continue;
1950
+ const matchedKey = metaKeys.find((k) => k === typeName.toLowerCase());
1951
+ if (!matchedKey) continue;
1952
+ for (const label of asList(meta[matchedKey])) {
1953
+ results.push({ label, type: typeName });
1954
+ }
1955
+ }
1956
+ return results.filter((t) => t.label.length > 0);
1957
+ }
1958
+ function markdownToEffort(raw) {
1959
+ const match = FRONTMATTER_BLOCK_RE.exec(raw);
1960
+ if (!match) return null;
1961
+ const meta = readFrontmatterMeta(raw);
1962
+ const body = raw.slice(match[0].length).trim();
1963
+ const id = scalarOf(meta["id"]);
1964
+ const slug = scalarOf(meta["slug"]);
1965
+ const label = scalarOf(meta["label"]);
1966
+ if (!id || !slug || !label) return null;
1967
+ const baseAttributes = {};
1968
+ const blockRe = /^\s{2,}([^:]+):\s*(.+)$/;
1969
+ let inAttrs = false;
1970
+ for (const line of match[1].split(/\r?\n/)) {
1971
+ if (/^baseAttributes:/.test(line)) {
1972
+ inAttrs = true;
1973
+ continue;
1974
+ }
1975
+ if (inAttrs) {
1976
+ const attr = blockRe.exec(line);
1977
+ if (attr && /^ {2,}/.test(line)) {
1978
+ const v = attr[2].trim();
1979
+ baseAttributes[attr[1].trim()] = /^\d+(\.\d+)?$/.test(v) ? Number(v) : v;
1980
+ } else if (/^\S/.test(line)) {
1981
+ inAttrs = false;
1982
+ }
1983
+ }
1984
+ }
1985
+ if (typeof baseAttributes["met"] !== "number") return null;
1986
+ return {
1987
+ id,
1988
+ slug,
1989
+ label,
1990
+ aliases: asList(meta["aliases"]),
1991
+ baseAttributes,
1992
+ ...body ? { body } : {}
1993
+ };
1994
+ }
1995
+ function effortToMarkdown(effort) {
1996
+ const lines = ["---", `id: ${effort.id}`, `slug: ${effort.slug}`, `label: ${effort.label}`];
1997
+ if (effort.aliases && effort.aliases.length > 0) {
1998
+ lines.push("aliases:");
1999
+ for (const alias of effort.aliases) lines.push(` - ${alias}`);
2000
+ }
2001
+ const attrs = Object.entries(effort.baseAttributes).filter(([, v]) => typeof v === "number" || typeof v === "string");
2002
+ if (attrs.length > 0) {
2003
+ lines.push("baseAttributes:");
2004
+ for (const [k, v] of attrs) lines.push(` ${k}: ${String(v)}`);
2005
+ }
2006
+ lines.push("---", "");
2007
+ return `${lines.join("\n")}
2008
+ ${(effort.body ?? "").trim()}`.trimEnd() + "\n";
2009
+ }
2010
+ function scalarOf(value) {
2011
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
2012
+ }
2013
+ function equipmentToCsv(labels) {
2014
+ return ["label", ...labels].map((l) => escapeCsvField(l)).join("\n") + "\n";
2015
+ }
2016
+ function csvToEquipmentLabels(csv) {
2017
+ const lines = splitCsvRecords(csv);
2018
+ if (lines.length === 0) return [];
2019
+ const header = parseCsvLine(lines[0]);
2020
+ const labelIdx = header[0] === "label" ? 0 : -1;
2021
+ const start = labelIdx >= 0 ? 1 : 0;
2022
+ const labels = [];
2023
+ for (const line of lines.slice(start)) {
2024
+ const cells = parseCsvLine(line);
2025
+ const label = (labelIdx >= 0 ? cells[0] : cells.join(",")).trim();
2026
+ if (label) labels.push(label);
2027
+ }
2028
+ return [...new Set(labels)];
2029
+ }
2030
+ var SESSION_HEADER = [
2031
+ "sessionId",
2032
+ "noteId",
2033
+ "segmentId",
2034
+ "segmentVersion",
2035
+ "blockId",
2036
+ "blockContentId",
2037
+ "origin",
2038
+ "status",
2039
+ "startTime",
2040
+ "endTime",
2041
+ "duration",
2042
+ "completed",
2043
+ "sessionCreatedAt"
2044
+ ];
2045
+ var EVENT_HEADER = [
2046
+ "eventId",
2047
+ "grain",
2048
+ "outputType",
2049
+ "timestamp",
2050
+ "pageId",
2051
+ "effortSlug",
2052
+ "eventSegmentId",
2053
+ "eventSegmentVersion",
2054
+ "eventBlockContentId",
2055
+ "eventOrigin",
2056
+ "timeSpan",
2057
+ "sourceBlockKey",
2058
+ "sourceStatementId",
2059
+ "line",
2060
+ "text",
2061
+ "dialect",
2062
+ "stackLevel",
2063
+ "completionReason",
2064
+ "representationKind",
2065
+ "metricType",
2066
+ "metricValue",
2067
+ "metricUnit",
2068
+ "metricImage",
2069
+ "metricOrigin",
2070
+ "metricMetadata",
2071
+ "metricTemporal",
2072
+ // Verbatim JSON backstops: make the machine round-trip lossless across
2073
+ // schema fields that have no dedicated spreadsheet column. Explicit cells
2074
+ // above still win on import, so hand-edited values are honored.
2075
+ "metricJson",
2076
+ "eventExtraJson"
2077
+ ];
2078
+ var SESSION_CSV_HEADER = [...SESSION_HEADER, ...EVENT_HEADER];
529
2079
  function escapeCsvField(field) {
2080
+ if (field === void 0 || field === null) return "";
2081
+ const str = typeof field === "object" ? JSON.stringify(field) : String(field);
2082
+ if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
2083
+ return `"${str.replace(/"/g, '""')}"`;
2084
+ }
2085
+ return str;
2086
+ }
2087
+ var metricValueToCell = (value) => {
2088
+ if (value === void 0 || value === null) return "";
2089
+ if (typeof value === "object") return JSON.stringify(value);
2090
+ return String(value);
2091
+ };
2092
+ var metricValueFromCell = (cell) => {
2093
+ if (cell === "") return void 0;
2094
+ if (/^-?\d+(\.\d+)?$/.test(cell)) return Number(cell);
2095
+ if (cell === "true" || cell === "false") return cell === "true";
2096
+ if (cell.startsWith("{") || cell.startsWith("[")) {
2097
+ try {
2098
+ return JSON.parse(cell);
2099
+ } catch {
2100
+ return cell;
2101
+ }
2102
+ }
2103
+ return cell;
2104
+ };
2105
+ function sessionCells(session) {
2106
+ return [
2107
+ session.id,
2108
+ session.noteId,
2109
+ session.segmentId,
2110
+ session.segmentVersion,
2111
+ session.blockId,
2112
+ session.blockContentId,
2113
+ session.origin,
2114
+ session.status,
2115
+ session.startTime,
2116
+ session.endTime,
2117
+ session.duration,
2118
+ session.completed,
2119
+ session.createdAt
2120
+ ].map(escapeCsvField);
2121
+ }
2122
+ function metricCells(event, metric, index) {
2123
+ return [
2124
+ event.id,
2125
+ event.grain,
2126
+ event.outputType,
2127
+ event.timestamp,
2128
+ event.pageId,
2129
+ event.effortSlug,
2130
+ event.segmentId,
2131
+ event.segmentVersion,
2132
+ event.blockContentId,
2133
+ event.origin,
2134
+ event.timeSpan,
2135
+ event.sourceBlockKey,
2136
+ event.sourceStatementId,
2137
+ event.line,
2138
+ event.text,
2139
+ event.dialect,
2140
+ event.stackLevel,
2141
+ event.completionReason,
2142
+ event.representationKind,
2143
+ metric.type,
2144
+ metricValueToCell(metric.value),
2145
+ metric.unit,
2146
+ metric.image,
2147
+ metric.origin,
2148
+ metric.metadata === void 0 ? "" : JSON.stringify(metric.metadata),
2149
+ event.metricTemporal?.[index] === void 0 ? "" : JSON.stringify(event.metricTemporal[index]),
2150
+ JSON.stringify(metric)
2151
+ ].map(escapeCsvField);
2152
+ }
2153
+ function eventExtraCells(event) {
2154
+ const { metrics: _metrics, ...rest } = event;
2155
+ return escapeCsvField(JSON.stringify(rest));
2156
+ }
2157
+ function sessionToCsv(session, events) {
2158
+ const lines = [SESSION_CSV_HEADER.join(",")];
2159
+ lines.push([...sessionCells(session), ...EVENT_HEADER.map(() => "")].join(","));
2160
+ const ordered = [...events].sort((a, b) => a.id.localeCompare(b.id));
2161
+ for (const event of ordered) {
2162
+ const extra = eventExtraCells(event);
2163
+ const metrics = event.metrics ?? [];
2164
+ if (metrics.length === 0) {
2165
+ lines.push([...sessionCells(session), ...metricCells(event, { type: "" }, 0), extra].join(","));
2166
+ continue;
2167
+ }
2168
+ for (let i = 0; i < metrics.length; i++) {
2169
+ lines.push([...sessionCells(session), ...metricCells(event, metrics[i], i), extra].join(","));
2170
+ }
2171
+ }
2172
+ return lines.join("\n") + "\n";
2173
+ }
2174
+ function parseCsvLine(line) {
2175
+ const cells = [];
2176
+ let current = "";
2177
+ let quoted = false;
2178
+ for (let i = 0; i < line.length; i++) {
2179
+ const ch = line[i];
2180
+ if (quoted) {
2181
+ if (ch === '"') {
2182
+ if (line[i + 1] === '"') {
2183
+ current += '"';
2184
+ i += 1;
2185
+ } else {
2186
+ quoted = false;
2187
+ }
2188
+ } else {
2189
+ current += ch;
2190
+ }
2191
+ } else if (ch === '"' && current === "") {
2192
+ quoted = true;
2193
+ } else if (ch === ",") {
2194
+ cells.push(current);
2195
+ current = "";
2196
+ } else {
2197
+ current += ch;
2198
+ }
2199
+ }
2200
+ cells.push(current);
2201
+ return cells;
2202
+ }
2203
+ function splitCsvRecords(csv) {
2204
+ const records = [];
2205
+ let current = "";
2206
+ let quoted = false;
2207
+ for (let i = 0; i < csv.length; i++) {
2208
+ const ch = csv[i];
2209
+ if (ch === '"') {
2210
+ if (quoted && csv[i + 1] === '"') {
2211
+ current += '""';
2212
+ i += 1;
2213
+ continue;
2214
+ }
2215
+ quoted = !quoted;
2216
+ current += ch;
2217
+ } else if (!quoted && (ch === "\n" || ch === "\r")) {
2218
+ if (ch === "\r" && csv[i + 1] === "\n") i += 1;
2219
+ if (current.length > 0) records.push(current);
2220
+ current = "";
2221
+ } else {
2222
+ current += ch;
2223
+ }
2224
+ }
2225
+ if (current.length > 0) records.push(current);
2226
+ return records;
2227
+ }
2228
+ var num = (cell) => cell === "" ? void 0 : Number(cell);
2229
+ var nonempty = (cell) => cell === "" ? void 0 : cell;
2230
+ function jsonCell(cell) {
2231
+ if (!cell) return void 0;
2232
+ try {
2233
+ return JSON.parse(cell);
2234
+ } catch {
2235
+ return void 0;
2236
+ }
2237
+ }
2238
+ function csvToSessionRows(csv) {
2239
+ const lines = splitCsvRecords(csv);
2240
+ if (lines.length < 2) throw new Error("[wod import] Session CSV has no data rows");
2241
+ const header = parseCsvLine(lines[0]);
2242
+ const col = (name) => {
2243
+ const idx = header.indexOf(name);
2244
+ if (idx < 0) throw new Error(`[wod import] Session CSV missing column "${name}"`);
2245
+ return idx;
2246
+ };
2247
+ const at = (cells, name) => cells[col(name)] ?? "";
2248
+ const eventRows = /* @__PURE__ */ new Map();
2249
+ let session;
2250
+ const ensureSession = (cells) => {
2251
+ if (session) return session;
2252
+ session = {
2253
+ id: at(cells, "sessionId"),
2254
+ noteId: at(cells, "noteId"),
2255
+ segmentId: at(cells, "segmentId") || void 0,
2256
+ segmentVersion: num(at(cells, "segmentVersion")),
2257
+ blockId: at(cells, "blockId") || void 0,
2258
+ blockContentId: at(cells, "blockContentId") || void 0,
2259
+ origin: at(cells, "origin") || void 0,
2260
+ status: at(cells, "status") || void 0,
2261
+ startTime: Number(at(cells, "startTime") || 0),
2262
+ endTime: Number(at(cells, "endTime") || 0),
2263
+ duration: Number(at(cells, "duration") || 0),
2264
+ completed: at(cells, "completed") === "true",
2265
+ createdAt: Number(at(cells, "sessionCreatedAt") || 0)
2266
+ };
2267
+ return session;
2268
+ };
2269
+ for (const line of lines.slice(1)) {
2270
+ const cells = parseCsvLine(line);
2271
+ if (cells.length === 1 && cells[0] === "") continue;
2272
+ const currentSession = ensureSession(cells);
2273
+ const eventId = at(cells, "eventId");
2274
+ if (!eventId) continue;
2275
+ let event = eventRows.get(eventId);
2276
+ if (!event) {
2277
+ const extra = jsonCell(at(cells, "eventExtraJson")) ?? {};
2278
+ event = {
2279
+ ...extra,
2280
+ id: eventId,
2281
+ resultId: currentSession.id,
2282
+ noteId: nonempty(at(cells, "noteId")) ?? extra.noteId ?? currentSession.noteId,
2283
+ blockContentId: nonempty(at(cells, "eventBlockContentId")) ?? extra.blockContentId,
2284
+ pageId: nonempty(at(cells, "pageId")) ?? extra.pageId,
2285
+ origin: nonempty(at(cells, "eventOrigin")) ?? extra.origin,
2286
+ timestamp: Number(at(cells, "timestamp") || extra.timestamp || 0),
2287
+ grain: nonempty(at(cells, "grain")) ?? extra.grain ?? "event",
2288
+ effortSlug: nonempty(at(cells, "effortSlug")) ?? extra.effortSlug,
2289
+ outputType: nonempty(at(cells, "outputType")) ?? extra.outputType ?? "event",
2290
+ segmentId: nonempty(at(cells, "eventSegmentId")) ?? extra.segmentId,
2291
+ segmentVersion: num(at(cells, "eventSegmentVersion")) ?? extra.segmentVersion,
2292
+ timeSpan: jsonCell(at(cells, "timeSpan")) ?? extra.timeSpan,
2293
+ sourceBlockKey: nonempty(at(cells, "sourceBlockKey")) ?? extra.sourceBlockKey,
2294
+ sourceStatementId: num(at(cells, "sourceStatementId")) ?? extra.sourceStatementId,
2295
+ line: num(at(cells, "line")) ?? extra.line,
2296
+ text: nonempty(at(cells, "text")) ?? extra.text,
2297
+ dialect: nonempty(at(cells, "dialect")) ?? extra.dialect,
2298
+ stackLevel: num(at(cells, "stackLevel")) ?? extra.stackLevel,
2299
+ completionReason: nonempty(at(cells, "completionReason")) ?? extra.completionReason,
2300
+ representationKind: nonempty(at(cells, "representationKind")) ?? extra.representationKind,
2301
+ metrics: [],
2302
+ // Rebuilt per-row from the metricTemporal column below — never
2303
+ // inherited from the backstop, or rows would double-collect.
2304
+ metricTemporal: void 0
2305
+ };
2306
+ eventRows.set(eventId, event);
2307
+ }
2308
+ const metricType = at(cells, "metricType");
2309
+ if (!metricType) continue;
2310
+ const baseMetric = jsonCell(at(cells, "metricJson")) ?? {};
2311
+ const metric = {
2312
+ ...baseMetric,
2313
+ type: metricType,
2314
+ ...at(cells, "metricValue") !== "" ? { value: metricValueFromCell(at(cells, "metricValue")) } : {},
2315
+ ...nonempty(at(cells, "metricUnit")) ? { unit: at(cells, "metricUnit") } : {},
2316
+ ...nonempty(at(cells, "metricImage")) ? { image: at(cells, "metricImage") } : {},
2317
+ ...nonempty(at(cells, "metricOrigin")) ? { origin: at(cells, "metricOrigin") } : {},
2318
+ ...at(cells, "metricMetadata") !== "" ? { metadata: jsonCell(at(cells, "metricMetadata")) } : {}
2319
+ };
2320
+ event.metrics.push(metric);
2321
+ const temporal = jsonCell(at(cells, "metricTemporal"));
2322
+ if (temporal !== void 0) {
2323
+ (event.metricTemporal ??= []).push(temporal);
2324
+ }
2325
+ }
2326
+ if (!session) throw new Error("[wod import] Session CSV did not contain session columns");
2327
+ return { session, events: [...eventRows.values()] };
2328
+ }
2329
+
2330
+ // src/cli/seed.ts
2331
+ var SEED_NAMESPACE = "1b671a64-40d5-491e-99b0-da01ff1f3341";
2332
+ var SEED_SCHEMA = 7;
2333
+ var CANVAS_CHUNK_ID = "canvas";
2334
+ var DASHBOARDS_CHUNK_ID = "dashboards";
2335
+ var EFFORTS_CHUNK_ID = "efforts";
2336
+ var TEMPLATE_CHUNK_ID = "template";
2337
+ var scalarOf2 = (value) => typeof value === "string" ? value : void 0;
2338
+ function seedNoteId(path) {
2339
+ return formatV5Digest(nodeCrypto().createHash("sha1").update(`${SEED_NAMESPACE}:${path}`).digest());
2340
+ }
2341
+ function titleFromPath(path) {
2342
+ const stem = path.split("/").pop() ?? path;
2343
+ return stem.replace(/\.md$/, "").replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2344
+ }
2345
+ function catalogForChunkId(chunkId) {
2346
+ const match = /^(?:collection|feed)\.(.+)$/.exec(chunkId);
2347
+ const value = match?.[1];
2348
+ return value && value !== "_root" ? value : void 0;
2349
+ }
2350
+ function canvasRouteSlug(content) {
2351
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
2352
+ if (fm?.[1] && !/^template:\s*canvas\s*$/m.test(fm[1])) return null;
2353
+ const route = fm?.[1]?.match(/^route:\s*(\S+)\s*$/m)?.[1];
2354
+ if (!route) return null;
2355
+ return route.replace(/^\//, "") || null;
2356
+ }
2357
+ function seedNoteSourceId(path, content, chunkId) {
2358
+ const parts = path.split("/");
2359
+ const stem = (parts[parts.length - 1] ?? "").replace(/\.md$/i, "");
2360
+ const under = parts.slice(2).map((p) => p.replace(/\.md$/i, "")).join("/");
2361
+ if (chunkId === CANVAS_CHUNK_ID) {
2362
+ const route = canvasRouteSlug(content);
2363
+ return route ? `guides:${route}` : `guides:${under}`;
2364
+ }
2365
+ if (chunkId === DASHBOARDS_CHUNK_ID) return "dashboards";
2366
+ if (chunkId === EFFORTS_CHUNK_ID) return "efforts";
2367
+ if (chunkId === TEMPLATE_CHUNK_ID) return "playground";
2368
+ if (chunkId.startsWith("collection.")) {
2369
+ const catalog = catalogForChunkId(chunkId);
2370
+ if (!catalog) return "collection:_root";
2371
+ return stem.toLowerCase() === "readme" ? `collection:${catalog}` : `collection:${catalog}/${stem}`;
2372
+ }
2373
+ if (chunkId.startsWith("feed.")) return `feed:feeds/${under}`;
2374
+ return void 0;
2375
+ }
2376
+ function buildNoteRecord(row, chunkId, seedVersion) {
2377
+ const id = seedNoteId(row.path);
2378
+ const createdAt = seedVersion;
2379
+ const meta = readFrontmatterMeta(row.content);
2380
+ const tags = extractTypedTags(row.content);
2381
+ let date;
2382
+ let page;
2383
+ let pageNote;
2384
+ const rawDate = scalarOf2(meta["date"]);
2385
+ if (rawDate && /^\d{4}-\d{2}-\d{2}$/.test(rawDate)) {
2386
+ date = (/* @__PURE__ */ new Date(`${rawDate}T12:00:00Z`)).getTime();
2387
+ }
2388
+ const slug = scalarOf2(meta["slug"]);
2389
+ if (slug) {
2390
+ const pageId = seedNoteId(`page:${slug}`);
2391
+ page = { id: pageId, slug, title: titleFromPath(row.path), createdAt };
2392
+ pageNote = { id: seedNoteId(`pagenote:${slug}:${row.path}`), pageId, noteId: id, createdAt };
2393
+ }
2394
+ const sourceId = seedNoteSourceId(row.path, row.content, chunkId);
2395
+ if (!sourceId) {
2396
+ throw new Error(`[wod seed] chunk "${chunkId}" row "${row.path}" has no source family \u2014 refusing to import it as journal`);
2397
+ }
2398
+ const metaTitle = scalarOf2(meta["title"]);
2399
+ return {
2400
+ note: {
2401
+ id,
2402
+ title: metaTitle?.trim() || titleFromPath(row.path),
2403
+ date,
2404
+ createdAt,
2405
+ type: chunkId === DASHBOARDS_CHUNK_ID ? "dashboard" : "note",
2406
+ sourceId,
2407
+ catalog: catalogForChunkId(chunkId),
2408
+ seedOrigin: "seed",
2409
+ seedVersion,
2410
+ seedChunkId: chunkId,
2411
+ sourcePath: row.path
2412
+ },
2413
+ segment: {
2414
+ id: `seed:${id}`,
2415
+ version: 1,
2416
+ noteId: id,
2417
+ position: 0,
2418
+ dataType: "markdown",
2419
+ data: null,
2420
+ rawContent: row.content,
2421
+ createdAt
2422
+ },
2423
+ tags,
2424
+ page,
2425
+ pageNote,
2426
+ rawContent: row.content,
2427
+ sourcePath: row.path
2428
+ };
2429
+ }
2430
+ async function metaRecord(sql, db, sourceId) {
2431
+ const rows = await sql.listRows(db, sql.storeDefinition("meta"));
2432
+ const existing = rows.find((r) => r.key === `seed:${sourceId}`);
2433
+ return existing ?? {
2434
+ key: `seed:${sourceId}`,
2435
+ schema: SEED_SCHEMA,
2436
+ seedVersion: 0,
2437
+ builtAt: "",
2438
+ importedAt: 0,
2439
+ chunks: {},
2440
+ claim: null
2441
+ };
2442
+ }
2443
+ async function applyOps(db, ops) {
2444
+ if (ops.length === 0) return;
2445
+ const sql = await loadSql();
2446
+ await db.db.transaction(async (tx) => {
2447
+ await sql.refreshDomain(tx, ops);
2448
+ });
2449
+ }
2450
+ async function importSeedDir(db, dir, sourceId) {
2451
+ const sql = await loadSql();
2452
+ const fs = nodeFs();
2453
+ const source = sourceId || nodePath().basename(dir);
2454
+ const manifest = JSON.parse(fs.readFileSync(nodePath().resolve(dir, "manifest.json"), "utf-8"));
2455
+ const prior = await metaRecord(sql, db.db, source);
2456
+ const notesById = new Map(
2457
+ (await sql.listRows(db.db, sql.storeDefinition("notes"))).map((row) => [row.id, row])
2458
+ );
2459
+ const tagRowByLabel = new Map(
2460
+ (await sql.listRows(db.db, sql.storeDefinition("tags"))).map((t) => [t.label, t])
2461
+ );
2462
+ const summary = {
2463
+ sourceId: source,
2464
+ version: manifest.version,
2465
+ notes: 0,
2466
+ efforts: 0,
2467
+ blocks: 0,
2468
+ blockEfforts: 0,
2469
+ deleted: 0,
2470
+ skippedUserOwned: 0
2471
+ };
2472
+ const nextMeta = {
2473
+ ...prior,
2474
+ schema: SEED_SCHEMA,
2475
+ seedVersion: manifest.version,
2476
+ builtAt: manifest.builtAt,
2477
+ importedAt: Date.now(),
2478
+ chunks: {}
2479
+ };
2480
+ const ordered = [
2481
+ ...manifest.chunks.filter((c) => (c.kind ?? "notes") === "notes"),
2482
+ ...manifest.chunks.filter((c) => c.kind === "block-index"),
2483
+ ...manifest.chunks.filter((c) => c.kind === "block-efforts")
2484
+ ];
2485
+ const bySourcePath = /* @__PURE__ */ new Map();
2486
+ const noteIdsByChunk = {};
2487
+ const blockIdsByChunk = {};
2488
+ for (const chunk of ordered) {
2489
+ const payload = JSON.parse(fs.readFileSync(nodePath().resolve(dir, chunk.path), "utf-8"));
2490
+ const ops = [];
2491
+ if ((chunk.kind ?? "notes") === "notes") {
2492
+ const rows2 = payload;
2493
+ const built = rows2.map((row) => buildNoteRecord(row, chunk.id, manifest.version));
2494
+ const noteIds = [];
2495
+ for (const record of built) {
2496
+ bySourcePath.set(record.sourcePath, record.note.id);
2497
+ const existing = notesById.get(record.note.id);
2498
+ if (existing && existing.seedOrigin !== "seed") {
2499
+ summary.skippedUserOwned += 1;
2500
+ continue;
2501
+ }
2502
+ noteIds.push(record.note.id);
2503
+ ops.push({ op: "put", store: "notes", value: record.note });
2504
+ ops.push({ op: "put", store: "segments", value: record.segment });
2505
+ if (record.page) ops.push({ op: "put", store: "page", value: record.page });
2506
+ if (record.pageNote) ops.push({ op: "put", store: "page_notes", value: record.pageNote });
2507
+ for (const item of record.tags) {
2508
+ const label = item.label;
2509
+ let tag = tagRowByLabel.get(label);
2510
+ if (!tag) {
2511
+ tag = { id: randomId(), label, type: item.type, createdAt: Date.now() };
2512
+ tagRowByLabel.set(label, tag);
2513
+ ops.push({ op: "put", store: "tags", value: tag });
2514
+ } else if (item.type && !tag.type) {
2515
+ tag = { ...tag, type: item.type };
2516
+ tagRowByLabel.set(label, tag);
2517
+ ops.push({ op: "put", store: "tags", value: tag });
2518
+ }
2519
+ const tagId = tag.id;
2520
+ ops.push({
2521
+ op: "put",
2522
+ store: "note_tags",
2523
+ // Deterministic junction id — re-seeds must upsert the same row
2524
+ // (note_tags has a unique [noteId, tagId] index).
2525
+ value: { id: uuid5(CLI_UUID_NAMESPACE, `note-tag:${record.note.id}:${tagId}`), noteId: record.note.id, tagId }
2526
+ });
2527
+ }
2528
+ if (chunk.id === EFFORTS_CHUNK_ID) {
2529
+ const effort = markdownToEffort(record.rawContent);
2530
+ if (effort) {
2531
+ ops.push({ op: "put", store: "efforts", value: { ...effort, registrySource: "bundled" } });
2532
+ summary.efforts += 1;
2533
+ }
2534
+ }
2535
+ }
2536
+ const priorIds = prior.chunks[chunk.id]?.noteIds ?? [];
2537
+ const goneIds = priorIds.filter((id) => !noteIds.includes(id) && notesById.get(id)?.seedOrigin === "seed");
2538
+ for (const id of goneIds) {
2539
+ ops.push(...await noteCascadeOps(sql, db, id));
2540
+ summary.deleted += 1;
2541
+ }
2542
+ noteIdsByChunk[chunk.id] = rows2.map((row) => seedNoteId(row.path));
2543
+ nextMeta.chunks[chunk.id] = { sha256: chunk.sha256, noteIds: noteIdsByChunk[chunk.id] };
2544
+ summary.notes += noteIds.length;
2545
+ await applyOps(db, ops);
2546
+ continue;
2547
+ }
2548
+ const rows = payload.filter((row) => row.isStatic === true);
2549
+ const rekeyed = rows.map((row) => {
2550
+ const noteId = row.sourcePath ? bySourcePath.get(row.sourcePath) : void 0;
2551
+ if (!noteId) {
2552
+ throw new Error(
2553
+ `[wod seed] static row ${row.id}: sourcePath "${row.sourcePath ?? ""}" has no imported note \u2014 chunk aborted`
2554
+ );
2555
+ }
2556
+ return chunk.kind === "block-efforts" ? { ...row, noteId } : { ...row, noteId, routeId: row.noteId };
2557
+ });
2558
+ const store = chunk.kind === "block-index" ? "block_index" : "block_efforts";
2559
+ for (const row of rekeyed) ops.push({ op: "put", store, value: row });
2560
+ const priorBlockIds = prior.chunks[chunk.id]?.blockIds ?? [];
2561
+ const writtenIds = new Set(rekeyed.map((row) => row.id));
2562
+ const goneBlocks = priorBlockIds.filter((id) => !writtenIds.has(id));
2563
+ for (const id of goneBlocks) ops.push({ op: "delete", store, key: id });
2564
+ summary.deleted += goneBlocks.length;
2565
+ blockIdsByChunk[chunk.id] = payload.map((row) => row.id);
2566
+ nextMeta.chunks[chunk.id] = { sha256: chunk.sha256, blockIds: blockIdsByChunk[chunk.id] };
2567
+ if (chunk.kind === "block-index") summary.blocks += rekeyed.length;
2568
+ else summary.blockEfforts += rekeyed.length;
2569
+ await applyOps(db, ops);
2570
+ }
2571
+ const manifestIds = new Set(manifest.chunks.map((c) => c.id));
2572
+ for (const [chunkId, checkpoint] of Object.entries(prior.chunks)) {
2573
+ if (manifestIds.has(chunkId)) continue;
2574
+ const sweep = [];
2575
+ for (const id of checkpoint.noteIds ?? []) {
2576
+ if (notesById.get(id)?.seedOrigin === "seed") {
2577
+ sweep.push(...await noteCascadeOps(sql, db, id));
2578
+ summary.deleted += 1;
2579
+ }
2580
+ }
2581
+ for (const id of checkpoint.blockIds ?? []) {
2582
+ sweep.push({ op: "delete", store: "block_index", key: id });
2583
+ summary.deleted += 1;
2584
+ }
2585
+ await applyOps(db, sweep);
2586
+ }
2587
+ await applyOps(db, [{ op: "put", store: "meta", value: nextMeta }]);
2588
+ return summary;
2589
+ }
2590
+ async function noteCascadeOps(sql, db, noteId) {
2591
+ const ops = [{ op: "delete", store: "notes", key: noteId }];
2592
+ for (const store of ["segments", "block_index", "block_efforts", "note_tags"]) {
2593
+ const rows = await sql.listRows(db.db, sql.storeDefinition(store), { only: noteId }, void 0, "by-note");
2594
+ for (const row of rows) {
2595
+ ops.push({ op: "delete", store, key: store === "segments" ? [row.id, row.version ?? 1] : row.id });
2596
+ }
2597
+ }
2598
+ return ops;
2599
+ }
2600
+ function defaultSeedDirs(cwd) {
2601
+ const base = nodePath().resolve(cwd, "apps", "playground", "public", "seed");
2602
+ return ["core", "community"].map((id) => nodePath().resolve(base, id)).filter((dir) => nodeFs().existsSync(nodePath().resolve(dir, "manifest.json")));
2603
+ }
2604
+
2605
+ // src/cli/formatters.ts
2606
+ function escapeCsvField2(field) {
530
2607
  if (field === void 0 || field === null) return "";
531
2608
  const str = String(field);
532
2609
  if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
@@ -572,9 +2649,9 @@ function formatParseOutput(ir, format, pretty = true) {
572
2649
  n.type,
573
2650
  n.from,
574
2651
  n.to,
575
- escapeCsvField(n.raw),
576
- escapeCsvField(n.metrics.map(formatMetric).join("; ")),
577
- escapeCsvField(n.hints?.join("; ") ?? "")
2652
+ escapeCsvField2(n.raw),
2653
+ escapeCsvField2(n.metrics.map(formatMetric).join("; ")),
2654
+ escapeCsvField2(n.hints?.join("; ") ?? "")
578
2655
  ].join(",")
579
2656
  );
580
2657
  return [header, ...rows].join("\n");
@@ -615,11 +2692,11 @@ function formatExecutionOutput(ir, format, pretty = true) {
615
2692
  s.outputType,
616
2693
  s.timeSpan?.started ?? s.timestamp ?? 0,
617
2694
  s.timeSpan?.ended ?? "",
618
- escapeCsvField(s.sourceBlockKey),
2695
+ escapeCsvField2(s.sourceBlockKey),
619
2696
  s.stackLevel,
620
- escapeCsvField(s.completionReason ?? ""),
621
- escapeCsvField(s.metrics.map(formatMetric).join("; ")),
622
- escapeCsvField(s.hints?.join("; ") ?? "")
2697
+ escapeCsvField2(s.completionReason ?? ""),
2698
+ escapeCsvField2(s.metrics.map(formatMetric).join("; ")),
2699
+ escapeCsvField2(s.hints?.join("; ") ?? "")
623
2700
  ].join(",")
624
2701
  );
625
2702
  return [header, ...rows].join("\n");
@@ -668,7 +2745,7 @@ function formatQueryOutput(ir, format, pretty = true) {
668
2745
  run.timestamp,
669
2746
  event.outputType,
670
2747
  event.id,
671
- escapeCsvField(event.metrics.map(formatMetric).join("; "))
2748
+ escapeCsvField2(event.metrics.map(formatMetric).join("; "))
672
2749
  ].join(",")
673
2750
  );
674
2751
  }
@@ -700,13 +2777,13 @@ function formatQueryOutput(ir, format, pretty = true) {
700
2777
  const header = "target,id,title_or_label,sourceId";
701
2778
  const rows = [];
702
2779
  for (const n of data2.notes) {
703
- rows.push(["note", n.id, escapeCsvField(n.title), escapeCsvField(n.sourceId ?? "")].join(","));
2780
+ rows.push(["note", n.id, escapeCsvField2(n.title), escapeCsvField2(n.sourceId ?? "")].join(","));
704
2781
  }
705
2782
  for (const b of data2.blocks) {
706
- rows.push(["block", b.id, escapeCsvField(b.noteTitle), escapeCsvField(b.sourceId ?? "")].join(","));
2783
+ rows.push(["block", b.id, escapeCsvField2(b.noteTitle), escapeCsvField2(b.sourceId ?? "")].join(","));
707
2784
  }
708
2785
  for (const e of data2.efforts ?? []) {
709
- rows.push(["effort", e.id, escapeCsvField(e.label), escapeCsvField(e.slug)].join(","));
2786
+ rows.push(["effort", e.id, escapeCsvField2(e.label), escapeCsvField2(e.slug)].join(","));
710
2787
  }
711
2788
  return [header, ...rows].join("\n");
712
2789
  }
@@ -728,11 +2805,48 @@ function formatQueryOutput(ir, format, pretty = true) {
728
2805
  }
729
2806
  return lines2.join("\n");
730
2807
  }
2808
+ if (kind === "pipeline-result") {
2809
+ const data2 = ir.data;
2810
+ const sink = data2.chart ? `:${data2.chart.head}` : "data";
2811
+ if (format === "csv") {
2812
+ const header = "series,timestamp,date,value,unit";
2813
+ const rows = [];
2814
+ for (const s of data2.series ?? []) {
2815
+ for (const pt of s.points) {
2816
+ rows.push(
2817
+ [
2818
+ escapeCsvField2(s.label || s.key),
2819
+ pt.ts,
2820
+ formatDate(pt.ts),
2821
+ pt.value,
2822
+ escapeCsvField2(s.unit ?? data2.unit ?? "")
2823
+ ].join(",")
2824
+ );
2825
+ }
2826
+ }
2827
+ return [header, ...rows].join("\n");
2828
+ }
2829
+ const lines2 = [];
2830
+ if (data2.error) lines2.push(`Pipeline Error: ${data2.error}`);
2831
+ lines2.push(`Pipeline Result (\u2192 ${sink}):`);
2832
+ lines2.push("\u2500".repeat(80));
2833
+ if (data2.table) {
2834
+ lines2.push(`Table: ${data2.table.rows.length} of ${data2.table.totalCount} rows`);
2835
+ }
2836
+ for (const s of data2.series ?? []) {
2837
+ lines2.push(`${s.label || s.key}: ${s.points.length} points${s.unit ? ` (${s.unit})` : ""}`);
2838
+ }
2839
+ if (data2.notes?.length) lines2.push(`notes: ${data2.notes.length}`);
2840
+ if (data2.blocks?.length) lines2.push(`blocks: ${data2.blocks.length}`);
2841
+ if (data2.efforts?.length) lines2.push(`efforts: ${data2.efforts.length}`);
2842
+ if (data2.runs?.length) lines2.push(`runs: ${data2.runs.length}`);
2843
+ return lines2.join("\n");
2844
+ }
731
2845
  const data = ir.data;
732
2846
  if (data.scalar !== void 0 && data.series.length === 0) {
733
2847
  if (format === "csv") {
734
2848
  return `metric,scalar,unit
735
- ${escapeCsvField(data.parsed.metric)},${data.scalar},${escapeCsvField(data.unit ?? "")}`;
2849
+ ${escapeCsvField2(data.parsed.metric)},${data.scalar},${escapeCsvField2(data.unit ?? "")}`;
736
2850
  }
737
2851
  return `Scalar Result: ${data.scalar}${data.unit ? " " + data.unit : ""}`;
738
2852
  }
@@ -743,11 +2857,11 @@ ${escapeCsvField(data.parsed.metric)},${data.scalar},${escapeCsvField(data.unit
743
2857
  for (const pt of s.points) {
744
2858
  rows.push(
745
2859
  [
746
- escapeCsvField(s.label || s.key),
2860
+ escapeCsvField2(s.label || s.key),
747
2861
  pt.ts,
748
2862
  formatDate(pt.ts),
749
2863
  pt.value,
750
- escapeCsvField(s.unit ?? data.unit ?? "")
2864
+ escapeCsvField2(s.unit ?? data.unit ?? "")
751
2865
  ].join(",")
752
2866
  );
753
2867
  }
@@ -788,7 +2902,8 @@ var VERSION = "0.6.0";
788
2902
  function parseCliArgs(args) {
789
2903
  const result = {
790
2904
  format: "json",
791
- packs: []
2905
+ packs: [],
2906
+ sources: []
792
2907
  };
793
2908
  let i = 0;
794
2909
  while (i < args.length) {
@@ -839,8 +2954,54 @@ function parseCliArgs(args) {
839
2954
  } else if (arg === "--unit") {
840
2955
  result.unit = args[++i];
841
2956
  i++;
2957
+ } else if (arg === "--db") {
2958
+ result.db = args[++i];
2959
+ i++;
2960
+ } else if (arg.startsWith("--db=")) {
2961
+ result.db = arg.slice("--db=".length);
2962
+ i++;
2963
+ } else if (arg === "--debug") {
2964
+ result.debug = "";
2965
+ i++;
2966
+ } else if (arg.startsWith("--debug=")) {
2967
+ result.debug = arg.slice("--debug=".length);
2968
+ i++;
2969
+ } else if (arg === "--at") {
2970
+ result.at = args[++i];
2971
+ i++;
2972
+ } else if (arg.startsWith("--at=")) {
2973
+ result.at = arg.slice("--at=".length);
2974
+ i++;
2975
+ } else if (arg === "--source") {
2976
+ const source = args[++i];
2977
+ if (source) result.sources.push(source);
2978
+ i++;
2979
+ } else if (arg.startsWith("--source=")) {
2980
+ result.sources.push(arg.slice("--source=".length));
2981
+ i++;
2982
+ } else if (arg === "--scaffold") {
2983
+ result.scaffold = true;
2984
+ i++;
2985
+ } else if (arg === "--template") {
2986
+ result.template = args[++i];
2987
+ i++;
2988
+ } else if (arg.startsWith("--template=")) {
2989
+ result.template = arg.slice("--template=".length);
2990
+ i++;
2991
+ } else if (arg === "--out") {
2992
+ result.output = args[++i];
2993
+ i++;
2994
+ } else if (arg.startsWith("--out=")) {
2995
+ result.output = arg.slice("--out=".length);
2996
+ i++;
2997
+ } else if (arg === "--dir") {
2998
+ result.dir = args[++i];
2999
+ i++;
3000
+ } else if (arg.startsWith("--dir=")) {
3001
+ result.dir = arg.slice("--dir=".length);
3002
+ i++;
842
3003
  } else if (!result.subcommand) {
843
- if (arg === "parse" || arg === "run" || arg === "query" || arg === "help" || arg === "version") {
3004
+ if (arg === "parse" || arg === "run" || arg === "query" || arg === "download" || arg === "import" || arg === "build" || arg === "seed" || arg === "reset" || arg === "help" || arg === "version") {
844
3005
  result.subcommand = arg;
845
3006
  } else {
846
3007
  result.target = arg;
@@ -849,16 +3010,32 @@ function parseCliArgs(args) {
849
3010
  } else if (!result.target) {
850
3011
  result.target = arg;
851
3012
  i++;
3013
+ } else if (!result.target2) {
3014
+ result.target2 = arg;
3015
+ i++;
852
3016
  } else {
853
3017
  i++;
854
3018
  }
855
3019
  }
856
3020
  return result;
857
3021
  }
858
- async function readStdin() {
859
- if (typeof globalThis.Bun?.stdin?.text === "function") {
860
- return globalThis.Bun.stdin.text();
3022
+ function readBunStdinText() {
3023
+ const g = globalThis;
3024
+ if (g && typeof g === "object" && "Bun" in g) {
3025
+ const bun = g.Bun;
3026
+ if (bun && typeof bun === "object" && "stdin" in bun) {
3027
+ const stdin = bun.stdin;
3028
+ if (stdin && typeof stdin === "object" && "text" in stdin) {
3029
+ const text = stdin.text;
3030
+ if (typeof text === "function") return text;
3031
+ }
3032
+ }
861
3033
  }
3034
+ return void 0;
3035
+ }
3036
+ async function readStdin() {
3037
+ const bunText = readBunStdinText();
3038
+ if (bunText) return bunText();
862
3039
  return new Promise((resolve, reject) => {
863
3040
  const chunks = [];
864
3041
  process.stdin.on("data", (chunk) => {
@@ -880,20 +3057,84 @@ USAGE:
880
3057
  wod parse <file | -> [options]
881
3058
  wod run <file | -> [options]
882
3059
  wod query '<wql-string>' [options]
3060
+ wod download <note|session|effort|equipment> <id> [options]
3061
+ wod import <note|session|effort|equipment> <file> [options]
3062
+ wod build [dir] [options]
3063
+ wod seed [options]
3064
+ wod reset [options]
883
3065
 
884
3066
  SUBCOMMANDS:
885
- parse <file | -> Parse Whiteboard Script into AST StatementNode IR (kind: 'parse-tree')
886
- run <file | -> Execute Whiteboard Script state machine and emit ExecutionLog IR (kind: 'execution-log')
887
- query '<wql>' Evaluate WQL query and emit QueryResult / RowsQueryResult / FindQueryResult IR
3067
+ parse <file | -> Parse Whiteboard Script, create a note in the database
3068
+ (segments + block index), optionally emit parse-tree IR.
3069
+ Markdown carrying a 'wod-note-id' frontmatter stamp
3070
+ (see download) imports back into that same note.
3071
+ run <file | -> Execute the script state machine, record a session +
3072
+ event/summary rows in the database, optionally emit
3073
+ execution-log IR
3074
+ query '<wql>' Evaluate WQL against the database (or an explicit
3075
+ dataset) and emit QueryResult / RowsQueryResult IR
3076
+ download note <id> Compose the note's markdown from its segment rows and
3077
+ write it to <id>.md. Use --at <version> to compose
3078
+ as-of a segment version (default: latest). The stamp
3079
+ in the frontmatter makes 'wod parse' re-import it.
3080
+ Frontmatter tags (general + typed equipment/quality/\u2026)
3081
+ are materialized into the tag graph on parse/import.
3082
+ download session <id>
3083
+ Write the session's generated metrics (event + summary
3084
+ rows, one CSV row per metric) to <id>.csv. Import back
3085
+ with 'wod import session'.
3086
+ download effort <slug>
3087
+ Write the effort registry row as corpus-style markdown
3088
+ (frontmatter id/slug/label/aliases/baseAttributes + body)
3089
+ to <slug>.md. Import back with 'wod import effort'.
3090
+ download equipment
3091
+ Write every equipment tag as a one-column CSV to
3092
+ equipment.csv. Import back with 'wod import equipment'.
3093
+ import note <file> Re-import exported note markdown (same as parse with
3094
+ the stamped identity).
3095
+ import session <file>
3096
+ Recreate a session + its events from a downloaded CSV
3097
+ into the same or a clean database (id-keyed upsert).
3098
+ import effort <file>
3099
+ Upsert an effort registry row from corpus-style markdown.
3100
+ import equipment <file>
3101
+ Create/upgrade equipment tags (CSV, one label per row).
3102
+ build [dir] Compile a seed corpus: validate every .md under
3103
+ <dir>/markdown (parity gate + dashboard format gate) and
3104
+ write manifest.json + content-hashed chunks/ +
3105
+ block-parent-map.json + report.html to <dir>/dist
3106
+ (override with -o/--output). Version stamps derive from
3107
+ <dir>'s git history (full checkout required).
3108
+
3109
+ BUILD OPTIONS (for 'build'):
3110
+ --scaffold Also scaffold markdown/efforts/discovered/*.md stubs for
3111
+ workout effort labels missing from the registry.
3112
+ --template <file> Compile an extra markdown file as the 'template' chunk
3113
+ (the host's playground intake template).
888
3114
 
889
- INPUT SOURCING (for 'query'):
3115
+ INPUT SOURCING (for 'query'; without these, the database is queried):
890
3116
  -c, --corpus <path> Load historical corpus dataset from a versioned JSON file
891
3117
  --stdin-log Read kind: 'execution-log' JSON payload from stdin
892
3118
  --stdin-facts Read raw kind: 'fact-set' JSON payload from stdin
893
3119
 
3120
+ DATABASE (parse / run / query / download / import / seed / reset):
3121
+ --db <path> SQLite database file (default: $WOD_WIKI_DB or
3122
+ ./wodwiki.db). Created on demand by parse/run/seed.
3123
+
3124
+ SEED SOURCING (for 'seed'):
3125
+ --source <dir> Extra compiled seed directory (manifest.json + chunks);
3126
+ repeatable. Defaults to the bundled core + community
3127
+ artifacts under apps/playground/public/seed.
3128
+
894
3129
  OPTIONS:
895
3130
  --format <fmt> Output format: json (default), table, csv
896
- -o, --output <path> Write output to a file instead of stdout
3131
+ -o, --output <path> Write the IR payload (or download) to a path instead of
3132
+ the default; '-' writes to stdout
3133
+ --at <version> With 'download note': compose the markdown as-of the
3134
+ given segment version (default: latest)
3135
+ --debug[=name] Also write the IR payload to <name> (default
3136
+ wod-<subcommand>-debug.json); without it, parse/run
3137
+ print a one-line summary of what was persisted
897
3138
  --pack <module> Dynamically load and register a Language Pack (repeatable)
898
3139
  --sport <sport> Target sport fence dialect (e.g. climbing, crossfit)
899
3140
  --unit <unit> Preferred display unit for queries (e.g. lb, kg)
@@ -912,7 +3153,7 @@ function printUsage(writer) {
912
3153
  if (writer) {
913
3154
  writer(text);
914
3155
  } else {
915
- console.log(text);
3156
+ process.stdout.write(text.endsWith("\n") ? text : text + "\n");
916
3157
  }
917
3158
  }
918
3159
  async function cliMain(argv = process.argv.slice(2), io = {}) {
@@ -951,6 +3192,13 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
951
3192
  writeOut(formattedText);
952
3193
  }
953
3194
  };
3195
+ const debugTarget = (subcommand) => parsed.debug === void 0 ? void 0 : parsed.debug || `wod-${subcommand}-debug.json`;
3196
+ const writeDebugFile = (subcommand, ir) => {
3197
+ const target = debugTarget(subcommand);
3198
+ if (target !== void 0) {
3199
+ getFs2().writeFileSync(target, JSON.stringify(ir, null, 2) + "\n", "utf-8");
3200
+ }
3201
+ };
954
3202
  try {
955
3203
  if (parsed.subcommand === "parse") {
956
3204
  if (!parsed.target) {
@@ -968,10 +3216,29 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
968
3216
  }
969
3217
  source = fs.readFileSync(parsed.target, "utf-8");
970
3218
  }
971
- const ir = runParse(source, { sport: parsed.sport, sourceLabel: `cli:wod parse ${parsed.target}` });
972
- const pretty = !parsed.output && Boolean(process.stdout.isTTY);
973
- const formatted = formatParseOutput(ir, parsed.format, pretty);
974
- deliverOutput(formatted);
3219
+ const ir = runParse(source, { sport: parsed.sport, sourceLabel: `cli:wod parse ${parsed.target}` });
3220
+ const identity = extractNoteIdentity(source);
3221
+ const tags = extractTypedTags(source);
3222
+ const db = await openCliDb(parsed.db);
3223
+ try {
3224
+ const persisted = await persistNote(db, {
3225
+ source,
3226
+ sourceLabel: parsed.target,
3227
+ noteId: identity?.noteId,
3228
+ title: identity?.title,
3229
+ tags
3230
+ });
3231
+ writeOut(
3232
+ `[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}`
3233
+ );
3234
+ } finally {
3235
+ await db.close();
3236
+ }
3237
+ if (parsed.output) {
3238
+ const pretty = Boolean(process.stdout.isTTY);
3239
+ deliverOutput(formatParseOutput(ir, parsed.format, pretty));
3240
+ }
3241
+ writeDebugFile("parse", ir);
975
3242
  return 0;
976
3243
  }
977
3244
  if (parsed.subcommand === "run") {
@@ -991,9 +3258,24 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
991
3258
  source = fs.readFileSync(parsed.target, "utf-8");
992
3259
  }
993
3260
  const ir = await runExecution(source, { sport: parsed.sport, sourceLabel: `cli:wod run ${parsed.target}` });
994
- const pretty = !parsed.output && Boolean(process.stdout.isTTY);
995
- const formatted = formatExecutionOutput(ir, parsed.format, pretty);
996
- deliverOutput(formatted);
3261
+ const db = await openCliDb(parsed.db);
3262
+ try {
3263
+ const persisted = await persistRun(db, {
3264
+ source,
3265
+ sourceLabel: parsed.target,
3266
+ executionLog: ir.data
3267
+ });
3268
+ writeOut(
3269
+ `[wod run] session ${persisted.resultId} \u2014 ${persisted.events} events, ${persisted.summaries} summaries on note ${persisted.noteId} \u2192 ${db.path}`
3270
+ );
3271
+ } finally {
3272
+ await db.close();
3273
+ }
3274
+ if (parsed.output) {
3275
+ const pretty = Boolean(process.stdout.isTTY);
3276
+ deliverOutput(formatExecutionOutput(ir, parsed.format, pretty));
3277
+ }
3278
+ writeDebugFile("run", ir);
997
3279
  return 0;
998
3280
  }
999
3281
  if (parsed.subcommand === "query") {
@@ -1001,26 +3283,269 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
1001
3283
  writeErr("[wod query] Error: Missing WQL query argument");
1002
3284
  return 1;
1003
3285
  }
3286
+ const useDb = !parsed.corpus && !parsed.stdinLog && !parsed.stdinFacts;
1004
3287
  let stdinLogContent;
1005
3288
  let stdinFactsContent;
1006
3289
  if (parsed.stdinLog) {
1007
3290
  stdinLogContent = await getStdin();
1008
3291
  } else if (parsed.stdinFacts) {
1009
3292
  stdinFactsContent = await getStdin();
1010
- } else if (!parsed.corpus) {
1011
- writeErr("[wod query] Error: Query requires input data. Specify --corpus <file>, --stdin-log, or --stdin-facts.");
1012
- return 1;
1013
3293
  }
1014
- const ir = await runQueryCli(parsed.target, {
1015
- corpusPath: parsed.corpus,
1016
- stdinLog: stdinLogContent,
1017
- stdinFacts: stdinFactsContent,
1018
- preferredUnit: parsed.unit,
1019
- sourceLabel: `cli:wod query "${parsed.target}"`
1020
- });
3294
+ let ir;
3295
+ if (useDb) {
3296
+ const dbPath = resolveDbPath(parsed.db);
3297
+ if (!nodeFs().existsSync(dbPath)) {
3298
+ writeErr(
3299
+ `[wod query] No input dataset and no database at ${dbPath}. Specify --corpus <file>, --stdin-log, --stdin-facts, or run \`wod seed\` first.`
3300
+ );
3301
+ return 1;
3302
+ }
3303
+ const db = await openCliDb(parsed.db);
3304
+ try {
3305
+ ir = await runQueryCli(parsed.target, {
3306
+ preferredUnit: parsed.unit,
3307
+ sourceLabel: `cli:wod query "${parsed.target}"`,
3308
+ stores: {
3309
+ eventStore: sqliteEventStore(db.db),
3310
+ noteStore: sqliteNoteStore(db.db),
3311
+ blockStore: sqliteBlockStore(db.db),
3312
+ effortStore: sqliteEffortStore(db.db),
3313
+ blockEffortsStore: sqliteIndexStore(db.db, "block_efforts"),
3314
+ tagsStore: sqliteIndexStore(db.db, "tags"),
3315
+ noteTagsStore: sqliteIndexStore(db.db, "note_tags")
3316
+ }
3317
+ });
3318
+ } finally {
3319
+ await db.close();
3320
+ }
3321
+ } else {
3322
+ if (!parsed.corpus && !stdinLogContent && !stdinFactsContent) {
3323
+ writeErr("[wod query] Error: Query requires input data. Specify --corpus <file>, --stdin-log, or --stdin-facts.");
3324
+ return 1;
3325
+ }
3326
+ ir = await runQueryCli(parsed.target, {
3327
+ corpusPath: parsed.corpus,
3328
+ stdinLog: stdinLogContent,
3329
+ stdinFacts: stdinFactsContent,
3330
+ preferredUnit: parsed.unit,
3331
+ sourceLabel: `cli:wod query "${parsed.target}"`
3332
+ });
3333
+ }
1021
3334
  const pretty = !parsed.output && Boolean(process.stdout.isTTY);
1022
3335
  const formatted = formatQueryOutput(ir, parsed.format, pretty);
1023
3336
  deliverOutput(formatted);
3337
+ writeDebugFile("query", ir);
3338
+ return 0;
3339
+ }
3340
+ if (parsed.subcommand === "download") {
3341
+ const kind = parsed.target;
3342
+ const id = parsed.target2;
3343
+ const DOWNLOAD_KINDS = ["note", "session", "effort", "equipment"];
3344
+ if (!id && kind !== "equipment" || !DOWNLOAD_KINDS.includes(kind ?? "")) {
3345
+ writeErr("[wod download] Usage: wod download <note|session|effort|equipment> <id> [--at <version>] [-o <path>]");
3346
+ return 1;
3347
+ }
3348
+ const targetId = id;
3349
+ const db = await openCliDb(parsed.db);
3350
+ try {
3351
+ if (kind === "note") {
3352
+ const bundle = await getNoteBundle(db.db, targetId);
3353
+ if (!bundle.note) {
3354
+ writeErr(`[wod download] Note not found in ${db.path}: ${id}`);
3355
+ return 1;
3356
+ }
3357
+ const at = parsed.at === void 0 ? void 0 : Number(parsed.at);
3358
+ if (parsed.at !== void 0 && (!Number.isInteger(at) || at < 1)) {
3359
+ writeErr(`[wod download] Invalid --at version: ${parsed.at}`);
3360
+ return 1;
3361
+ }
3362
+ const markdown = composeNoteMarkdown(bundle.note, bundle.segments, at);
3363
+ const target = parsed.output && parsed.output !== "-" ? parsed.output : `${id}${at ? `.v${at}` : ""}.md`;
3364
+ if (parsed.output === "-") {
3365
+ writeOut(markdown);
3366
+ } else {
3367
+ nodeFs().writeFileSync(target, markdown, "utf-8");
3368
+ }
3369
+ writeOut(
3370
+ `[wod download] note ${id}${at ? ` v${at}` : ""} \u2192 ${target} (${bundle.segments.length} segment rows)`
3371
+ );
3372
+ } else if (kind === "session") {
3373
+ const bundle = await getSessionBundle(db.db, targetId);
3374
+ if (!bundle) {
3375
+ writeErr(`[wod download] Session not found in ${db.path}: ${id}`);
3376
+ return 1;
3377
+ }
3378
+ const csv = sessionToCsv(bundle.session, bundle.events);
3379
+ const target = parsed.output && parsed.output !== "-" ? parsed.output : `${id}.csv`;
3380
+ if (parsed.output === "-") {
3381
+ writeOut(csv);
3382
+ } else {
3383
+ nodeFs().writeFileSync(target, csv, "utf-8");
3384
+ }
3385
+ writeOut(`[wod download] session ${id} \u2192 ${target} (${bundle.events.length} events)`);
3386
+ } else if (kind === "effort") {
3387
+ const effort = await getEffortBySlug(db.db, targetId);
3388
+ if (!effort) {
3389
+ writeErr(`[wod download] Effort not found in ${db.path}: ${id}`);
3390
+ return 1;
3391
+ }
3392
+ const markdown = effortToMarkdown(effort);
3393
+ const target = parsed.output && parsed.output !== "-" ? parsed.output : `${id}.md`;
3394
+ if (parsed.output === "-") {
3395
+ writeOut(markdown);
3396
+ } else {
3397
+ nodeFs().writeFileSync(target, markdown, "utf-8");
3398
+ }
3399
+ writeOut(`[wod download] effort ${id} \u2192 ${target}`);
3400
+ } else {
3401
+ const labels = (await listTagsByType(db.db, "equipment")).map((t) => t.label).sort();
3402
+ const csv = equipmentToCsv(labels);
3403
+ const target = parsed.output && parsed.output !== "-" ? parsed.output : "equipment.csv";
3404
+ if (parsed.output === "-") {
3405
+ writeOut(csv);
3406
+ } else {
3407
+ nodeFs().writeFileSync(target, csv, "utf-8");
3408
+ }
3409
+ writeOut(`[wod download] equipment \u2192 ${target} (${labels.length} labels)`);
3410
+ }
3411
+ } finally {
3412
+ await db.close();
3413
+ }
3414
+ return 0;
3415
+ }
3416
+ if (parsed.subcommand === "import") {
3417
+ const kind = parsed.target;
3418
+ const file = parsed.target2;
3419
+ const IMPORT_KINDS = ["note", "session", "effort", "equipment"];
3420
+ if (!file || !IMPORT_KINDS.includes(kind ?? "")) {
3421
+ writeErr("[wod import] Usage: wod import <note|session|effort|equipment> <file>");
3422
+ return 1;
3423
+ }
3424
+ const fs = nodeFs();
3425
+ if (!fs.existsSync(file)) {
3426
+ writeErr(`[wod import] File not found: ${file}`);
3427
+ return 1;
3428
+ }
3429
+ if (kind === "note") {
3430
+ const source = fs.readFileSync(file, "utf-8");
3431
+ const identity = extractNoteIdentity(source);
3432
+ const tags = extractTypedTags(source);
3433
+ const db2 = await openCliDb(parsed.db);
3434
+ try {
3435
+ const persisted = await persistNote(db2, {
3436
+ source,
3437
+ sourceLabel: file,
3438
+ noteId: identity?.noteId,
3439
+ title: identity?.title,
3440
+ tags
3441
+ });
3442
+ writeOut(
3443
+ `[wod import] note ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.segments} segments${persisted.tags > 0 ? `, ${persisted.tags} tags` : ""} \u2192 ${db2.path}`
3444
+ );
3445
+ } finally {
3446
+ await db2.close();
3447
+ }
3448
+ return 0;
3449
+ }
3450
+ if (kind === "effort") {
3451
+ const effort = markdownToEffort(fs.readFileSync(file, "utf-8"));
3452
+ if (!effort) {
3453
+ writeErr(`[wod import] Unparseable effort file (need frontmatter id/slug/label + numeric baseAttributes.met): ${file}`);
3454
+ return 1;
3455
+ }
3456
+ const db2 = await openCliDb(parsed.db);
3457
+ try {
3458
+ await persistEffort(db2, { ...effort, registrySource: "user" });
3459
+ writeOut(`[wod import] effort ${effort.slug} \u2014 ${effort.label} \u2192 ${db2.path}`);
3460
+ } finally {
3461
+ await db2.close();
3462
+ }
3463
+ return 0;
3464
+ }
3465
+ if (kind === "equipment") {
3466
+ const labels = csvToEquipmentLabels(fs.readFileSync(file, "utf-8"));
3467
+ if (labels.length === 0) {
3468
+ writeErr(`[wod import] No equipment labels found in ${file}`);
3469
+ return 1;
3470
+ }
3471
+ const db2 = await openCliDb(parsed.db);
3472
+ try {
3473
+ const added = await persistTypedTags(db2, labels, "equipment");
3474
+ writeOut(`[wod import] equipment \u2014 ${labels.length} labels (${added} new) \u2192 ${db2.path}`);
3475
+ } finally {
3476
+ await db2.close();
3477
+ }
3478
+ return 0;
3479
+ }
3480
+ const { session, events } = csvToSessionRows(fs.readFileSync(file, "utf-8"));
3481
+ const db = await openCliDb(parsed.db);
3482
+ try {
3483
+ const result = await persistSessionBundle(db, session, events);
3484
+ writeOut(
3485
+ `[wod import] session ${session.id} \u2014 ${result.events} events${result.noteCreated ? " (stub note created)" : ""} \u2192 ${db.path}`
3486
+ );
3487
+ } finally {
3488
+ await db.close();
3489
+ }
3490
+ return 0;
3491
+ }
3492
+ if (parsed.subcommand === "build") {
3493
+ const cwd = process.cwd();
3494
+ const root = nodePath().resolve(cwd, parsed.dir ?? parsed.target ?? ".");
3495
+ if (!nodeFs().existsSync(nodePath().resolve(root, "markdown"))) {
3496
+ writeErr(`[wod build] No markdown/ corpus under ${root} \u2014 run from the content repo root (or pass its directory).`);
3497
+ return 1;
3498
+ }
3499
+ const { generateSeed: generateSeed2 } = await Promise.resolve().then(() => (init_compiler(), compiler_exports));
3500
+ try {
3501
+ const result = generateSeed2({
3502
+ corpusRoot: root,
3503
+ ...parsed.output ? { outDir: nodePath().resolve(cwd, parsed.output) } : {},
3504
+ ...parsed.template ? { templateFile: nodePath().resolve(cwd, parsed.template) } : {},
3505
+ scaffoldDiscoveredEfforts: parsed.scaffold === true
3506
+ });
3507
+ const kb = (n) => `${(n / 1024).toFixed(0)} KB`;
3508
+ const totalBytes = result.manifest.chunks.reduce((s, c) => s + c.bytes, 0);
3509
+ writeOut(`[wod build] schema v${result.manifest.schema} version ${result.manifest.version} (${result.manifest.versionLabel}) \u2192 ${result.outDir}`);
3510
+ for (const c of result.manifest.chunks) {
3511
+ writeOut(` ${c.id.padEnd(48)} ${String(c.count).padStart(4)} ${c.kind === "block-index" ? "rows " : "files"} ${kb(c.bytes).padStart(8)}`);
3512
+ }
3513
+ 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`);
3514
+ return 0;
3515
+ } catch (error) {
3516
+ writeErr(`[wod build] ${error.message}`);
3517
+ return 1;
3518
+ }
3519
+ }
3520
+ if (parsed.subcommand === "seed") {
3521
+ const dirs = parsed.sources.length > 0 ? parsed.sources.map((s) => nodePath().resolve(process.cwd(), s)) : defaultSeedDirs(process.cwd());
3522
+ if (dirs.length === 0) {
3523
+ writeErr(
3524
+ "[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)."
3525
+ );
3526
+ return 1;
3527
+ }
3528
+ const db = await openCliDb(parsed.db);
3529
+ try {
3530
+ for (const dir of dirs) {
3531
+ const summary = await importSeedDir(db, dir, "");
3532
+ writeOut(
3533
+ `[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}`
3534
+ );
3535
+ }
3536
+ } finally {
3537
+ await db.close();
3538
+ }
3539
+ return 0;
3540
+ }
3541
+ if (parsed.subcommand === "reset") {
3542
+ const target = resolveDbPath(parsed.db);
3543
+ if (nodeFs().existsSync(target)) {
3544
+ nodeFs().unlinkSync(target);
3545
+ writeOut(`[wod reset] removed ${target}`);
3546
+ } else {
3547
+ writeOut(`[wod reset] nothing to reset \u2014 ${target} does not exist`);
3548
+ }
1024
3549
  return 0;
1025
3550
  }
1026
3551
  writeErr(`[wod] Unknown subcommand "${parsed.subcommand}"`);
@@ -1040,13 +3565,13 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
1040
3565
  }
1041
3566
 
1042
3567
  // src/version.ts
1043
- var VERSION2 = "0.11.43";
1044
- var GIT_SHA = "ed28149";
1045
- var BUILD_TIME = "2026-08-26T16:34:15.927Z";
3568
+ var VERSION2 = "0.35.2115";
3569
+ var GIT_SHA = "37944a8";
3570
+ var BUILD_TIME = "2026-10-11T23:54:36.367Z";
1046
3571
  var SEMVER = {
1047
- version: "0.11.43",
1048
- gitSha: "ed28149",
1049
- buildTime: "2026-08-26T16:34:15.927Z"
3572
+ version: "0.35.2115",
3573
+ gitSha: "37944a8",
3574
+ buildTime: "2026-10-11T23:54:36.367Z"
1050
3575
  };
1051
3576
 
1052
3577
  Object.defineProperty(exports, "ABSENT", {
@@ -1209,6 +3734,10 @@ Object.defineProperty(exports, "ExitBehavior", {
1209
3734
  enumerable: true,
1210
3735
  get: function () { return wodWikiLang.ExitBehavior; }
1211
3736
  });
3737
+ Object.defineProperty(exports, "FixtureSyntaxError", {
3738
+ enumerable: true,
3739
+ get: function () { return wodWikiLang.FixtureSyntaxError; }
3740
+ });
1212
3741
  Object.defineProperty(exports, "GenericGroupStrategy", {
1213
3742
  enumerable: true,
1214
3743
  get: function () { return wodWikiLang.GenericGroupStrategy; }
@@ -1429,10 +3958,18 @@ Object.defineProperty(exports, "calculateElapsed", {
1429
3958
  enumerable: true,
1430
3959
  get: function () { return wodWikiLang.calculateElapsed; }
1431
3960
  });
3961
+ Object.defineProperty(exports, "canonicalizeType", {
3962
+ enumerable: true,
3963
+ get: function () { return wodWikiLang.canonicalizeType; }
3964
+ });
1432
3965
  Object.defineProperty(exports, "collapseUnresolvedChoices", {
1433
3966
  enumerable: true,
1434
3967
  get: function () { return wodWikiLang.collapseUnresolvedChoices; }
1435
3968
  });
3969
+ Object.defineProperty(exports, "compareStatement", {
3970
+ enumerable: true,
3971
+ get: function () { return wodWikiLang.compareStatement; }
3972
+ });
1436
3973
  Object.defineProperty(exports, "compileLineForm", {
1437
3974
  enumerable: true,
1438
3975
  get: function () { return wodWikiLang.compileLineForm; }
@@ -1485,6 +4022,10 @@ Object.defineProperty(exports, "dialectStack", {
1485
4022
  enumerable: true,
1486
4023
  get: function () { return wodWikiLang.dialectStack; }
1487
4024
  });
4025
+ Object.defineProperty(exports, "diffStatement", {
4026
+ enumerable: true,
4027
+ get: function () { return wodWikiLang.diffStatement; }
4028
+ });
1488
4029
  Object.defineProperty(exports, "evaluate", {
1489
4030
  enumerable: true,
1490
4031
  get: function () { return wodWikiLang.evaluate; }
@@ -1589,6 +4130,10 @@ Object.defineProperty(exports, "parseExpression", {
1589
4130
  enumerable: true,
1590
4131
  get: function () { return wodWikiLang.parseExpression; }
1591
4132
  });
4133
+ Object.defineProperty(exports, "parseMetricLine", {
4134
+ enumerable: true,
4135
+ get: function () { return wodWikiLang.parseMetricLine; }
4136
+ });
1592
4137
  Object.defineProperty(exports, "parseScript", {
1593
4138
  enumerable: true,
1594
4139
  get: function () { return wodWikiLang.parseScript; }
@@ -1597,6 +4142,10 @@ Object.defineProperty(exports, "realtimeProcessorRegistry", {
1597
4142
  enumerable: true,
1598
4143
  get: function () { return wodWikiLang.realtimeProcessorRegistry; }
1599
4144
  });
4145
+ Object.defineProperty(exports, "renderMetric", {
4146
+ enumerable: true,
4147
+ get: function () { return wodWikiLang.renderMetric; }
4148
+ });
1600
4149
  Object.defineProperty(exports, "strategyRegistry", {
1601
4150
  enumerable: true,
1602
4151
  get: function () { return wodWikiLang.strategyRegistry; }
@@ -1633,86 +4182,6 @@ Object.defineProperty(exports, "writeChoiceSelection", {
1633
4182
  enumerable: true,
1634
4183
  get: function () { return wodWikiLang.writeChoiceSelection; }
1635
4184
  });
1636
- Object.defineProperty(exports, "BlockTimerDisplay", {
1637
- enumerable: true,
1638
- get: function () { return react.BlockTimerDisplay; }
1639
- });
1640
- Object.defineProperty(exports, "ScriptRuntimeProvider", {
1641
- enumerable: true,
1642
- get: function () { return react.ScriptRuntimeProvider; }
1643
- });
1644
- Object.defineProperty(exports, "useActiveControls", {
1645
- enumerable: true,
1646
- get: function () { return react.useActiveControls; }
1647
- });
1648
- Object.defineProperty(exports, "useBlockMemory", {
1649
- enumerable: true,
1650
- get: function () { return react.useBlockMemory; }
1651
- });
1652
- Object.defineProperty(exports, "useLiveAnalytics", {
1653
- enumerable: true,
1654
- get: function () { return react.useLiveAnalytics; }
1655
- });
1656
- Object.defineProperty(exports, "useMemorySubscription", {
1657
- enumerable: true,
1658
- get: function () { return react.useMemorySubscription; }
1659
- });
1660
- Object.defineProperty(exports, "useNextPreview", {
1661
- enumerable: true,
1662
- get: function () { return react.useNextPreview; }
1663
- });
1664
- Object.defineProperty(exports, "useOutputStatements", {
1665
- enumerable: true,
1666
- get: function () { return react.useOutputStatements; }
1667
- });
1668
- Object.defineProperty(exports, "usePrimaryTimer", {
1669
- enumerable: true,
1670
- get: function () { return react.usePrimaryTimer; }
1671
- });
1672
- Object.defineProperty(exports, "useRoundDisplay", {
1673
- enumerable: true,
1674
- get: function () { return react.useRoundDisplay; }
1675
- });
1676
- Object.defineProperty(exports, "useRuntimeExecution", {
1677
- enumerable: true,
1678
- get: function () { return react.useRuntimeExecution; }
1679
- });
1680
- Object.defineProperty(exports, "useScriptRuntime", {
1681
- enumerable: true,
1682
- get: function () { return react.useScriptRuntime; }
1683
- });
1684
- Object.defineProperty(exports, "useSecondaryTimers", {
1685
- enumerable: true,
1686
- get: function () { return react.useSecondaryTimers; }
1687
- });
1688
- Object.defineProperty(exports, "useSnapshotBlocks", {
1689
- enumerable: true,
1690
- get: function () { return react.useSnapshotBlocks; }
1691
- });
1692
- Object.defineProperty(exports, "useStackDisplayRows", {
1693
- enumerable: true,
1694
- get: function () { return react.useStackDisplayRows; }
1695
- });
1696
- Object.defineProperty(exports, "useStackFragmentSources", {
1697
- enumerable: true,
1698
- get: function () { return react.useStackFragmentSources; }
1699
- });
1700
- Object.defineProperty(exports, "useStackSnapshot", {
1701
- enumerable: true,
1702
- get: function () { return react.useStackSnapshot; }
1703
- });
1704
- Object.defineProperty(exports, "useStackTimers", {
1705
- enumerable: true,
1706
- get: function () { return react.useStackTimers; }
1707
- });
1708
- Object.defineProperty(exports, "useTimerDisplay", {
1709
- enumerable: true,
1710
- get: function () { return react.useTimerDisplay; }
1711
- });
1712
- Object.defineProperty(exports, "useTimerElapsed", {
1713
- enumerable: true,
1714
- get: function () { return react.useTimerElapsed; }
1715
- });
1716
4185
  Object.defineProperty(exports, "DASHBOARD_GRID_MAX_COLS", {
1717
4186
  enumerable: true,
1718
4187
  get: function () { return wodWikiWql.DASHBOARD_GRID_MAX_COLS; }
@@ -1741,6 +4210,10 @@ Object.defineProperty(exports, "WQL_CALC_TARGETS", {
1741
4210
  enumerable: true,
1742
4211
  get: function () { return wodWikiWql.WQL_CALC_TARGETS; }
1743
4212
  });
4213
+ Object.defineProperty(exports, "WQL_CHART_HEADS", {
4214
+ enumerable: true,
4215
+ get: function () { return wodWikiWql.WQL_CHART_HEADS; }
4216
+ });
1744
4217
  Object.defineProperty(exports, "WQL_COMPARISON_OPS", {
1745
4218
  enumerable: true,
1746
4219
  get: function () { return wodWikiWql.WQL_COMPARISON_OPS; }
@@ -1757,6 +4230,10 @@ Object.defineProperty(exports, "WQL_FIND_TARGETS", {
1757
4230
  enumerable: true,
1758
4231
  get: function () { return wodWikiWql.WQL_FIND_TARGETS; }
1759
4232
  });
4233
+ Object.defineProperty(exports, "WQL_FUNCTION_HEADS", {
4234
+ enumerable: true,
4235
+ get: function () { return wodWikiWql.WQL_FUNCTION_HEADS; }
4236
+ });
1760
4237
  Object.defineProperty(exports, "WQL_GRAINS", {
1761
4238
  enumerable: true,
1762
4239
  get: function () { return wodWikiWql.WQL_GRAINS; }
@@ -1785,22 +4262,22 @@ Object.defineProperty(exports, "WQL_ROLLUP_PERIODS", {
1785
4262
  enumerable: true,
1786
4263
  get: function () { return wodWikiWql.WQL_ROLLUP_PERIODS; }
1787
4264
  });
1788
- Object.defineProperty(exports, "WQL_ROWS_SCOPE_KEYS", {
1789
- enumerable: true,
1790
- get: function () { return wodWikiWql.WQL_ROWS_SCOPE_KEYS; }
1791
- });
1792
- Object.defineProperty(exports, "WQL_ROWS_TARGETS", {
1793
- enumerable: true,
1794
- get: function () { return wodWikiWql.WQL_ROWS_TARGETS; }
1795
- });
1796
4265
  Object.defineProperty(exports, "WQL_SOURCES", {
1797
4266
  enumerable: true,
1798
4267
  get: function () { return wodWikiWql.WQL_SOURCES; }
1799
4268
  });
4269
+ Object.defineProperty(exports, "WQL_SOURCE_HEADS", {
4270
+ enumerable: true,
4271
+ get: function () { return wodWikiWql.WQL_SOURCE_HEADS; }
4272
+ });
1800
4273
  Object.defineProperty(exports, "WQL_SOURCE_VALUES", {
1801
4274
  enumerable: true,
1802
4275
  get: function () { return wodWikiWql.WQL_SOURCE_VALUES; }
1803
4276
  });
4277
+ Object.defineProperty(exports, "WQL_STANDARD_DATASETS", {
4278
+ enumerable: true,
4279
+ get: function () { return wodWikiWql.WQL_STANDARD_DATASETS; }
4280
+ });
1804
4281
  Object.defineProperty(exports, "WQL_TAG_KEYS", {
1805
4282
  enumerable: true,
1806
4283
  get: function () { return wodWikiWql.WQL_TAG_KEYS; }
@@ -1845,9 +4322,9 @@ Object.defineProperty(exports, "isFindQuery", {
1845
4322
  enumerable: true,
1846
4323
  get: function () { return wodWikiWql.isFindQuery; }
1847
4324
  });
1848
- Object.defineProperty(exports, "isRowsQuery", {
4325
+ Object.defineProperty(exports, "isPipelineQuery", {
1849
4326
  enumerable: true,
1850
- get: function () { return wodWikiWql.isRowsQuery; }
4327
+ get: function () { return wodWikiWql.isPipelineQuery; }
1851
4328
  });
1852
4329
  Object.defineProperty(exports, "normalizeWql", {
1853
4330
  enumerable: true,
@@ -1877,10 +4354,6 @@ Object.defineProperty(exports, "splitAtWhere", {
1877
4354
  enumerable: true,
1878
4355
  get: function () { return wodWikiWql.splitAtWhere; }
1879
4356
  });
1880
- Object.defineProperty(exports, "splitWidgetBody", {
1881
- enumerable: true,
1882
- get: function () { return wodWikiWql.splitWidgetBody; }
1883
- });
1884
4357
  Object.defineProperty(exports, "substituteTokens", {
1885
4358
  enumerable: true,
1886
4359
  get: function () { return wodWikiWql.substituteTokens; }
@@ -1909,36 +4382,128 @@ Object.defineProperty(exports, "wqlTerms", {
1909
4382
  enumerable: true,
1910
4383
  get: function () { return wodWikiWql.wqlTerms; }
1911
4384
  });
4385
+ Object.defineProperty(exports, "BlockTimerDisplay", {
4386
+ enumerable: true,
4387
+ get: function () { return react.BlockTimerDisplay; }
4388
+ });
4389
+ Object.defineProperty(exports, "ScriptRuntimeProvider", {
4390
+ enumerable: true,
4391
+ get: function () { return react.ScriptRuntimeProvider; }
4392
+ });
4393
+ Object.defineProperty(exports, "useActiveControls", {
4394
+ enumerable: true,
4395
+ get: function () { return react.useActiveControls; }
4396
+ });
4397
+ Object.defineProperty(exports, "useBlockMemory", {
4398
+ enumerable: true,
4399
+ get: function () { return react.useBlockMemory; }
4400
+ });
4401
+ Object.defineProperty(exports, "useLiveAnalytics", {
4402
+ enumerable: true,
4403
+ get: function () { return react.useLiveAnalytics; }
4404
+ });
4405
+ Object.defineProperty(exports, "useMemorySubscription", {
4406
+ enumerable: true,
4407
+ get: function () { return react.useMemorySubscription; }
4408
+ });
4409
+ Object.defineProperty(exports, "useNextPreview", {
4410
+ enumerable: true,
4411
+ get: function () { return react.useNextPreview; }
4412
+ });
4413
+ Object.defineProperty(exports, "useOutputStatements", {
4414
+ enumerable: true,
4415
+ get: function () { return react.useOutputStatements; }
4416
+ });
4417
+ Object.defineProperty(exports, "usePrimaryTimer", {
4418
+ enumerable: true,
4419
+ get: function () { return react.usePrimaryTimer; }
4420
+ });
4421
+ Object.defineProperty(exports, "useRoundDisplay", {
4422
+ enumerable: true,
4423
+ get: function () { return react.useRoundDisplay; }
4424
+ });
4425
+ Object.defineProperty(exports, "useRuntimeExecution", {
4426
+ enumerable: true,
4427
+ get: function () { return react.useRuntimeExecution; }
4428
+ });
4429
+ Object.defineProperty(exports, "useScriptRuntime", {
4430
+ enumerable: true,
4431
+ get: function () { return react.useScriptRuntime; }
4432
+ });
4433
+ Object.defineProperty(exports, "useSecondaryTimers", {
4434
+ enumerable: true,
4435
+ get: function () { return react.useSecondaryTimers; }
4436
+ });
4437
+ Object.defineProperty(exports, "useSnapshotBlocks", {
4438
+ enumerable: true,
4439
+ get: function () { return react.useSnapshotBlocks; }
4440
+ });
4441
+ Object.defineProperty(exports, "useStackDisplayRows", {
4442
+ enumerable: true,
4443
+ get: function () { return react.useStackDisplayRows; }
4444
+ });
4445
+ Object.defineProperty(exports, "useStackFragmentSources", {
4446
+ enumerable: true,
4447
+ get: function () { return react.useStackFragmentSources; }
4448
+ });
4449
+ Object.defineProperty(exports, "useStackSnapshot", {
4450
+ enumerable: true,
4451
+ get: function () { return react.useStackSnapshot; }
4452
+ });
4453
+ Object.defineProperty(exports, "useStackTimers", {
4454
+ enumerable: true,
4455
+ get: function () { return react.useStackTimers; }
4456
+ });
4457
+ Object.defineProperty(exports, "useTimerDisplay", {
4458
+ enumerable: true,
4459
+ get: function () { return react.useTimerDisplay; }
4460
+ });
4461
+ Object.defineProperty(exports, "useTimerElapsed", {
4462
+ enumerable: true,
4463
+ get: function () { return react.useTimerElapsed; }
4464
+ });
1912
4465
  exports.BUILD_TIME = BUILD_TIME;
1913
4466
  exports.GIT_SHA = GIT_SHA;
4467
+ exports.NOTE_ID_FRONTMATTER_KEY = NOTE_ID_FRONTMATTER_KEY;
1914
4468
  exports.PackLoadError = PackLoadError;
1915
4469
  exports.ParseSyntaxError = ParseSyntaxError;
1916
4470
  exports.SEMVER = SEMVER;
4471
+ exports.SESSION_CSV_HEADER = SESSION_CSV_HEADER;
1917
4472
  exports.VERSION = VERSION2;
1918
4473
  exports.WqlSyntaxError = WqlSyntaxError;
1919
4474
  exports.buildStatementTree = buildStatementTree;
1920
4475
  exports.buildWorkoutResults = buildWorkoutResults;
1921
4476
  exports.cliMain = cliMain;
4477
+ exports.composeNoteMarkdown = composeNoteMarkdown;
1922
4478
  exports.createIRFile = createIRFile;
4479
+ exports.csvToEquipmentLabels = csvToEquipmentLabels;
4480
+ exports.csvToSessionRows = csvToSessionRows;
1923
4481
  exports.defineLanguagePack = defineLanguagePack;
1924
- exports.factRowsToEventRows = factRowsToEventRows;
4482
+ exports.effortToMarkdown = effortToMarkdown;
4483
+ exports.equipmentToCsv = equipmentToCsv;
4484
+ exports.extractNoteIdentity = extractNoteIdentity;
4485
+ exports.extractTypedTags = extractTypedTags;
1925
4486
  exports.formatExecutionOutput = formatExecutionOutput;
1926
4487
  exports.formatParseOutput = formatParseOutput;
1927
4488
  exports.formatQueryOutput = formatQueryOutput;
1928
4489
  exports.getRegisteredLanguagePacks = getRegisteredLanguagePacks;
1929
4490
  exports.inMemoryEventStore = inMemoryEventStore;
1930
- exports.inMemoryEventStoreFromFacts = inMemoryEventStoreFromFacts;
1931
- exports.inMemoryFactStore = inMemoryFactStore;
4491
+ exports.injectIdentityFrontmatter = injectIdentityFrontmatter;
1932
4492
  exports.isIRFile = isIRFile;
1933
4493
  exports.listLanguagePacks = listLanguagePacks;
1934
4494
  exports.loadLanguagePack = loadLanguagePack;
1935
4495
  exports.loadQueryData = loadQueryData;
4496
+ exports.markdownToEffort = markdownToEffort;
1936
4497
  exports.parseCliArgs = parseCliArgs;
4498
+ exports.parseCsvLine = parseCsvLine;
4499
+ exports.readFrontmatterMeta = readFrontmatterMeta;
1937
4500
  exports.readStdin = readStdin;
1938
4501
  exports.registerLanguagePack = registerLanguagePack;
1939
4502
  exports.runExecution = runExecution;
1940
4503
  exports.runParse = runParse;
1941
4504
  exports.runQueryCli = runQueryCli;
4505
+ exports.sessionToCsv = sessionToCsv;
4506
+ exports.splitCsvRecords = splitCsvRecords;
1942
4507
  exports.statementToNode = statementToNode;
1943
4508
  exports.unregisterLanguagePack = unregisterLanguagePack;
1944
4509
  Object.keys(wodWikiCore).forEach(function (k) {