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