@bitcobblers/wod-wiki-engine 0.35.2116 → 0.35.2117
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 +912 -937
- package/dist/bin/wod.cjs.map +1 -1
- package/dist/bin/wod.mjs +2 -2
- package/dist/{chunk-B2OYLNEZ.mjs → chunk-AKNXDA75.mjs} +735 -679
- package/dist/chunk-AKNXDA75.mjs.map +1 -0
- package/dist/chunk-Y6WO6MXS.mjs +187 -0
- package/dist/chunk-Y6WO6MXS.mjs.map +1 -0
- package/dist/{compiler-KTBR6OEL.mjs → compiler-MQRZVZQW.mjs} +64 -326
- package/dist/compiler-MQRZVZQW.mjs.map +1 -0
- package/dist/index.cjs +924 -936
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +93 -23
- package/dist/index.d.ts +93 -23
- package/dist/index.mjs +8 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/dist/chunk-6L5ZDZJ7.mjs +0 -15
- package/dist/chunk-6L5ZDZJ7.mjs.map +0 -1
- package/dist/chunk-B2OYLNEZ.mjs.map +0 -1
- package/dist/compiler-KTBR6OEL.mjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var wodWikiLang = require('@bitcobblers/wod-wiki-lang');
|
|
4
|
+
var wodWikiCore = require('@bitcobblers/wod-wiki-core');
|
|
4
5
|
var child_process = require('child_process');
|
|
5
6
|
var fs = require('fs');
|
|
6
7
|
var path = require('path');
|
|
7
|
-
var wodWikiCore = require('@bitcobblers/wod-wiki-core');
|
|
8
8
|
var wodWikiWql = require('@bitcobblers/wod-wiki-wql');
|
|
9
|
-
var crypto = require('crypto');
|
|
9
|
+
var crypto$1 = require('crypto');
|
|
10
10
|
var react = require('@bitcobblers/wod-wiki-lang/react');
|
|
11
|
+
var wodWikiStorage = require('@bitcobblers/wod-wiki-storage');
|
|
11
12
|
|
|
12
13
|
var __defProp = Object.defineProperty;
|
|
13
14
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -25,93 +26,6 @@ var __export = (target, all) => {
|
|
|
25
26
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
26
27
|
};
|
|
27
28
|
|
|
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
29
|
// src/cli/seedbuild/frontmatter.ts
|
|
116
30
|
function unquote(value) {
|
|
117
31
|
return value.replace(
|
|
@@ -254,6 +168,116 @@ var init_effort_file = __esm({
|
|
|
254
168
|
init_frontmatter();
|
|
255
169
|
}
|
|
256
170
|
});
|
|
171
|
+
function seedMetaKey(sourceId) {
|
|
172
|
+
return `seed:${sourceId}`;
|
|
173
|
+
}
|
|
174
|
+
function emptySeedMeta(key) {
|
|
175
|
+
return { key, schema: exports.SEED_SCHEMA, seedVersion: 0, builtAt: "", importedAt: 0, chunks: {}, claim: null };
|
|
176
|
+
}
|
|
177
|
+
function catalogForChunkId(chunkId) {
|
|
178
|
+
const value = /^(?:collection|feed)\.(.+)$/.exec(chunkId)?.[1];
|
|
179
|
+
return value && value !== "_root" ? value : void 0;
|
|
180
|
+
}
|
|
181
|
+
function canvasRouteSlug(content) {
|
|
182
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
183
|
+
if (fm?.[1] && !/^template:\s*canvas\s*$/m.test(fm[1])) return null;
|
|
184
|
+
return fm?.[1]?.match(/^route:\s*(\S+)\s*$/m)?.[1]?.replace(/^\//, "") || null;
|
|
185
|
+
}
|
|
186
|
+
function seedNoteSourceId(path, content, chunkId) {
|
|
187
|
+
const parts = path.split("/");
|
|
188
|
+
const stem = (parts.at(-1) ?? "").replace(/\.md$/i, "");
|
|
189
|
+
const under = parts.slice(2).map((p) => p.replace(/\.md$/i, "")).join("/");
|
|
190
|
+
if (chunkId === exports.CANVAS_CHUNK_ID) return `guides:${canvasRouteSlug(content) ?? under}`;
|
|
191
|
+
if (chunkId === exports.DASHBOARDS_CHUNK_ID) return "dashboards";
|
|
192
|
+
if (chunkId === exports.EFFORTS_CHUNK_ID) return "efforts";
|
|
193
|
+
if (chunkId === exports.TEMPLATE_CHUNK_ID) return "playground";
|
|
194
|
+
if (chunkId.startsWith("collection.")) {
|
|
195
|
+
const catalog = catalogForChunkId(chunkId);
|
|
196
|
+
return !catalog ? "collection:_root" : stem.toLowerCase() === "readme" ? `collection:${catalog}` : `collection:${catalog}/${stem}`;
|
|
197
|
+
}
|
|
198
|
+
if (chunkId.startsWith("feed.")) return `feed:feeds/${under}`;
|
|
199
|
+
return void 0;
|
|
200
|
+
}
|
|
201
|
+
exports.SEED_SCHEMA = void 0; exports.SEED_REGISTERED_WIDGETS = void 0; exports.SEED_BROADCAST_CHANNEL = void 0; exports.EFFORTS_CHUNK_ID = void 0; exports.CANVAS_CHUNK_ID = void 0; exports.DASHBOARDS_CHUNK_ID = void 0; exports.TEMPLATE_CHUNK_ID = void 0;
|
|
202
|
+
var init_contract = __esm({
|
|
203
|
+
"src/cli/seedbuild/contract.ts"() {
|
|
204
|
+
exports.SEED_SCHEMA = 8;
|
|
205
|
+
exports.SEED_REGISTERED_WIDGETS = wodWikiCore.KNOWN_SECTION_WIDGETS;
|
|
206
|
+
exports.SEED_BROADCAST_CHANNEL = "wodwiki.seed";
|
|
207
|
+
exports.EFFORTS_CHUNK_ID = "efforts";
|
|
208
|
+
exports.CANVAS_CHUNK_ID = "canvas";
|
|
209
|
+
exports.DASHBOARDS_CHUNK_ID = "dashboards";
|
|
210
|
+
exports.TEMPLATE_CHUNK_ID = "template";
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// src/cli/seedbuild/effort-slugs.ts
|
|
215
|
+
function extractEffortSlugs2(statements) {
|
|
216
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
217
|
+
for (const statement of statements) {
|
|
218
|
+
if (statement.exerciseId) {
|
|
219
|
+
slugs.add(statement.exerciseId);
|
|
220
|
+
} else {
|
|
221
|
+
const effortMetric = statement.metrics?.getMetric?.("effort");
|
|
222
|
+
const effort = effortMetric && typeof effortMetric.value === "string" ? effortMetric.value : void 0;
|
|
223
|
+
if (effort) {
|
|
224
|
+
const slug = effort.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
225
|
+
if (slug) slugs.add(slug);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return Array.from(slugs);
|
|
230
|
+
}
|
|
231
|
+
var init_effort_slugs = __esm({
|
|
232
|
+
"src/cli/seedbuild/effort-slugs.ts"() {
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// src/cli/seedbuild/filename.ts
|
|
237
|
+
function fileToDisplayName(filename) {
|
|
238
|
+
const base = filename.replace(/\.md$/, "");
|
|
239
|
+
if (base.toUpperCase() === "README") return "Overview";
|
|
240
|
+
const cleaned = base.replace(/^day-\d+-/, "");
|
|
241
|
+
const tokens = cleaned.split(/[-_]/);
|
|
242
|
+
const titled = [];
|
|
243
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
244
|
+
const token = tokens[i];
|
|
245
|
+
const next = tokens[i + 1];
|
|
246
|
+
const after = tokens[i + 2];
|
|
247
|
+
const prev = tokens[i - 1];
|
|
248
|
+
if (/^\d{1,2}$/.test(token) && /^\d{1,2}$/.test(next ?? "") && !/^\d+$/.test(prev ?? "") && !/^\d+$/.test(after ?? "")) {
|
|
249
|
+
titled.push(`${token}.${next}`);
|
|
250
|
+
i++;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (titled.length > 0 && FILE_TITLE_STOP_WORDS[token.toLowerCase()]) {
|
|
254
|
+
titled.push(token.toLowerCase());
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
titled.push(token.charAt(0).toUpperCase() + token.slice(1));
|
|
258
|
+
}
|
|
259
|
+
return titled.join(" ");
|
|
260
|
+
}
|
|
261
|
+
var FILE_TITLE_STOP_WORDS;
|
|
262
|
+
var init_filename = __esm({
|
|
263
|
+
"src/cli/seedbuild/filename.ts"() {
|
|
264
|
+
FILE_TITLE_STOP_WORDS = {
|
|
265
|
+
and: true,
|
|
266
|
+
or: true,
|
|
267
|
+
of: true,
|
|
268
|
+
for: true,
|
|
269
|
+
to: true,
|
|
270
|
+
in: true,
|
|
271
|
+
on: true,
|
|
272
|
+
at: true,
|
|
273
|
+
by: true,
|
|
274
|
+
with: true,
|
|
275
|
+
the: true,
|
|
276
|
+
a: true,
|
|
277
|
+
an: true
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
});
|
|
257
281
|
function resolveGitVersion(repoRoot) {
|
|
258
282
|
try {
|
|
259
283
|
const run = (args) => child_process.execSync(`git ${args}`, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
@@ -346,9 +370,9 @@ var compiler_exports = {};
|
|
|
346
370
|
__export(compiler_exports, {
|
|
347
371
|
buildBlockEffortRows: () => buildBlockEffortRows,
|
|
348
372
|
buildBlockIndexRows: () => buildBlockIndexRows,
|
|
349
|
-
buildStaticParentMap: () => buildStaticParentMap,
|
|
350
373
|
chunkIdFor: () => chunkIdFor,
|
|
351
374
|
collectCorpusMarkdown: () => collectCorpusMarkdown,
|
|
375
|
+
compileSeedRow: () => compileSeedRow,
|
|
352
376
|
discoverUncatalogedEfforts: () => discoverUncatalogedEfforts,
|
|
353
377
|
feedDateToCreatedAt: () => feedDateToCreatedAt,
|
|
354
378
|
generateSeed: () => generateSeed,
|
|
@@ -432,7 +456,7 @@ function validateDashboardNote(raw) {
|
|
|
432
456
|
}
|
|
433
457
|
function gateDashboards(groups, corpusRoot) {
|
|
434
458
|
for (const group of groups) {
|
|
435
|
-
if (group.id !==
|
|
459
|
+
if (group.id !== exports.DASHBOARDS_CHUNK_ID) continue;
|
|
436
460
|
for (const p of group.files) {
|
|
437
461
|
const problems = validateDashboardNote(fs.readFileSync(path.join(corpusRoot, p), "utf8"));
|
|
438
462
|
if (problems.length > 0) {
|
|
@@ -442,26 +466,6 @@ function gateDashboards(groups, corpusRoot) {
|
|
|
442
466
|
}
|
|
443
467
|
}
|
|
444
468
|
}
|
|
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
469
|
function getFileCreatedAt(file, repoRoot) {
|
|
466
470
|
try {
|
|
467
471
|
const out = child_process.execSync(`git log -1 --format=%ct -- '${file}'`, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
@@ -476,105 +480,32 @@ function feedDateToCreatedAt(dateKey) {
|
|
|
476
480
|
const ts = Date.parse(`${dateKey}T00:00:00Z`);
|
|
477
481
|
return Number.isNaN(ts) ? 0 : ts;
|
|
478
482
|
}
|
|
483
|
+
function compileSeedRow(path, content, metadata = {}) {
|
|
484
|
+
let ordinal = 0;
|
|
485
|
+
const blocks = wodWikiCore.reconcileNoteBlocks(content, { sourceRevision: 1, registeredWidgets: exports.SEED_REGISTERED_WIDGETS, createId: () => `seed:${path}:${ordinal++}` });
|
|
486
|
+
const noteId = metadata.routeId ?? path;
|
|
487
|
+
const sections = wodWikiCore.parseDocumentSections(content);
|
|
488
|
+
const blockIndex = Object.values(blocks).map((block, position) => {
|
|
489
|
+
const section = sections.find((section2) => section2.from === block.from && section2.to === block.to);
|
|
490
|
+
return { id: `${noteId}:${block.id}`, noteId, blockId: block.id, blockVersion: 1, sourceRevision: 1, from: block.from, to: block.to, position, dataType: block.kind, ...block.kind === "wod" ? { blockContentId: wodWikiCore.blockContentId(section?.scriptBlock?.content ?? content.slice(block.from, block.to)) } : {}, rawContent: content.slice(block.from, block.to), noteTitle: metadata.noteTitle ?? fileToDisplayName(path.split("/").at(-1) ?? path), createdAt: metadata.createdAt ?? 0, isStatic: true, sourceId: metadata.sourceId, sourcePath: path, routeId: metadata.routeId };
|
|
491
|
+
});
|
|
492
|
+
return { path, content, blocks, blockIndex, blockEfforts: [] };
|
|
493
|
+
}
|
|
479
494
|
function buildBlockIndexRows(markdownDir, corpusRoot) {
|
|
480
|
-
const
|
|
481
|
-
for (const
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
const
|
|
489
|
-
const
|
|
490
|
-
const
|
|
491
|
-
|
|
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
|
-
}
|
|
495
|
+
const rows = [];
|
|
496
|
+
for (const path$1 of collectCorpusMarkdown(markdownDir)) {
|
|
497
|
+
const chunkId = chunkIdFor(path$1);
|
|
498
|
+
if (!chunkId) continue;
|
|
499
|
+
const content = fs.readFileSync(path.join(corpusRoot, path$1), "utf8");
|
|
500
|
+
const parts = path$1.split("/");
|
|
501
|
+
const stem = (parts.at(-1) ?? "").replace(/\.md$/, "");
|
|
502
|
+
const catalog = parts[2];
|
|
503
|
+
const routeId = chunkId === "canvas" ? canvasRouteSlug(content) ?? void 0 : chunkId.startsWith("collection.") ? stem.toLowerCase() === "readme" ? catalog : `${catalog}/${stem}` : chunkId.startsWith("feed.") ? parts.slice(1).join("/").replace(/\.md$/, "") : void 0;
|
|
504
|
+
const sourceId = seedNoteSourceId(path$1, content, chunkId);
|
|
505
|
+
const createdAt = chunkId.startsWith("feed.") ? feedDateToCreatedAt(parts.at(-2) ?? "") : getFileCreatedAt(path$1, corpusRoot);
|
|
506
|
+
rows.push(...compileSeedRow(path$1, content, { routeId, sourceId, createdAt }).blockIndex);
|
|
514
507
|
}
|
|
515
|
-
|
|
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;
|
|
508
|
+
return rows;
|
|
578
509
|
}
|
|
579
510
|
function formatDiscoveredLabel(raw) {
|
|
580
511
|
const clean = raw.replace(/[-_]+/g, " ").trim();
|
|
@@ -605,7 +536,8 @@ function discoverUncatalogedEfforts(markdownDir, corpusRoot) {
|
|
|
605
536
|
const discovered = /* @__PURE__ */ new Map();
|
|
606
537
|
for (const block of blocks) {
|
|
607
538
|
if (block.dataType !== "wod" || !block.rawContent) continue;
|
|
608
|
-
const
|
|
539
|
+
const section = wodWikiCore.parseDocumentSections(block.rawContent, { registeredWidgets: exports.SEED_REGISTERED_WIDGETS }).find((section2) => section2.scriptBlock);
|
|
540
|
+
const script = wodWikiLang.parseScript(section?.scriptBlock?.content ?? block.rawContent);
|
|
609
541
|
for (const statement of script.statements) {
|
|
610
542
|
const effortMetric = statement.metrics?.getMetric?.("effort");
|
|
611
543
|
const effortLabel = (effortMetric && typeof effortMetric.value === "string" ? effortMetric.value : void 0) ?? statement.exerciseId;
|
|
@@ -661,24 +593,27 @@ ${aliases.join("\n")}` : "aliases: []",
|
|
|
661
593
|
}
|
|
662
594
|
return createdPaths;
|
|
663
595
|
}
|
|
664
|
-
function buildBlockEffortRows(markdownDir, corpusRoot) {
|
|
596
|
+
function buildBlockEffortRows(markdownDir, corpusRoot, blocks = buildBlockIndexRows(markdownDir, corpusRoot)) {
|
|
665
597
|
const efforts = [];
|
|
666
598
|
const { aliasToCanonical } = loadExistingEffortMap(markdownDir);
|
|
667
|
-
const blocks = buildBlockIndexRows(markdownDir, corpusRoot);
|
|
668
599
|
for (const block of blocks) {
|
|
669
600
|
if (block.dataType !== "wod" || !block.rawContent) continue;
|
|
670
|
-
const
|
|
601
|
+
const section = wodWikiCore.parseDocumentSections(block.rawContent, { registeredWidgets: exports.SEED_REGISTERED_WIDGETS }).find((section2) => section2.scriptBlock);
|
|
602
|
+
const script = wodWikiLang.parseScript(section?.scriptBlock?.content ?? block.rawContent);
|
|
671
603
|
const effortSlugs = extractEffortSlugs2(script.statements);
|
|
604
|
+
const linked = /* @__PURE__ */ new Set();
|
|
672
605
|
for (const rawSlug of effortSlugs) {
|
|
673
606
|
const norm = rawSlug.toLowerCase().trim();
|
|
674
607
|
const canonicalSlug = aliasToCanonical.get(norm) ?? aliasToCanonical.get(norm.replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "")) ?? norm.replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
675
608
|
if (!canonicalSlug || NON_EFFORT_SLUGS[canonicalSlug] || NON_EFFORT_SLUGS[norm]) {
|
|
676
609
|
continue;
|
|
677
610
|
}
|
|
611
|
+
if (linked.has(canonicalSlug)) continue;
|
|
612
|
+
linked.add(canonicalSlug);
|
|
678
613
|
efforts.push({
|
|
679
614
|
id: `${block.id}:${canonicalSlug}`,
|
|
680
615
|
noteId: block.noteId,
|
|
681
|
-
blockId: block.
|
|
616
|
+
blockId: block.blockId,
|
|
682
617
|
blockContentId: block.blockContentId,
|
|
683
618
|
effortSlug: canonicalSlug,
|
|
684
619
|
isStatic: block.isStatic,
|
|
@@ -689,22 +624,8 @@ function buildBlockEffortRows(markdownDir, corpusRoot) {
|
|
|
689
624
|
}
|
|
690
625
|
return efforts;
|
|
691
626
|
}
|
|
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
627
|
function sha256Hex(data) {
|
|
707
|
-
return crypto.createHash("sha256").update(data).digest("hex");
|
|
628
|
+
return crypto$1.createHash("sha256").update(data).digest("hex");
|
|
708
629
|
}
|
|
709
630
|
function generateSeed(options = {}) {
|
|
710
631
|
const t0 = Date.now();
|
|
@@ -725,21 +646,42 @@ function generateSeed(options = {}) {
|
|
|
725
646
|
gateDashboards(groups, corpusRoot);
|
|
726
647
|
if (options.templateFile && fs.existsSync(options.templateFile)) {
|
|
727
648
|
const templateRowPath = options.templateRowPath ?? path.relative(corpusRoot, options.templateFile).split("\\").join("/");
|
|
728
|
-
groups.push({ id:
|
|
649
|
+
groups.push({ id: exports.TEMPLATE_CHUNK_ID, files: [templateRowPath] });
|
|
729
650
|
}
|
|
730
651
|
passMs.scan = Date.now() - tScan;
|
|
731
652
|
const chunksDir = path.join(outDir, "chunks");
|
|
732
653
|
fs.rmSync(outDir, { recursive: true, force: true });
|
|
733
654
|
fs.mkdirSync(chunksDir, { recursive: true });
|
|
734
655
|
const manifestChunks = [];
|
|
656
|
+
const tBlockIndex = Date.now();
|
|
657
|
+
const blockRows = buildBlockIndexRows(markdownDir, corpusRoot);
|
|
658
|
+
passMs.blockIndex = Date.now() - tBlockIndex;
|
|
659
|
+
const tBlockEfforts = Date.now();
|
|
660
|
+
const effortRows = buildBlockEffortRows(markdownDir, corpusRoot, blockRows);
|
|
661
|
+
passMs.blockEfforts = Date.now() - tBlockEfforts;
|
|
662
|
+
const blocksByPath = /* @__PURE__ */ new Map();
|
|
663
|
+
const effortsByPath = /* @__PURE__ */ new Map();
|
|
664
|
+
for (const row of blockRows) {
|
|
665
|
+
const rows = blocksByPath.get(row.sourcePath) ?? [];
|
|
666
|
+
rows.push(row);
|
|
667
|
+
blocksByPath.set(row.sourcePath, rows);
|
|
668
|
+
}
|
|
669
|
+
for (const row of effortRows) {
|
|
670
|
+
const rows = effortsByPath.get(row.sourcePath) ?? [];
|
|
671
|
+
rows.push(row);
|
|
672
|
+
effortsByPath.set(row.sourcePath, rows);
|
|
673
|
+
}
|
|
735
674
|
let totalFiles = 0;
|
|
736
675
|
const tWrite = Date.now();
|
|
737
676
|
for (const { id, files } of groups) {
|
|
738
|
-
const templateSource = id ===
|
|
739
|
-
const rows = files.map((p) =>
|
|
740
|
-
path
|
|
741
|
-
|
|
742
|
-
|
|
677
|
+
const templateSource = id === exports.TEMPLATE_CHUNK_ID ? options.templateFile : void 0;
|
|
678
|
+
const rows = files.map((p) => {
|
|
679
|
+
const content = fs.readFileSync(templateSource ?? path.join(corpusRoot, p), "utf8");
|
|
680
|
+
const compiled = compileSeedRow(p, content);
|
|
681
|
+
compiled.blockIndex = blocksByPath.get(p) ?? compiled.blockIndex;
|
|
682
|
+
compiled.blockEfforts = effortsByPath.get(p) ?? [];
|
|
683
|
+
return compiled;
|
|
684
|
+
});
|
|
743
685
|
const bytes = Buffer.from(JSON.stringify(rows), "utf8");
|
|
744
686
|
const sha = sha256Hex(bytes);
|
|
745
687
|
const file = `chunks/${id}.${sha.slice(0, 8)}.json`;
|
|
@@ -747,39 +689,9 @@ function generateSeed(options = {}) {
|
|
|
747
689
|
manifestChunks.push({ id, path: file, sha256: sha, bytes: bytes.length, count: rows.length });
|
|
748
690
|
totalFiles += rows.length;
|
|
749
691
|
}
|
|
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
692
|
passMs.write = Date.now() - tWrite;
|
|
781
693
|
const manifest = {
|
|
782
|
-
schema:
|
|
694
|
+
schema: exports.SEED_SCHEMA,
|
|
783
695
|
version: gitVersion.version,
|
|
784
696
|
versionLabel: gitVersion.versionLabel,
|
|
785
697
|
commit: gitVersion.commit,
|
|
@@ -795,7 +707,7 @@ function generateSeed(options = {}) {
|
|
|
795
707
|
fileCount: paths.length});
|
|
796
708
|
return { manifest, outDir, totalFiles, reportPath, passMs };
|
|
797
709
|
}
|
|
798
|
-
var
|
|
710
|
+
var NON_EFFORT_SLUGS;
|
|
799
711
|
var init_compiler = __esm({
|
|
800
712
|
"src/cli/seedbuild/compiler.ts"() {
|
|
801
713
|
init_contract();
|
|
@@ -805,7 +717,6 @@ var init_compiler = __esm({
|
|
|
805
717
|
init_git_version();
|
|
806
718
|
init_report();
|
|
807
719
|
init_frontmatter();
|
|
808
|
-
BLOCK_INDEX_ROWS_PER_CHUNK = 4e3;
|
|
809
720
|
NON_EFFORT_SLUGS = {
|
|
810
721
|
"amrap": true,
|
|
811
722
|
"emom": true,
|
|
@@ -914,7 +825,7 @@ function inMemoryEventStore(events) {
|
|
|
914
825
|
getEventsByTimeRange: async (start, end) => rows.filter((r) => r.timestamp >= start && r.timestamp <= end),
|
|
915
826
|
getEventsByResult: async (resultId) => rows.filter((r) => r.resultId === resultId),
|
|
916
827
|
getEventsForNote: async (noteId) => rows.filter((r) => r.noteId === noteId),
|
|
917
|
-
getEventsByContent: async (
|
|
828
|
+
getEventsByContent: async (blockContentId5) => rows.filter((r) => r.blockContentId === blockContentId5),
|
|
918
829
|
scanAll: async () => rows,
|
|
919
830
|
appendEvents: async (appended) => {
|
|
920
831
|
rows.push(...appended);
|
|
@@ -1048,14 +959,14 @@ async function runExecution(source, options = {}) {
|
|
|
1048
959
|
now
|
|
1049
960
|
);
|
|
1050
961
|
const { engine, analyticsContext } = wodWikiLang.createAnalyticsEngineForBlock({
|
|
1051
|
-
id: "cli-block",
|
|
962
|
+
id: options.blockId ?? "cli-block",
|
|
1052
963
|
content: source,
|
|
1053
964
|
statements: script.statements,
|
|
1054
965
|
sport: options.sport,
|
|
1055
|
-
version: 1,
|
|
966
|
+
version: options.blockVersion ?? 1,
|
|
1056
967
|
startLine: 0,
|
|
1057
968
|
endLine: 0,
|
|
1058
|
-
dialect: "time",
|
|
969
|
+
dialect: options.dialect ?? "time",
|
|
1059
970
|
state: "idle",
|
|
1060
971
|
createdAt: Date.now()
|
|
1061
972
|
});
|
|
@@ -1085,7 +996,8 @@ async function runExecution(source, options = {}) {
|
|
|
1085
996
|
{
|
|
1086
997
|
results: workoutResults,
|
|
1087
998
|
logs,
|
|
1088
|
-
statements: logs
|
|
999
|
+
statements: logs,
|
|
1000
|
+
sourceScript: source
|
|
1089
1001
|
},
|
|
1090
1002
|
{ source: options.sourceLabel ?? "cli:wod run" }
|
|
1091
1003
|
);
|
|
@@ -1106,10 +1018,6 @@ var WqlSyntaxError = class extends Error {
|
|
|
1106
1018
|
query;
|
|
1107
1019
|
syntaxError;
|
|
1108
1020
|
};
|
|
1109
|
-
function legacyLogsOf(result) {
|
|
1110
|
-
const legacy = result;
|
|
1111
|
-
return legacy.data?.logs ?? [];
|
|
1112
|
-
}
|
|
1113
1021
|
function extractPayload(rawJson) {
|
|
1114
1022
|
try {
|
|
1115
1023
|
const parsed = JSON.parse(rawJson);
|
|
@@ -1122,16 +1030,6 @@ function extractPayload(rawJson) {
|
|
|
1122
1030
|
}
|
|
1123
1031
|
}
|
|
1124
1032
|
function buildStoresFromData(data) {
|
|
1125
|
-
const identity = (r) => ({
|
|
1126
|
-
noteId: r.noteId,
|
|
1127
|
-
resultId: r.id,
|
|
1128
|
-
segmentId: r.segmentId,
|
|
1129
|
-
segmentVersion: r.segmentVersion,
|
|
1130
|
-
blockContentId: r.blockContentId,
|
|
1131
|
-
origin: r.origin,
|
|
1132
|
-
pageId: r.pageId,
|
|
1133
|
-
workoutTimestamp: r.createdAt
|
|
1134
|
-
});
|
|
1135
1033
|
const events = [
|
|
1136
1034
|
...data.eventRecords,
|
|
1137
1035
|
...data.facts.map((f, i) => ({
|
|
@@ -1151,17 +1049,17 @@ function buildStoresFromData(data) {
|
|
|
1151
1049
|
...f.unit ? { unit: f.unit } : {},
|
|
1152
1050
|
origin: "engine"
|
|
1153
1051
|
}],
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
}))
|
|
1157
|
-
...data.results.flatMap((r) => {
|
|
1158
|
-
const logs = legacyLogsOf(r);
|
|
1159
|
-
return [...wodWikiWql.toEventRows(logs, identity(r)), ...wodWikiWql.toSummaryEventRows(logs, identity(r))];
|
|
1160
|
-
})
|
|
1052
|
+
blockId: f.blockId,
|
|
1053
|
+
blockVersion: f.blockVersion
|
|
1054
|
+
}))
|
|
1161
1055
|
];
|
|
1162
1056
|
const eventStore = inMemoryEventStore(events);
|
|
1163
1057
|
const noteStore = {
|
|
1164
1058
|
getAllNotes: async () => [...data.notes],
|
|
1059
|
+
getNotesByIds: async (ids) => {
|
|
1060
|
+
const selected = new Set(ids);
|
|
1061
|
+
return data.notes.filter((note) => selected.has(note.id));
|
|
1062
|
+
},
|
|
1165
1063
|
getNoteIdsForTag: async (tag) => {
|
|
1166
1064
|
const matching = /* @__PURE__ */ new Set();
|
|
1167
1065
|
for (const [noteId, tags] of Object.entries(data.noteTags)) {
|
|
@@ -1186,6 +1084,7 @@ function loadQueryData(options) {
|
|
|
1186
1084
|
results: [],
|
|
1187
1085
|
notes: [],
|
|
1188
1086
|
blocks: [],
|
|
1087
|
+
revisions: [],
|
|
1189
1088
|
efforts: [...wodWikiLang.bundledEfforts],
|
|
1190
1089
|
noteTags: {}
|
|
1191
1090
|
};
|
|
@@ -1200,7 +1099,10 @@ function loadQueryData(options) {
|
|
|
1200
1099
|
data.facts = payload;
|
|
1201
1100
|
} else if (payload && typeof payload === "object") {
|
|
1202
1101
|
const obj = payload;
|
|
1102
|
+
if ("segments" in obj) throw new Error("Incompatible corpus segments; whole Notes and block revisions are required");
|
|
1203
1103
|
if (obj.kind === "event-journal" || Array.isArray(obj.records)) {
|
|
1104
|
+
if (Array.isArray(obj.blockRevisions)) data.revisions = obj.blockRevisions;
|
|
1105
|
+
if (Array.isArray(obj.sessions)) data.results = obj.sessions;
|
|
1204
1106
|
if (Array.isArray(obj.records)) {
|
|
1205
1107
|
data.eventRecords = obj.records;
|
|
1206
1108
|
}
|
|
@@ -1219,20 +1121,16 @@ function loadQueryData(options) {
|
|
|
1219
1121
|
if (corpus.notes) data.notes = corpus.notes;
|
|
1220
1122
|
if (corpus.blocks) data.blocks = corpus.blocks;
|
|
1221
1123
|
if (corpus.efforts) data.efforts = corpus.efforts;
|
|
1124
|
+
if (corpus.revisions) data.revisions = corpus.revisions;
|
|
1222
1125
|
if (corpus.tags) data.noteTags = corpus.tags;
|
|
1223
|
-
if (corpus.logs
|
|
1224
|
-
const
|
|
1225
|
-
id: "corpus-result-1",
|
|
1126
|
+
if (corpus.logs) {
|
|
1127
|
+
const identity = {
|
|
1226
1128
|
noteId: "corpus-note-1",
|
|
1227
|
-
|
|
1129
|
+
resultId: "corpus-result-1",
|
|
1228
1130
|
origin: "journal",
|
|
1229
|
-
|
|
1230
|
-
startTime: corpus.logs[0]?.timeSpan?.started ?? Date.now(),
|
|
1231
|
-
endTime: corpus.logs[corpus.logs.length - 1]?.timeSpan?.ended ?? Date.now(),
|
|
1232
|
-
duration: 0,
|
|
1233
|
-
completed: true
|
|
1131
|
+
workoutTimestamp: corpus.logs[0]?.timeSpan?.started ?? Date.now()
|
|
1234
1132
|
};
|
|
1235
|
-
data.
|
|
1133
|
+
data.eventRecords = [...wodWikiWql.toEventRows(corpus.logs, identity), ...wodWikiWql.toSummaryEventRows(corpus.logs, identity)];
|
|
1236
1134
|
}
|
|
1237
1135
|
} else {
|
|
1238
1136
|
throw new Error(`Unrecognized corpus payload shape in "${options.corpusPath}" (expected event-journal, fact-set, or CorpusIRData)`);
|
|
@@ -1247,29 +1145,16 @@ function loadQueryData(options) {
|
|
|
1247
1145
|
} else {
|
|
1248
1146
|
executionLog = { results: payload, logs: rawLogs, statements: rawLogs };
|
|
1249
1147
|
}
|
|
1250
|
-
const
|
|
1251
|
-
const workoutResult = {
|
|
1252
|
-
id: resultId,
|
|
1148
|
+
const identity = {
|
|
1253
1149
|
noteId: "stdin-note-1",
|
|
1254
|
-
|
|
1150
|
+
resultId: "stdin-result-1",
|
|
1255
1151
|
origin: "journal",
|
|
1256
|
-
|
|
1257
|
-
startTime: executionLog.results.startTime,
|
|
1258
|
-
endTime: executionLog.results.endTime,
|
|
1259
|
-
duration: executionLog.results.duration ?? 0,
|
|
1260
|
-
completed: executionLog.results.completed
|
|
1152
|
+
workoutTimestamp: executionLog.results.startTime
|
|
1261
1153
|
};
|
|
1262
|
-
data.
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
segmentId: workoutResult.segmentId,
|
|
1267
|
-
segmentVersion: workoutResult.segmentVersion,
|
|
1268
|
-
blockContentId: workoutResult.blockContentId,
|
|
1269
|
-
origin: workoutResult.origin,
|
|
1270
|
-
pageId: workoutResult.pageId,
|
|
1271
|
-
workoutTimestamp: workoutResult.createdAt
|
|
1272
|
-
});
|
|
1154
|
+
data.eventRecords = [
|
|
1155
|
+
...wodWikiWql.toEventRows(executionLog.logs, identity),
|
|
1156
|
+
...wodWikiWql.toSummaryEventRows(executionLog.logs, identity)
|
|
1157
|
+
];
|
|
1273
1158
|
} else if (options.stdinFacts) {
|
|
1274
1159
|
const payload = extractPayload(options.stdinFacts);
|
|
1275
1160
|
if (Array.isArray(payload)) {
|
|
@@ -1280,6 +1165,60 @@ function loadQueryData(options) {
|
|
|
1280
1165
|
} else {
|
|
1281
1166
|
throw new Error("No input dataset provided. Specify --corpus <path>, --stdin-log, or --stdin-facts.");
|
|
1282
1167
|
}
|
|
1168
|
+
for (const note of data.notes) {
|
|
1169
|
+
if (typeof note.rawContent !== "string" || !Number.isSafeInteger(note.sourceRevision) || note.sourceRevision < 1) {
|
|
1170
|
+
throw new Error("Incompatible corpus Note; exact rawContent and sourceRevision are required");
|
|
1171
|
+
}
|
|
1172
|
+
wodWikiCore.validateNoteBlocks(note.rawContent, note.blocks, note.sourceRevision);
|
|
1173
|
+
if (!data.blocks.some((row) => row.noteId === note.id)) {
|
|
1174
|
+
for (const [position, block] of Object.values(note.blocks).sort((a, b) => a.from - b.from).entries()) {
|
|
1175
|
+
const rawContent = note.rawContent.slice(block.from, block.to);
|
|
1176
|
+
const body = wodWikiCore.parseDocumentSections(rawContent)[0]?.displayContent ?? rawContent;
|
|
1177
|
+
data.blocks.push({
|
|
1178
|
+
id: `${note.id}:${block.id}`,
|
|
1179
|
+
noteId: note.id,
|
|
1180
|
+
blockId: block.id,
|
|
1181
|
+
blockVersion: block.version,
|
|
1182
|
+
sourceRevision: note.sourceRevision,
|
|
1183
|
+
from: block.from,
|
|
1184
|
+
to: block.to,
|
|
1185
|
+
position,
|
|
1186
|
+
dataType: block.kind,
|
|
1187
|
+
rawContent,
|
|
1188
|
+
noteTitle: note.title,
|
|
1189
|
+
createdAt: note.createdAt,
|
|
1190
|
+
...block.kind === "wod" ? { blockContentId: wodWikiCore.blockContentId(body) } : {}
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
for (const session of data.results) {
|
|
1196
|
+
if ("segmentId" in session || "segmentVersion" in session || "data" in session || "version" in session || "rawContent" in session || !session.blockId || !Number.isSafeInteger(session.blockVersion) || session.blockVersion < 1) {
|
|
1197
|
+
throw new Error("Incompatible corpus Session; exact blockId/blockVersion pin required");
|
|
1198
|
+
}
|
|
1199
|
+
if (!data.revisions.some((row) => row.noteId === session.noteId && row.blockId === session.blockId && row.version === session.blockVersion && row.kind === "wod")) {
|
|
1200
|
+
throw new Error("Corpus Session immutable revision is unavailable");
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
for (const row of [...data.eventRecords, ...data.facts, ...data.blocks]) {
|
|
1204
|
+
if ("segmentId" in row || "segmentVersion" in row) throw new Error("Incompatible corpus segment pin");
|
|
1205
|
+
}
|
|
1206
|
+
const revisions = new Map(data.revisions.map((row) => [JSON.stringify([row.noteId, row.blockId, row.version]), row]));
|
|
1207
|
+
for (const row of data.eventRecords) {
|
|
1208
|
+
if (row.blockId !== void 0 || row.blockVersion !== void 0) {
|
|
1209
|
+
if (!row.blockId || !Number.isSafeInteger(row.blockVersion) || !revisions.has(JSON.stringify([row.noteId, row.blockId, row.blockVersion]))) {
|
|
1210
|
+
throw new Error("Corpus Event immutable revision is unavailable");
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
for (const row of data.blocks) {
|
|
1215
|
+
const note = data.notes.find((candidate) => candidate.id === row.noteId);
|
|
1216
|
+
const block = note?.blocks[row.blockId];
|
|
1217
|
+
if (!note || !block || row.blockVersion !== block.version || row.sourceRevision !== note.sourceRevision || row.from !== block.from || row.to !== block.to || row.dataType !== block.kind) {
|
|
1218
|
+
throw new Error("Corpus block projection does not match current Note source");
|
|
1219
|
+
}
|
|
1220
|
+
row.rawContent = note.rawContent.slice(block.from, block.to);
|
|
1221
|
+
}
|
|
1283
1222
|
return data;
|
|
1284
1223
|
}
|
|
1285
1224
|
async function runQueryCli(wqlString, options = {}) {
|
|
@@ -1388,9 +1327,14 @@ async function openCliDb(dbPath) {
|
|
|
1388
1327
|
const resolved = resolveDbPath(dbPath);
|
|
1389
1328
|
nodeFs().mkdirSync(nodePath().dirname(resolved), { recursive: true });
|
|
1390
1329
|
const db = sql.libsqlDatabase(createClient({ url: `file:${resolved}` }));
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1330
|
+
try {
|
|
1331
|
+
await sql.initializeStore(db);
|
|
1332
|
+
await sql.initializeDomain(db);
|
|
1333
|
+
return { db, storage: new sql.SqlStorage(db), path: resolved, close: () => db.close() };
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
await db.close();
|
|
1336
|
+
throw error;
|
|
1337
|
+
}
|
|
1394
1338
|
}
|
|
1395
1339
|
function formatV5Digest(digest) {
|
|
1396
1340
|
const bytes = digest.slice(0, 16);
|
|
@@ -1424,7 +1368,7 @@ function sqliteEventStore(db) {
|
|
|
1424
1368
|
getEventsByTimeRange: async (start, end) => (await all()).filter((r) => r.timestamp >= start && r.timestamp <= end),
|
|
1425
1369
|
getEventsByResult: async (resultId) => (await all()).filter((r) => r.resultId === resultId),
|
|
1426
1370
|
getEventsForNote: async (noteId) => (await all()).filter((r) => r.noteId === noteId),
|
|
1427
|
-
getEventsByContent: async (
|
|
1371
|
+
getEventsByContent: async (blockContentId5) => (await all()).filter((r) => r.blockContentId === blockContentId5),
|
|
1428
1372
|
scanAll: all,
|
|
1429
1373
|
appendEvents: async (appended) => {
|
|
1430
1374
|
const sql = await loadSql();
|
|
@@ -1456,6 +1400,11 @@ function sqliteEventStore(db) {
|
|
|
1456
1400
|
function sqliteNoteStore(db) {
|
|
1457
1401
|
return {
|
|
1458
1402
|
getAllNotes: async () => await allRows(await loadSql(), db, "notes"),
|
|
1403
|
+
getNotesByIds: async (noteIds) => {
|
|
1404
|
+
const sql = await loadSql();
|
|
1405
|
+
const rows = await sql.getRows(db, sql.storeDefinition("notes"), [...noteIds]);
|
|
1406
|
+
return rows.filter((row) => row !== void 0);
|
|
1407
|
+
},
|
|
1459
1408
|
getNoteIdsForTag: async (label) => {
|
|
1460
1409
|
const sql = await loadSql();
|
|
1461
1410
|
const tags = (await allRows(sql, db, "tags")).filter((t) => t.label === label);
|
|
@@ -1510,17 +1459,14 @@ function sqliteIndexStore(db, store) {
|
|
|
1510
1459
|
}
|
|
1511
1460
|
async function getNoteBundle(db, noteId) {
|
|
1512
1461
|
const sql = await loadSql();
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
const segments = await rowsByIndex(sql, db, "segments", "by-note", noteId);
|
|
1516
|
-
return { note, segments };
|
|
1462
|
+
const note = await sql.getRow(db, sql.storeDefinition("notes"), noteId);
|
|
1463
|
+
return { note };
|
|
1517
1464
|
}
|
|
1518
1465
|
async function getSessionBundle(db, resultId) {
|
|
1519
1466
|
const sql = await loadSql();
|
|
1520
|
-
const
|
|
1521
|
-
const session = sessions.find((s) => s.id === resultId);
|
|
1467
|
+
const session = await sql.getRow(db, sql.storeDefinition("sessions"), resultId);
|
|
1522
1468
|
if (!session) return void 0;
|
|
1523
|
-
const events =
|
|
1469
|
+
const events = await sql.listRows(db, sql.storeDefinition("events"), { only: resultId }, void 0, "by-result-grain");
|
|
1524
1470
|
return { session, events };
|
|
1525
1471
|
}
|
|
1526
1472
|
async function getEffortBySlug(db, slug) {
|
|
@@ -1533,38 +1479,8 @@ async function listTagsByType(db, type) {
|
|
|
1533
1479
|
const tags = await allRows(sql, db, "tags");
|
|
1534
1480
|
return tags.filter((t) => t.type === type);
|
|
1535
1481
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
case "time":
|
|
1539
|
-
case "log":
|
|
1540
|
-
return "wod";
|
|
1541
|
-
case "title": {
|
|
1542
|
-
const level = Math.min(6, Math.max(1, section.level ?? 1));
|
|
1543
|
-
return `h${level}`;
|
|
1544
|
-
}
|
|
1545
|
-
case "frontmatter":
|
|
1546
|
-
return "frontmatter";
|
|
1547
|
-
case "markdown":
|
|
1548
|
-
default:
|
|
1549
|
-
return "markdown";
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
function sectionSourceFragments(content, sections) {
|
|
1553
|
-
const starts = [0];
|
|
1554
|
-
for (let i = 0; i < content.length; i++) {
|
|
1555
|
-
if (content[i] === "\n") starts.push(i + 1);
|
|
1556
|
-
}
|
|
1557
|
-
return sections.map((section, index) => content.slice(
|
|
1558
|
-
starts[section.startLine],
|
|
1559
|
-
index + 1 < sections.length ? starts[sections[index + 1].startLine] : content.length
|
|
1560
|
-
));
|
|
1561
|
-
}
|
|
1562
|
-
function scriptBlockContentId(data) {
|
|
1563
|
-
if (data && typeof data === "object" && "contentId" in data && typeof data.contentId === "string") {
|
|
1564
|
-
return data.contentId;
|
|
1565
|
-
}
|
|
1566
|
-
return void 0;
|
|
1567
|
-
}
|
|
1482
|
+
var registeredWidgets = wodWikiCore.KNOWN_SECTION_WIDGETS;
|
|
1483
|
+
var saveStores = ["notes", "block_revisions", "block_index", "block_efforts", "tags", "note_tags", "sessions"];
|
|
1568
1484
|
function extractEffortSlugs(statements) {
|
|
1569
1485
|
const slugs = /* @__PURE__ */ new Set();
|
|
1570
1486
|
for (const statement of statements) {
|
|
@@ -1581,227 +1497,233 @@ function extractEffortSlugs(statements) {
|
|
|
1581
1497
|
}
|
|
1582
1498
|
return [...slugs];
|
|
1583
1499
|
}
|
|
1584
|
-
async function rowsByNote(sql, db, store, noteId) {
|
|
1585
|
-
return sql.listRows(db, sql.storeDefinition(store), { only: noteId }, void 0, "by-note");
|
|
1586
|
-
}
|
|
1587
1500
|
function titleFromLabel(sourceLabel) {
|
|
1588
1501
|
if (!sourceLabel || sourceLabel === "-") return "stdin";
|
|
1589
1502
|
const stem = sourceLabel.split("/").pop() ?? sourceLabel;
|
|
1590
1503
|
return stem.replace(/\.[^.]+$/, "") || sourceLabel;
|
|
1591
1504
|
}
|
|
1592
|
-
async function
|
|
1593
|
-
const sql = await loadSql();
|
|
1594
|
-
const now = options.now ?? Date.now();
|
|
1505
|
+
async function saveSource(db, options, startTime) {
|
|
1595
1506
|
const noteId = options.noteId ?? stableNoteId(options.sourceLabel, options.source);
|
|
1596
|
-
const
|
|
1597
|
-
const
|
|
1598
|
-
const
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
versionBySegmentId[segment.id] = Math.max(versionBySegmentId[segment.id] ?? 0, segment.version);
|
|
1604
|
-
}
|
|
1605
|
-
const createdAt = existingNote?.createdAt ?? now;
|
|
1606
|
-
const note = {
|
|
1607
|
-
id: noteId,
|
|
1608
|
-
title,
|
|
1609
|
-
type: existingNote?.type ?? "note",
|
|
1610
|
-
date: existingNote?.date ?? now,
|
|
1611
|
-
createdAt
|
|
1612
|
-
};
|
|
1613
|
-
const ops = [];
|
|
1614
|
-
const segments = sections.map((section, position) => {
|
|
1615
|
-
const id = `${noteId}:${section.id}`;
|
|
1616
|
-
const version = (versionBySegmentId[id] ?? 0) + 1;
|
|
1617
|
-
return {
|
|
1618
|
-
id,
|
|
1619
|
-
version,
|
|
1620
|
-
noteId,
|
|
1621
|
-
position,
|
|
1622
|
-
dataType: toSegmentDataType(section),
|
|
1623
|
-
data: section.scriptBlock || null,
|
|
1624
|
-
rawContent: section.displayContent,
|
|
1625
|
-
sourceContent: fragments[position],
|
|
1626
|
-
createdAt: version === 1 ? createdAt : now,
|
|
1627
|
-
updatedAt: now,
|
|
1628
|
-
isHistory: false
|
|
1629
|
-
};
|
|
1630
|
-
});
|
|
1631
|
-
for (const segment of priorSegments) {
|
|
1632
|
-
if (segment.isHistory !== true) {
|
|
1633
|
-
ops.push({ op: "put", store: "segments", value: { ...segment, isHistory: true, updatedAt: now } });
|
|
1507
|
+
const observed = await db.storage.readonly("notes").get(noteId);
|
|
1508
|
+
const expected = options.expectedSourceRevision ?? observed?.sourceRevision ?? 0;
|
|
1509
|
+
const now = options.now ?? Date.now();
|
|
1510
|
+
return db.storage.transaction([...saveStores], "readwrite", async (tx) => {
|
|
1511
|
+
const existing = await tx.readonly("notes").get(noteId);
|
|
1512
|
+
if ((existing?.sourceRevision ?? 0) !== expected) {
|
|
1513
|
+
throw new Error(`Stale Note source revision for ${noteId}; expected ${expected}`);
|
|
1634
1514
|
}
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
for (const row of priorBlockEfforts) ops.push({ op: "delete", store: "block_efforts", key: row.id });
|
|
1642
|
-
let blockCount = 0;
|
|
1643
|
-
let effortCount = 0;
|
|
1644
|
-
for (const segment of segments) {
|
|
1645
|
-
const blockContentId = segment.dataType === "wod" ? scriptBlockContentId(segment.data) : void 0;
|
|
1646
|
-
ops.push({
|
|
1647
|
-
op: "put",
|
|
1648
|
-
store: "block_index",
|
|
1649
|
-
value: {
|
|
1650
|
-
id: `${noteId}:${segment.id}:${segment.version}`,
|
|
1651
|
-
noteId,
|
|
1652
|
-
segmentId: segment.id,
|
|
1653
|
-
segmentVersion: segment.version,
|
|
1654
|
-
position: segment.position,
|
|
1655
|
-
dataType: segment.dataType,
|
|
1656
|
-
blockContentId,
|
|
1657
|
-
rawContent: segment.rawContent,
|
|
1658
|
-
noteTitle: title,
|
|
1659
|
-
createdAt: segment.createdAt
|
|
1660
|
-
}
|
|
1515
|
+
let sourceRevision = existing?.rawContent === options.source ? existing.sourceRevision : (existing?.sourceRevision ?? 0) + 1;
|
|
1516
|
+
const blocks = wodWikiCore.reconcileNoteBlocks(options.source, {
|
|
1517
|
+
sourceRevision,
|
|
1518
|
+
blocks: options.blocks ?? (existing?.rawContent === options.source ? existing.blocks : void 0),
|
|
1519
|
+
registeredWidgets,
|
|
1520
|
+
createId: randomId
|
|
1661
1521
|
});
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1522
|
+
wodWikiCore.validateNoteBlocks(options.source, blocks, sourceRevision, { registeredWidgets });
|
|
1523
|
+
const history = await tx.readonly("block_revisions").getAllFromIndex("by-note", noteId);
|
|
1524
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1525
|
+
for (const row of history) {
|
|
1526
|
+
if ((latest.get(row.blockId)?.version ?? 0) < row.version) latest.set(row.blockId, row);
|
|
1527
|
+
}
|
|
1528
|
+
const revisions = [];
|
|
1529
|
+
for (const block of Object.values(blocks)) {
|
|
1530
|
+
const saved = latest.get(block.id);
|
|
1531
|
+
const rawContent = options.source.slice(block.from, block.to);
|
|
1532
|
+
const unchanged = saved && saved.rawContent === rawContent && saved.kind === block.kind && saved.dialect === block.dialect && saved.sport === block.sport;
|
|
1533
|
+
block.version = unchanged ? saved.version : (saved?.version ?? 0) + 1;
|
|
1534
|
+
if (!unchanged) revisions.push({
|
|
1535
|
+
noteId,
|
|
1536
|
+
blockId: block.id,
|
|
1537
|
+
version: block.version,
|
|
1538
|
+
kind: block.kind,
|
|
1539
|
+
rawContent,
|
|
1540
|
+
savedAt: now,
|
|
1541
|
+
...block.dialect ? { dialect: block.dialect } : {},
|
|
1542
|
+
...block.sport ? { sport: block.sport } : {}
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
if (existing?.rawContent === options.source && !wodWikiStorage.rowsEqual(existing.blocks, blocks)) {
|
|
1546
|
+
sourceRevision++;
|
|
1547
|
+
for (const block of Object.values(blocks)) block.sourceRevision = sourceRevision;
|
|
1548
|
+
}
|
|
1549
|
+
const note = {
|
|
1550
|
+
...existing,
|
|
1551
|
+
id: noteId,
|
|
1552
|
+
title: options.title ?? existing?.title ?? titleFromLabel(options.sourceLabel),
|
|
1553
|
+
type: existing?.type ?? "note",
|
|
1554
|
+
date: existing?.date ?? now,
|
|
1555
|
+
createdAt: existing?.createdAt ?? now,
|
|
1556
|
+
rawContent: options.source,
|
|
1557
|
+
sourceRevision,
|
|
1558
|
+
blocks
|
|
1559
|
+
};
|
|
1560
|
+
const priorRows = await tx.readonly("block_index").getAllFromIndex("by-note", noteId);
|
|
1561
|
+
const priorEfforts = await tx.readonly("block_efforts").getAllFromIndex("by-note", noteId);
|
|
1562
|
+
const priorById = new Map(priorRows.map((row) => [row.id, row]));
|
|
1563
|
+
const nextRows = [];
|
|
1564
|
+
const nextEfforts = [];
|
|
1565
|
+
for (const [position, block] of Object.values(blocks).sort((a, b) => a.from - b.from).entries()) {
|
|
1566
|
+
const id = `${noteId}:${block.id}`;
|
|
1567
|
+
const prior = priorById.get(id);
|
|
1568
|
+
const rawContent = options.source.slice(block.from, block.to);
|
|
1569
|
+
const body = wodWikiCore.parseDocumentSections(rawContent, { registeredWidgets })[0]?.displayContent ?? rawContent;
|
|
1570
|
+
const fingerprint = block.kind === "wod" ? wodWikiCore.blockContentId(body) : void 0;
|
|
1571
|
+
nextRows.push({
|
|
1572
|
+
...prior,
|
|
1573
|
+
id,
|
|
1574
|
+
noteId,
|
|
1575
|
+
blockId: block.id,
|
|
1576
|
+
blockVersion: block.version,
|
|
1577
|
+
dataType: block.kind,
|
|
1578
|
+
sourceRevision,
|
|
1579
|
+
from: block.from,
|
|
1580
|
+
to: block.to,
|
|
1581
|
+
position,
|
|
1582
|
+
rawContent,
|
|
1583
|
+
noteTitle: note.title,
|
|
1584
|
+
createdAt: prior?.createdAt ?? now,
|
|
1585
|
+
blockContentId: fingerprint,
|
|
1586
|
+
...note.sourceId ? { sourceId: note.sourceId } : {}
|
|
1587
|
+
});
|
|
1588
|
+
if (block.kind === "wod") {
|
|
1589
|
+
for (const effortSlug of extractEffortSlugs(wodWikiLang.parseScript(body, { sport: block.sport, dialect: block.dialect }).statements)) {
|
|
1590
|
+
const effortId = `${noteId}:${block.id}:${effortSlug}`;
|
|
1591
|
+
const previous = priorEfforts.find((row) => row.id === effortId);
|
|
1592
|
+
nextEfforts.push({
|
|
1593
|
+
...previous,
|
|
1594
|
+
id: effortId,
|
|
1671
1595
|
noteId,
|
|
1672
|
-
blockId:
|
|
1673
|
-
blockContentId,
|
|
1596
|
+
blockId: block.id,
|
|
1597
|
+
blockContentId: fingerprint,
|
|
1674
1598
|
effortSlug,
|
|
1675
|
-
createdAt:
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
effortCount += 1;
|
|
1599
|
+
createdAt: previous?.createdAt ?? now
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1679
1602
|
}
|
|
1680
1603
|
}
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
)
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
});
|
|
1604
|
+
if (!wodWikiStorage.rowsEqual(existing, note)) await tx.readwrite("notes").put(note);
|
|
1605
|
+
if (revisions.length) await tx.readwrite("block_revisions").putMany(revisions);
|
|
1606
|
+
await diffProjection(tx, "block_index", priorRows, nextRows);
|
|
1607
|
+
await diffProjection(tx, "block_efforts", priorEfforts, nextEfforts);
|
|
1608
|
+
const tags = await saveTags(tx, noteId, options.tags, now);
|
|
1609
|
+
let session;
|
|
1610
|
+
let revision;
|
|
1611
|
+
if (startTime !== void 0) {
|
|
1612
|
+
const block = Object.values(blocks).filter((row) => row.kind === "wod").sort((a, b) => a.from - b.from)[0];
|
|
1613
|
+
if (!block) throw new Error("[wod run] Note has no actionable workout block");
|
|
1614
|
+
revision = revisions.find((row) => row.blockId === block.id) ?? latest.get(block.id);
|
|
1615
|
+
if (!revision) throw new Error("[wod run] Saved workout revision is unavailable");
|
|
1616
|
+
session = {
|
|
1617
|
+
id: randomId(),
|
|
1618
|
+
noteId,
|
|
1619
|
+
blockId: block.id,
|
|
1620
|
+
blockVersion: block.version,
|
|
1621
|
+
blockContentId: wodWikiCore.blockContentId(wodWikiCore.parseDocumentSections(revision.rawContent)[0]?.displayContent ?? revision.rawContent),
|
|
1622
|
+
origin: "journal",
|
|
1623
|
+
status: "in-progress",
|
|
1624
|
+
startTime,
|
|
1625
|
+
endTime: startTime,
|
|
1626
|
+
duration: 0,
|
|
1627
|
+
completed: false,
|
|
1628
|
+
createdAt: startTime
|
|
1629
|
+
};
|
|
1630
|
+
await tx.readwrite("sessions").put(session);
|
|
1709
1631
|
}
|
|
1710
|
-
|
|
1711
|
-
await db.db.transaction(async (tx) => {
|
|
1712
|
-
await sql.refreshDomain(tx, ops);
|
|
1632
|
+
return { noteId, note, blocks: nextRows.length, efforts: nextEfforts.length, tags, created: !existing, session, revision };
|
|
1713
1633
|
});
|
|
1714
|
-
return {
|
|
1715
|
-
noteId,
|
|
1716
|
-
note,
|
|
1717
|
-
segments: segments.length,
|
|
1718
|
-
blocks: blockCount,
|
|
1719
|
-
efforts: effortCount,
|
|
1720
|
-
tags: tagCount,
|
|
1721
|
-
created: !existingNote
|
|
1722
|
-
};
|
|
1723
1634
|
}
|
|
1724
|
-
async function
|
|
1725
|
-
const
|
|
1726
|
-
|
|
1635
|
+
async function diffProjection(tx, store, previous, next) {
|
|
1636
|
+
const nextIds = new Set(next.map((row) => row.id));
|
|
1637
|
+
for (const row of previous) if (!nextIds.has(row.id)) await tx.readwrite(store).delete(row.id);
|
|
1638
|
+
const old = new Map(previous.map((row) => [row.id, row]));
|
|
1639
|
+
for (const row of next) {
|
|
1640
|
+
if (!wodWikiStorage.rowsEqual(old.get(row.id), row)) {
|
|
1641
|
+
if (store === "block_index" && "blockVersion" in row) await tx.readwrite("block_index").put(row);
|
|
1642
|
+
if (store === "block_efforts" && "effortSlug" in row) await tx.readwrite("block_efforts").put(row);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1727
1645
|
}
|
|
1728
|
-
async function
|
|
1729
|
-
const
|
|
1730
|
-
|
|
1731
|
-
const
|
|
1732
|
-
const
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
const
|
|
1646
|
+
async function saveTags(tx, noteId, items, now) {
|
|
1647
|
+
const previous = await tx.readonly("note_tags").getAllFromIndex("by-note", noteId);
|
|
1648
|
+
if (!items) return previous.length;
|
|
1649
|
+
const next = /* @__PURE__ */ new Map();
|
|
1650
|
+
for (const item of items) {
|
|
1651
|
+
const label = item.label.trim();
|
|
1652
|
+
if (!label) continue;
|
|
1653
|
+
const found = (await tx.readonly("tags").getAllFromIndex("by-label", label))[0];
|
|
1654
|
+
const tag = found ? { ...found, ...!found.type && item.type ? { type: item.type } : {} } : { id: uuid5(CLI_UUID_NAMESPACE, `tag:${label}`), label, type: item.type, createdAt: now };
|
|
1655
|
+
if (!wodWikiStorage.rowsEqual(found, tag)) await tx.readwrite("tags").put(tag);
|
|
1656
|
+
const id = previous.find((link) => link.tagId === tag.id)?.id ?? uuid5(CLI_UUID_NAMESPACE, `note-tag:${noteId}:${tag.id}`);
|
|
1657
|
+
next.set(id, { ...previous.find((link) => link.id === id), id, noteId, tagId: tag.id });
|
|
1658
|
+
}
|
|
1659
|
+
for (const link of previous) if (!next.has(link.id)) await tx.readwrite("note_tags").delete(link.id);
|
|
1660
|
+
const old = new Map(previous.map((row) => [row.id, row]));
|
|
1661
|
+
for (const link of next.values()) if (!wodWikiStorage.rowsEqual(old.get(link.id), link)) await tx.readwrite("note_tags").put(link);
|
|
1662
|
+
return next.size;
|
|
1663
|
+
}
|
|
1664
|
+
function persistNote(db, options) {
|
|
1665
|
+
return saveSource(db, options);
|
|
1666
|
+
}
|
|
1667
|
+
async function startRun(db, options, startTime = Date.now()) {
|
|
1668
|
+
const saved = await saveSource(db, options, startTime);
|
|
1669
|
+
if (!saved.session || !saved.revision) throw new Error("[wod run] Atomic run capture failed");
|
|
1670
|
+
return { session: saved.session, revision: saved.revision, noteCreated: saved.created };
|
|
1671
|
+
}
|
|
1672
|
+
async function persistRun(db, started, executionLog) {
|
|
1673
|
+
const results = executionLog.results;
|
|
1742
1674
|
const session = {
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
segmentVersion: runSegment?.version,
|
|
1746
|
-
noteId: noteState.noteId,
|
|
1747
|
-
blockId: runSegment?.id,
|
|
1748
|
-
blockContentId,
|
|
1749
|
-
origin: "journal",
|
|
1750
|
-
status: results.completed === false ? "in-progress" : "completed",
|
|
1751
|
-
startTime: results.startTime,
|
|
1675
|
+
...started.session,
|
|
1676
|
+
status: results.completed ? "completed" : "in-progress",
|
|
1752
1677
|
endTime: results.endTime,
|
|
1753
1678
|
duration: results.duration ?? 0,
|
|
1754
|
-
completed: results.completed
|
|
1755
|
-
createdAt: results.endTime || Date.now()
|
|
1679
|
+
completed: results.completed
|
|
1756
1680
|
};
|
|
1757
1681
|
const identity = {
|
|
1758
|
-
noteId:
|
|
1759
|
-
resultId,
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
blockContentId,
|
|
1682
|
+
noteId: session.noteId,
|
|
1683
|
+
resultId: session.id,
|
|
1684
|
+
blockId: session.blockId,
|
|
1685
|
+
blockVersion: session.blockVersion,
|
|
1686
|
+
blockContentId: session.blockContentId,
|
|
1763
1687
|
origin: session.origin,
|
|
1764
1688
|
pageId: session.pageId,
|
|
1765
1689
|
workoutTimestamp: session.createdAt
|
|
1766
1690
|
};
|
|
1767
|
-
const
|
|
1768
|
-
const
|
|
1769
|
-
await db.
|
|
1770
|
-
await
|
|
1771
|
-
|
|
1772
|
-
|
|
1691
|
+
const events = wodWikiWql.toEventRows(executionLog.logs, identity);
|
|
1692
|
+
const summaries = wodWikiWql.toSummaryEventRows(executionLog.logs, identity);
|
|
1693
|
+
await db.storage.transaction(["sessions", "events", "block_revisions"], "readwrite", async (tx) => {
|
|
1694
|
+
const saved = await tx.readonly("sessions").get(session.id);
|
|
1695
|
+
if (!saved || saved.noteId !== session.noteId || saved.blockId !== session.blockId || saved.blockVersion !== session.blockVersion) {
|
|
1696
|
+
throw new Error("[wod run] Captured Session source pin changed");
|
|
1697
|
+
}
|
|
1698
|
+
const revision = await tx.readonly("block_revisions").get([session.noteId, session.blockId, session.blockVersion]);
|
|
1699
|
+
if (!revision || !wodWikiStorage.rowsEqual(revision, started.revision)) throw new Error("[wod run] Captured revision is unavailable");
|
|
1700
|
+
await tx.readwrite("sessions").put(session);
|
|
1701
|
+
await tx.readwrite("events").putMany([...events, ...summaries]);
|
|
1773
1702
|
});
|
|
1774
|
-
return {
|
|
1775
|
-
noteId: noteState.noteId,
|
|
1776
|
-
resultId,
|
|
1777
|
-
session,
|
|
1778
|
-
events: eventRows.length,
|
|
1779
|
-
summaries: summaryRows.length,
|
|
1780
|
-
noteCreated: noteState.created
|
|
1781
|
-
};
|
|
1703
|
+
return { noteId: session.noteId, resultId: session.id, session, events: events.length, summaries: summaries.length, noteCreated: started.noteCreated };
|
|
1782
1704
|
}
|
|
1783
1705
|
async function persistSessionBundle(db, session, events) {
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
let noteCreated = false;
|
|
1787
|
-
if (!existing) {
|
|
1788
|
-
noteCreated = true;
|
|
1789
|
-
const now = session.createdAt || Date.now();
|
|
1790
|
-
await db.db.transaction(async (tx) => {
|
|
1791
|
-
await sql.putRow(tx, sql.storeDefinition("notes"), {
|
|
1792
|
-
id: session.noteId,
|
|
1793
|
-
title: `imported-${session.noteId.slice(0, 8)}`,
|
|
1794
|
-
createdAt: now,
|
|
1795
|
-
type: "note"
|
|
1796
|
-
});
|
|
1797
|
-
});
|
|
1706
|
+
if ("segmentId" in session || "segmentVersion" in session || "version" in session || "rawContent" in session || "data" in session || !session.noteId || !session.blockId || !Number.isSafeInteger(session.blockVersion) || session.blockVersion < 1) {
|
|
1707
|
+
throw new Error("[wod import] Session requires an exact block revision pin");
|
|
1798
1708
|
}
|
|
1799
|
-
await db.
|
|
1800
|
-
await
|
|
1801
|
-
|
|
1802
|
-
for (const
|
|
1709
|
+
await db.storage.transaction(["sessions", "events", "block_revisions"], "readwrite", async (tx) => {
|
|
1710
|
+
const revision = await tx.readonly("block_revisions").get([session.noteId, session.blockId, session.blockVersion]);
|
|
1711
|
+
if (!revision || revision.kind !== "wod") throw new Error("[wod import] Exact immutable workout revision is unavailable");
|
|
1712
|
+
for (const event of events) {
|
|
1713
|
+
if (event.resultId !== session.id || event.noteId !== session.noteId || event.blockId !== session.blockId || event.blockVersion !== session.blockVersion) {
|
|
1714
|
+
throw new Error("[wod import] Event source pin differs from Session source pin");
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
const existing = await tx.readonly("sessions").get(session.id);
|
|
1718
|
+
if (existing && !wodWikiStorage.rowsEqual(existing, session)) throw new Error("[wod import] Session identity conflicts with existing row");
|
|
1719
|
+
if (!existing) await tx.readwrite("sessions").put(session);
|
|
1720
|
+
for (const event of events) {
|
|
1721
|
+
const old = await tx.readonly("events").get(event.id);
|
|
1722
|
+
if (old && !wodWikiStorage.rowsEqual(old, event)) throw new Error("[wod import] Event identity conflicts with existing row");
|
|
1723
|
+
if (!old) await tx.readwrite("events").put(event);
|
|
1724
|
+
}
|
|
1803
1725
|
});
|
|
1804
|
-
return { events: events.length
|
|
1726
|
+
return { events: events.length };
|
|
1805
1727
|
}
|
|
1806
1728
|
async function persistEffort(db, effort) {
|
|
1807
1729
|
const sql = await loadSql();
|
|
@@ -1810,30 +1732,21 @@ async function persistEffort(db, effort) {
|
|
|
1810
1732
|
});
|
|
1811
1733
|
}
|
|
1812
1734
|
async function persistTypedTags(db, labels, type) {
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
ops.push({ op: "put", store: "tags", value: { id: randomId(), label, type, createdAt: now } });
|
|
1826
|
-
added += 1;
|
|
1827
|
-
} else if (!tag.type) {
|
|
1828
|
-
ops.push({ op: "put", store: "tags", value: { ...tag, type } });
|
|
1735
|
+
return db.storage.transaction(["tags"], "readwrite", async (tx) => {
|
|
1736
|
+
let added = 0;
|
|
1737
|
+
for (const raw of labels) {
|
|
1738
|
+
const label = raw.trim();
|
|
1739
|
+
if (!label) continue;
|
|
1740
|
+
const tag = (await tx.readonly("tags").getAllFromIndex("by-label", label))[0];
|
|
1741
|
+
if (!tag) {
|
|
1742
|
+
await tx.readwrite("tags").put({ id: uuid5(CLI_UUID_NAMESPACE, `tag:${label}`), label, type, createdAt: Date.now() });
|
|
1743
|
+
added++;
|
|
1744
|
+
} else if (!tag.type) {
|
|
1745
|
+
await tx.readwrite("tags").put({ ...tag, type });
|
|
1746
|
+
}
|
|
1829
1747
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
await db.db.transaction(async (tx) => {
|
|
1833
|
-
await sql.refreshDomain(tx, ops);
|
|
1834
|
-
});
|
|
1835
|
-
}
|
|
1836
|
-
return added;
|
|
1748
|
+
return added;
|
|
1749
|
+
});
|
|
1837
1750
|
}
|
|
1838
1751
|
var NOTE_ID_FRONTMATTER_KEY = "wod-note-id";
|
|
1839
1752
|
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;
|
|
@@ -1867,10 +1780,9 @@ function injectIdentityFrontmatter(markdown, identity) {
|
|
|
1867
1780
|
};
|
|
1868
1781
|
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
|
|
1869
1782
|
if (block) {
|
|
1783
|
+
const newline = markdown.startsWith("---\r\n") ? "\r\n" : "\n";
|
|
1870
1784
|
const lines2 = stamp(block[1].split(/\r?\n/));
|
|
1871
|
-
return
|
|
1872
|
-
${lines2.join("\n")}
|
|
1873
|
-
---${markdown.slice(block[0].length)}`;
|
|
1785
|
+
return `---${newline}${lines2.join(newline)}${newline}---${markdown.slice(block[0].length)}`;
|
|
1874
1786
|
}
|
|
1875
1787
|
const lines = stamp([]);
|
|
1876
1788
|
return `---
|
|
@@ -1879,35 +1791,8 @@ ${lines.join("\n")}
|
|
|
1879
1791
|
|
|
1880
1792
|
${markdown}`;
|
|
1881
1793
|
}
|
|
1882
|
-
function
|
|
1883
|
-
|
|
1884
|
-
if (segment.dataType === "wod") {
|
|
1885
|
-
const block = segment.data;
|
|
1886
|
-
const dialect = block?.dialect ?? "time";
|
|
1887
|
-
const fenceTag = block?.sport ? `${dialect}:${block.sport}` : dialect;
|
|
1888
|
-
return `\`\`\`${fenceTag}
|
|
1889
|
-
${segment.rawContent}
|
|
1890
|
-
\`\`\``;
|
|
1891
|
-
}
|
|
1892
|
-
if (segment.dataType === "frontmatter") {
|
|
1893
|
-
return `---
|
|
1894
|
-
${segment.rawContent}
|
|
1895
|
-
---`;
|
|
1896
|
-
}
|
|
1897
|
-
return segment.rawContent;
|
|
1898
|
-
}
|
|
1899
|
-
function composeNoteMarkdown(note, segments, version) {
|
|
1900
|
-
const pool = version === void 0 ? segments.filter((s) => s.isHistory !== true) : segments;
|
|
1901
|
-
const cap = version ?? Number.MAX_SAFE_INTEGER;
|
|
1902
|
-
const bySegmentId = {};
|
|
1903
|
-
for (const segment of pool) {
|
|
1904
|
-
if (segment.version > cap) continue;
|
|
1905
|
-
const current = bySegmentId[segment.id];
|
|
1906
|
-
if (!current || segment.version > current.version) bySegmentId[segment.id] = segment;
|
|
1907
|
-
}
|
|
1908
|
-
const picked = Object.values(bySegmentId).sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
|
|
1909
|
-
const markdown = picked.map(segmentToFragment).join("");
|
|
1910
|
-
return injectIdentityFrontmatter(markdown, { noteId: note.id, title: note.title });
|
|
1794
|
+
function composeNoteMarkdown(note) {
|
|
1795
|
+
return injectIdentityFrontmatter(note.rawContent, { noteId: note.id, title: note.title });
|
|
1911
1796
|
}
|
|
1912
1797
|
var FRONTMATTER_BLOCK_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
1913
1798
|
function readFrontmatterMeta(markdown) {
|
|
@@ -2030,9 +1915,8 @@ function csvToEquipmentLabels(csv) {
|
|
|
2030
1915
|
var SESSION_HEADER = [
|
|
2031
1916
|
"sessionId",
|
|
2032
1917
|
"noteId",
|
|
2033
|
-
"segmentId",
|
|
2034
|
-
"segmentVersion",
|
|
2035
1918
|
"blockId",
|
|
1919
|
+
"blockVersion",
|
|
2036
1920
|
"blockContentId",
|
|
2037
1921
|
"origin",
|
|
2038
1922
|
"status",
|
|
@@ -2040,7 +1924,8 @@ var SESSION_HEADER = [
|
|
|
2040
1924
|
"endTime",
|
|
2041
1925
|
"duration",
|
|
2042
1926
|
"completed",
|
|
2043
|
-
"sessionCreatedAt"
|
|
1927
|
+
"sessionCreatedAt",
|
|
1928
|
+
"sessionExtraJson"
|
|
2044
1929
|
];
|
|
2045
1930
|
var EVENT_HEADER = [
|
|
2046
1931
|
"eventId",
|
|
@@ -2049,8 +1934,8 @@ var EVENT_HEADER = [
|
|
|
2049
1934
|
"timestamp",
|
|
2050
1935
|
"pageId",
|
|
2051
1936
|
"effortSlug",
|
|
2052
|
-
"
|
|
2053
|
-
"
|
|
1937
|
+
"eventBlockId",
|
|
1938
|
+
"eventBlockVersion",
|
|
2054
1939
|
"eventBlockContentId",
|
|
2055
1940
|
"eventOrigin",
|
|
2056
1941
|
"timeSpan",
|
|
@@ -2106,9 +1991,8 @@ function sessionCells(session) {
|
|
|
2106
1991
|
return [
|
|
2107
1992
|
session.id,
|
|
2108
1993
|
session.noteId,
|
|
2109
|
-
session.segmentId,
|
|
2110
|
-
session.segmentVersion,
|
|
2111
1994
|
session.blockId,
|
|
1995
|
+
session.blockVersion,
|
|
2112
1996
|
session.blockContentId,
|
|
2113
1997
|
session.origin,
|
|
2114
1998
|
session.status,
|
|
@@ -2116,7 +2000,8 @@ function sessionCells(session) {
|
|
|
2116
2000
|
session.endTime,
|
|
2117
2001
|
session.duration,
|
|
2118
2002
|
session.completed,
|
|
2119
|
-
session.createdAt
|
|
2003
|
+
session.createdAt,
|
|
2004
|
+
JSON.stringify(session)
|
|
2120
2005
|
].map(escapeCsvField);
|
|
2121
2006
|
}
|
|
2122
2007
|
function metricCells(event, metric, index) {
|
|
@@ -2127,8 +2012,8 @@ function metricCells(event, metric, index) {
|
|
|
2127
2012
|
event.timestamp,
|
|
2128
2013
|
event.pageId,
|
|
2129
2014
|
event.effortSlug,
|
|
2130
|
-
event.
|
|
2131
|
-
event.
|
|
2015
|
+
event.blockId,
|
|
2016
|
+
event.blockVersion,
|
|
2132
2017
|
event.blockContentId,
|
|
2133
2018
|
event.origin,
|
|
2134
2019
|
event.timeSpan,
|
|
@@ -2239,6 +2124,9 @@ function csvToSessionRows(csv) {
|
|
|
2239
2124
|
const lines = splitCsvRecords(csv);
|
|
2240
2125
|
if (lines.length < 2) throw new Error("[wod import] Session CSV has no data rows");
|
|
2241
2126
|
const header = parseCsvLine(lines[0]);
|
|
2127
|
+
if (header.some((name) => /segment/i.test(name))) {
|
|
2128
|
+
throw new Error("[wod import] Incompatible Session CSV segment pins; blockId/blockVersion are required");
|
|
2129
|
+
}
|
|
2242
2130
|
const col = (name) => {
|
|
2243
2131
|
const idx = header.indexOf(name);
|
|
2244
2132
|
if (idx < 0) throw new Error(`[wod import] Session CSV missing column "${name}"`);
|
|
@@ -2248,13 +2136,29 @@ function csvToSessionRows(csv) {
|
|
|
2248
2136
|
const eventRows = /* @__PURE__ */ new Map();
|
|
2249
2137
|
let session;
|
|
2250
2138
|
const ensureSession = (cells) => {
|
|
2251
|
-
|
|
2139
|
+
const id = at(cells, "sessionId");
|
|
2140
|
+
const noteId = at(cells, "noteId");
|
|
2141
|
+
const blockId = at(cells, "blockId");
|
|
2142
|
+
const blockVersion = Number(at(cells, "blockVersion"));
|
|
2143
|
+
if (!id || !noteId || !blockId || !Number.isSafeInteger(blockVersion) || blockVersion < 1) {
|
|
2144
|
+
throw new Error("[wod import] Session CSV requires sessionId, noteId, blockId and positive blockVersion");
|
|
2145
|
+
}
|
|
2146
|
+
if (session) {
|
|
2147
|
+
if (session.id !== id || session.noteId !== noteId || session.blockId !== blockId || session.blockVersion !== blockVersion) {
|
|
2148
|
+
throw new Error("[wod import] Session CSV contains inconsistent source pins");
|
|
2149
|
+
}
|
|
2150
|
+
return session;
|
|
2151
|
+
}
|
|
2152
|
+
const extra = jsonCell(at(cells, "sessionExtraJson")) ?? {};
|
|
2153
|
+
if ("segmentId" in extra || "segmentVersion" in extra || "version" in extra || "rawContent" in extra || "data" in extra) {
|
|
2154
|
+
throw new Error("[wod import] Incompatible Session CSV source snapshot or segment pin");
|
|
2155
|
+
}
|
|
2252
2156
|
session = {
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2157
|
+
...extra,
|
|
2158
|
+
id,
|
|
2159
|
+
noteId,
|
|
2160
|
+
blockId,
|
|
2161
|
+
blockVersion,
|
|
2258
2162
|
blockContentId: at(cells, "blockContentId") || void 0,
|
|
2259
2163
|
origin: at(cells, "origin") || void 0,
|
|
2260
2164
|
status: at(cells, "status") || void 0,
|
|
@@ -2264,6 +2168,9 @@ function csvToSessionRows(csv) {
|
|
|
2264
2168
|
completed: at(cells, "completed") === "true",
|
|
2265
2169
|
createdAt: Number(at(cells, "sessionCreatedAt") || 0)
|
|
2266
2170
|
};
|
|
2171
|
+
if (![session.startTime, session.endTime, session.duration, session.createdAt].every(Number.isFinite)) {
|
|
2172
|
+
throw new Error("[wod import] Session CSV lifecycle values must be finite numbers");
|
|
2173
|
+
}
|
|
2267
2174
|
return session;
|
|
2268
2175
|
};
|
|
2269
2176
|
for (const line of lines.slice(1)) {
|
|
@@ -2275,6 +2182,14 @@ function csvToSessionRows(csv) {
|
|
|
2275
2182
|
let event = eventRows.get(eventId);
|
|
2276
2183
|
if (!event) {
|
|
2277
2184
|
const extra = jsonCell(at(cells, "eventExtraJson")) ?? {};
|
|
2185
|
+
if ("segmentId" in extra || "segmentVersion" in extra) {
|
|
2186
|
+
throw new Error("[wod import] Incompatible Session CSV event segment pins");
|
|
2187
|
+
}
|
|
2188
|
+
const blockId = nonempty(at(cells, "eventBlockId"));
|
|
2189
|
+
const blockVersion = num(at(cells, "eventBlockVersion"));
|
|
2190
|
+
if (blockId !== currentSession.blockId || blockVersion !== currentSession.blockVersion) {
|
|
2191
|
+
throw new Error("[wod import] Event source pin differs from Session source pin");
|
|
2192
|
+
}
|
|
2278
2193
|
event = {
|
|
2279
2194
|
...extra,
|
|
2280
2195
|
id: eventId,
|
|
@@ -2287,8 +2202,8 @@ function csvToSessionRows(csv) {
|
|
|
2287
2202
|
grain: nonempty(at(cells, "grain")) ?? extra.grain ?? "event",
|
|
2288
2203
|
effortSlug: nonempty(at(cells, "effortSlug")) ?? extra.effortSlug,
|
|
2289
2204
|
outputType: nonempty(at(cells, "outputType")) ?? extra.outputType ?? "event",
|
|
2290
|
-
|
|
2291
|
-
|
|
2205
|
+
blockId,
|
|
2206
|
+
blockVersion,
|
|
2292
2207
|
timeSpan: jsonCell(at(cells, "timeSpan")) ?? extra.timeSpan,
|
|
2293
2208
|
sourceBlockKey: nonempty(at(cells, "sourceBlockKey")) ?? extra.sourceBlockKey,
|
|
2294
2209
|
sourceStatementId: num(at(cells, "sourceStatementId")) ?? extra.sourceStatementId,
|
|
@@ -2299,9 +2214,8 @@ function csvToSessionRows(csv) {
|
|
|
2299
2214
|
completionReason: nonempty(at(cells, "completionReason")) ?? extra.completionReason,
|
|
2300
2215
|
representationKind: nonempty(at(cells, "representationKind")) ?? extra.representationKind,
|
|
2301
2216
|
metrics: [],
|
|
2302
|
-
//
|
|
2303
|
-
|
|
2304
|
-
metricTemporal: void 0
|
|
2217
|
+
// Rebuild anchors from metric rows while preserving an authored empty array.
|
|
2218
|
+
metricTemporal: Array.isArray(extra.metricTemporal) ? [] : void 0
|
|
2305
2219
|
};
|
|
2306
2220
|
eventRows.set(eventId, event);
|
|
2307
2221
|
}
|
|
@@ -2327,275 +2241,322 @@ function csvToSessionRows(csv) {
|
|
|
2327
2241
|
return { session, events: [...eventRows.values()] };
|
|
2328
2242
|
}
|
|
2329
2243
|
|
|
2330
|
-
// src/cli/
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
return
|
|
2344
|
-
}
|
|
2345
|
-
function
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
const
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
return route.replace(/^\//, "") || null;
|
|
2356
|
-
}
|
|
2357
|
-
function seedNoteSourceId(path, content, chunkId) {
|
|
2358
|
-
const parts = path.split("/");
|
|
2359
|
-
const stem = (parts[parts.length - 1] ?? "").replace(/\.md$/i, "");
|
|
2360
|
-
const under = parts.slice(2).map((p) => p.replace(/\.md$/i, "")).join("/");
|
|
2361
|
-
if (chunkId === CANVAS_CHUNK_ID) {
|
|
2362
|
-
const route = canvasRouteSlug(content);
|
|
2363
|
-
return route ? `guides:${route}` : `guides:${under}`;
|
|
2364
|
-
}
|
|
2365
|
-
if (chunkId === DASHBOARDS_CHUNK_ID) return "dashboards";
|
|
2366
|
-
if (chunkId === EFFORTS_CHUNK_ID) return "efforts";
|
|
2367
|
-
if (chunkId === TEMPLATE_CHUNK_ID) return "playground";
|
|
2368
|
-
if (chunkId.startsWith("collection.")) {
|
|
2369
|
-
const catalog = catalogForChunkId(chunkId);
|
|
2370
|
-
if (!catalog) return "collection:_root";
|
|
2371
|
-
return stem.toLowerCase() === "readme" ? `collection:${catalog}` : `collection:${catalog}/${stem}`;
|
|
2372
|
-
}
|
|
2373
|
-
if (chunkId.startsWith("feed.")) return `feed:feeds/${under}`;
|
|
2374
|
-
return void 0;
|
|
2375
|
-
}
|
|
2376
|
-
function buildNoteRecord(row, chunkId, seedVersion) {
|
|
2377
|
-
const id = seedNoteId(row.path);
|
|
2378
|
-
const createdAt = seedVersion;
|
|
2379
|
-
const meta = readFrontmatterMeta(row.content);
|
|
2380
|
-
const tags = extractTypedTags(row.content);
|
|
2381
|
-
let date;
|
|
2382
|
-
let page;
|
|
2383
|
-
let pageNote;
|
|
2384
|
-
const rawDate = scalarOf2(meta["date"]);
|
|
2385
|
-
if (rawDate && /^\d{4}-\d{2}-\d{2}$/.test(rawDate)) {
|
|
2386
|
-
date = (/* @__PURE__ */ new Date(`${rawDate}T12:00:00Z`)).getTime();
|
|
2387
|
-
}
|
|
2388
|
-
const slug = scalarOf2(meta["slug"]);
|
|
2389
|
-
if (slug) {
|
|
2390
|
-
const pageId = seedNoteId(`page:${slug}`);
|
|
2391
|
-
page = { id: pageId, slug, title: titleFromPath(row.path), createdAt };
|
|
2392
|
-
pageNote = { id: seedNoteId(`pagenote:${slug}:${row.path}`), pageId, noteId: id, createdAt };
|
|
2244
|
+
// src/cli/seedbuild/runtime.ts
|
|
2245
|
+
init_effort_file();
|
|
2246
|
+
init_contract();
|
|
2247
|
+
|
|
2248
|
+
// src/cli/seedbuild/validation.ts
|
|
2249
|
+
init_contract();
|
|
2250
|
+
function object(value) {
|
|
2251
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2252
|
+
}
|
|
2253
|
+
function positive(value) {
|
|
2254
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
2255
|
+
}
|
|
2256
|
+
function nonnegative(value) {
|
|
2257
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
2258
|
+
}
|
|
2259
|
+
function assertSeedManifest(value) {
|
|
2260
|
+
if (!object(value) || value.schema !== exports.SEED_SCHEMA) throw new Error("Incompatible seed schema; rebuild seed artifacts");
|
|
2261
|
+
if (!nonnegative(value.version) || typeof value.builtAt !== "string" || !Number.isFinite(Date.parse(value.builtAt)) || !Array.isArray(value.chunks)) throw new Error("seed manifest failed shape validation");
|
|
2262
|
+
for (const key of ["versionLabel", "commit"]) if (value[key] !== void 0 && typeof value[key] !== "string") throw new Error("invalid seed manifest metadata");
|
|
2263
|
+
if (value.dirty !== void 0 && typeof value.dirty !== "boolean") throw new Error("invalid seed manifest metadata");
|
|
2264
|
+
const ids = /* @__PURE__ */ new Set(), paths = /* @__PURE__ */ new Set();
|
|
2265
|
+
for (const chunk of value.chunks) {
|
|
2266
|
+
if (!object(chunk) || typeof chunk.id !== "string" || !chunk.id || typeof chunk.path !== "string" || !/^chunks\/[a-zA-Z0-9._-]+\.json$/.test(chunk.path) || typeof chunk.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(chunk.sha256) || !nonnegative(chunk.bytes) || !nonnegative(chunk.count) || chunk.kind !== void 0 && chunk.kind !== "notes" || ids.has(chunk.id) || paths.has(chunk.path)) throw new Error("seed manifest failed shape validation");
|
|
2267
|
+
ids.add(chunk.id);
|
|
2268
|
+
paths.add(chunk.path);
|
|
2393
2269
|
}
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2270
|
+
return value;
|
|
2271
|
+
}
|
|
2272
|
+
function assertSeedRows(value) {
|
|
2273
|
+
if (!Array.isArray(value)) throw new Error("seed chunk is not an array");
|
|
2274
|
+
const paths = /* @__PURE__ */ new Set();
|
|
2275
|
+
for (const row of value) {
|
|
2276
|
+
if (!object(row) || typeof row.path !== "string" || !row.path || row.path.startsWith("/") || row.path.split("/").includes("..") || paths.has(row.path) || typeof row.content !== "string" || !object(row.blocks) || !Array.isArray(row.blockIndex) || !Array.isArray(row.blockEfforts)) throw new Error("seed chunk row failed shape validation");
|
|
2277
|
+
paths.add(row.path);
|
|
2278
|
+
for (const [id, block] of Object.entries(row.blocks)) {
|
|
2279
|
+
if (!object(block) || block.id !== id || !id || block.version !== 1 || block.sourceRevision !== 1 || !nonnegative(block.from) || !positive(block.to) || block.from >= block.to || block.to > row.content.length || !["wod", "query", "widget", "embed"].includes(String(block.kind)) || (block.kind === "wod" ? !["time", "log"].includes(String(block.dialect)) : block.dialect !== void 0) || block.sport !== void 0 && typeof block.sport !== "string") throw new Error("invalid seed block range or revision");
|
|
2280
|
+
}
|
|
2281
|
+
wodWikiCore.validateNoteBlocks(row.content, row.blocks, 1, { registeredWidgets: exports.SEED_REGISTERED_WIDGETS });
|
|
2282
|
+
const indexed = /* @__PURE__ */ new Set();
|
|
2283
|
+
const sections = wodWikiCore.parseDocumentSections(row.content, { registeredWidgets: exports.SEED_REGISTERED_WIDGETS });
|
|
2284
|
+
for (const projection of row.blockIndex) {
|
|
2285
|
+
if (!object(projection) || typeof projection.blockId !== "string") throw new Error("invalid seed projection");
|
|
2286
|
+
const block = row.blocks[projection.blockId];
|
|
2287
|
+
if (!object(block) || indexed.has(projection.blockId) || typeof projection.id !== "string" || typeof projection.noteId !== "string" || projection.id !== `${projection.noteId}:${projection.blockId}` || projection.blockVersion !== 1 || projection.sourceRevision !== 1 || projection.from !== block.from || projection.to !== block.to || projection.dataType !== block.kind || projection.rawContent !== row.content.slice(Number(block.from), Number(block.to)) || typeof projection.noteTitle !== "string" || typeof projection.createdAt !== "number" || !Number.isFinite(projection.createdAt) || projection.sourcePath !== row.path || projection.isStatic !== true) throw new Error("orphan or incompatible seed projection");
|
|
2288
|
+
if ("segmentId" in projection || "segmentVersion" in projection) throw new Error("incompatible seed projection");
|
|
2289
|
+
for (const key of ["sourceId", "routeId", "blockContentId"]) if (projection[key] !== void 0 && typeof projection[key] !== "string") throw new Error("invalid seed projection metadata");
|
|
2290
|
+
if (projection.position !== void 0 && !nonnegative(projection.position)) throw new Error("invalid seed projection position");
|
|
2291
|
+
if (block.kind === "wod") {
|
|
2292
|
+
const section = sections.find((s) => s.from === block.from && s.to === block.to);
|
|
2293
|
+
if (projection.blockContentId !== wodWikiCore.blockContentId(section?.scriptBlock?.content ?? row.content.slice(Number(block.from), Number(block.to)))) throw new Error("invalid seed workout fingerprint");
|
|
2294
|
+
} else if (projection.blockContentId !== void 0) throw new Error("non-workout seed fingerprint");
|
|
2295
|
+
indexed.add(projection.blockId);
|
|
2296
|
+
}
|
|
2297
|
+
if (indexed.size !== Object.keys(row.blocks).length) throw new Error("missing actionable seed projection");
|
|
2298
|
+
const links = /* @__PURE__ */ new Set();
|
|
2299
|
+
for (const link of row.blockEfforts) {
|
|
2300
|
+
if (!object(link) || typeof link.blockId !== "string" || typeof link.noteId !== "string" || typeof link.effortSlug !== "string" || !link.effortSlug) throw new Error("invalid seed effort identity");
|
|
2301
|
+
const block = row.blocks[link.blockId];
|
|
2302
|
+
if (link.id !== `${link.noteId}:${link.blockId}:${link.effortSlug}` || links.has(String(link.id)) || !object(block) || block.kind !== "wod" || !indexed.has(link.blockId) || link.sourcePath !== row.path || link.isStatic !== true || typeof link.createdAt !== "number") throw new Error("orphan or incompatible seed effort link");
|
|
2303
|
+
const projection = row.blockIndex.find((p) => object(p) && p.blockId === link.blockId);
|
|
2304
|
+
if (!object(projection) || link.noteId !== projection.noteId || link.blockContentId !== projection.blockContentId || !Number.isFinite(link.createdAt)) throw new Error("invalid seed effort metadata");
|
|
2305
|
+
links.add(String(link.id));
|
|
2306
|
+
}
|
|
2397
2307
|
}
|
|
2398
|
-
|
|
2399
|
-
return {
|
|
2400
|
-
note: {
|
|
2401
|
-
id,
|
|
2402
|
-
title: metaTitle?.trim() || titleFromPath(row.path),
|
|
2403
|
-
date,
|
|
2404
|
-
createdAt,
|
|
2405
|
-
type: chunkId === DASHBOARDS_CHUNK_ID ? "dashboard" : "note",
|
|
2406
|
-
sourceId,
|
|
2407
|
-
catalog: catalogForChunkId(chunkId),
|
|
2408
|
-
seedOrigin: "seed",
|
|
2409
|
-
seedVersion,
|
|
2410
|
-
seedChunkId: chunkId,
|
|
2411
|
-
sourcePath: row.path
|
|
2412
|
-
},
|
|
2413
|
-
segment: {
|
|
2414
|
-
id: `seed:${id}`,
|
|
2415
|
-
version: 1,
|
|
2416
|
-
noteId: id,
|
|
2417
|
-
position: 0,
|
|
2418
|
-
dataType: "markdown",
|
|
2419
|
-
data: null,
|
|
2420
|
-
rawContent: row.content,
|
|
2421
|
-
createdAt
|
|
2422
|
-
},
|
|
2423
|
-
tags,
|
|
2424
|
-
page,
|
|
2425
|
-
pageNote,
|
|
2426
|
-
rawContent: row.content,
|
|
2427
|
-
sourcePath: row.path
|
|
2428
|
-
};
|
|
2308
|
+
return value;
|
|
2429
2309
|
}
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
claim: null
|
|
2441
|
-
};
|
|
2310
|
+
|
|
2311
|
+
// src/cli/seedbuild/runtime.ts
|
|
2312
|
+
var SEED_NAMESPACE = "1b671a64-40d5-491e-99b0-da01ff1f3341";
|
|
2313
|
+
async function seedNoteId(path) {
|
|
2314
|
+
const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(`${SEED_NAMESPACE}:${path}`));
|
|
2315
|
+
const bytes = new Uint8Array(digest.slice(0, 16));
|
|
2316
|
+
bytes[6] = bytes[6] & 15 | 80;
|
|
2317
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
2318
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
2319
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
2442
2320
|
}
|
|
2443
|
-
|
|
2444
|
-
if (
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
});
|
|
2321
|
+
function equal(a, b) {
|
|
2322
|
+
if (Object.is(a, b)) return true;
|
|
2323
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
|
|
2324
|
+
const ak = Object.keys(a).filter((key) => Reflect.get(a, key) !== void 0), bk = Object.keys(b).filter((key) => Reflect.get(b, key) !== void 0);
|
|
2325
|
+
return ak.length === bk.length && ak.every((key) => key in b && equal(Reflect.get(a, key), Reflect.get(b, key)));
|
|
2449
2326
|
}
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
const
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
);
|
|
2462
|
-
const
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
const
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
if (effort) {
|
|
2531
|
-
ops.push({ op: "put", store: "efforts", value: { ...effort, registrySource: "bundled" } });
|
|
2532
|
-
summary.efforts += 1;
|
|
2533
|
-
}
|
|
2534
|
-
}
|
|
2327
|
+
function titleFromPath(path) {
|
|
2328
|
+
return (path.split("/").at(-1) ?? path).replace(/\.md$/, "").replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2329
|
+
}
|
|
2330
|
+
function refreshedBlocks(existing, rawContent, sourceRevision) {
|
|
2331
|
+
const old = existing.rawContent;
|
|
2332
|
+
let from = 0, endOld = old.length, endNew = rawContent.length;
|
|
2333
|
+
while (from < endOld && from < endNew && old[from] === rawContent[from]) from++;
|
|
2334
|
+
while (endOld > from && endNew > from && old[endOld - 1] === rawContent[endNew - 1]) {
|
|
2335
|
+
endOld--;
|
|
2336
|
+
endNew--;
|
|
2337
|
+
}
|
|
2338
|
+
const intersect = Object.values(existing.blocks).filter((b) => from < b.to && endOld > b.from);
|
|
2339
|
+
const trusted = intersect.length <= 1 ? existing.blocks : Object.fromEntries(Object.entries(existing.blocks).filter(([, b]) => !intersect.includes(b)));
|
|
2340
|
+
const mapped = wodWikiCore.mapNoteBlockChanges(trusted, [{ from, to: endOld, insert: rawContent.slice(from, endNew) }]);
|
|
2341
|
+
let ordinal = 0;
|
|
2342
|
+
return wodWikiCore.reconcileNoteBlocks(rawContent, { sourceRevision, blocks: mapped, registeredWidgets: exports.SEED_REGISTERED_WIDGETS, createId: () => `seed:${existing.id}:r${sourceRevision}:${ordinal++}` });
|
|
2343
|
+
}
|
|
2344
|
+
async function putChanged(store, values, keys) {
|
|
2345
|
+
if (!values.length) return;
|
|
2346
|
+
const prior = await store.getMany(keys);
|
|
2347
|
+
const changed = values.filter((value, i) => !equal(value, prior[i]));
|
|
2348
|
+
if (changed.length) await store.putMany(changed);
|
|
2349
|
+
}
|
|
2350
|
+
async function applySeedChunk(storage, write) {
|
|
2351
|
+
assertSeedManifest(write.manifest);
|
|
2352
|
+
assertSeedRows(write.rows);
|
|
2353
|
+
const result = { totalNotes: 0, totalEfforts: 0, totalBlocks: 0, skippedUserOwned: 0, deleted: 0 };
|
|
2354
|
+
const ids = await Promise.all(write.rows.map((row) => seedNoteId(row.path)));
|
|
2355
|
+
const pagesBySlug = /* @__PURE__ */ new Map();
|
|
2356
|
+
const linkIds = /* @__PURE__ */ new Map();
|
|
2357
|
+
const tagIds = /* @__PURE__ */ new Map();
|
|
2358
|
+
for (const [i, row] of write.rows.entries()) {
|
|
2359
|
+
const slug = readFrontmatterMeta(row.content)["slug"];
|
|
2360
|
+
if (typeof slug === "string" && slug.trim()) {
|
|
2361
|
+
pagesBySlug.set(slug, await seedNoteId(`page:${slug}`));
|
|
2362
|
+
linkIds.set(ids[i], await seedNoteId(`pagenote:${slug}:${row.path}`));
|
|
2363
|
+
}
|
|
2364
|
+
for (const { label } of extractTypedTags(row.content)) {
|
|
2365
|
+
if (!tagIds.has(label)) tagIds.set(label, await seedNoteId(`tag:${label}`));
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
return storage.transaction(["notes", "block_revisions", "block_index", "block_efforts", "page", "page_notes", "tags", "note_tags", "efforts", "meta"], "readwrite", async (tx) => {
|
|
2369
|
+
const metaStore = tx.readwrite("meta");
|
|
2370
|
+
const actualMeta = await metaStore.get(write.meta.key);
|
|
2371
|
+
if (!equal(actualMeta, write.expectedMeta)) throw new Error("Seed checkpoint changed during import");
|
|
2372
|
+
if (write.meta.schema !== exports.SEED_SCHEMA || write.manifest.schema !== exports.SEED_SCHEMA) throw new Error("Incompatible seed schema; recreate artifacts");
|
|
2373
|
+
const noteStore = tx.readwrite("notes");
|
|
2374
|
+
const priorIds = write.chunkId ? write.expectedMeta?.chunks[write.chunkId]?.noteIds ?? [] : [];
|
|
2375
|
+
const declared = new Set(ids);
|
|
2376
|
+
const goneIds = priorIds.filter((id) => !declared.has(id));
|
|
2377
|
+
const allIds = [.../* @__PURE__ */ new Set([...ids, ...goneIds])];
|
|
2378
|
+
const stored = await noteStore.getMany(allIds);
|
|
2379
|
+
const existingById = new Map(allIds.map((id, i) => [id, stored[i]]));
|
|
2380
|
+
const ownedIds = allIds.filter((id) => {
|
|
2381
|
+
const note = existingById.get(id);
|
|
2382
|
+
return !note || note.seedOrigin === "seed" && (declared.has(id) || note.seedChunkId === write.chunkId);
|
|
2383
|
+
});
|
|
2384
|
+
const owned = new Set(ownedIds);
|
|
2385
|
+
const revisionStore = tx.readwrite("block_revisions");
|
|
2386
|
+
const historyIds = ids.filter((id, i) => owned.has(id) && existingById.get(id)?.rawContent !== write.rows[i].content);
|
|
2387
|
+
const histories = await revisionStore.getAllFromIndexMany("by-note", historyIds);
|
|
2388
|
+
const history = new Map(historyIds.map((id, i) => [id, histories[i]]));
|
|
2389
|
+
const indexStore = tx.readwrite("block_index"), effortLinks = tx.readwrite("block_efforts");
|
|
2390
|
+
const tagLinks = tx.readwrite("note_tags"), pageLinks = tx.readwrite("page_notes");
|
|
2391
|
+
const currentIndexes = await indexStore.getAllFromIndexMany("by-note", ownedIds);
|
|
2392
|
+
const currentEfforts = await effortLinks.getAllFromIndexMany("by-note", ownedIds);
|
|
2393
|
+
const currentTags = await tagLinks.getAllFromIndexMany("by-note", ownedIds);
|
|
2394
|
+
const currentPages = await pageLinks.getAllFromIndexMany("by-note", ownedIds);
|
|
2395
|
+
const slot = new Map(ownedIds.map((id, i) => [id, i]));
|
|
2396
|
+
const tagsStore = tx.readwrite("tags");
|
|
2397
|
+
const labels = [...tagIds.keys()];
|
|
2398
|
+
const matches = await tagsStore.getAllFromIndexMany("by-label", labels);
|
|
2399
|
+
const tagsByLabel = new Map(labels.map((label, i) => [label, matches[i][0]]));
|
|
2400
|
+
const changedTags = [], changedNotes = [], revisions = [], pages = [], pageNotes = [], noteTags = [];
|
|
2401
|
+
const blocks = [], blockEfforts = [];
|
|
2402
|
+
for (const [i, row] of write.rows.entries()) {
|
|
2403
|
+
const id = ids[i], existing = existingById.get(id);
|
|
2404
|
+
if (!owned.has(id)) {
|
|
2405
|
+
result.skippedUserOwned++;
|
|
2406
|
+
continue;
|
|
2535
2407
|
}
|
|
2536
|
-
|
|
2537
|
-
const
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2408
|
+
wodWikiCore.validateNoteBlocks(row.content, row.blocks, 1, { registeredWidgets: exports.SEED_REGISTERED_WIDGETS });
|
|
2409
|
+
const sourceRevision = existing ? existing.sourceRevision + (existing.rawContent === row.content ? 0 : 1) : 1;
|
|
2410
|
+
let ordinal = 0;
|
|
2411
|
+
const retainedHistory = history.get(id) ?? [];
|
|
2412
|
+
const map = !existing ? retainedHistory.length ? wodWikiCore.reconcileNoteBlocks(row.content, { sourceRevision, registeredWidgets: exports.SEED_REGISTERED_WIDGETS, createId: () => `seed:${id}:new${retainedHistory.length}:${ordinal++}` }) : structuredClone(row.blocks) : existing.rawContent === row.content ? structuredClone(existing.blocks) : refreshedBlocks(existing, row.content, sourceRevision);
|
|
2413
|
+
for (const block of Object.values(map)) {
|
|
2414
|
+
const saved = (history.get(id) ?? []).filter((r) => r.blockId === block.id).sort((a, b) => b.version - a.version)[0];
|
|
2415
|
+
const source = row.content.slice(block.from, block.to);
|
|
2416
|
+
if (existing?.rawContent === row.content) continue;
|
|
2417
|
+
const same = saved && saved.kind === block.kind && saved.rawContent === source && saved.dialect === block.dialect && saved.sport === block.sport;
|
|
2418
|
+
block.version = !existing ? 1 : same ? saved.version : (saved?.version ?? 0) + 1;
|
|
2419
|
+
if (!existing || !same) revisions.push({ noteId: id, blockId: block.id, version: block.version, kind: block.kind, rawContent: source, savedAt: write.meta.importedAt, ...block.dialect ? { dialect: block.dialect } : {}, ...block.sport ? { sport: block.sport } : {} });
|
|
2541
2420
|
}
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2421
|
+
wodWikiCore.validateNoteBlocks(row.content, map, sourceRevision, { registeredWidgets: exports.SEED_REGISTERED_WIDGETS });
|
|
2422
|
+
const fm = readFrontmatterMeta(row.content);
|
|
2423
|
+
const title = typeof fm["title"] === "string" && fm["title"].trim() ? fm["title"].trim() : titleFromPath(row.path);
|
|
2424
|
+
const sourceId = write.chunkId ? seedNoteSourceId(row.path, row.content, write.chunkId) : void 0;
|
|
2425
|
+
if (!sourceId) throw new Error(`Unknown seed source family ${write.chunkId}`);
|
|
2426
|
+
const date = typeof fm["date"] === "string" && /^\d{4}-\d{2}-\d{2}$/.test(fm["date"]) ? Date.parse(`${fm["date"]}T12:00:00Z`) : void 0;
|
|
2427
|
+
const note = { ...existing, id, title, rawContent: row.content, sourceRevision, blocks: map, createdAt: write.manifest.version, ...date === void 0 ? {} : { date }, type: write.chunkId === exports.DASHBOARDS_CHUNK_ID ? "dashboard" : "note", sourceId, catalog: catalogForChunkId(write.chunkId ?? ""), seedOrigin: "seed", seedVersion: write.manifest.version, seedChunkId: write.chunkId ?? void 0, sourcePath: row.path };
|
|
2428
|
+
if (date === void 0) delete note.date;
|
|
2429
|
+
if (!equal(note, existing)) changedNotes.push(note);
|
|
2430
|
+
const byRange = new Map(Object.values(map).map((b) => [`${b.from}:${b.to}`, b]));
|
|
2431
|
+
const compiledById = new Map(row.blockIndex.map((b) => [b.blockId, b]));
|
|
2432
|
+
const remap = /* @__PURE__ */ new Map();
|
|
2433
|
+
for (const compiled of row.blockIndex) {
|
|
2434
|
+
const block = byRange.get(`${compiled.from}:${compiled.to}`);
|
|
2435
|
+
if (!block || block.kind !== compiled.dataType) throw new Error("Orphan seed projection");
|
|
2436
|
+
remap.set(compiled.blockId, block.id);
|
|
2437
|
+
const projection = { ...compiled, id: `${id}:${block.id}`, noteId: id, blockId: block.id, blockVersion: block.version, sourceRevision, noteTitle: title, sourceId, sourcePath: row.path, rawContent: row.content.slice(block.from, block.to) };
|
|
2438
|
+
const prior = currentIndexes[slot.get(id)].find((b) => b.id === projection.id);
|
|
2439
|
+
if (!prior || prior.isStatic === true) blocks.push(projection);
|
|
2555
2440
|
}
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2441
|
+
for (const compiled of row.blockEfforts) {
|
|
2442
|
+
const blockId = remap.get(compiled.blockId);
|
|
2443
|
+
if (!blockId || compiledById.get(compiled.blockId)?.dataType !== "wod") throw new Error("Orphan seed effort link");
|
|
2444
|
+
const link = { ...compiled, id: `${id}:${blockId}:${compiled.effortSlug}`, noteId: id, blockId, sourcePath: row.path };
|
|
2445
|
+
const prior = currentEfforts[slot.get(id)].find((b) => b.id === link.id);
|
|
2446
|
+
if (!prior || prior.isStatic === true) blockEfforts.push(prior ? { ...link, createdAt: prior.createdAt } : link);
|
|
2447
|
+
}
|
|
2448
|
+
const n = slot.get(id);
|
|
2449
|
+
const wantedTags = /* @__PURE__ */ new Set();
|
|
2450
|
+
for (const item of extractTypedTags(row.content)) {
|
|
2451
|
+
let tag = tagsByLabel.get(item.label);
|
|
2452
|
+
if (!tag) {
|
|
2453
|
+
tag = { id: tagIds.get(item.label), label: item.label, type: item.type, createdAt: write.meta.importedAt };
|
|
2454
|
+
changedTags.push(tag);
|
|
2455
|
+
tagsByLabel.set(item.label, tag);
|
|
2456
|
+
} else if (item.type && !tag.type) {
|
|
2457
|
+
tag = { ...tag, type: item.type };
|
|
2458
|
+
changedTags.push(tag);
|
|
2459
|
+
tagsByLabel.set(item.label, tag);
|
|
2460
|
+
}
|
|
2461
|
+
if (wantedTags.has(tag.id)) continue;
|
|
2462
|
+
wantedTags.add(tag.id);
|
|
2463
|
+
const link = currentTags[n].find((l) => l.tagId === tag.id);
|
|
2464
|
+
if (!link) noteTags.push({ id: `${id}:${tag.id}`, noteId: id, tagId: tag.id });
|
|
2465
|
+
}
|
|
2466
|
+
for (const link of currentTags[n]) if (!wantedTags.has(link.tagId)) await tagLinks.delete(link.id);
|
|
2467
|
+
const slug = fm["slug"];
|
|
2468
|
+
if (typeof slug === "string" && pagesBySlug.has(slug)) {
|
|
2469
|
+
const pageId = pagesBySlug.get(slug);
|
|
2470
|
+
pages.push({ id: pageId, slug, title: titleFromPath(row.path), createdAt: write.manifest.version });
|
|
2471
|
+
pageNotes.push({ id: linkIds.get(id), pageId, noteId: id, createdAt: write.manifest.version });
|
|
2579
2472
|
}
|
|
2473
|
+
result.totalNotes++;
|
|
2474
|
+
result.totalBlocks += Object.keys(map).length;
|
|
2580
2475
|
}
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2476
|
+
const wantedBlocks = new Set(blocks.map((b) => b.id)), wantedEfforts = new Set(blockEfforts.map((b) => b.id));
|
|
2477
|
+
for (const rows of currentIndexes) for (const row of rows) if (row.isStatic === true && !wantedBlocks.has(row.id)) await indexStore.delete(row.id);
|
|
2478
|
+
for (const rows of currentEfforts) for (const row of rows) if (row.isStatic === true && !wantedEfforts.has(row.id)) await effortLinks.delete(row.id);
|
|
2479
|
+
for (const id of goneIds) {
|
|
2480
|
+
if (!owned.has(id) || !existingById.get(id)) continue;
|
|
2481
|
+
const n = slot.get(id);
|
|
2482
|
+
await noteStore.delete(id);
|
|
2483
|
+
for (const link of currentTags[n]) await tagLinks.delete(link.id);
|
|
2484
|
+
for (const link of currentPages[n]) await pageLinks.delete(link.id);
|
|
2485
|
+
result.deleted++;
|
|
2584
2486
|
}
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2487
|
+
const effortStore = tx.readwrite("efforts");
|
|
2488
|
+
if (write.chunkId === exports.EFFORTS_CHUNK_ID) {
|
|
2489
|
+
const efforts = write.rows.filter((_row, i) => owned.has(ids[i])).map((r) => parseEffortFile(r.content)).filter((e) => e !== null).map((e) => ({ ...e, registrySource: "bundled" }));
|
|
2490
|
+
const prior = await effortStore.getMany(efforts.map((e) => e.slug));
|
|
2491
|
+
const changed = efforts.filter((e, i) => (!prior[i] || prior[i]?.registrySource === "bundled") && !equal(e, prior[i]));
|
|
2492
|
+
if (changed.length) await effortStore.putMany(changed);
|
|
2493
|
+
result.totalEfforts = changed.length;
|
|
2494
|
+
const wantedSlugs = new Set(efforts.map((effort) => effort.slug));
|
|
2495
|
+
const removed = allIds.map((id) => {
|
|
2496
|
+
const note = existingById.get(id);
|
|
2497
|
+
return owned.has(id) && note ? parseEffortFile(note.rawContent)?.slug : void 0;
|
|
2498
|
+
}).filter((slug) => !!slug && !wantedSlugs.has(slug));
|
|
2499
|
+
const old = await effortStore.getMany(removed);
|
|
2500
|
+
for (const [i, effort] of old.entries()) if (effort?.registrySource === "bundled") await effortStore.delete(removed[i]);
|
|
2596
2501
|
}
|
|
2502
|
+
if (changedNotes.length) await noteStore.putMany(changedNotes);
|
|
2503
|
+
if (revisions.length) await revisionStore.putMany(revisions);
|
|
2504
|
+
await putChanged(indexStore, blocks, blocks.map((b) => b.id));
|
|
2505
|
+
await putChanged(effortLinks, blockEfforts, blockEfforts.map((b) => b.id));
|
|
2506
|
+
if (changedTags.length) await tagsStore.putMany([...new Map(changedTags.map((t) => [t.id, t])).values()]);
|
|
2507
|
+
if (noteTags.length) await tagLinks.putMany(noteTags);
|
|
2508
|
+
await putChanged(tx.readwrite("page"), pages, pages.map((p) => p.id));
|
|
2509
|
+
await putChanged(pageLinks, pageNotes, pageNotes.map((p) => p.id));
|
|
2510
|
+
await metaStore.put(write.meta);
|
|
2511
|
+
return result;
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
// src/cli/seed.ts
|
|
2516
|
+
init_contract();
|
|
2517
|
+
async function importSeedDir(db, dir, sourceId) {
|
|
2518
|
+
const fs = nodeFs(), path = nodePath();
|
|
2519
|
+
const source = sourceId || path.basename(dir);
|
|
2520
|
+
const manifest = assertSeedManifest(JSON.parse(fs.readFileSync(path.resolve(dir, "manifest.json"), "utf-8")));
|
|
2521
|
+
const key = seedMetaKey(source);
|
|
2522
|
+
let expected = await db.storage.readonly("meta").get(key);
|
|
2523
|
+
if (expected && expected.schema !== exports.SEED_SCHEMA) throw new Error("Incompatible seed checkpoint; recreate storage");
|
|
2524
|
+
let meta = expected ?? emptySeedMeta(key);
|
|
2525
|
+
if (manifest.version < meta.seedVersion) throw new Error("Seed manifest is older than stored checkpoint");
|
|
2526
|
+
const summary = { sourceId: source, version: manifest.version, notes: 0, efforts: 0, blocks: 0, blockEfforts: 0, deleted: 0, skippedUserOwned: 0 };
|
|
2527
|
+
const metadataChanged = manifest.version !== meta.seedVersion || manifest.builtAt !== meta.builtAt;
|
|
2528
|
+
const plan = manifest.chunks.filter((c) => metadataChanged || meta.chunks[c.id]?.sha256 !== c.sha256);
|
|
2529
|
+
plan.sort((a, b) => Number(b.id === exports.CANVAS_CHUNK_ID) - Number(a.id === exports.CANVAS_CHUNK_ID));
|
|
2530
|
+
const accumulate = (result) => {
|
|
2531
|
+
summary.notes += result.totalNotes;
|
|
2532
|
+
summary.efforts += result.totalEfforts;
|
|
2533
|
+
summary.blocks += result.totalBlocks;
|
|
2534
|
+
summary.deleted += result.deleted;
|
|
2535
|
+
summary.skippedUserOwned += result.skippedUserOwned;
|
|
2536
|
+
};
|
|
2537
|
+
for (const chunk of plan) {
|
|
2538
|
+
const rows = assertSeedRows(JSON.parse(fs.readFileSync(path.resolve(dir, chunk.path), "utf-8")));
|
|
2539
|
+
if (rows.length !== chunk.count) throw new Error("Seed chunk count mismatch");
|
|
2540
|
+
const next = { ...meta, chunks: { ...meta.chunks, [chunk.id]: { sha256: chunk.sha256, noteIds: await Promise.all(rows.map((r) => seedNoteId(r.path))) } }, importedAt: Date.now() };
|
|
2541
|
+
accumulate(await applySeedChunk(db.storage, { chunkId: chunk.id, rows, manifest, expectedMeta: expected, meta: next }));
|
|
2542
|
+
summary.blockEfforts += rows.reduce((count, row) => count + row.blockEfforts.length, 0);
|
|
2543
|
+
meta = next;
|
|
2544
|
+
expected = next;
|
|
2545
|
+
}
|
|
2546
|
+
for (const chunkId of Object.keys(meta.chunks)) {
|
|
2547
|
+
if (manifest.chunks.some((c) => c.id === chunkId)) continue;
|
|
2548
|
+
const chunks = { ...meta.chunks };
|
|
2549
|
+
delete chunks[chunkId];
|
|
2550
|
+
const next = { ...meta, chunks, importedAt: Date.now() };
|
|
2551
|
+
accumulate(await applySeedChunk(db.storage, { chunkId, rows: [], manifest, expectedMeta: expected, meta: next }));
|
|
2552
|
+
meta = next;
|
|
2553
|
+
expected = next;
|
|
2597
2554
|
}
|
|
2598
|
-
|
|
2555
|
+
if (plan.length || metadataChanged || Object.keys(meta.chunks).length !== Object.keys(expected?.chunks ?? {}).length) {
|
|
2556
|
+
const next = { ...meta, schema: exports.SEED_SCHEMA, seedVersion: manifest.version, builtAt: manifest.builtAt, importedAt: Date.now() };
|
|
2557
|
+
await applySeedChunk(db.storage, { chunkId: null, rows: [], manifest, expectedMeta: expected, meta: next });
|
|
2558
|
+
}
|
|
2559
|
+
return summary;
|
|
2599
2560
|
}
|
|
2600
2561
|
function defaultSeedDirs(cwd) {
|
|
2601
2562
|
const base = nodePath().resolve(cwd, "apps", "playground", "public", "seed");
|
|
@@ -2890,8 +2851,6 @@ ${escapeCsvField2(data.parsed.metric)},${data.scalar},${escapeCsvField2(data.uni
|
|
|
2890
2851
|
}
|
|
2891
2852
|
return lines.join("\n");
|
|
2892
2853
|
}
|
|
2893
|
-
|
|
2894
|
-
// src/cli/runner.ts
|
|
2895
2854
|
function getFs2() {
|
|
2896
2855
|
if (typeof globalThis.process?.versions?.node === "undefined" && typeof globalThis.Bun === "undefined") {
|
|
2897
2856
|
throw new Error("File system operations are only supported in Node / Bun environments");
|
|
@@ -2899,6 +2858,16 @@ function getFs2() {
|
|
|
2899
2858
|
return __require("fs");
|
|
2900
2859
|
}
|
|
2901
2860
|
var VERSION = "0.6.0";
|
|
2861
|
+
function cliNoteSource(source, label, sport) {
|
|
2862
|
+
if (/\.(?:md|markdown)$/i.test(label) || wodWikiCore.parseDocumentSections(source).some((section) => section.scriptBlock)) return source;
|
|
2863
|
+
let length = 3;
|
|
2864
|
+
for (const match of source.matchAll(/`+/g)) length = Math.max(length, match[0].length + 1);
|
|
2865
|
+
const fence = "`".repeat(length);
|
|
2866
|
+
return `${fence}time${sport ? `:${sport}` : ""}
|
|
2867
|
+
${source}
|
|
2868
|
+
${fence}
|
|
2869
|
+
`;
|
|
2870
|
+
}
|
|
2902
2871
|
function parseCliArgs(args) {
|
|
2903
2872
|
const result = {
|
|
2904
2873
|
format: "json",
|
|
@@ -2908,6 +2877,7 @@ function parseCliArgs(args) {
|
|
|
2908
2877
|
let i = 0;
|
|
2909
2878
|
while (i < args.length) {
|
|
2910
2879
|
const arg = args[i];
|
|
2880
|
+
if (arg === "--at" || arg.startsWith("--at=")) throw new Error("Unknown option --at; whole Note exports have no as-of version");
|
|
2911
2881
|
if (arg === "--help" || arg === "-h") {
|
|
2912
2882
|
result.help = true;
|
|
2913
2883
|
i++;
|
|
@@ -2966,12 +2936,6 @@ function parseCliArgs(args) {
|
|
|
2966
2936
|
} else if (arg.startsWith("--debug=")) {
|
|
2967
2937
|
result.debug = arg.slice("--debug=".length);
|
|
2968
2938
|
i++;
|
|
2969
|
-
} else if (arg === "--at") {
|
|
2970
|
-
result.at = args[++i];
|
|
2971
|
-
i++;
|
|
2972
|
-
} else if (arg.startsWith("--at=")) {
|
|
2973
|
-
result.at = arg.slice("--at=".length);
|
|
2974
|
-
i++;
|
|
2975
2939
|
} else if (arg === "--source") {
|
|
2976
2940
|
const source = args[++i];
|
|
2977
2941
|
if (source) result.sources.push(source);
|
|
@@ -3065,7 +3029,7 @@ USAGE:
|
|
|
3065
3029
|
|
|
3066
3030
|
SUBCOMMANDS:
|
|
3067
3031
|
parse <file | -> Parse Whiteboard Script, create a note in the database
|
|
3068
|
-
(
|
|
3032
|
+
(whole source + saved revisions + sparse block index), optionally emit parse-tree IR.
|
|
3069
3033
|
Markdown carrying a 'wod-note-id' frontmatter stamp
|
|
3070
3034
|
(see download) imports back into that same note.
|
|
3071
3035
|
run <file | -> Execute the script state machine, record a session +
|
|
@@ -3073,10 +3037,8 @@ SUBCOMMANDS:
|
|
|
3073
3037
|
execution-log IR
|
|
3074
3038
|
query '<wql>' Evaluate WQL against the database (or an explicit
|
|
3075
3039
|
dataset) and emit QueryResult / RowsQueryResult IR
|
|
3076
|
-
download note <id>
|
|
3077
|
-
|
|
3078
|
-
as-of a segment version (default: latest). The stamp
|
|
3079
|
-
in the frontmatter makes 'wod parse' re-import it.
|
|
3040
|
+
download note <id> Export the note's authored markdown to <id>.md.
|
|
3041
|
+
The frontmatter identity stamp allows re-import.
|
|
3080
3042
|
Frontmatter tags (general + typed equipment/quality/\u2026)
|
|
3081
3043
|
are materialized into the tag graph on parse/import.
|
|
3082
3044
|
download session <id>
|
|
@@ -3093,8 +3055,8 @@ SUBCOMMANDS:
|
|
|
3093
3055
|
import note <file> Re-import exported note markdown (same as parse with
|
|
3094
3056
|
the stamped identity).
|
|
3095
3057
|
import session <file>
|
|
3096
|
-
|
|
3097
|
-
|
|
3058
|
+
Import a session + its events from a downloaded CSV.
|
|
3059
|
+
The exact saved block revision must already exist.
|
|
3098
3060
|
import effort <file>
|
|
3099
3061
|
Upsert an effort registry row from corpus-style markdown.
|
|
3100
3062
|
import equipment <file>
|
|
@@ -3102,7 +3064,7 @@ SUBCOMMANDS:
|
|
|
3102
3064
|
build [dir] Compile a seed corpus: validate every .md under
|
|
3103
3065
|
<dir>/markdown (parity gate + dashboard format gate) and
|
|
3104
3066
|
write manifest.json + content-hashed chunks/ +
|
|
3105
|
-
|
|
3067
|
+
report.html to <dir>/dist with source/maps/projections bundled
|
|
3106
3068
|
(override with -o/--output). Version stamps derive from
|
|
3107
3069
|
<dir>'s git history (full checkout required).
|
|
3108
3070
|
|
|
@@ -3130,8 +3092,6 @@ OPTIONS:
|
|
|
3130
3092
|
--format <fmt> Output format: json (default), table, csv
|
|
3131
3093
|
-o, --output <path> Write the IR payload (or download) to a path instead of
|
|
3132
3094
|
the default; '-' writes to stdout
|
|
3133
|
-
--at <version> With 'download note': compose the markdown as-of the
|
|
3134
|
-
given segment version (default: latest)
|
|
3135
3095
|
--debug[=name] Also write the IR payload to <name> (default
|
|
3136
3096
|
wod-<subcommand>-debug.json); without it, parse/run
|
|
3137
3097
|
print a one-line summary of what was persisted
|
|
@@ -3160,7 +3120,13 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3160
3120
|
const writeOut = io.stdout ?? ((t) => process.stdout.write(t + "\n"));
|
|
3161
3121
|
const writeErr = io.stderr ?? ((t) => process.stderr.write(t + "\n"));
|
|
3162
3122
|
const getStdin = io.readStdinFn ?? readStdin;
|
|
3163
|
-
|
|
3123
|
+
let parsed;
|
|
3124
|
+
try {
|
|
3125
|
+
parsed = parseCliArgs(argv);
|
|
3126
|
+
} catch (error) {
|
|
3127
|
+
writeErr(error instanceof Error ? error.message : String(error));
|
|
3128
|
+
return 1;
|
|
3129
|
+
}
|
|
3164
3130
|
if (parsed.version || parsed.subcommand === "version") {
|
|
3165
3131
|
writeOut(`wod v${VERSION}`);
|
|
3166
3132
|
return 0;
|
|
@@ -3186,7 +3152,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3186
3152
|
}
|
|
3187
3153
|
}
|
|
3188
3154
|
const deliverOutput = (formattedText) => {
|
|
3189
|
-
if (parsed.output) {
|
|
3155
|
+
if (parsed.output && parsed.output !== "-") {
|
|
3190
3156
|
getFs2().writeFileSync(parsed.output, formattedText, "utf-8");
|
|
3191
3157
|
} else {
|
|
3192
3158
|
writeOut(formattedText);
|
|
@@ -3216,20 +3182,21 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3216
3182
|
}
|
|
3217
3183
|
source = fs.readFileSync(parsed.target, "utf-8");
|
|
3218
3184
|
}
|
|
3185
|
+
const noteSource = cliNoteSource(source, parsed.target, parsed.sport);
|
|
3219
3186
|
const ir = runParse(source, { sport: parsed.sport, sourceLabel: `cli:wod parse ${parsed.target}` });
|
|
3220
3187
|
const identity = extractNoteIdentity(source);
|
|
3221
3188
|
const tags = extractTypedTags(source);
|
|
3222
3189
|
const db = await openCliDb(parsed.db);
|
|
3223
3190
|
try {
|
|
3224
3191
|
const persisted = await persistNote(db, {
|
|
3225
|
-
source,
|
|
3192
|
+
source: noteSource,
|
|
3226
3193
|
sourceLabel: parsed.target,
|
|
3227
3194
|
noteId: identity?.noteId,
|
|
3228
3195
|
title: identity?.title,
|
|
3229
3196
|
tags
|
|
3230
3197
|
});
|
|
3231
3198
|
writeOut(
|
|
3232
|
-
`[wod parse] note ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.
|
|
3199
|
+
`[wod parse] note ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.blocks} blocks, ${persisted.efforts} efforts${persisted.tags > 0 ? `, ${persisted.tags} tags` : ""} \u2192 ${db.path}`
|
|
3233
3200
|
);
|
|
3234
3201
|
} finally {
|
|
3235
3202
|
await db.close();
|
|
@@ -3257,14 +3224,30 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3257
3224
|
}
|
|
3258
3225
|
source = fs.readFileSync(parsed.target, "utf-8");
|
|
3259
3226
|
}
|
|
3260
|
-
const
|
|
3227
|
+
const identity = extractNoteIdentity(source);
|
|
3261
3228
|
const db = await openCliDb(parsed.db);
|
|
3229
|
+
let ir;
|
|
3262
3230
|
try {
|
|
3263
|
-
const
|
|
3264
|
-
source,
|
|
3231
|
+
const started = await startRun(db, {
|
|
3232
|
+
source: cliNoteSource(source, parsed.target, parsed.sport),
|
|
3265
3233
|
sourceLabel: parsed.target,
|
|
3266
|
-
|
|
3234
|
+
noteId: identity?.noteId,
|
|
3235
|
+
title: identity?.title,
|
|
3236
|
+
tags: extractTypedTags(source)
|
|
3237
|
+
});
|
|
3238
|
+
const section = wodWikiCore.parseDocumentSections(started.revision.rawContent).find((row) => row.scriptBlock);
|
|
3239
|
+
if (!section?.scriptBlock) throw new Error("[wod run] Captured workout source cannot be parsed");
|
|
3240
|
+
const bare = !/\.(?:md|markdown)$/i.test(parsed.target) && !wodWikiCore.parseDocumentSections(source).some((row) => row.scriptBlock);
|
|
3241
|
+
const frozenBody = bare ? section.scriptBlock.content.slice(0, -1) : section.scriptBlock.content;
|
|
3242
|
+
ir = await runExecution(frozenBody, {
|
|
3243
|
+
sport: started.revision.sport,
|
|
3244
|
+
dialect: started.revision.dialect,
|
|
3245
|
+
blockId: started.session.blockId,
|
|
3246
|
+
blockVersion: started.session.blockVersion,
|
|
3247
|
+
startTime: started.session.startTime,
|
|
3248
|
+
sourceLabel: `cli:wod run ${parsed.target}`
|
|
3267
3249
|
});
|
|
3250
|
+
const persisted = await persistRun(db, started, ir.data);
|
|
3268
3251
|
writeOut(
|
|
3269
3252
|
`[wod run] session ${persisted.resultId} \u2014 ${persisted.events} events, ${persisted.summaries} summaries on note ${persisted.noteId} \u2192 ${db.path}`
|
|
3270
3253
|
);
|
|
@@ -3342,7 +3325,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3342
3325
|
const id = parsed.target2;
|
|
3343
3326
|
const DOWNLOAD_KINDS = ["note", "session", "effort", "equipment"];
|
|
3344
3327
|
if (!id && kind !== "equipment" || !DOWNLOAD_KINDS.includes(kind ?? "")) {
|
|
3345
|
-
writeErr("[wod download] Usage: wod download <note|session|effort|equipment> <id> [
|
|
3328
|
+
writeErr("[wod download] Usage: wod download <note|session|effort|equipment> <id> [-o <path>]");
|
|
3346
3329
|
return 1;
|
|
3347
3330
|
}
|
|
3348
3331
|
const targetId = id;
|
|
@@ -3354,21 +3337,14 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3354
3337
|
writeErr(`[wod download] Note not found in ${db.path}: ${id}`);
|
|
3355
3338
|
return 1;
|
|
3356
3339
|
}
|
|
3357
|
-
const
|
|
3358
|
-
|
|
3359
|
-
writeErr(`[wod download] Invalid --at version: ${parsed.at}`);
|
|
3360
|
-
return 1;
|
|
3361
|
-
}
|
|
3362
|
-
const markdown = composeNoteMarkdown(bundle.note, bundle.segments, at);
|
|
3363
|
-
const target = parsed.output && parsed.output !== "-" ? parsed.output : `${id}${at ? `.v${at}` : ""}.md`;
|
|
3340
|
+
const markdown = composeNoteMarkdown(bundle.note);
|
|
3341
|
+
const target = parsed.output && parsed.output !== "-" ? parsed.output : `${id}.md`;
|
|
3364
3342
|
if (parsed.output === "-") {
|
|
3365
3343
|
writeOut(markdown);
|
|
3366
3344
|
} else {
|
|
3367
3345
|
nodeFs().writeFileSync(target, markdown, "utf-8");
|
|
3368
3346
|
}
|
|
3369
|
-
writeOut(
|
|
3370
|
-
`[wod download] note ${id}${at ? ` v${at}` : ""} \u2192 ${target} (${bundle.segments.length} segment rows)`
|
|
3371
|
-
);
|
|
3347
|
+
if (parsed.output !== "-") writeOut(`[wod download] note ${id} \u2192 ${target}`);
|
|
3372
3348
|
} else if (kind === "session") {
|
|
3373
3349
|
const bundle = await getSessionBundle(db.db, targetId);
|
|
3374
3350
|
if (!bundle) {
|
|
@@ -3382,7 +3358,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3382
3358
|
} else {
|
|
3383
3359
|
nodeFs().writeFileSync(target, csv, "utf-8");
|
|
3384
3360
|
}
|
|
3385
|
-
writeOut(`[wod download] session ${id} \u2192 ${target} (${bundle.events.length} events)`);
|
|
3361
|
+
if (parsed.output !== "-") writeOut(`[wod download] session ${id} \u2192 ${target} (${bundle.events.length} events)`);
|
|
3386
3362
|
} else if (kind === "effort") {
|
|
3387
3363
|
const effort = await getEffortBySlug(db.db, targetId);
|
|
3388
3364
|
if (!effort) {
|
|
@@ -3440,7 +3416,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3440
3416
|
tags
|
|
3441
3417
|
});
|
|
3442
3418
|
writeOut(
|
|
3443
|
-
`[wod import] note ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.
|
|
3419
|
+
`[wod import] note ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.blocks} blocks${persisted.tags > 0 ? `, ${persisted.tags} tags` : ""} \u2192 ${db2.path}`
|
|
3444
3420
|
);
|
|
3445
3421
|
} finally {
|
|
3446
3422
|
await db2.close();
|
|
@@ -3482,7 +3458,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3482
3458
|
try {
|
|
3483
3459
|
const result = await persistSessionBundle(db, session, events);
|
|
3484
3460
|
writeOut(
|
|
3485
|
-
`[wod import] session ${session.id} \u2014 ${result.events} events
|
|
3461
|
+
`[wod import] session ${session.id} \u2014 ${result.events} events \u2192 ${db.path}`
|
|
3486
3462
|
);
|
|
3487
3463
|
} finally {
|
|
3488
3464
|
await db.close();
|
|
@@ -3508,7 +3484,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3508
3484
|
const totalBytes = result.manifest.chunks.reduce((s, c) => s + c.bytes, 0);
|
|
3509
3485
|
writeOut(`[wod build] schema v${result.manifest.schema} version ${result.manifest.version} (${result.manifest.versionLabel}) \u2192 ${result.outDir}`);
|
|
3510
3486
|
for (const c of result.manifest.chunks) {
|
|
3511
|
-
writeOut(` ${c.id.padEnd(48)} ${String(c.count).padStart(4)}
|
|
3487
|
+
writeOut(` ${c.id.padEnd(48)} ${String(c.count).padStart(4)} files ${kb(c.bytes).padStart(8)}`);
|
|
3512
3488
|
}
|
|
3513
3489
|
writeOut(`[wod build] ${result.manifest.chunks.length} chunks, ${result.totalFiles} files, ${kb(totalBytes)} total \u2014 scan ${result.passMs.scan}ms, block-index ${result.passMs.blockIndex}ms, block-efforts ${result.passMs.blockEfforts}ms, write ${result.passMs.write}ms`);
|
|
3514
3490
|
return 0;
|
|
@@ -3565,15 +3541,18 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3565
3541
|
}
|
|
3566
3542
|
|
|
3567
3543
|
// src/version.ts
|
|
3568
|
-
var VERSION2 = "0.35.
|
|
3569
|
-
var GIT_SHA = "
|
|
3570
|
-
var BUILD_TIME = "2026-10-
|
|
3544
|
+
var VERSION2 = "0.35.2117";
|
|
3545
|
+
var GIT_SHA = "03d1dfc";
|
|
3546
|
+
var BUILD_TIME = "2026-10-12T20:08:12.374Z";
|
|
3571
3547
|
var SEMVER = {
|
|
3572
|
-
version: "0.35.
|
|
3573
|
-
gitSha: "
|
|
3574
|
-
buildTime: "2026-10-
|
|
3548
|
+
version: "0.35.2117",
|
|
3549
|
+
gitSha: "03d1dfc",
|
|
3550
|
+
buildTime: "2026-10-12T20:08:12.374Z"
|
|
3575
3551
|
};
|
|
3576
3552
|
|
|
3553
|
+
// src/index.ts
|
|
3554
|
+
init_contract();
|
|
3555
|
+
|
|
3577
3556
|
Object.defineProperty(exports, "ABSENT", {
|
|
3578
3557
|
enumerable: true,
|
|
3579
3558
|
get: function () { return wodWikiLang.ABSENT; }
|
|
@@ -4471,8 +4450,13 @@ exports.SEMVER = SEMVER;
|
|
|
4471
4450
|
exports.SESSION_CSV_HEADER = SESSION_CSV_HEADER;
|
|
4472
4451
|
exports.VERSION = VERSION2;
|
|
4473
4452
|
exports.WqlSyntaxError = WqlSyntaxError;
|
|
4453
|
+
exports.applySeedChunk = applySeedChunk;
|
|
4454
|
+
exports.assertSeedManifest = assertSeedManifest;
|
|
4455
|
+
exports.assertSeedRows = assertSeedRows;
|
|
4474
4456
|
exports.buildStatementTree = buildStatementTree;
|
|
4475
4457
|
exports.buildWorkoutResults = buildWorkoutResults;
|
|
4458
|
+
exports.canvasRouteSlug = canvasRouteSlug;
|
|
4459
|
+
exports.catalogForChunkId = catalogForChunkId;
|
|
4476
4460
|
exports.cliMain = cliMain;
|
|
4477
4461
|
exports.composeNoteMarkdown = composeNoteMarkdown;
|
|
4478
4462
|
exports.createIRFile = createIRFile;
|
|
@@ -4480,6 +4464,7 @@ exports.csvToEquipmentLabels = csvToEquipmentLabels;
|
|
|
4480
4464
|
exports.csvToSessionRows = csvToSessionRows;
|
|
4481
4465
|
exports.defineLanguagePack = defineLanguagePack;
|
|
4482
4466
|
exports.effortToMarkdown = effortToMarkdown;
|
|
4467
|
+
exports.emptySeedMeta = emptySeedMeta;
|
|
4483
4468
|
exports.equipmentToCsv = equipmentToCsv;
|
|
4484
4469
|
exports.extractNoteIdentity = extractNoteIdentity;
|
|
4485
4470
|
exports.extractTypedTags = extractTypedTags;
|
|
@@ -4502,6 +4487,9 @@ exports.registerLanguagePack = registerLanguagePack;
|
|
|
4502
4487
|
exports.runExecution = runExecution;
|
|
4503
4488
|
exports.runParse = runParse;
|
|
4504
4489
|
exports.runQueryCli = runQueryCli;
|
|
4490
|
+
exports.seedMetaKey = seedMetaKey;
|
|
4491
|
+
exports.seedNoteId = seedNoteId;
|
|
4492
|
+
exports.seedNoteSourceId = seedNoteSourceId;
|
|
4505
4493
|
exports.sessionToCsv = sessionToCsv;
|
|
4506
4494
|
exports.splitCsvRecords = splitCsvRecords;
|
|
4507
4495
|
exports.statementToNode = statementToNode;
|