@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/bin/wod.cjs
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
var wodWikiLang = require('@bitcobblers/wod-wiki-lang');
|
|
5
|
+
var wodWikiCore = require('@bitcobblers/wod-wiki-core');
|
|
5
6
|
var child_process = require('child_process');
|
|
6
7
|
var fs = require('fs');
|
|
7
8
|
var path = require('path');
|
|
8
|
-
var wodWikiCore = require('@bitcobblers/wod-wiki-core');
|
|
9
9
|
var wodWikiWql = require('@bitcobblers/wod-wiki-wql');
|
|
10
|
-
var crypto = require('crypto');
|
|
10
|
+
var crypto$1 = require('crypto');
|
|
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,115 @@ 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: 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 === CANVAS_CHUNK_ID) return `guides:${canvasRouteSlug(content) ?? under}`;
|
|
191
|
+
if (chunkId === DASHBOARDS_CHUNK_ID) return "dashboards";
|
|
192
|
+
if (chunkId === EFFORTS_CHUNK_ID) return "efforts";
|
|
193
|
+
if (chunkId === 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
|
+
var SEED_SCHEMA, SEED_REGISTERED_WIDGETS, EFFORTS_CHUNK_ID, CANVAS_CHUNK_ID, DASHBOARDS_CHUNK_ID, TEMPLATE_CHUNK_ID;
|
|
202
|
+
var init_contract = __esm({
|
|
203
|
+
"src/cli/seedbuild/contract.ts"() {
|
|
204
|
+
SEED_SCHEMA = 8;
|
|
205
|
+
SEED_REGISTERED_WIDGETS = wodWikiCore.KNOWN_SECTION_WIDGETS;
|
|
206
|
+
EFFORTS_CHUNK_ID = "efforts";
|
|
207
|
+
CANVAS_CHUNK_ID = "canvas";
|
|
208
|
+
DASHBOARDS_CHUNK_ID = "dashboards";
|
|
209
|
+
TEMPLATE_CHUNK_ID = "template";
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// src/cli/seedbuild/effort-slugs.ts
|
|
214
|
+
function extractEffortSlugs2(statements) {
|
|
215
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
216
|
+
for (const statement of statements) {
|
|
217
|
+
if (statement.exerciseId) {
|
|
218
|
+
slugs.add(statement.exerciseId);
|
|
219
|
+
} else {
|
|
220
|
+
const effortMetric = statement.metrics?.getMetric?.("effort");
|
|
221
|
+
const effort = effortMetric && typeof effortMetric.value === "string" ? effortMetric.value : void 0;
|
|
222
|
+
if (effort) {
|
|
223
|
+
const slug = effort.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
224
|
+
if (slug) slugs.add(slug);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return Array.from(slugs);
|
|
229
|
+
}
|
|
230
|
+
var init_effort_slugs = __esm({
|
|
231
|
+
"src/cli/seedbuild/effort-slugs.ts"() {
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// src/cli/seedbuild/filename.ts
|
|
236
|
+
function fileToDisplayName(filename) {
|
|
237
|
+
const base = filename.replace(/\.md$/, "");
|
|
238
|
+
if (base.toUpperCase() === "README") return "Overview";
|
|
239
|
+
const cleaned = base.replace(/^day-\d+-/, "");
|
|
240
|
+
const tokens = cleaned.split(/[-_]/);
|
|
241
|
+
const titled = [];
|
|
242
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
243
|
+
const token = tokens[i];
|
|
244
|
+
const next = tokens[i + 1];
|
|
245
|
+
const after = tokens[i + 2];
|
|
246
|
+
const prev = tokens[i - 1];
|
|
247
|
+
if (/^\d{1,2}$/.test(token) && /^\d{1,2}$/.test(next ?? "") && !/^\d+$/.test(prev ?? "") && !/^\d+$/.test(after ?? "")) {
|
|
248
|
+
titled.push(`${token}.${next}`);
|
|
249
|
+
i++;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (titled.length > 0 && FILE_TITLE_STOP_WORDS[token.toLowerCase()]) {
|
|
253
|
+
titled.push(token.toLowerCase());
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
titled.push(token.charAt(0).toUpperCase() + token.slice(1));
|
|
257
|
+
}
|
|
258
|
+
return titled.join(" ");
|
|
259
|
+
}
|
|
260
|
+
var FILE_TITLE_STOP_WORDS;
|
|
261
|
+
var init_filename = __esm({
|
|
262
|
+
"src/cli/seedbuild/filename.ts"() {
|
|
263
|
+
FILE_TITLE_STOP_WORDS = {
|
|
264
|
+
and: true,
|
|
265
|
+
or: true,
|
|
266
|
+
of: true,
|
|
267
|
+
for: true,
|
|
268
|
+
to: true,
|
|
269
|
+
in: true,
|
|
270
|
+
on: true,
|
|
271
|
+
at: true,
|
|
272
|
+
by: true,
|
|
273
|
+
with: true,
|
|
274
|
+
the: true,
|
|
275
|
+
a: true,
|
|
276
|
+
an: true
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
});
|
|
257
280
|
function resolveGitVersion(repoRoot) {
|
|
258
281
|
try {
|
|
259
282
|
const run = (args) => child_process.execSync(`git ${args}`, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
@@ -346,9 +369,9 @@ var compiler_exports = {};
|
|
|
346
369
|
__export(compiler_exports, {
|
|
347
370
|
buildBlockEffortRows: () => buildBlockEffortRows,
|
|
348
371
|
buildBlockIndexRows: () => buildBlockIndexRows,
|
|
349
|
-
buildStaticParentMap: () => buildStaticParentMap,
|
|
350
372
|
chunkIdFor: () => chunkIdFor,
|
|
351
373
|
collectCorpusMarkdown: () => collectCorpusMarkdown,
|
|
374
|
+
compileSeedRow: () => compileSeedRow,
|
|
352
375
|
discoverUncatalogedEfforts: () => discoverUncatalogedEfforts,
|
|
353
376
|
feedDateToCreatedAt: () => feedDateToCreatedAt,
|
|
354
377
|
generateSeed: () => generateSeed,
|
|
@@ -432,7 +455,7 @@ function validateDashboardNote(raw) {
|
|
|
432
455
|
}
|
|
433
456
|
function gateDashboards(groups, corpusRoot) {
|
|
434
457
|
for (const group of groups) {
|
|
435
|
-
if (group.id !==
|
|
458
|
+
if (group.id !== DASHBOARDS_CHUNK_ID) continue;
|
|
436
459
|
for (const p of group.files) {
|
|
437
460
|
const problems = validateDashboardNote(fs.readFileSync(path.join(corpusRoot, p), "utf8"));
|
|
438
461
|
if (problems.length > 0) {
|
|
@@ -442,26 +465,6 @@ function gateDashboards(groups, corpusRoot) {
|
|
|
442
465
|
}
|
|
443
466
|
}
|
|
444
467
|
}
|
|
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
468
|
function getFileCreatedAt(file, repoRoot) {
|
|
466
469
|
try {
|
|
467
470
|
const out = child_process.execSync(`git log -1 --format=%ct -- '${file}'`, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
@@ -476,105 +479,32 @@ function feedDateToCreatedAt(dateKey) {
|
|
|
476
479
|
const ts = Date.parse(`${dateKey}T00:00:00Z`);
|
|
477
480
|
return Number.isNaN(ts) ? 0 : ts;
|
|
478
481
|
}
|
|
482
|
+
function compileSeedRow(path, content, metadata = {}) {
|
|
483
|
+
let ordinal = 0;
|
|
484
|
+
const blocks = wodWikiCore.reconcileNoteBlocks(content, { sourceRevision: 1, registeredWidgets: SEED_REGISTERED_WIDGETS, createId: () => `seed:${path}:${ordinal++}` });
|
|
485
|
+
const noteId = metadata.routeId ?? path;
|
|
486
|
+
const sections = wodWikiCore.parseDocumentSections(content);
|
|
487
|
+
const blockIndex = Object.values(blocks).map((block, position) => {
|
|
488
|
+
const section = sections.find((section2) => section2.from === block.from && section2.to === block.to);
|
|
489
|
+
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 };
|
|
490
|
+
});
|
|
491
|
+
return { path, content, blocks, blockIndex, blockEfforts: [] };
|
|
492
|
+
}
|
|
479
493
|
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
|
-
|
|
493
|
-
|
|
494
|
-
const sections = wodWikiCore.parseDocumentSections(content);
|
|
495
|
-
let position = 0;
|
|
496
|
-
for (const section of sections) {
|
|
497
|
-
const blockContentId = section.scriptBlock?.contentId ?? void 0;
|
|
498
|
-
index.push({
|
|
499
|
-
id: `static:${noteId}:${section.id}:1`,
|
|
500
|
-
noteId,
|
|
501
|
-
segmentId: section.id,
|
|
502
|
-
segmentVersion: 1,
|
|
503
|
-
position: position++,
|
|
504
|
-
dataType: toSegmentDataType2(section),
|
|
505
|
-
blockContentId,
|
|
506
|
-
rawContent: section.displayContent,
|
|
507
|
-
noteTitle,
|
|
508
|
-
createdAt,
|
|
509
|
-
isStatic: true,
|
|
510
|
-
sourceId: isReadme ? `collection:${dirName}` : `collection:${noteId}`,
|
|
511
|
-
sourcePath: rel
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
for (const file of listMarkdownFiles(path.join(markdownDir, "feeds"))) {
|
|
516
|
-
const rel = `markdown/feeds/${file}`;
|
|
517
|
-
const parts = rel.split("/");
|
|
518
|
-
if (parts.length < 5) continue;
|
|
519
|
-
const dirName = parts[parts.length - 3];
|
|
520
|
-
const dateKey = parts[parts.length - 2];
|
|
521
|
-
const fileNameExt = parts[parts.length - 1];
|
|
522
|
-
if (fileNameExt.toLowerCase() === "readme.md") continue;
|
|
523
|
-
const fileName = fileNameExt.replace(/\.md$/, "");
|
|
524
|
-
const noteId = `feeds/${dirName}/${dateKey}/${fileName}`;
|
|
525
|
-
const noteTitle = fileToDisplayName(fileNameExt);
|
|
526
|
-
const content = fs.readFileSync(path.join(corpusRoot, rel), "utf8");
|
|
527
|
-
const sections = wodWikiCore.parseDocumentSections(content);
|
|
528
|
-
let position = 0;
|
|
529
|
-
for (const section of sections) {
|
|
530
|
-
const blockContentId = section.scriptBlock?.contentId ?? void 0;
|
|
531
|
-
index.push({
|
|
532
|
-
id: `static:${noteId}:${section.id}:1`,
|
|
533
|
-
noteId,
|
|
534
|
-
segmentId: section.id,
|
|
535
|
-
segmentVersion: 1,
|
|
536
|
-
position: position++,
|
|
537
|
-
dataType: toSegmentDataType2(section),
|
|
538
|
-
blockContentId,
|
|
539
|
-
rawContent: section.displayContent,
|
|
540
|
-
noteTitle,
|
|
541
|
-
createdAt: feedDateToCreatedAt(dateKey),
|
|
542
|
-
isStatic: true,
|
|
543
|
-
sourceId: `feed:${noteId}`,
|
|
544
|
-
sourcePath: rel
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
for (const file of listMarkdownFiles(path.join(markdownDir, "canvas"))) {
|
|
549
|
-
const rel = `markdown/canvas/${file}`;
|
|
550
|
-
const content = fs.readFileSync(path.join(corpusRoot, rel), "utf8");
|
|
551
|
-
const noteId = canvasRouteSlug2(content);
|
|
552
|
-
if (!noteId) continue;
|
|
553
|
-
const fileNameExt = file.split("/").pop() ?? file;
|
|
554
|
-
const noteTitle = fileToDisplayName(fileNameExt);
|
|
555
|
-
const createdAt = getFileCreatedAt(rel, corpusRoot);
|
|
556
|
-
const sections = wodWikiCore.parseDocumentSections(content);
|
|
557
|
-
let position = 0;
|
|
558
|
-
for (const section of sections) {
|
|
559
|
-
const blockContentId = section.scriptBlock?.contentId ?? void 0;
|
|
560
|
-
index.push({
|
|
561
|
-
id: `static:${noteId}:${section.id}:1`,
|
|
562
|
-
noteId,
|
|
563
|
-
segmentId: section.id,
|
|
564
|
-
segmentVersion: 1,
|
|
565
|
-
position: position++,
|
|
566
|
-
dataType: toSegmentDataType2(section),
|
|
567
|
-
blockContentId,
|
|
568
|
-
rawContent: section.displayContent,
|
|
569
|
-
noteTitle,
|
|
570
|
-
createdAt,
|
|
571
|
-
isStatic: true,
|
|
572
|
-
sourceId: `guides:${noteId}`,
|
|
573
|
-
sourcePath: rel
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
return index;
|
|
494
|
+
const rows = [];
|
|
495
|
+
for (const path$1 of collectCorpusMarkdown(markdownDir)) {
|
|
496
|
+
const chunkId = chunkIdFor(path$1);
|
|
497
|
+
if (!chunkId) continue;
|
|
498
|
+
const content = fs.readFileSync(path.join(corpusRoot, path$1), "utf8");
|
|
499
|
+
const parts = path$1.split("/");
|
|
500
|
+
const stem = (parts.at(-1) ?? "").replace(/\.md$/, "");
|
|
501
|
+
const catalog = parts[2];
|
|
502
|
+
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;
|
|
503
|
+
const sourceId = seedNoteSourceId(path$1, content, chunkId);
|
|
504
|
+
const createdAt = chunkId.startsWith("feed.") ? feedDateToCreatedAt(parts.at(-2) ?? "") : getFileCreatedAt(path$1, corpusRoot);
|
|
505
|
+
rows.push(...compileSeedRow(path$1, content, { routeId, sourceId, createdAt }).blockIndex);
|
|
506
|
+
}
|
|
507
|
+
return rows;
|
|
578
508
|
}
|
|
579
509
|
function formatDiscoveredLabel(raw) {
|
|
580
510
|
const clean = raw.replace(/[-_]+/g, " ").trim();
|
|
@@ -605,7 +535,8 @@ function discoverUncatalogedEfforts(markdownDir, corpusRoot) {
|
|
|
605
535
|
const discovered = /* @__PURE__ */ new Map();
|
|
606
536
|
for (const block of blocks) {
|
|
607
537
|
if (block.dataType !== "wod" || !block.rawContent) continue;
|
|
608
|
-
const
|
|
538
|
+
const section = wodWikiCore.parseDocumentSections(block.rawContent, { registeredWidgets: SEED_REGISTERED_WIDGETS }).find((section2) => section2.scriptBlock);
|
|
539
|
+
const script = wodWikiLang.parseScript(section?.scriptBlock?.content ?? block.rawContent);
|
|
609
540
|
for (const statement of script.statements) {
|
|
610
541
|
const effortMetric = statement.metrics?.getMetric?.("effort");
|
|
611
542
|
const effortLabel = (effortMetric && typeof effortMetric.value === "string" ? effortMetric.value : void 0) ?? statement.exerciseId;
|
|
@@ -661,24 +592,27 @@ ${aliases.join("\n")}` : "aliases: []",
|
|
|
661
592
|
}
|
|
662
593
|
return createdPaths;
|
|
663
594
|
}
|
|
664
|
-
function buildBlockEffortRows(markdownDir, corpusRoot) {
|
|
595
|
+
function buildBlockEffortRows(markdownDir, corpusRoot, blocks = buildBlockIndexRows(markdownDir, corpusRoot)) {
|
|
665
596
|
const efforts = [];
|
|
666
597
|
const { aliasToCanonical } = loadExistingEffortMap(markdownDir);
|
|
667
|
-
const blocks = buildBlockIndexRows(markdownDir, corpusRoot);
|
|
668
598
|
for (const block of blocks) {
|
|
669
599
|
if (block.dataType !== "wod" || !block.rawContent) continue;
|
|
670
|
-
const
|
|
600
|
+
const section = wodWikiCore.parseDocumentSections(block.rawContent, { registeredWidgets: SEED_REGISTERED_WIDGETS }).find((section2) => section2.scriptBlock);
|
|
601
|
+
const script = wodWikiLang.parseScript(section?.scriptBlock?.content ?? block.rawContent);
|
|
671
602
|
const effortSlugs = extractEffortSlugs2(script.statements);
|
|
603
|
+
const linked = /* @__PURE__ */ new Set();
|
|
672
604
|
for (const rawSlug of effortSlugs) {
|
|
673
605
|
const norm = rawSlug.toLowerCase().trim();
|
|
674
606
|
const canonicalSlug = aliasToCanonical.get(norm) ?? aliasToCanonical.get(norm.replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "")) ?? norm.replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
675
607
|
if (!canonicalSlug || NON_EFFORT_SLUGS[canonicalSlug] || NON_EFFORT_SLUGS[norm]) {
|
|
676
608
|
continue;
|
|
677
609
|
}
|
|
610
|
+
if (linked.has(canonicalSlug)) continue;
|
|
611
|
+
linked.add(canonicalSlug);
|
|
678
612
|
efforts.push({
|
|
679
613
|
id: `${block.id}:${canonicalSlug}`,
|
|
680
614
|
noteId: block.noteId,
|
|
681
|
-
blockId: block.
|
|
615
|
+
blockId: block.blockId,
|
|
682
616
|
blockContentId: block.blockContentId,
|
|
683
617
|
effortSlug: canonicalSlug,
|
|
684
618
|
isStatic: block.isStatic,
|
|
@@ -689,22 +623,8 @@ function buildBlockEffortRows(markdownDir, corpusRoot) {
|
|
|
689
623
|
}
|
|
690
624
|
return efforts;
|
|
691
625
|
}
|
|
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
626
|
function sha256Hex(data) {
|
|
707
|
-
return crypto.createHash("sha256").update(data).digest("hex");
|
|
627
|
+
return crypto$1.createHash("sha256").update(data).digest("hex");
|
|
708
628
|
}
|
|
709
629
|
function generateSeed(options = {}) {
|
|
710
630
|
const t0 = Date.now();
|
|
@@ -725,21 +645,42 @@ function generateSeed(options = {}) {
|
|
|
725
645
|
gateDashboards(groups, corpusRoot);
|
|
726
646
|
if (options.templateFile && fs.existsSync(options.templateFile)) {
|
|
727
647
|
const templateRowPath = options.templateRowPath ?? path.relative(corpusRoot, options.templateFile).split("\\").join("/");
|
|
728
|
-
groups.push({ id:
|
|
648
|
+
groups.push({ id: TEMPLATE_CHUNK_ID, files: [templateRowPath] });
|
|
729
649
|
}
|
|
730
650
|
passMs.scan = Date.now() - tScan;
|
|
731
651
|
const chunksDir = path.join(outDir, "chunks");
|
|
732
652
|
fs.rmSync(outDir, { recursive: true, force: true });
|
|
733
653
|
fs.mkdirSync(chunksDir, { recursive: true });
|
|
734
654
|
const manifestChunks = [];
|
|
655
|
+
const tBlockIndex = Date.now();
|
|
656
|
+
const blockRows = buildBlockIndexRows(markdownDir, corpusRoot);
|
|
657
|
+
passMs.blockIndex = Date.now() - tBlockIndex;
|
|
658
|
+
const tBlockEfforts = Date.now();
|
|
659
|
+
const effortRows = buildBlockEffortRows(markdownDir, corpusRoot, blockRows);
|
|
660
|
+
passMs.blockEfforts = Date.now() - tBlockEfforts;
|
|
661
|
+
const blocksByPath = /* @__PURE__ */ new Map();
|
|
662
|
+
const effortsByPath = /* @__PURE__ */ new Map();
|
|
663
|
+
for (const row of blockRows) {
|
|
664
|
+
const rows = blocksByPath.get(row.sourcePath) ?? [];
|
|
665
|
+
rows.push(row);
|
|
666
|
+
blocksByPath.set(row.sourcePath, rows);
|
|
667
|
+
}
|
|
668
|
+
for (const row of effortRows) {
|
|
669
|
+
const rows = effortsByPath.get(row.sourcePath) ?? [];
|
|
670
|
+
rows.push(row);
|
|
671
|
+
effortsByPath.set(row.sourcePath, rows);
|
|
672
|
+
}
|
|
735
673
|
let totalFiles = 0;
|
|
736
674
|
const tWrite = Date.now();
|
|
737
675
|
for (const { id, files } of groups) {
|
|
738
|
-
const templateSource = id ===
|
|
739
|
-
const rows = files.map((p) =>
|
|
740
|
-
path
|
|
741
|
-
|
|
742
|
-
|
|
676
|
+
const templateSource = id === TEMPLATE_CHUNK_ID ? options.templateFile : void 0;
|
|
677
|
+
const rows = files.map((p) => {
|
|
678
|
+
const content = fs.readFileSync(templateSource ?? path.join(corpusRoot, p), "utf8");
|
|
679
|
+
const compiled = compileSeedRow(p, content);
|
|
680
|
+
compiled.blockIndex = blocksByPath.get(p) ?? compiled.blockIndex;
|
|
681
|
+
compiled.blockEfforts = effortsByPath.get(p) ?? [];
|
|
682
|
+
return compiled;
|
|
683
|
+
});
|
|
743
684
|
const bytes = Buffer.from(JSON.stringify(rows), "utf8");
|
|
744
685
|
const sha = sha256Hex(bytes);
|
|
745
686
|
const file = `chunks/${id}.${sha.slice(0, 8)}.json`;
|
|
@@ -747,39 +688,9 @@ function generateSeed(options = {}) {
|
|
|
747
688
|
manifestChunks.push({ id, path: file, sha256: sha, bytes: bytes.length, count: rows.length });
|
|
748
689
|
totalFiles += rows.length;
|
|
749
690
|
}
|
|
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
691
|
passMs.write = Date.now() - tWrite;
|
|
781
692
|
const manifest = {
|
|
782
|
-
schema:
|
|
693
|
+
schema: SEED_SCHEMA,
|
|
783
694
|
version: gitVersion.version,
|
|
784
695
|
versionLabel: gitVersion.versionLabel,
|
|
785
696
|
commit: gitVersion.commit,
|
|
@@ -795,7 +706,7 @@ function generateSeed(options = {}) {
|
|
|
795
706
|
fileCount: paths.length});
|
|
796
707
|
return { manifest, outDir, totalFiles, reportPath, passMs };
|
|
797
708
|
}
|
|
798
|
-
var
|
|
709
|
+
var NON_EFFORT_SLUGS;
|
|
799
710
|
var init_compiler = __esm({
|
|
800
711
|
"src/cli/seedbuild/compiler.ts"() {
|
|
801
712
|
init_contract();
|
|
@@ -805,7 +716,6 @@ var init_compiler = __esm({
|
|
|
805
716
|
init_git_version();
|
|
806
717
|
init_report();
|
|
807
718
|
init_frontmatter();
|
|
808
|
-
BLOCK_INDEX_ROWS_PER_CHUNK = 4e3;
|
|
809
719
|
NON_EFFORT_SLUGS = {
|
|
810
720
|
"amrap": true,
|
|
811
721
|
"emom": true,
|
|
@@ -1038,14 +948,14 @@ async function runExecution(source, options = {}) {
|
|
|
1038
948
|
now
|
|
1039
949
|
);
|
|
1040
950
|
const { engine, analyticsContext } = wodWikiLang.createAnalyticsEngineForBlock({
|
|
1041
|
-
id: "cli-block",
|
|
951
|
+
id: options.blockId ?? "cli-block",
|
|
1042
952
|
content: source,
|
|
1043
953
|
statements: script.statements,
|
|
1044
954
|
sport: options.sport,
|
|
1045
|
-
version: 1,
|
|
955
|
+
version: options.blockVersion ?? 1,
|
|
1046
956
|
startLine: 0,
|
|
1047
957
|
endLine: 0,
|
|
1048
|
-
dialect: "time",
|
|
958
|
+
dialect: options.dialect ?? "time",
|
|
1049
959
|
state: "idle",
|
|
1050
960
|
createdAt: Date.now()
|
|
1051
961
|
});
|
|
@@ -1075,7 +985,8 @@ async function runExecution(source, options = {}) {
|
|
|
1075
985
|
{
|
|
1076
986
|
results: workoutResults,
|
|
1077
987
|
logs,
|
|
1078
|
-
statements: logs
|
|
988
|
+
statements: logs,
|
|
989
|
+
sourceScript: source
|
|
1079
990
|
},
|
|
1080
991
|
{ source: options.sourceLabel ?? "cli:wod run" }
|
|
1081
992
|
);
|
|
@@ -1088,7 +999,7 @@ function inMemoryEventStore(events) {
|
|
|
1088
999
|
getEventsByTimeRange: async (start, end) => rows.filter((r) => r.timestamp >= start && r.timestamp <= end),
|
|
1089
1000
|
getEventsByResult: async (resultId) => rows.filter((r) => r.resultId === resultId),
|
|
1090
1001
|
getEventsForNote: async (noteId) => rows.filter((r) => r.noteId === noteId),
|
|
1091
|
-
getEventsByContent: async (
|
|
1002
|
+
getEventsByContent: async (blockContentId5) => rows.filter((r) => r.blockContentId === blockContentId5),
|
|
1092
1003
|
scanAll: async () => rows,
|
|
1093
1004
|
appendEvents: async (appended) => {
|
|
1094
1005
|
rows.push(...appended);
|
|
@@ -1124,10 +1035,6 @@ var WqlSyntaxError = class extends Error {
|
|
|
1124
1035
|
query;
|
|
1125
1036
|
syntaxError;
|
|
1126
1037
|
};
|
|
1127
|
-
function legacyLogsOf(result) {
|
|
1128
|
-
const legacy = result;
|
|
1129
|
-
return legacy.data?.logs ?? [];
|
|
1130
|
-
}
|
|
1131
1038
|
function extractPayload(rawJson) {
|
|
1132
1039
|
try {
|
|
1133
1040
|
const parsed = JSON.parse(rawJson);
|
|
@@ -1140,16 +1047,6 @@ function extractPayload(rawJson) {
|
|
|
1140
1047
|
}
|
|
1141
1048
|
}
|
|
1142
1049
|
function buildStoresFromData(data) {
|
|
1143
|
-
const identity = (r) => ({
|
|
1144
|
-
noteId: r.noteId,
|
|
1145
|
-
resultId: r.id,
|
|
1146
|
-
segmentId: r.segmentId,
|
|
1147
|
-
segmentVersion: r.segmentVersion,
|
|
1148
|
-
blockContentId: r.blockContentId,
|
|
1149
|
-
origin: r.origin,
|
|
1150
|
-
pageId: r.pageId,
|
|
1151
|
-
workoutTimestamp: r.createdAt
|
|
1152
|
-
});
|
|
1153
1050
|
const events = [
|
|
1154
1051
|
...data.eventRecords,
|
|
1155
1052
|
...data.facts.map((f, i) => ({
|
|
@@ -1169,17 +1066,17 @@ function buildStoresFromData(data) {
|
|
|
1169
1066
|
...f.unit ? { unit: f.unit } : {},
|
|
1170
1067
|
origin: "engine"
|
|
1171
1068
|
}],
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
}))
|
|
1175
|
-
...data.results.flatMap((r) => {
|
|
1176
|
-
const logs = legacyLogsOf(r);
|
|
1177
|
-
return [...wodWikiWql.toEventRows(logs, identity(r)), ...wodWikiWql.toSummaryEventRows(logs, identity(r))];
|
|
1178
|
-
})
|
|
1069
|
+
blockId: f.blockId,
|
|
1070
|
+
blockVersion: f.blockVersion
|
|
1071
|
+
}))
|
|
1179
1072
|
];
|
|
1180
1073
|
const eventStore = inMemoryEventStore(events);
|
|
1181
1074
|
const noteStore = {
|
|
1182
1075
|
getAllNotes: async () => [...data.notes],
|
|
1076
|
+
getNotesByIds: async (ids) => {
|
|
1077
|
+
const selected = new Set(ids);
|
|
1078
|
+
return data.notes.filter((note) => selected.has(note.id));
|
|
1079
|
+
},
|
|
1183
1080
|
getNoteIdsForTag: async (tag) => {
|
|
1184
1081
|
const matching = /* @__PURE__ */ new Set();
|
|
1185
1082
|
for (const [noteId, tags] of Object.entries(data.noteTags)) {
|
|
@@ -1204,6 +1101,7 @@ function loadQueryData(options) {
|
|
|
1204
1101
|
results: [],
|
|
1205
1102
|
notes: [],
|
|
1206
1103
|
blocks: [],
|
|
1104
|
+
revisions: [],
|
|
1207
1105
|
efforts: [...wodWikiLang.bundledEfforts],
|
|
1208
1106
|
noteTags: {}
|
|
1209
1107
|
};
|
|
@@ -1218,7 +1116,10 @@ function loadQueryData(options) {
|
|
|
1218
1116
|
data.facts = payload;
|
|
1219
1117
|
} else if (payload && typeof payload === "object") {
|
|
1220
1118
|
const obj = payload;
|
|
1119
|
+
if ("segments" in obj) throw new Error("Incompatible corpus segments; whole Notes and block revisions are required");
|
|
1221
1120
|
if (obj.kind === "event-journal" || Array.isArray(obj.records)) {
|
|
1121
|
+
if (Array.isArray(obj.blockRevisions)) data.revisions = obj.blockRevisions;
|
|
1122
|
+
if (Array.isArray(obj.sessions)) data.results = obj.sessions;
|
|
1222
1123
|
if (Array.isArray(obj.records)) {
|
|
1223
1124
|
data.eventRecords = obj.records;
|
|
1224
1125
|
}
|
|
@@ -1237,20 +1138,16 @@ function loadQueryData(options) {
|
|
|
1237
1138
|
if (corpus.notes) data.notes = corpus.notes;
|
|
1238
1139
|
if (corpus.blocks) data.blocks = corpus.blocks;
|
|
1239
1140
|
if (corpus.efforts) data.efforts = corpus.efforts;
|
|
1141
|
+
if (corpus.revisions) data.revisions = corpus.revisions;
|
|
1240
1142
|
if (corpus.tags) data.noteTags = corpus.tags;
|
|
1241
|
-
if (corpus.logs
|
|
1242
|
-
const
|
|
1243
|
-
id: "corpus-result-1",
|
|
1143
|
+
if (corpus.logs) {
|
|
1144
|
+
const identity = {
|
|
1244
1145
|
noteId: "corpus-note-1",
|
|
1245
|
-
|
|
1146
|
+
resultId: "corpus-result-1",
|
|
1246
1147
|
origin: "journal",
|
|
1247
|
-
|
|
1248
|
-
startTime: corpus.logs[0]?.timeSpan?.started ?? Date.now(),
|
|
1249
|
-
endTime: corpus.logs[corpus.logs.length - 1]?.timeSpan?.ended ?? Date.now(),
|
|
1250
|
-
duration: 0,
|
|
1251
|
-
completed: true
|
|
1148
|
+
workoutTimestamp: corpus.logs[0]?.timeSpan?.started ?? Date.now()
|
|
1252
1149
|
};
|
|
1253
|
-
data.
|
|
1150
|
+
data.eventRecords = [...wodWikiWql.toEventRows(corpus.logs, identity), ...wodWikiWql.toSummaryEventRows(corpus.logs, identity)];
|
|
1254
1151
|
}
|
|
1255
1152
|
} else {
|
|
1256
1153
|
throw new Error(`Unrecognized corpus payload shape in "${options.corpusPath}" (expected event-journal, fact-set, or CorpusIRData)`);
|
|
@@ -1265,29 +1162,16 @@ function loadQueryData(options) {
|
|
|
1265
1162
|
} else {
|
|
1266
1163
|
executionLog = { results: payload, logs: rawLogs, statements: rawLogs };
|
|
1267
1164
|
}
|
|
1268
|
-
const
|
|
1269
|
-
const workoutResult = {
|
|
1270
|
-
id: resultId,
|
|
1165
|
+
const identity = {
|
|
1271
1166
|
noteId: "stdin-note-1",
|
|
1272
|
-
|
|
1167
|
+
resultId: "stdin-result-1",
|
|
1273
1168
|
origin: "journal",
|
|
1274
|
-
|
|
1275
|
-
startTime: executionLog.results.startTime,
|
|
1276
|
-
endTime: executionLog.results.endTime,
|
|
1277
|
-
duration: executionLog.results.duration ?? 0,
|
|
1278
|
-
completed: executionLog.results.completed
|
|
1169
|
+
workoutTimestamp: executionLog.results.startTime
|
|
1279
1170
|
};
|
|
1280
|
-
data.
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
segmentId: workoutResult.segmentId,
|
|
1285
|
-
segmentVersion: workoutResult.segmentVersion,
|
|
1286
|
-
blockContentId: workoutResult.blockContentId,
|
|
1287
|
-
origin: workoutResult.origin,
|
|
1288
|
-
pageId: workoutResult.pageId,
|
|
1289
|
-
workoutTimestamp: workoutResult.createdAt
|
|
1290
|
-
});
|
|
1171
|
+
data.eventRecords = [
|
|
1172
|
+
...wodWikiWql.toEventRows(executionLog.logs, identity),
|
|
1173
|
+
...wodWikiWql.toSummaryEventRows(executionLog.logs, identity)
|
|
1174
|
+
];
|
|
1291
1175
|
} else if (options.stdinFacts) {
|
|
1292
1176
|
const payload = extractPayload(options.stdinFacts);
|
|
1293
1177
|
if (Array.isArray(payload)) {
|
|
@@ -1298,6 +1182,60 @@ function loadQueryData(options) {
|
|
|
1298
1182
|
} else {
|
|
1299
1183
|
throw new Error("No input dataset provided. Specify --corpus <path>, --stdin-log, or --stdin-facts.");
|
|
1300
1184
|
}
|
|
1185
|
+
for (const note of data.notes) {
|
|
1186
|
+
if (typeof note.rawContent !== "string" || !Number.isSafeInteger(note.sourceRevision) || note.sourceRevision < 1) {
|
|
1187
|
+
throw new Error("Incompatible corpus Note; exact rawContent and sourceRevision are required");
|
|
1188
|
+
}
|
|
1189
|
+
wodWikiCore.validateNoteBlocks(note.rawContent, note.blocks, note.sourceRevision);
|
|
1190
|
+
if (!data.blocks.some((row) => row.noteId === note.id)) {
|
|
1191
|
+
for (const [position, block] of Object.values(note.blocks).sort((a, b) => a.from - b.from).entries()) {
|
|
1192
|
+
const rawContent = note.rawContent.slice(block.from, block.to);
|
|
1193
|
+
const body = wodWikiCore.parseDocumentSections(rawContent)[0]?.displayContent ?? rawContent;
|
|
1194
|
+
data.blocks.push({
|
|
1195
|
+
id: `${note.id}:${block.id}`,
|
|
1196
|
+
noteId: note.id,
|
|
1197
|
+
blockId: block.id,
|
|
1198
|
+
blockVersion: block.version,
|
|
1199
|
+
sourceRevision: note.sourceRevision,
|
|
1200
|
+
from: block.from,
|
|
1201
|
+
to: block.to,
|
|
1202
|
+
position,
|
|
1203
|
+
dataType: block.kind,
|
|
1204
|
+
rawContent,
|
|
1205
|
+
noteTitle: note.title,
|
|
1206
|
+
createdAt: note.createdAt,
|
|
1207
|
+
...block.kind === "wod" ? { blockContentId: wodWikiCore.blockContentId(body) } : {}
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
for (const session of data.results) {
|
|
1213
|
+
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) {
|
|
1214
|
+
throw new Error("Incompatible corpus Session; exact blockId/blockVersion pin required");
|
|
1215
|
+
}
|
|
1216
|
+
if (!data.revisions.some((row) => row.noteId === session.noteId && row.blockId === session.blockId && row.version === session.blockVersion && row.kind === "wod")) {
|
|
1217
|
+
throw new Error("Corpus Session immutable revision is unavailable");
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
for (const row of [...data.eventRecords, ...data.facts, ...data.blocks]) {
|
|
1221
|
+
if ("segmentId" in row || "segmentVersion" in row) throw new Error("Incompatible corpus segment pin");
|
|
1222
|
+
}
|
|
1223
|
+
const revisions = new Map(data.revisions.map((row) => [JSON.stringify([row.noteId, row.blockId, row.version]), row]));
|
|
1224
|
+
for (const row of data.eventRecords) {
|
|
1225
|
+
if (row.blockId !== void 0 || row.blockVersion !== void 0) {
|
|
1226
|
+
if (!row.blockId || !Number.isSafeInteger(row.blockVersion) || !revisions.has(JSON.stringify([row.noteId, row.blockId, row.blockVersion]))) {
|
|
1227
|
+
throw new Error("Corpus Event immutable revision is unavailable");
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
for (const row of data.blocks) {
|
|
1232
|
+
const note = data.notes.find((candidate) => candidate.id === row.noteId);
|
|
1233
|
+
const block = note?.blocks[row.blockId];
|
|
1234
|
+
if (!note || !block || row.blockVersion !== block.version || row.sourceRevision !== note.sourceRevision || row.from !== block.from || row.to !== block.to || row.dataType !== block.kind) {
|
|
1235
|
+
throw new Error("Corpus block projection does not match current Note source");
|
|
1236
|
+
}
|
|
1237
|
+
row.rawContent = note.rawContent.slice(block.from, block.to);
|
|
1238
|
+
}
|
|
1301
1239
|
return data;
|
|
1302
1240
|
}
|
|
1303
1241
|
async function runQueryCli(wqlString, options = {}) {
|
|
@@ -1358,9 +1296,14 @@ async function openCliDb(dbPath) {
|
|
|
1358
1296
|
const resolved = resolveDbPath(dbPath);
|
|
1359
1297
|
nodeFs().mkdirSync(nodePath().dirname(resolved), { recursive: true });
|
|
1360
1298
|
const db = sql.libsqlDatabase(createClient({ url: `file:${resolved}` }));
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1299
|
+
try {
|
|
1300
|
+
await sql.initializeStore(db);
|
|
1301
|
+
await sql.initializeDomain(db);
|
|
1302
|
+
return { db, storage: new sql.SqlStorage(db), path: resolved, close: () => db.close() };
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
await db.close();
|
|
1305
|
+
throw error;
|
|
1306
|
+
}
|
|
1364
1307
|
}
|
|
1365
1308
|
function formatV5Digest(digest) {
|
|
1366
1309
|
const bytes = digest.slice(0, 16);
|
|
@@ -1394,7 +1337,7 @@ function sqliteEventStore(db) {
|
|
|
1394
1337
|
getEventsByTimeRange: async (start, end) => (await all()).filter((r) => r.timestamp >= start && r.timestamp <= end),
|
|
1395
1338
|
getEventsByResult: async (resultId) => (await all()).filter((r) => r.resultId === resultId),
|
|
1396
1339
|
getEventsForNote: async (noteId) => (await all()).filter((r) => r.noteId === noteId),
|
|
1397
|
-
getEventsByContent: async (
|
|
1340
|
+
getEventsByContent: async (blockContentId5) => (await all()).filter((r) => r.blockContentId === blockContentId5),
|
|
1398
1341
|
scanAll: all,
|
|
1399
1342
|
appendEvents: async (appended) => {
|
|
1400
1343
|
const sql = await loadSql();
|
|
@@ -1426,6 +1369,11 @@ function sqliteEventStore(db) {
|
|
|
1426
1369
|
function sqliteNoteStore(db) {
|
|
1427
1370
|
return {
|
|
1428
1371
|
getAllNotes: async () => await allRows(await loadSql(), db, "notes"),
|
|
1372
|
+
getNotesByIds: async (noteIds) => {
|
|
1373
|
+
const sql = await loadSql();
|
|
1374
|
+
const rows = await sql.getRows(db, sql.storeDefinition("notes"), [...noteIds]);
|
|
1375
|
+
return rows.filter((row) => row !== void 0);
|
|
1376
|
+
},
|
|
1429
1377
|
getNoteIdsForTag: async (label) => {
|
|
1430
1378
|
const sql = await loadSql();
|
|
1431
1379
|
const tags = (await allRows(sql, db, "tags")).filter((t) => t.label === label);
|
|
@@ -1480,17 +1428,14 @@ function sqliteIndexStore(db, store) {
|
|
|
1480
1428
|
}
|
|
1481
1429
|
async function getNoteBundle(db, noteId) {
|
|
1482
1430
|
const sql = await loadSql();
|
|
1483
|
-
const
|
|
1484
|
-
|
|
1485
|
-
const segments = await rowsByIndex(sql, db, "segments", "by-note", noteId);
|
|
1486
|
-
return { note, segments };
|
|
1431
|
+
const note = await sql.getRow(db, sql.storeDefinition("notes"), noteId);
|
|
1432
|
+
return { note };
|
|
1487
1433
|
}
|
|
1488
1434
|
async function getSessionBundle(db, resultId) {
|
|
1489
1435
|
const sql = await loadSql();
|
|
1490
|
-
const
|
|
1491
|
-
const session = sessions.find((s) => s.id === resultId);
|
|
1436
|
+
const session = await sql.getRow(db, sql.storeDefinition("sessions"), resultId);
|
|
1492
1437
|
if (!session) return void 0;
|
|
1493
|
-
const events =
|
|
1438
|
+
const events = await sql.listRows(db, sql.storeDefinition("events"), { only: resultId }, void 0, "by-result-grain");
|
|
1494
1439
|
return { session, events };
|
|
1495
1440
|
}
|
|
1496
1441
|
async function getEffortBySlug(db, slug) {
|
|
@@ -1503,38 +1448,8 @@ async function listTagsByType(db, type) {
|
|
|
1503
1448
|
const tags = await allRows(sql, db, "tags");
|
|
1504
1449
|
return tags.filter((t) => t.type === type);
|
|
1505
1450
|
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
case "time":
|
|
1509
|
-
case "log":
|
|
1510
|
-
return "wod";
|
|
1511
|
-
case "title": {
|
|
1512
|
-
const level = Math.min(6, Math.max(1, section.level ?? 1));
|
|
1513
|
-
return `h${level}`;
|
|
1514
|
-
}
|
|
1515
|
-
case "frontmatter":
|
|
1516
|
-
return "frontmatter";
|
|
1517
|
-
case "markdown":
|
|
1518
|
-
default:
|
|
1519
|
-
return "markdown";
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
function sectionSourceFragments(content, sections) {
|
|
1523
|
-
const starts = [0];
|
|
1524
|
-
for (let i = 0; i < content.length; i++) {
|
|
1525
|
-
if (content[i] === "\n") starts.push(i + 1);
|
|
1526
|
-
}
|
|
1527
|
-
return sections.map((section, index) => content.slice(
|
|
1528
|
-
starts[section.startLine],
|
|
1529
|
-
index + 1 < sections.length ? starts[sections[index + 1].startLine] : content.length
|
|
1530
|
-
));
|
|
1531
|
-
}
|
|
1532
|
-
function scriptBlockContentId(data) {
|
|
1533
|
-
if (data && typeof data === "object" && "contentId" in data && typeof data.contentId === "string") {
|
|
1534
|
-
return data.contentId;
|
|
1535
|
-
}
|
|
1536
|
-
return void 0;
|
|
1537
|
-
}
|
|
1451
|
+
var registeredWidgets = wodWikiCore.KNOWN_SECTION_WIDGETS;
|
|
1452
|
+
var saveStores = ["notes", "block_revisions", "block_index", "block_efforts", "tags", "note_tags", "sessions"];
|
|
1538
1453
|
function extractEffortSlugs(statements) {
|
|
1539
1454
|
const slugs = /* @__PURE__ */ new Set();
|
|
1540
1455
|
for (const statement of statements) {
|
|
@@ -1551,227 +1466,233 @@ function extractEffortSlugs(statements) {
|
|
|
1551
1466
|
}
|
|
1552
1467
|
return [...slugs];
|
|
1553
1468
|
}
|
|
1554
|
-
async function rowsByNote(sql, db, store, noteId) {
|
|
1555
|
-
return sql.listRows(db, sql.storeDefinition(store), { only: noteId }, void 0, "by-note");
|
|
1556
|
-
}
|
|
1557
1469
|
function titleFromLabel(sourceLabel) {
|
|
1558
1470
|
if (!sourceLabel || sourceLabel === "-") return "stdin";
|
|
1559
1471
|
const stem = sourceLabel.split("/").pop() ?? sourceLabel;
|
|
1560
1472
|
return stem.replace(/\.[^.]+$/, "") || sourceLabel;
|
|
1561
1473
|
}
|
|
1562
|
-
async function
|
|
1563
|
-
const sql = await loadSql();
|
|
1564
|
-
const now = options.now ?? Date.now();
|
|
1474
|
+
async function saveSource(db, options, startTime) {
|
|
1565
1475
|
const noteId = options.noteId ?? stableNoteId(options.sourceLabel, options.source);
|
|
1566
|
-
const
|
|
1567
|
-
const
|
|
1568
|
-
const
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1476
|
+
const observed = await db.storage.readonly("notes").get(noteId);
|
|
1477
|
+
const expected = options.expectedSourceRevision ?? observed?.sourceRevision ?? 0;
|
|
1478
|
+
const now = options.now ?? Date.now();
|
|
1479
|
+
return db.storage.transaction([...saveStores], "readwrite", async (tx) => {
|
|
1480
|
+
const existing = await tx.readonly("notes").get(noteId);
|
|
1481
|
+
if ((existing?.sourceRevision ?? 0) !== expected) {
|
|
1482
|
+
throw new Error(`Stale Note source revision for ${noteId}; expected ${expected}`);
|
|
1483
|
+
}
|
|
1484
|
+
let sourceRevision = existing?.rawContent === options.source ? existing.sourceRevision : (existing?.sourceRevision ?? 0) + 1;
|
|
1485
|
+
const blocks = wodWikiCore.reconcileNoteBlocks(options.source, {
|
|
1486
|
+
sourceRevision,
|
|
1487
|
+
blocks: options.blocks ?? (existing?.rawContent === options.source ? existing.blocks : void 0),
|
|
1488
|
+
registeredWidgets,
|
|
1489
|
+
createId: randomId
|
|
1490
|
+
});
|
|
1491
|
+
wodWikiCore.validateNoteBlocks(options.source, blocks, sourceRevision, { registeredWidgets });
|
|
1492
|
+
const history = await tx.readonly("block_revisions").getAllFromIndex("by-note", noteId);
|
|
1493
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1494
|
+
for (const row of history) {
|
|
1495
|
+
if ((latest.get(row.blockId)?.version ?? 0) < row.version) latest.set(row.blockId, row);
|
|
1496
|
+
}
|
|
1497
|
+
const revisions = [];
|
|
1498
|
+
for (const block of Object.values(blocks)) {
|
|
1499
|
+
const saved = latest.get(block.id);
|
|
1500
|
+
const rawContent = options.source.slice(block.from, block.to);
|
|
1501
|
+
const unchanged = saved && saved.rawContent === rawContent && saved.kind === block.kind && saved.dialect === block.dialect && saved.sport === block.sport;
|
|
1502
|
+
block.version = unchanged ? saved.version : (saved?.version ?? 0) + 1;
|
|
1503
|
+
if (!unchanged) revisions.push({
|
|
1504
|
+
noteId,
|
|
1505
|
+
blockId: block.id,
|
|
1506
|
+
version: block.version,
|
|
1507
|
+
kind: block.kind,
|
|
1508
|
+
rawContent,
|
|
1509
|
+
savedAt: now,
|
|
1510
|
+
...block.dialect ? { dialect: block.dialect } : {},
|
|
1511
|
+
...block.sport ? { sport: block.sport } : {}
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
if (existing?.rawContent === options.source && !wodWikiStorage.rowsEqual(existing.blocks, blocks)) {
|
|
1515
|
+
sourceRevision++;
|
|
1516
|
+
for (const block of Object.values(blocks)) block.sourceRevision = sourceRevision;
|
|
1517
|
+
}
|
|
1518
|
+
const note = {
|
|
1519
|
+
...existing,
|
|
1520
|
+
id: noteId,
|
|
1521
|
+
title: options.title ?? existing?.title ?? titleFromLabel(options.sourceLabel),
|
|
1522
|
+
type: existing?.type ?? "note",
|
|
1523
|
+
date: existing?.date ?? now,
|
|
1524
|
+
createdAt: existing?.createdAt ?? now,
|
|
1525
|
+
rawContent: options.source,
|
|
1526
|
+
sourceRevision,
|
|
1527
|
+
blocks
|
|
1599
1528
|
};
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
for (const segment of segments) {
|
|
1615
|
-
const blockContentId = segment.dataType === "wod" ? scriptBlockContentId(segment.data) : void 0;
|
|
1616
|
-
ops.push({
|
|
1617
|
-
op: "put",
|
|
1618
|
-
store: "block_index",
|
|
1619
|
-
value: {
|
|
1620
|
-
id: `${noteId}:${segment.id}:${segment.version}`,
|
|
1529
|
+
const priorRows = await tx.readonly("block_index").getAllFromIndex("by-note", noteId);
|
|
1530
|
+
const priorEfforts = await tx.readonly("block_efforts").getAllFromIndex("by-note", noteId);
|
|
1531
|
+
const priorById = new Map(priorRows.map((row) => [row.id, row]));
|
|
1532
|
+
const nextRows = [];
|
|
1533
|
+
const nextEfforts = [];
|
|
1534
|
+
for (const [position, block] of Object.values(blocks).sort((a, b) => a.from - b.from).entries()) {
|
|
1535
|
+
const id = `${noteId}:${block.id}`;
|
|
1536
|
+
const prior = priorById.get(id);
|
|
1537
|
+
const rawContent = options.source.slice(block.from, block.to);
|
|
1538
|
+
const body = wodWikiCore.parseDocumentSections(rawContent, { registeredWidgets })[0]?.displayContent ?? rawContent;
|
|
1539
|
+
const fingerprint = block.kind === "wod" ? wodWikiCore.blockContentId(body) : void 0;
|
|
1540
|
+
nextRows.push({
|
|
1541
|
+
...prior,
|
|
1542
|
+
id,
|
|
1621
1543
|
noteId,
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1544
|
+
blockId: block.id,
|
|
1545
|
+
blockVersion: block.version,
|
|
1546
|
+
dataType: block.kind,
|
|
1547
|
+
sourceRevision,
|
|
1548
|
+
from: block.from,
|
|
1549
|
+
to: block.to,
|
|
1550
|
+
position,
|
|
1551
|
+
rawContent,
|
|
1552
|
+
noteTitle: note.title,
|
|
1553
|
+
createdAt: prior?.createdAt ?? now,
|
|
1554
|
+
blockContentId: fingerprint,
|
|
1555
|
+
...note.sourceId ? { sourceId: note.sourceId } : {}
|
|
1556
|
+
});
|
|
1557
|
+
if (block.kind === "wod") {
|
|
1558
|
+
for (const effortSlug of extractEffortSlugs(wodWikiLang.parseScript(body, { sport: block.sport, dialect: block.dialect }).statements)) {
|
|
1559
|
+
const effortId = `${noteId}:${block.id}:${effortSlug}`;
|
|
1560
|
+
const previous = priorEfforts.find((row) => row.id === effortId);
|
|
1561
|
+
nextEfforts.push({
|
|
1562
|
+
...previous,
|
|
1563
|
+
id: effortId,
|
|
1641
1564
|
noteId,
|
|
1642
|
-
blockId:
|
|
1643
|
-
blockContentId,
|
|
1565
|
+
blockId: block.id,
|
|
1566
|
+
blockContentId: fingerprint,
|
|
1644
1567
|
effortSlug,
|
|
1645
|
-
createdAt:
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
effortCount += 1;
|
|
1568
|
+
createdAt: previous?.createdAt ?? now
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1649
1571
|
}
|
|
1650
1572
|
}
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
)
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
});
|
|
1573
|
+
if (!wodWikiStorage.rowsEqual(existing, note)) await tx.readwrite("notes").put(note);
|
|
1574
|
+
if (revisions.length) await tx.readwrite("block_revisions").putMany(revisions);
|
|
1575
|
+
await diffProjection(tx, "block_index", priorRows, nextRows);
|
|
1576
|
+
await diffProjection(tx, "block_efforts", priorEfforts, nextEfforts);
|
|
1577
|
+
const tags = await saveTags(tx, noteId, options.tags, now);
|
|
1578
|
+
let session;
|
|
1579
|
+
let revision;
|
|
1580
|
+
if (startTime !== void 0) {
|
|
1581
|
+
const block = Object.values(blocks).filter((row) => row.kind === "wod").sort((a, b) => a.from - b.from)[0];
|
|
1582
|
+
if (!block) throw new Error("[wod run] Note has no actionable workout block");
|
|
1583
|
+
revision = revisions.find((row) => row.blockId === block.id) ?? latest.get(block.id);
|
|
1584
|
+
if (!revision) throw new Error("[wod run] Saved workout revision is unavailable");
|
|
1585
|
+
session = {
|
|
1586
|
+
id: randomId(),
|
|
1587
|
+
noteId,
|
|
1588
|
+
blockId: block.id,
|
|
1589
|
+
blockVersion: block.version,
|
|
1590
|
+
blockContentId: wodWikiCore.blockContentId(wodWikiCore.parseDocumentSections(revision.rawContent)[0]?.displayContent ?? revision.rawContent),
|
|
1591
|
+
origin: "journal",
|
|
1592
|
+
status: "in-progress",
|
|
1593
|
+
startTime,
|
|
1594
|
+
endTime: startTime,
|
|
1595
|
+
duration: 0,
|
|
1596
|
+
completed: false,
|
|
1597
|
+
createdAt: startTime
|
|
1598
|
+
};
|
|
1599
|
+
await tx.readwrite("sessions").put(session);
|
|
1679
1600
|
}
|
|
1680
|
-
|
|
1681
|
-
await db.db.transaction(async (tx) => {
|
|
1682
|
-
await sql.refreshDomain(tx, ops);
|
|
1601
|
+
return { noteId, note, blocks: nextRows.length, efforts: nextEfforts.length, tags, created: !existing, session, revision };
|
|
1683
1602
|
});
|
|
1684
|
-
return {
|
|
1685
|
-
noteId,
|
|
1686
|
-
note,
|
|
1687
|
-
segments: segments.length,
|
|
1688
|
-
blocks: blockCount,
|
|
1689
|
-
efforts: effortCount,
|
|
1690
|
-
tags: tagCount,
|
|
1691
|
-
created: !existingNote
|
|
1692
|
-
};
|
|
1693
1603
|
}
|
|
1694
|
-
async function
|
|
1695
|
-
const
|
|
1696
|
-
|
|
1604
|
+
async function diffProjection(tx, store, previous, next) {
|
|
1605
|
+
const nextIds = new Set(next.map((row) => row.id));
|
|
1606
|
+
for (const row of previous) if (!nextIds.has(row.id)) await tx.readwrite(store).delete(row.id);
|
|
1607
|
+
const old = new Map(previous.map((row) => [row.id, row]));
|
|
1608
|
+
for (const row of next) {
|
|
1609
|
+
if (!wodWikiStorage.rowsEqual(old.get(row.id), row)) {
|
|
1610
|
+
if (store === "block_index" && "blockVersion" in row) await tx.readwrite("block_index").put(row);
|
|
1611
|
+
if (store === "block_efforts" && "effortSlug" in row) await tx.readwrite("block_efforts").put(row);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1697
1614
|
}
|
|
1698
|
-
async function
|
|
1699
|
-
const
|
|
1700
|
-
|
|
1701
|
-
const
|
|
1702
|
-
const
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
const
|
|
1615
|
+
async function saveTags(tx, noteId, items, now) {
|
|
1616
|
+
const previous = await tx.readonly("note_tags").getAllFromIndex("by-note", noteId);
|
|
1617
|
+
if (!items) return previous.length;
|
|
1618
|
+
const next = /* @__PURE__ */ new Map();
|
|
1619
|
+
for (const item of items) {
|
|
1620
|
+
const label = item.label.trim();
|
|
1621
|
+
if (!label) continue;
|
|
1622
|
+
const found = (await tx.readonly("tags").getAllFromIndex("by-label", label))[0];
|
|
1623
|
+
const tag = found ? { ...found, ...!found.type && item.type ? { type: item.type } : {} } : { id: uuid5(CLI_UUID_NAMESPACE, `tag:${label}`), label, type: item.type, createdAt: now };
|
|
1624
|
+
if (!wodWikiStorage.rowsEqual(found, tag)) await tx.readwrite("tags").put(tag);
|
|
1625
|
+
const id = previous.find((link) => link.tagId === tag.id)?.id ?? uuid5(CLI_UUID_NAMESPACE, `note-tag:${noteId}:${tag.id}`);
|
|
1626
|
+
next.set(id, { ...previous.find((link) => link.id === id), id, noteId, tagId: tag.id });
|
|
1627
|
+
}
|
|
1628
|
+
for (const link of previous) if (!next.has(link.id)) await tx.readwrite("note_tags").delete(link.id);
|
|
1629
|
+
const old = new Map(previous.map((row) => [row.id, row]));
|
|
1630
|
+
for (const link of next.values()) if (!wodWikiStorage.rowsEqual(old.get(link.id), link)) await tx.readwrite("note_tags").put(link);
|
|
1631
|
+
return next.size;
|
|
1632
|
+
}
|
|
1633
|
+
function persistNote(db, options) {
|
|
1634
|
+
return saveSource(db, options);
|
|
1635
|
+
}
|
|
1636
|
+
async function startRun(db, options, startTime = Date.now()) {
|
|
1637
|
+
const saved = await saveSource(db, options, startTime);
|
|
1638
|
+
if (!saved.session || !saved.revision) throw new Error("[wod run] Atomic run capture failed");
|
|
1639
|
+
return { session: saved.session, revision: saved.revision, noteCreated: saved.created };
|
|
1640
|
+
}
|
|
1641
|
+
async function persistRun(db, started, executionLog) {
|
|
1642
|
+
const results = executionLog.results;
|
|
1712
1643
|
const session = {
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
segmentVersion: runSegment?.version,
|
|
1716
|
-
noteId: noteState.noteId,
|
|
1717
|
-
blockId: runSegment?.id,
|
|
1718
|
-
blockContentId,
|
|
1719
|
-
origin: "journal",
|
|
1720
|
-
status: results.completed === false ? "in-progress" : "completed",
|
|
1721
|
-
startTime: results.startTime,
|
|
1644
|
+
...started.session,
|
|
1645
|
+
status: results.completed ? "completed" : "in-progress",
|
|
1722
1646
|
endTime: results.endTime,
|
|
1723
1647
|
duration: results.duration ?? 0,
|
|
1724
|
-
completed: results.completed
|
|
1725
|
-
createdAt: results.endTime || Date.now()
|
|
1648
|
+
completed: results.completed
|
|
1726
1649
|
};
|
|
1727
1650
|
const identity = {
|
|
1728
|
-
noteId:
|
|
1729
|
-
resultId,
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
blockContentId,
|
|
1651
|
+
noteId: session.noteId,
|
|
1652
|
+
resultId: session.id,
|
|
1653
|
+
blockId: session.blockId,
|
|
1654
|
+
blockVersion: session.blockVersion,
|
|
1655
|
+
blockContentId: session.blockContentId,
|
|
1733
1656
|
origin: session.origin,
|
|
1734
1657
|
pageId: session.pageId,
|
|
1735
1658
|
workoutTimestamp: session.createdAt
|
|
1736
1659
|
};
|
|
1737
|
-
const
|
|
1738
|
-
const
|
|
1739
|
-
await db.
|
|
1740
|
-
await
|
|
1741
|
-
|
|
1742
|
-
|
|
1660
|
+
const events = wodWikiWql.toEventRows(executionLog.logs, identity);
|
|
1661
|
+
const summaries = wodWikiWql.toSummaryEventRows(executionLog.logs, identity);
|
|
1662
|
+
await db.storage.transaction(["sessions", "events", "block_revisions"], "readwrite", async (tx) => {
|
|
1663
|
+
const saved = await tx.readonly("sessions").get(session.id);
|
|
1664
|
+
if (!saved || saved.noteId !== session.noteId || saved.blockId !== session.blockId || saved.blockVersion !== session.blockVersion) {
|
|
1665
|
+
throw new Error("[wod run] Captured Session source pin changed");
|
|
1666
|
+
}
|
|
1667
|
+
const revision = await tx.readonly("block_revisions").get([session.noteId, session.blockId, session.blockVersion]);
|
|
1668
|
+
if (!revision || !wodWikiStorage.rowsEqual(revision, started.revision)) throw new Error("[wod run] Captured revision is unavailable");
|
|
1669
|
+
await tx.readwrite("sessions").put(session);
|
|
1670
|
+
await tx.readwrite("events").putMany([...events, ...summaries]);
|
|
1743
1671
|
});
|
|
1744
|
-
return {
|
|
1745
|
-
noteId: noteState.noteId,
|
|
1746
|
-
resultId,
|
|
1747
|
-
session,
|
|
1748
|
-
events: eventRows.length,
|
|
1749
|
-
summaries: summaryRows.length,
|
|
1750
|
-
noteCreated: noteState.created
|
|
1751
|
-
};
|
|
1672
|
+
return { noteId: session.noteId, resultId: session.id, session, events: events.length, summaries: summaries.length, noteCreated: started.noteCreated };
|
|
1752
1673
|
}
|
|
1753
1674
|
async function persistSessionBundle(db, session, events) {
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1675
|
+
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) {
|
|
1676
|
+
throw new Error("[wod import] Session requires an exact block revision pin");
|
|
1677
|
+
}
|
|
1678
|
+
await db.storage.transaction(["sessions", "events", "block_revisions"], "readwrite", async (tx) => {
|
|
1679
|
+
const revision = await tx.readonly("block_revisions").get([session.noteId, session.blockId, session.blockVersion]);
|
|
1680
|
+
if (!revision || revision.kind !== "wod") throw new Error("[wod import] Exact immutable workout revision is unavailable");
|
|
1681
|
+
for (const event of events) {
|
|
1682
|
+
if (event.resultId !== session.id || event.noteId !== session.noteId || event.blockId !== session.blockId || event.blockVersion !== session.blockVersion) {
|
|
1683
|
+
throw new Error("[wod import] Event source pin differs from Session source pin");
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
const existing = await tx.readonly("sessions").get(session.id);
|
|
1687
|
+
if (existing && !wodWikiStorage.rowsEqual(existing, session)) throw new Error("[wod import] Session identity conflicts with existing row");
|
|
1688
|
+
if (!existing) await tx.readwrite("sessions").put(session);
|
|
1689
|
+
for (const event of events) {
|
|
1690
|
+
const old = await tx.readonly("events").get(event.id);
|
|
1691
|
+
if (old && !wodWikiStorage.rowsEqual(old, event)) throw new Error("[wod import] Event identity conflicts with existing row");
|
|
1692
|
+
if (!old) await tx.readwrite("events").put(event);
|
|
1693
|
+
}
|
|
1773
1694
|
});
|
|
1774
|
-
return { events: events.length
|
|
1695
|
+
return { events: events.length };
|
|
1775
1696
|
}
|
|
1776
1697
|
async function persistEffort(db, effort) {
|
|
1777
1698
|
const sql = await loadSql();
|
|
@@ -1780,30 +1701,21 @@ async function persistEffort(db, effort) {
|
|
|
1780
1701
|
});
|
|
1781
1702
|
}
|
|
1782
1703
|
async function persistTypedTags(db, labels, type) {
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
ops.push({ op: "put", store: "tags", value: { id: randomId(), label, type, createdAt: now } });
|
|
1796
|
-
added += 1;
|
|
1797
|
-
} else if (!tag.type) {
|
|
1798
|
-
ops.push({ op: "put", store: "tags", value: { ...tag, type } });
|
|
1704
|
+
return db.storage.transaction(["tags"], "readwrite", async (tx) => {
|
|
1705
|
+
let added = 0;
|
|
1706
|
+
for (const raw of labels) {
|
|
1707
|
+
const label = raw.trim();
|
|
1708
|
+
if (!label) continue;
|
|
1709
|
+
const tag = (await tx.readonly("tags").getAllFromIndex("by-label", label))[0];
|
|
1710
|
+
if (!tag) {
|
|
1711
|
+
await tx.readwrite("tags").put({ id: uuid5(CLI_UUID_NAMESPACE, `tag:${label}`), label, type, createdAt: Date.now() });
|
|
1712
|
+
added++;
|
|
1713
|
+
} else if (!tag.type) {
|
|
1714
|
+
await tx.readwrite("tags").put({ ...tag, type });
|
|
1715
|
+
}
|
|
1799
1716
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
await db.db.transaction(async (tx) => {
|
|
1803
|
-
await sql.refreshDomain(tx, ops);
|
|
1804
|
-
});
|
|
1805
|
-
}
|
|
1806
|
-
return added;
|
|
1717
|
+
return added;
|
|
1718
|
+
});
|
|
1807
1719
|
}
|
|
1808
1720
|
var NOTE_ID_FRONTMATTER_KEY = "wod-note-id";
|
|
1809
1721
|
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;
|
|
@@ -1837,10 +1749,9 @@ function injectIdentityFrontmatter(markdown, identity) {
|
|
|
1837
1749
|
};
|
|
1838
1750
|
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
|
|
1839
1751
|
if (block) {
|
|
1752
|
+
const newline = markdown.startsWith("---\r\n") ? "\r\n" : "\n";
|
|
1840
1753
|
const lines2 = stamp(block[1].split(/\r?\n/));
|
|
1841
|
-
return
|
|
1842
|
-
${lines2.join("\n")}
|
|
1843
|
-
---${markdown.slice(block[0].length)}`;
|
|
1754
|
+
return `---${newline}${lines2.join(newline)}${newline}---${markdown.slice(block[0].length)}`;
|
|
1844
1755
|
}
|
|
1845
1756
|
const lines = stamp([]);
|
|
1846
1757
|
return `---
|
|
@@ -1849,35 +1760,8 @@ ${lines.join("\n")}
|
|
|
1849
1760
|
|
|
1850
1761
|
${markdown}`;
|
|
1851
1762
|
}
|
|
1852
|
-
function
|
|
1853
|
-
|
|
1854
|
-
if (segment.dataType === "wod") {
|
|
1855
|
-
const block = segment.data;
|
|
1856
|
-
const dialect = block?.dialect ?? "time";
|
|
1857
|
-
const fenceTag = block?.sport ? `${dialect}:${block.sport}` : dialect;
|
|
1858
|
-
return `\`\`\`${fenceTag}
|
|
1859
|
-
${segment.rawContent}
|
|
1860
|
-
\`\`\``;
|
|
1861
|
-
}
|
|
1862
|
-
if (segment.dataType === "frontmatter") {
|
|
1863
|
-
return `---
|
|
1864
|
-
${segment.rawContent}
|
|
1865
|
-
---`;
|
|
1866
|
-
}
|
|
1867
|
-
return segment.rawContent;
|
|
1868
|
-
}
|
|
1869
|
-
function composeNoteMarkdown(note, segments, version) {
|
|
1870
|
-
const pool = version === void 0 ? segments.filter((s) => s.isHistory !== true) : segments;
|
|
1871
|
-
const cap = version ?? Number.MAX_SAFE_INTEGER;
|
|
1872
|
-
const bySegmentId = {};
|
|
1873
|
-
for (const segment of pool) {
|
|
1874
|
-
if (segment.version > cap) continue;
|
|
1875
|
-
const current = bySegmentId[segment.id];
|
|
1876
|
-
if (!current || segment.version > current.version) bySegmentId[segment.id] = segment;
|
|
1877
|
-
}
|
|
1878
|
-
const picked = Object.values(bySegmentId).sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
|
|
1879
|
-
const markdown = picked.map(segmentToFragment).join("");
|
|
1880
|
-
return injectIdentityFrontmatter(markdown, { noteId: note.id, title: note.title });
|
|
1763
|
+
function composeNoteMarkdown(note) {
|
|
1764
|
+
return injectIdentityFrontmatter(note.rawContent, { noteId: note.id, title: note.title });
|
|
1881
1765
|
}
|
|
1882
1766
|
var FRONTMATTER_BLOCK_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
1883
1767
|
function readFrontmatterMeta(markdown) {
|
|
@@ -2000,9 +1884,8 @@ function csvToEquipmentLabels(csv) {
|
|
|
2000
1884
|
var SESSION_HEADER = [
|
|
2001
1885
|
"sessionId",
|
|
2002
1886
|
"noteId",
|
|
2003
|
-
"segmentId",
|
|
2004
|
-
"segmentVersion",
|
|
2005
1887
|
"blockId",
|
|
1888
|
+
"blockVersion",
|
|
2006
1889
|
"blockContentId",
|
|
2007
1890
|
"origin",
|
|
2008
1891
|
"status",
|
|
@@ -2010,7 +1893,8 @@ var SESSION_HEADER = [
|
|
|
2010
1893
|
"endTime",
|
|
2011
1894
|
"duration",
|
|
2012
1895
|
"completed",
|
|
2013
|
-
"sessionCreatedAt"
|
|
1896
|
+
"sessionCreatedAt",
|
|
1897
|
+
"sessionExtraJson"
|
|
2014
1898
|
];
|
|
2015
1899
|
var EVENT_HEADER = [
|
|
2016
1900
|
"eventId",
|
|
@@ -2019,8 +1903,8 @@ var EVENT_HEADER = [
|
|
|
2019
1903
|
"timestamp",
|
|
2020
1904
|
"pageId",
|
|
2021
1905
|
"effortSlug",
|
|
2022
|
-
"
|
|
2023
|
-
"
|
|
1906
|
+
"eventBlockId",
|
|
1907
|
+
"eventBlockVersion",
|
|
2024
1908
|
"eventBlockContentId",
|
|
2025
1909
|
"eventOrigin",
|
|
2026
1910
|
"timeSpan",
|
|
@@ -2076,9 +1960,8 @@ function sessionCells(session) {
|
|
|
2076
1960
|
return [
|
|
2077
1961
|
session.id,
|
|
2078
1962
|
session.noteId,
|
|
2079
|
-
session.segmentId,
|
|
2080
|
-
session.segmentVersion,
|
|
2081
1963
|
session.blockId,
|
|
1964
|
+
session.blockVersion,
|
|
2082
1965
|
session.blockContentId,
|
|
2083
1966
|
session.origin,
|
|
2084
1967
|
session.status,
|
|
@@ -2086,7 +1969,8 @@ function sessionCells(session) {
|
|
|
2086
1969
|
session.endTime,
|
|
2087
1970
|
session.duration,
|
|
2088
1971
|
session.completed,
|
|
2089
|
-
session.createdAt
|
|
1972
|
+
session.createdAt,
|
|
1973
|
+
JSON.stringify(session)
|
|
2090
1974
|
].map(escapeCsvField);
|
|
2091
1975
|
}
|
|
2092
1976
|
function metricCells(event, metric, index) {
|
|
@@ -2097,8 +1981,8 @@ function metricCells(event, metric, index) {
|
|
|
2097
1981
|
event.timestamp,
|
|
2098
1982
|
event.pageId,
|
|
2099
1983
|
event.effortSlug,
|
|
2100
|
-
event.
|
|
2101
|
-
event.
|
|
1984
|
+
event.blockId,
|
|
1985
|
+
event.blockVersion,
|
|
2102
1986
|
event.blockContentId,
|
|
2103
1987
|
event.origin,
|
|
2104
1988
|
event.timeSpan,
|
|
@@ -2209,6 +2093,9 @@ function csvToSessionRows(csv) {
|
|
|
2209
2093
|
const lines = splitCsvRecords(csv);
|
|
2210
2094
|
if (lines.length < 2) throw new Error("[wod import] Session CSV has no data rows");
|
|
2211
2095
|
const header = parseCsvLine(lines[0]);
|
|
2096
|
+
if (header.some((name) => /segment/i.test(name))) {
|
|
2097
|
+
throw new Error("[wod import] Incompatible Session CSV segment pins; blockId/blockVersion are required");
|
|
2098
|
+
}
|
|
2212
2099
|
const col = (name) => {
|
|
2213
2100
|
const idx = header.indexOf(name);
|
|
2214
2101
|
if (idx < 0) throw new Error(`[wod import] Session CSV missing column "${name}"`);
|
|
@@ -2218,13 +2105,29 @@ function csvToSessionRows(csv) {
|
|
|
2218
2105
|
const eventRows = /* @__PURE__ */ new Map();
|
|
2219
2106
|
let session;
|
|
2220
2107
|
const ensureSession = (cells) => {
|
|
2221
|
-
|
|
2108
|
+
const id = at(cells, "sessionId");
|
|
2109
|
+
const noteId = at(cells, "noteId");
|
|
2110
|
+
const blockId = at(cells, "blockId");
|
|
2111
|
+
const blockVersion = Number(at(cells, "blockVersion"));
|
|
2112
|
+
if (!id || !noteId || !blockId || !Number.isSafeInteger(blockVersion) || blockVersion < 1) {
|
|
2113
|
+
throw new Error("[wod import] Session CSV requires sessionId, noteId, blockId and positive blockVersion");
|
|
2114
|
+
}
|
|
2115
|
+
if (session) {
|
|
2116
|
+
if (session.id !== id || session.noteId !== noteId || session.blockId !== blockId || session.blockVersion !== blockVersion) {
|
|
2117
|
+
throw new Error("[wod import] Session CSV contains inconsistent source pins");
|
|
2118
|
+
}
|
|
2119
|
+
return session;
|
|
2120
|
+
}
|
|
2121
|
+
const extra = jsonCell(at(cells, "sessionExtraJson")) ?? {};
|
|
2122
|
+
if ("segmentId" in extra || "segmentVersion" in extra || "version" in extra || "rawContent" in extra || "data" in extra) {
|
|
2123
|
+
throw new Error("[wod import] Incompatible Session CSV source snapshot or segment pin");
|
|
2124
|
+
}
|
|
2222
2125
|
session = {
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2126
|
+
...extra,
|
|
2127
|
+
id,
|
|
2128
|
+
noteId,
|
|
2129
|
+
blockId,
|
|
2130
|
+
blockVersion,
|
|
2228
2131
|
blockContentId: at(cells, "blockContentId") || void 0,
|
|
2229
2132
|
origin: at(cells, "origin") || void 0,
|
|
2230
2133
|
status: at(cells, "status") || void 0,
|
|
@@ -2234,6 +2137,9 @@ function csvToSessionRows(csv) {
|
|
|
2234
2137
|
completed: at(cells, "completed") === "true",
|
|
2235
2138
|
createdAt: Number(at(cells, "sessionCreatedAt") || 0)
|
|
2236
2139
|
};
|
|
2140
|
+
if (![session.startTime, session.endTime, session.duration, session.createdAt].every(Number.isFinite)) {
|
|
2141
|
+
throw new Error("[wod import] Session CSV lifecycle values must be finite numbers");
|
|
2142
|
+
}
|
|
2237
2143
|
return session;
|
|
2238
2144
|
};
|
|
2239
2145
|
for (const line of lines.slice(1)) {
|
|
@@ -2245,6 +2151,14 @@ function csvToSessionRows(csv) {
|
|
|
2245
2151
|
let event = eventRows.get(eventId);
|
|
2246
2152
|
if (!event) {
|
|
2247
2153
|
const extra = jsonCell(at(cells, "eventExtraJson")) ?? {};
|
|
2154
|
+
if ("segmentId" in extra || "segmentVersion" in extra) {
|
|
2155
|
+
throw new Error("[wod import] Incompatible Session CSV event segment pins");
|
|
2156
|
+
}
|
|
2157
|
+
const blockId = nonempty(at(cells, "eventBlockId"));
|
|
2158
|
+
const blockVersion = num(at(cells, "eventBlockVersion"));
|
|
2159
|
+
if (blockId !== currentSession.blockId || blockVersion !== currentSession.blockVersion) {
|
|
2160
|
+
throw new Error("[wod import] Event source pin differs from Session source pin");
|
|
2161
|
+
}
|
|
2248
2162
|
event = {
|
|
2249
2163
|
...extra,
|
|
2250
2164
|
id: eventId,
|
|
@@ -2257,8 +2171,8 @@ function csvToSessionRows(csv) {
|
|
|
2257
2171
|
grain: nonempty(at(cells, "grain")) ?? extra.grain ?? "event",
|
|
2258
2172
|
effortSlug: nonempty(at(cells, "effortSlug")) ?? extra.effortSlug,
|
|
2259
2173
|
outputType: nonempty(at(cells, "outputType")) ?? extra.outputType ?? "event",
|
|
2260
|
-
|
|
2261
|
-
|
|
2174
|
+
blockId,
|
|
2175
|
+
blockVersion,
|
|
2262
2176
|
timeSpan: jsonCell(at(cells, "timeSpan")) ?? extra.timeSpan,
|
|
2263
2177
|
sourceBlockKey: nonempty(at(cells, "sourceBlockKey")) ?? extra.sourceBlockKey,
|
|
2264
2178
|
sourceStatementId: num(at(cells, "sourceStatementId")) ?? extra.sourceStatementId,
|
|
@@ -2269,9 +2183,8 @@ function csvToSessionRows(csv) {
|
|
|
2269
2183
|
completionReason: nonempty(at(cells, "completionReason")) ?? extra.completionReason,
|
|
2270
2184
|
representationKind: nonempty(at(cells, "representationKind")) ?? extra.representationKind,
|
|
2271
2185
|
metrics: [],
|
|
2272
|
-
//
|
|
2273
|
-
|
|
2274
|
-
metricTemporal: void 0
|
|
2186
|
+
// Rebuild anchors from metric rows while preserving an authored empty array.
|
|
2187
|
+
metricTemporal: Array.isArray(extra.metricTemporal) ? [] : void 0
|
|
2275
2188
|
};
|
|
2276
2189
|
eventRows.set(eventId, event);
|
|
2277
2190
|
}
|
|
@@ -2297,275 +2210,322 @@ function csvToSessionRows(csv) {
|
|
|
2297
2210
|
return { session, events: [...eventRows.values()] };
|
|
2298
2211
|
}
|
|
2299
2212
|
|
|
2300
|
-
// src/cli/
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
return
|
|
2314
|
-
}
|
|
2315
|
-
function
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
const
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
const
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
const
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
if (!sourceId) {
|
|
2366
|
-
throw new Error(`[wod seed] chunk "${chunkId}" row "${row.path}" has no source family \u2014 refusing to import it as journal`);
|
|
2367
|
-
}
|
|
2368
|
-
const metaTitle = scalarOf2(meta["title"]);
|
|
2369
|
-
return {
|
|
2370
|
-
note: {
|
|
2371
|
-
id,
|
|
2372
|
-
title: metaTitle?.trim() || titleFromPath(row.path),
|
|
2373
|
-
date,
|
|
2374
|
-
createdAt,
|
|
2375
|
-
type: chunkId === DASHBOARDS_CHUNK_ID ? "dashboard" : "note",
|
|
2376
|
-
sourceId,
|
|
2377
|
-
catalog: catalogForChunkId(chunkId),
|
|
2378
|
-
seedOrigin: "seed",
|
|
2379
|
-
seedVersion,
|
|
2380
|
-
seedChunkId: chunkId,
|
|
2381
|
-
sourcePath: row.path
|
|
2382
|
-
},
|
|
2383
|
-
segment: {
|
|
2384
|
-
id: `seed:${id}`,
|
|
2385
|
-
version: 1,
|
|
2386
|
-
noteId: id,
|
|
2387
|
-
position: 0,
|
|
2388
|
-
dataType: "markdown",
|
|
2389
|
-
data: null,
|
|
2390
|
-
rawContent: row.content,
|
|
2391
|
-
createdAt
|
|
2392
|
-
},
|
|
2393
|
-
tags,
|
|
2394
|
-
page,
|
|
2395
|
-
pageNote,
|
|
2396
|
-
rawContent: row.content,
|
|
2397
|
-
sourcePath: row.path
|
|
2398
|
-
};
|
|
2213
|
+
// src/cli/seedbuild/runtime.ts
|
|
2214
|
+
init_effort_file();
|
|
2215
|
+
init_contract();
|
|
2216
|
+
|
|
2217
|
+
// src/cli/seedbuild/validation.ts
|
|
2218
|
+
init_contract();
|
|
2219
|
+
function object(value) {
|
|
2220
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2221
|
+
}
|
|
2222
|
+
function positive(value) {
|
|
2223
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
2224
|
+
}
|
|
2225
|
+
function nonnegative(value) {
|
|
2226
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
2227
|
+
}
|
|
2228
|
+
function assertSeedManifest(value) {
|
|
2229
|
+
if (!object(value) || value.schema !== SEED_SCHEMA) throw new Error("Incompatible seed schema; rebuild seed artifacts");
|
|
2230
|
+
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");
|
|
2231
|
+
for (const key of ["versionLabel", "commit"]) if (value[key] !== void 0 && typeof value[key] !== "string") throw new Error("invalid seed manifest metadata");
|
|
2232
|
+
if (value.dirty !== void 0 && typeof value.dirty !== "boolean") throw new Error("invalid seed manifest metadata");
|
|
2233
|
+
const ids = /* @__PURE__ */ new Set(), paths = /* @__PURE__ */ new Set();
|
|
2234
|
+
for (const chunk of value.chunks) {
|
|
2235
|
+
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");
|
|
2236
|
+
ids.add(chunk.id);
|
|
2237
|
+
paths.add(chunk.path);
|
|
2238
|
+
}
|
|
2239
|
+
return value;
|
|
2240
|
+
}
|
|
2241
|
+
function assertSeedRows(value) {
|
|
2242
|
+
if (!Array.isArray(value)) throw new Error("seed chunk is not an array");
|
|
2243
|
+
const paths = /* @__PURE__ */ new Set();
|
|
2244
|
+
for (const row of value) {
|
|
2245
|
+
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");
|
|
2246
|
+
paths.add(row.path);
|
|
2247
|
+
for (const [id, block] of Object.entries(row.blocks)) {
|
|
2248
|
+
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");
|
|
2249
|
+
}
|
|
2250
|
+
wodWikiCore.validateNoteBlocks(row.content, row.blocks, 1, { registeredWidgets: SEED_REGISTERED_WIDGETS });
|
|
2251
|
+
const indexed = /* @__PURE__ */ new Set();
|
|
2252
|
+
const sections = wodWikiCore.parseDocumentSections(row.content, { registeredWidgets: SEED_REGISTERED_WIDGETS });
|
|
2253
|
+
for (const projection of row.blockIndex) {
|
|
2254
|
+
if (!object(projection) || typeof projection.blockId !== "string") throw new Error("invalid seed projection");
|
|
2255
|
+
const block = row.blocks[projection.blockId];
|
|
2256
|
+
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");
|
|
2257
|
+
if ("segmentId" in projection || "segmentVersion" in projection) throw new Error("incompatible seed projection");
|
|
2258
|
+
for (const key of ["sourceId", "routeId", "blockContentId"]) if (projection[key] !== void 0 && typeof projection[key] !== "string") throw new Error("invalid seed projection metadata");
|
|
2259
|
+
if (projection.position !== void 0 && !nonnegative(projection.position)) throw new Error("invalid seed projection position");
|
|
2260
|
+
if (block.kind === "wod") {
|
|
2261
|
+
const section = sections.find((s) => s.from === block.from && s.to === block.to);
|
|
2262
|
+
if (projection.blockContentId !== wodWikiCore.blockContentId(section?.scriptBlock?.content ?? row.content.slice(Number(block.from), Number(block.to)))) throw new Error("invalid seed workout fingerprint");
|
|
2263
|
+
} else if (projection.blockContentId !== void 0) throw new Error("non-workout seed fingerprint");
|
|
2264
|
+
indexed.add(projection.blockId);
|
|
2265
|
+
}
|
|
2266
|
+
if (indexed.size !== Object.keys(row.blocks).length) throw new Error("missing actionable seed projection");
|
|
2267
|
+
const links = /* @__PURE__ */ new Set();
|
|
2268
|
+
for (const link of row.blockEfforts) {
|
|
2269
|
+
if (!object(link) || typeof link.blockId !== "string" || typeof link.noteId !== "string" || typeof link.effortSlug !== "string" || !link.effortSlug) throw new Error("invalid seed effort identity");
|
|
2270
|
+
const block = row.blocks[link.blockId];
|
|
2271
|
+
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");
|
|
2272
|
+
const projection = row.blockIndex.find((p) => object(p) && p.blockId === link.blockId);
|
|
2273
|
+
if (!object(projection) || link.noteId !== projection.noteId || link.blockContentId !== projection.blockContentId || !Number.isFinite(link.createdAt)) throw new Error("invalid seed effort metadata");
|
|
2274
|
+
links.add(String(link.id));
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
return value;
|
|
2399
2278
|
}
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
claim: null
|
|
2411
|
-
};
|
|
2279
|
+
|
|
2280
|
+
// src/cli/seedbuild/runtime.ts
|
|
2281
|
+
var SEED_NAMESPACE = "1b671a64-40d5-491e-99b0-da01ff1f3341";
|
|
2282
|
+
async function seedNoteId(path) {
|
|
2283
|
+
const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(`${SEED_NAMESPACE}:${path}`));
|
|
2284
|
+
const bytes = new Uint8Array(digest.slice(0, 16));
|
|
2285
|
+
bytes[6] = bytes[6] & 15 | 80;
|
|
2286
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
2287
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
2288
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
2412
2289
|
}
|
|
2413
|
-
|
|
2414
|
-
if (
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
});
|
|
2290
|
+
function equal(a, b) {
|
|
2291
|
+
if (Object.is(a, b)) return true;
|
|
2292
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
|
|
2293
|
+
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);
|
|
2294
|
+
return ak.length === bk.length && ak.every((key) => key in b && equal(Reflect.get(a, key), Reflect.get(b, key)));
|
|
2419
2295
|
}
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
const
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
);
|
|
2432
|
-
const
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
const
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
if (effort) {
|
|
2501
|
-
ops.push({ op: "put", store: "efforts", value: { ...effort, registrySource: "bundled" } });
|
|
2502
|
-
summary.efforts += 1;
|
|
2503
|
-
}
|
|
2504
|
-
}
|
|
2296
|
+
function titleFromPath(path) {
|
|
2297
|
+
return (path.split("/").at(-1) ?? path).replace(/\.md$/, "").replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2298
|
+
}
|
|
2299
|
+
function refreshedBlocks(existing, rawContent, sourceRevision) {
|
|
2300
|
+
const old = existing.rawContent;
|
|
2301
|
+
let from = 0, endOld = old.length, endNew = rawContent.length;
|
|
2302
|
+
while (from < endOld && from < endNew && old[from] === rawContent[from]) from++;
|
|
2303
|
+
while (endOld > from && endNew > from && old[endOld - 1] === rawContent[endNew - 1]) {
|
|
2304
|
+
endOld--;
|
|
2305
|
+
endNew--;
|
|
2306
|
+
}
|
|
2307
|
+
const intersect = Object.values(existing.blocks).filter((b) => from < b.to && endOld > b.from);
|
|
2308
|
+
const trusted = intersect.length <= 1 ? existing.blocks : Object.fromEntries(Object.entries(existing.blocks).filter(([, b]) => !intersect.includes(b)));
|
|
2309
|
+
const mapped = wodWikiCore.mapNoteBlockChanges(trusted, [{ from, to: endOld, insert: rawContent.slice(from, endNew) }]);
|
|
2310
|
+
let ordinal = 0;
|
|
2311
|
+
return wodWikiCore.reconcileNoteBlocks(rawContent, { sourceRevision, blocks: mapped, registeredWidgets: SEED_REGISTERED_WIDGETS, createId: () => `seed:${existing.id}:r${sourceRevision}:${ordinal++}` });
|
|
2312
|
+
}
|
|
2313
|
+
async function putChanged(store, values, keys) {
|
|
2314
|
+
if (!values.length) return;
|
|
2315
|
+
const prior = await store.getMany(keys);
|
|
2316
|
+
const changed = values.filter((value, i) => !equal(value, prior[i]));
|
|
2317
|
+
if (changed.length) await store.putMany(changed);
|
|
2318
|
+
}
|
|
2319
|
+
async function applySeedChunk(storage, write) {
|
|
2320
|
+
assertSeedManifest(write.manifest);
|
|
2321
|
+
assertSeedRows(write.rows);
|
|
2322
|
+
const result = { totalNotes: 0, totalEfforts: 0, totalBlocks: 0, skippedUserOwned: 0, deleted: 0 };
|
|
2323
|
+
const ids = await Promise.all(write.rows.map((row) => seedNoteId(row.path)));
|
|
2324
|
+
const pagesBySlug = /* @__PURE__ */ new Map();
|
|
2325
|
+
const linkIds = /* @__PURE__ */ new Map();
|
|
2326
|
+
const tagIds = /* @__PURE__ */ new Map();
|
|
2327
|
+
for (const [i, row] of write.rows.entries()) {
|
|
2328
|
+
const slug = readFrontmatterMeta(row.content)["slug"];
|
|
2329
|
+
if (typeof slug === "string" && slug.trim()) {
|
|
2330
|
+
pagesBySlug.set(slug, await seedNoteId(`page:${slug}`));
|
|
2331
|
+
linkIds.set(ids[i], await seedNoteId(`pagenote:${slug}:${row.path}`));
|
|
2332
|
+
}
|
|
2333
|
+
for (const { label } of extractTypedTags(row.content)) {
|
|
2334
|
+
if (!tagIds.has(label)) tagIds.set(label, await seedNoteId(`tag:${label}`));
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
return storage.transaction(["notes", "block_revisions", "block_index", "block_efforts", "page", "page_notes", "tags", "note_tags", "efforts", "meta"], "readwrite", async (tx) => {
|
|
2338
|
+
const metaStore = tx.readwrite("meta");
|
|
2339
|
+
const actualMeta = await metaStore.get(write.meta.key);
|
|
2340
|
+
if (!equal(actualMeta, write.expectedMeta)) throw new Error("Seed checkpoint changed during import");
|
|
2341
|
+
if (write.meta.schema !== SEED_SCHEMA || write.manifest.schema !== SEED_SCHEMA) throw new Error("Incompatible seed schema; recreate artifacts");
|
|
2342
|
+
const noteStore = tx.readwrite("notes");
|
|
2343
|
+
const priorIds = write.chunkId ? write.expectedMeta?.chunks[write.chunkId]?.noteIds ?? [] : [];
|
|
2344
|
+
const declared = new Set(ids);
|
|
2345
|
+
const goneIds = priorIds.filter((id) => !declared.has(id));
|
|
2346
|
+
const allIds = [.../* @__PURE__ */ new Set([...ids, ...goneIds])];
|
|
2347
|
+
const stored = await noteStore.getMany(allIds);
|
|
2348
|
+
const existingById = new Map(allIds.map((id, i) => [id, stored[i]]));
|
|
2349
|
+
const ownedIds = allIds.filter((id) => {
|
|
2350
|
+
const note = existingById.get(id);
|
|
2351
|
+
return !note || note.seedOrigin === "seed" && (declared.has(id) || note.seedChunkId === write.chunkId);
|
|
2352
|
+
});
|
|
2353
|
+
const owned = new Set(ownedIds);
|
|
2354
|
+
const revisionStore = tx.readwrite("block_revisions");
|
|
2355
|
+
const historyIds = ids.filter((id, i) => owned.has(id) && existingById.get(id)?.rawContent !== write.rows[i].content);
|
|
2356
|
+
const histories = await revisionStore.getAllFromIndexMany("by-note", historyIds);
|
|
2357
|
+
const history = new Map(historyIds.map((id, i) => [id, histories[i]]));
|
|
2358
|
+
const indexStore = tx.readwrite("block_index"), effortLinks = tx.readwrite("block_efforts");
|
|
2359
|
+
const tagLinks = tx.readwrite("note_tags"), pageLinks = tx.readwrite("page_notes");
|
|
2360
|
+
const currentIndexes = await indexStore.getAllFromIndexMany("by-note", ownedIds);
|
|
2361
|
+
const currentEfforts = await effortLinks.getAllFromIndexMany("by-note", ownedIds);
|
|
2362
|
+
const currentTags = await tagLinks.getAllFromIndexMany("by-note", ownedIds);
|
|
2363
|
+
const currentPages = await pageLinks.getAllFromIndexMany("by-note", ownedIds);
|
|
2364
|
+
const slot = new Map(ownedIds.map((id, i) => [id, i]));
|
|
2365
|
+
const tagsStore = tx.readwrite("tags");
|
|
2366
|
+
const labels = [...tagIds.keys()];
|
|
2367
|
+
const matches = await tagsStore.getAllFromIndexMany("by-label", labels);
|
|
2368
|
+
const tagsByLabel = new Map(labels.map((label, i) => [label, matches[i][0]]));
|
|
2369
|
+
const changedTags = [], changedNotes = [], revisions = [], pages = [], pageNotes = [], noteTags = [];
|
|
2370
|
+
const blocks = [], blockEfforts = [];
|
|
2371
|
+
for (const [i, row] of write.rows.entries()) {
|
|
2372
|
+
const id = ids[i], existing = existingById.get(id);
|
|
2373
|
+
if (!owned.has(id)) {
|
|
2374
|
+
result.skippedUserOwned++;
|
|
2375
|
+
continue;
|
|
2505
2376
|
}
|
|
2506
|
-
|
|
2507
|
-
const
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2377
|
+
wodWikiCore.validateNoteBlocks(row.content, row.blocks, 1, { registeredWidgets: SEED_REGISTERED_WIDGETS });
|
|
2378
|
+
const sourceRevision = existing ? existing.sourceRevision + (existing.rawContent === row.content ? 0 : 1) : 1;
|
|
2379
|
+
let ordinal = 0;
|
|
2380
|
+
const retainedHistory = history.get(id) ?? [];
|
|
2381
|
+
const map = !existing ? retainedHistory.length ? wodWikiCore.reconcileNoteBlocks(row.content, { sourceRevision, registeredWidgets: 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);
|
|
2382
|
+
for (const block of Object.values(map)) {
|
|
2383
|
+
const saved = (history.get(id) ?? []).filter((r) => r.blockId === block.id).sort((a, b) => b.version - a.version)[0];
|
|
2384
|
+
const source = row.content.slice(block.from, block.to);
|
|
2385
|
+
if (existing?.rawContent === row.content) continue;
|
|
2386
|
+
const same = saved && saved.kind === block.kind && saved.rawContent === source && saved.dialect === block.dialect && saved.sport === block.sport;
|
|
2387
|
+
block.version = !existing ? 1 : same ? saved.version : (saved?.version ?? 0) + 1;
|
|
2388
|
+
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 } : {} });
|
|
2511
2389
|
}
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2390
|
+
wodWikiCore.validateNoteBlocks(row.content, map, sourceRevision, { registeredWidgets: SEED_REGISTERED_WIDGETS });
|
|
2391
|
+
const fm = readFrontmatterMeta(row.content);
|
|
2392
|
+
const title = typeof fm["title"] === "string" && fm["title"].trim() ? fm["title"].trim() : titleFromPath(row.path);
|
|
2393
|
+
const sourceId = write.chunkId ? seedNoteSourceId(row.path, row.content, write.chunkId) : void 0;
|
|
2394
|
+
if (!sourceId) throw new Error(`Unknown seed source family ${write.chunkId}`);
|
|
2395
|
+
const date = typeof fm["date"] === "string" && /^\d{4}-\d{2}-\d{2}$/.test(fm["date"]) ? Date.parse(`${fm["date"]}T12:00:00Z`) : void 0;
|
|
2396
|
+
const note = { ...existing, id, title, rawContent: row.content, sourceRevision, blocks: map, createdAt: write.manifest.version, ...date === void 0 ? {} : { date }, type: write.chunkId === DASHBOARDS_CHUNK_ID ? "dashboard" : "note", sourceId, catalog: catalogForChunkId(write.chunkId ?? ""), seedOrigin: "seed", seedVersion: write.manifest.version, seedChunkId: write.chunkId ?? void 0, sourcePath: row.path };
|
|
2397
|
+
if (date === void 0) delete note.date;
|
|
2398
|
+
if (!equal(note, existing)) changedNotes.push(note);
|
|
2399
|
+
const byRange = new Map(Object.values(map).map((b) => [`${b.from}:${b.to}`, b]));
|
|
2400
|
+
const compiledById = new Map(row.blockIndex.map((b) => [b.blockId, b]));
|
|
2401
|
+
const remap = /* @__PURE__ */ new Map();
|
|
2402
|
+
for (const compiled of row.blockIndex) {
|
|
2403
|
+
const block = byRange.get(`${compiled.from}:${compiled.to}`);
|
|
2404
|
+
if (!block || block.kind !== compiled.dataType) throw new Error("Orphan seed projection");
|
|
2405
|
+
remap.set(compiled.blockId, block.id);
|
|
2406
|
+
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) };
|
|
2407
|
+
const prior = currentIndexes[slot.get(id)].find((b) => b.id === projection.id);
|
|
2408
|
+
if (!prior || prior.isStatic === true) blocks.push(projection);
|
|
2525
2409
|
}
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2410
|
+
for (const compiled of row.blockEfforts) {
|
|
2411
|
+
const blockId = remap.get(compiled.blockId);
|
|
2412
|
+
if (!blockId || compiledById.get(compiled.blockId)?.dataType !== "wod") throw new Error("Orphan seed effort link");
|
|
2413
|
+
const link = { ...compiled, id: `${id}:${blockId}:${compiled.effortSlug}`, noteId: id, blockId, sourcePath: row.path };
|
|
2414
|
+
const prior = currentEfforts[slot.get(id)].find((b) => b.id === link.id);
|
|
2415
|
+
if (!prior || prior.isStatic === true) blockEfforts.push(prior ? { ...link, createdAt: prior.createdAt } : link);
|
|
2416
|
+
}
|
|
2417
|
+
const n = slot.get(id);
|
|
2418
|
+
const wantedTags = /* @__PURE__ */ new Set();
|
|
2419
|
+
for (const item of extractTypedTags(row.content)) {
|
|
2420
|
+
let tag = tagsByLabel.get(item.label);
|
|
2421
|
+
if (!tag) {
|
|
2422
|
+
tag = { id: tagIds.get(item.label), label: item.label, type: item.type, createdAt: write.meta.importedAt };
|
|
2423
|
+
changedTags.push(tag);
|
|
2424
|
+
tagsByLabel.set(item.label, tag);
|
|
2425
|
+
} else if (item.type && !tag.type) {
|
|
2426
|
+
tag = { ...tag, type: item.type };
|
|
2427
|
+
changedTags.push(tag);
|
|
2428
|
+
tagsByLabel.set(item.label, tag);
|
|
2429
|
+
}
|
|
2430
|
+
if (wantedTags.has(tag.id)) continue;
|
|
2431
|
+
wantedTags.add(tag.id);
|
|
2432
|
+
const link = currentTags[n].find((l) => l.tagId === tag.id);
|
|
2433
|
+
if (!link) noteTags.push({ id: `${id}:${tag.id}`, noteId: id, tagId: tag.id });
|
|
2434
|
+
}
|
|
2435
|
+
for (const link of currentTags[n]) if (!wantedTags.has(link.tagId)) await tagLinks.delete(link.id);
|
|
2436
|
+
const slug = fm["slug"];
|
|
2437
|
+
if (typeof slug === "string" && pagesBySlug.has(slug)) {
|
|
2438
|
+
const pageId = pagesBySlug.get(slug);
|
|
2439
|
+
pages.push({ id: pageId, slug, title: titleFromPath(row.path), createdAt: write.manifest.version });
|
|
2440
|
+
pageNotes.push({ id: linkIds.get(id), pageId, noteId: id, createdAt: write.manifest.version });
|
|
2441
|
+
}
|
|
2442
|
+
result.totalNotes++;
|
|
2443
|
+
result.totalBlocks += Object.keys(map).length;
|
|
2444
|
+
}
|
|
2445
|
+
const wantedBlocks = new Set(blocks.map((b) => b.id)), wantedEfforts = new Set(blockEfforts.map((b) => b.id));
|
|
2446
|
+
for (const rows of currentIndexes) for (const row of rows) if (row.isStatic === true && !wantedBlocks.has(row.id)) await indexStore.delete(row.id);
|
|
2447
|
+
for (const rows of currentEfforts) for (const row of rows) if (row.isStatic === true && !wantedEfforts.has(row.id)) await effortLinks.delete(row.id);
|
|
2448
|
+
for (const id of goneIds) {
|
|
2449
|
+
if (!owned.has(id) || !existingById.get(id)) continue;
|
|
2450
|
+
const n = slot.get(id);
|
|
2451
|
+
await noteStore.delete(id);
|
|
2452
|
+
for (const link of currentTags[n]) await tagLinks.delete(link.id);
|
|
2453
|
+
for (const link of currentPages[n]) await pageLinks.delete(link.id);
|
|
2454
|
+
result.deleted++;
|
|
2455
|
+
}
|
|
2456
|
+
const effortStore = tx.readwrite("efforts");
|
|
2457
|
+
if (write.chunkId === EFFORTS_CHUNK_ID) {
|
|
2458
|
+
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" }));
|
|
2459
|
+
const prior = await effortStore.getMany(efforts.map((e) => e.slug));
|
|
2460
|
+
const changed = efforts.filter((e, i) => (!prior[i] || prior[i]?.registrySource === "bundled") && !equal(e, prior[i]));
|
|
2461
|
+
if (changed.length) await effortStore.putMany(changed);
|
|
2462
|
+
result.totalEfforts = changed.length;
|
|
2463
|
+
const wantedSlugs = new Set(efforts.map((effort) => effort.slug));
|
|
2464
|
+
const removed = allIds.map((id) => {
|
|
2465
|
+
const note = existingById.get(id);
|
|
2466
|
+
return owned.has(id) && note ? parseEffortFile(note.rawContent)?.slug : void 0;
|
|
2467
|
+
}).filter((slug) => !!slug && !wantedSlugs.has(slug));
|
|
2468
|
+
const old = await effortStore.getMany(removed);
|
|
2469
|
+
for (const [i, effort] of old.entries()) if (effort?.registrySource === "bundled") await effortStore.delete(removed[i]);
|
|
2470
|
+
}
|
|
2471
|
+
if (changedNotes.length) await noteStore.putMany(changedNotes);
|
|
2472
|
+
if (revisions.length) await revisionStore.putMany(revisions);
|
|
2473
|
+
await putChanged(indexStore, blocks, blocks.map((b) => b.id));
|
|
2474
|
+
await putChanged(effortLinks, blockEfforts, blockEfforts.map((b) => b.id));
|
|
2475
|
+
if (changedTags.length) await tagsStore.putMany([...new Map(changedTags.map((t) => [t.id, t])).values()]);
|
|
2476
|
+
if (noteTags.length) await tagLinks.putMany(noteTags);
|
|
2477
|
+
await putChanged(tx.readwrite("page"), pages, pages.map((p) => p.id));
|
|
2478
|
+
await putChanged(pageLinks, pageNotes, pageNotes.map((p) => p.id));
|
|
2479
|
+
await metaStore.put(write.meta);
|
|
2480
|
+
return result;
|
|
2481
|
+
});
|
|
2559
2482
|
}
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2483
|
+
|
|
2484
|
+
// src/cli/seed.ts
|
|
2485
|
+
init_contract();
|
|
2486
|
+
async function importSeedDir(db, dir, sourceId) {
|
|
2487
|
+
const fs = nodeFs(), path = nodePath();
|
|
2488
|
+
const source = sourceId || path.basename(dir);
|
|
2489
|
+
const manifest = assertSeedManifest(JSON.parse(fs.readFileSync(path.resolve(dir, "manifest.json"), "utf-8")));
|
|
2490
|
+
const key = seedMetaKey(source);
|
|
2491
|
+
let expected = await db.storage.readonly("meta").get(key);
|
|
2492
|
+
if (expected && expected.schema !== SEED_SCHEMA) throw new Error("Incompatible seed checkpoint; recreate storage");
|
|
2493
|
+
let meta = expected ?? emptySeedMeta(key);
|
|
2494
|
+
if (manifest.version < meta.seedVersion) throw new Error("Seed manifest is older than stored checkpoint");
|
|
2495
|
+
const summary = { sourceId: source, version: manifest.version, notes: 0, efforts: 0, blocks: 0, blockEfforts: 0, deleted: 0, skippedUserOwned: 0 };
|
|
2496
|
+
const metadataChanged = manifest.version !== meta.seedVersion || manifest.builtAt !== meta.builtAt;
|
|
2497
|
+
const plan = manifest.chunks.filter((c) => metadataChanged || meta.chunks[c.id]?.sha256 !== c.sha256);
|
|
2498
|
+
plan.sort((a, b) => Number(b.id === CANVAS_CHUNK_ID) - Number(a.id === CANVAS_CHUNK_ID));
|
|
2499
|
+
const accumulate = (result) => {
|
|
2500
|
+
summary.notes += result.totalNotes;
|
|
2501
|
+
summary.efforts += result.totalEfforts;
|
|
2502
|
+
summary.blocks += result.totalBlocks;
|
|
2503
|
+
summary.deleted += result.deleted;
|
|
2504
|
+
summary.skippedUserOwned += result.skippedUserOwned;
|
|
2505
|
+
};
|
|
2506
|
+
for (const chunk of plan) {
|
|
2507
|
+
const rows = assertSeedRows(JSON.parse(fs.readFileSync(path.resolve(dir, chunk.path), "utf-8")));
|
|
2508
|
+
if (rows.length !== chunk.count) throw new Error("Seed chunk count mismatch");
|
|
2509
|
+
const next = { ...meta, chunks: { ...meta.chunks, [chunk.id]: { sha256: chunk.sha256, noteIds: await Promise.all(rows.map((r) => seedNoteId(r.path))) } }, importedAt: Date.now() };
|
|
2510
|
+
accumulate(await applySeedChunk(db.storage, { chunkId: chunk.id, rows, manifest, expectedMeta: expected, meta: next }));
|
|
2511
|
+
summary.blockEfforts += rows.reduce((count, row) => count + row.blockEfforts.length, 0);
|
|
2512
|
+
meta = next;
|
|
2513
|
+
expected = next;
|
|
2514
|
+
}
|
|
2515
|
+
for (const chunkId of Object.keys(meta.chunks)) {
|
|
2516
|
+
if (manifest.chunks.some((c) => c.id === chunkId)) continue;
|
|
2517
|
+
const chunks = { ...meta.chunks };
|
|
2518
|
+
delete chunks[chunkId];
|
|
2519
|
+
const next = { ...meta, chunks, importedAt: Date.now() };
|
|
2520
|
+
accumulate(await applySeedChunk(db.storage, { chunkId, rows: [], manifest, expectedMeta: expected, meta: next }));
|
|
2521
|
+
meta = next;
|
|
2522
|
+
expected = next;
|
|
2523
|
+
}
|
|
2524
|
+
if (plan.length || metadataChanged || Object.keys(meta.chunks).length !== Object.keys(expected?.chunks ?? {}).length) {
|
|
2525
|
+
const next = { ...meta, schema: SEED_SCHEMA, seedVersion: manifest.version, builtAt: manifest.builtAt, importedAt: Date.now() };
|
|
2526
|
+
await applySeedChunk(db.storage, { chunkId: null, rows: [], manifest, expectedMeta: expected, meta: next });
|
|
2567
2527
|
}
|
|
2568
|
-
return
|
|
2528
|
+
return summary;
|
|
2569
2529
|
}
|
|
2570
2530
|
function defaultSeedDirs(cwd) {
|
|
2571
2531
|
const base = nodePath().resolve(cwd, "apps", "playground", "public", "seed");
|
|
@@ -2860,8 +2820,6 @@ ${escapeCsvField2(data.parsed.metric)},${data.scalar},${escapeCsvField2(data.uni
|
|
|
2860
2820
|
}
|
|
2861
2821
|
return lines.join("\n");
|
|
2862
2822
|
}
|
|
2863
|
-
|
|
2864
|
-
// src/cli/runner.ts
|
|
2865
2823
|
function getFs2() {
|
|
2866
2824
|
if (typeof globalThis.process?.versions?.node === "undefined" && typeof globalThis.Bun === "undefined") {
|
|
2867
2825
|
throw new Error("File system operations are only supported in Node / Bun environments");
|
|
@@ -2869,6 +2827,16 @@ function getFs2() {
|
|
|
2869
2827
|
return __require("fs");
|
|
2870
2828
|
}
|
|
2871
2829
|
var VERSION = "0.6.0";
|
|
2830
|
+
function cliNoteSource(source, label, sport) {
|
|
2831
|
+
if (/\.(?:md|markdown)$/i.test(label) || wodWikiCore.parseDocumentSections(source).some((section) => section.scriptBlock)) return source;
|
|
2832
|
+
let length = 3;
|
|
2833
|
+
for (const match of source.matchAll(/`+/g)) length = Math.max(length, match[0].length + 1);
|
|
2834
|
+
const fence = "`".repeat(length);
|
|
2835
|
+
return `${fence}time${sport ? `:${sport}` : ""}
|
|
2836
|
+
${source}
|
|
2837
|
+
${fence}
|
|
2838
|
+
`;
|
|
2839
|
+
}
|
|
2872
2840
|
function parseCliArgs(args) {
|
|
2873
2841
|
const result = {
|
|
2874
2842
|
format: "json",
|
|
@@ -2878,6 +2846,7 @@ function parseCliArgs(args) {
|
|
|
2878
2846
|
let i = 0;
|
|
2879
2847
|
while (i < args.length) {
|
|
2880
2848
|
const arg = args[i];
|
|
2849
|
+
if (arg === "--at" || arg.startsWith("--at=")) throw new Error("Unknown option --at; whole Note exports have no as-of version");
|
|
2881
2850
|
if (arg === "--help" || arg === "-h") {
|
|
2882
2851
|
result.help = true;
|
|
2883
2852
|
i++;
|
|
@@ -2936,12 +2905,6 @@ function parseCliArgs(args) {
|
|
|
2936
2905
|
} else if (arg.startsWith("--debug=")) {
|
|
2937
2906
|
result.debug = arg.slice("--debug=".length);
|
|
2938
2907
|
i++;
|
|
2939
|
-
} else if (arg === "--at") {
|
|
2940
|
-
result.at = args[++i];
|
|
2941
|
-
i++;
|
|
2942
|
-
} else if (arg.startsWith("--at=")) {
|
|
2943
|
-
result.at = arg.slice("--at=".length);
|
|
2944
|
-
i++;
|
|
2945
2908
|
} else if (arg === "--source") {
|
|
2946
2909
|
const source = args[++i];
|
|
2947
2910
|
if (source) result.sources.push(source);
|
|
@@ -3035,7 +2998,7 @@ USAGE:
|
|
|
3035
2998
|
|
|
3036
2999
|
SUBCOMMANDS:
|
|
3037
3000
|
parse <file | -> Parse Whiteboard Script, create a note in the database
|
|
3038
|
-
(
|
|
3001
|
+
(whole source + saved revisions + sparse block index), optionally emit parse-tree IR.
|
|
3039
3002
|
Markdown carrying a 'wod-note-id' frontmatter stamp
|
|
3040
3003
|
(see download) imports back into that same note.
|
|
3041
3004
|
run <file | -> Execute the script state machine, record a session +
|
|
@@ -3043,10 +3006,8 @@ SUBCOMMANDS:
|
|
|
3043
3006
|
execution-log IR
|
|
3044
3007
|
query '<wql>' Evaluate WQL against the database (or an explicit
|
|
3045
3008
|
dataset) and emit QueryResult / RowsQueryResult IR
|
|
3046
|
-
download note <id>
|
|
3047
|
-
|
|
3048
|
-
as-of a segment version (default: latest). The stamp
|
|
3049
|
-
in the frontmatter makes 'wod parse' re-import it.
|
|
3009
|
+
download note <id> Export the note's authored markdown to <id>.md.
|
|
3010
|
+
The frontmatter identity stamp allows re-import.
|
|
3050
3011
|
Frontmatter tags (general + typed equipment/quality/\u2026)
|
|
3051
3012
|
are materialized into the tag graph on parse/import.
|
|
3052
3013
|
download session <id>
|
|
@@ -3063,8 +3024,8 @@ SUBCOMMANDS:
|
|
|
3063
3024
|
import note <file> Re-import exported note markdown (same as parse with
|
|
3064
3025
|
the stamped identity).
|
|
3065
3026
|
import session <file>
|
|
3066
|
-
|
|
3067
|
-
|
|
3027
|
+
Import a session + its events from a downloaded CSV.
|
|
3028
|
+
The exact saved block revision must already exist.
|
|
3068
3029
|
import effort <file>
|
|
3069
3030
|
Upsert an effort registry row from corpus-style markdown.
|
|
3070
3031
|
import equipment <file>
|
|
@@ -3072,7 +3033,7 @@ SUBCOMMANDS:
|
|
|
3072
3033
|
build [dir] Compile a seed corpus: validate every .md under
|
|
3073
3034
|
<dir>/markdown (parity gate + dashboard format gate) and
|
|
3074
3035
|
write manifest.json + content-hashed chunks/ +
|
|
3075
|
-
|
|
3036
|
+
report.html to <dir>/dist with source/maps/projections bundled
|
|
3076
3037
|
(override with -o/--output). Version stamps derive from
|
|
3077
3038
|
<dir>'s git history (full checkout required).
|
|
3078
3039
|
|
|
@@ -3100,8 +3061,6 @@ OPTIONS:
|
|
|
3100
3061
|
--format <fmt> Output format: json (default), table, csv
|
|
3101
3062
|
-o, --output <path> Write the IR payload (or download) to a path instead of
|
|
3102
3063
|
the default; '-' writes to stdout
|
|
3103
|
-
--at <version> With 'download note': compose the markdown as-of the
|
|
3104
|
-
given segment version (default: latest)
|
|
3105
3064
|
--debug[=name] Also write the IR payload to <name> (default
|
|
3106
3065
|
wod-<subcommand>-debug.json); without it, parse/run
|
|
3107
3066
|
print a one-line summary of what was persisted
|
|
@@ -3130,7 +3089,13 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3130
3089
|
const writeOut = io.stdout ?? ((t) => process.stdout.write(t + "\n"));
|
|
3131
3090
|
const writeErr = io.stderr ?? ((t) => process.stderr.write(t + "\n"));
|
|
3132
3091
|
const getStdin = io.readStdinFn ?? readStdin;
|
|
3133
|
-
|
|
3092
|
+
let parsed;
|
|
3093
|
+
try {
|
|
3094
|
+
parsed = parseCliArgs(argv);
|
|
3095
|
+
} catch (error) {
|
|
3096
|
+
writeErr(error instanceof Error ? error.message : String(error));
|
|
3097
|
+
return 1;
|
|
3098
|
+
}
|
|
3134
3099
|
if (parsed.version || parsed.subcommand === "version") {
|
|
3135
3100
|
writeOut(`wod v${VERSION}`);
|
|
3136
3101
|
return 0;
|
|
@@ -3156,7 +3121,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3156
3121
|
}
|
|
3157
3122
|
}
|
|
3158
3123
|
const deliverOutput = (formattedText) => {
|
|
3159
|
-
if (parsed.output) {
|
|
3124
|
+
if (parsed.output && parsed.output !== "-") {
|
|
3160
3125
|
getFs2().writeFileSync(parsed.output, formattedText, "utf-8");
|
|
3161
3126
|
} else {
|
|
3162
3127
|
writeOut(formattedText);
|
|
@@ -3186,20 +3151,21 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3186
3151
|
}
|
|
3187
3152
|
source = fs.readFileSync(parsed.target, "utf-8");
|
|
3188
3153
|
}
|
|
3154
|
+
const noteSource = cliNoteSource(source, parsed.target, parsed.sport);
|
|
3189
3155
|
const ir = runParse(source, { sport: parsed.sport, sourceLabel: `cli:wod parse ${parsed.target}` });
|
|
3190
3156
|
const identity = extractNoteIdentity(source);
|
|
3191
3157
|
const tags = extractTypedTags(source);
|
|
3192
3158
|
const db = await openCliDb(parsed.db);
|
|
3193
3159
|
try {
|
|
3194
3160
|
const persisted = await persistNote(db, {
|
|
3195
|
-
source,
|
|
3161
|
+
source: noteSource,
|
|
3196
3162
|
sourceLabel: parsed.target,
|
|
3197
3163
|
noteId: identity?.noteId,
|
|
3198
3164
|
title: identity?.title,
|
|
3199
3165
|
tags
|
|
3200
3166
|
});
|
|
3201
3167
|
writeOut(
|
|
3202
|
-
`[wod parse] note ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.
|
|
3168
|
+
`[wod parse] note ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.blocks} blocks, ${persisted.efforts} efforts${persisted.tags > 0 ? `, ${persisted.tags} tags` : ""} \u2192 ${db.path}`
|
|
3203
3169
|
);
|
|
3204
3170
|
} finally {
|
|
3205
3171
|
await db.close();
|
|
@@ -3227,14 +3193,30 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3227
3193
|
}
|
|
3228
3194
|
source = fs.readFileSync(parsed.target, "utf-8");
|
|
3229
3195
|
}
|
|
3230
|
-
const
|
|
3196
|
+
const identity = extractNoteIdentity(source);
|
|
3231
3197
|
const db = await openCliDb(parsed.db);
|
|
3198
|
+
let ir;
|
|
3232
3199
|
try {
|
|
3233
|
-
const
|
|
3234
|
-
source,
|
|
3200
|
+
const started = await startRun(db, {
|
|
3201
|
+
source: cliNoteSource(source, parsed.target, parsed.sport),
|
|
3235
3202
|
sourceLabel: parsed.target,
|
|
3236
|
-
|
|
3203
|
+
noteId: identity?.noteId,
|
|
3204
|
+
title: identity?.title,
|
|
3205
|
+
tags: extractTypedTags(source)
|
|
3237
3206
|
});
|
|
3207
|
+
const section = wodWikiCore.parseDocumentSections(started.revision.rawContent).find((row) => row.scriptBlock);
|
|
3208
|
+
if (!section?.scriptBlock) throw new Error("[wod run] Captured workout source cannot be parsed");
|
|
3209
|
+
const bare = !/\.(?:md|markdown)$/i.test(parsed.target) && !wodWikiCore.parseDocumentSections(source).some((row) => row.scriptBlock);
|
|
3210
|
+
const frozenBody = bare ? section.scriptBlock.content.slice(0, -1) : section.scriptBlock.content;
|
|
3211
|
+
ir = await runExecution(frozenBody, {
|
|
3212
|
+
sport: started.revision.sport,
|
|
3213
|
+
dialect: started.revision.dialect,
|
|
3214
|
+
blockId: started.session.blockId,
|
|
3215
|
+
blockVersion: started.session.blockVersion,
|
|
3216
|
+
startTime: started.session.startTime,
|
|
3217
|
+
sourceLabel: `cli:wod run ${parsed.target}`
|
|
3218
|
+
});
|
|
3219
|
+
const persisted = await persistRun(db, started, ir.data);
|
|
3238
3220
|
writeOut(
|
|
3239
3221
|
`[wod run] session ${persisted.resultId} \u2014 ${persisted.events} events, ${persisted.summaries} summaries on note ${persisted.noteId} \u2192 ${db.path}`
|
|
3240
3222
|
);
|
|
@@ -3312,7 +3294,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3312
3294
|
const id = parsed.target2;
|
|
3313
3295
|
const DOWNLOAD_KINDS = ["note", "session", "effort", "equipment"];
|
|
3314
3296
|
if (!id && kind !== "equipment" || !DOWNLOAD_KINDS.includes(kind ?? "")) {
|
|
3315
|
-
writeErr("[wod download] Usage: wod download <note|session|effort|equipment> <id> [
|
|
3297
|
+
writeErr("[wod download] Usage: wod download <note|session|effort|equipment> <id> [-o <path>]");
|
|
3316
3298
|
return 1;
|
|
3317
3299
|
}
|
|
3318
3300
|
const targetId = id;
|
|
@@ -3324,21 +3306,14 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3324
3306
|
writeErr(`[wod download] Note not found in ${db.path}: ${id}`);
|
|
3325
3307
|
return 1;
|
|
3326
3308
|
}
|
|
3327
|
-
const
|
|
3328
|
-
|
|
3329
|
-
writeErr(`[wod download] Invalid --at version: ${parsed.at}`);
|
|
3330
|
-
return 1;
|
|
3331
|
-
}
|
|
3332
|
-
const markdown = composeNoteMarkdown(bundle.note, bundle.segments, at);
|
|
3333
|
-
const target = parsed.output && parsed.output !== "-" ? parsed.output : `${id}${at ? `.v${at}` : ""}.md`;
|
|
3309
|
+
const markdown = composeNoteMarkdown(bundle.note);
|
|
3310
|
+
const target = parsed.output && parsed.output !== "-" ? parsed.output : `${id}.md`;
|
|
3334
3311
|
if (parsed.output === "-") {
|
|
3335
3312
|
writeOut(markdown);
|
|
3336
3313
|
} else {
|
|
3337
3314
|
nodeFs().writeFileSync(target, markdown, "utf-8");
|
|
3338
3315
|
}
|
|
3339
|
-
writeOut(
|
|
3340
|
-
`[wod download] note ${id}${at ? ` v${at}` : ""} \u2192 ${target} (${bundle.segments.length} segment rows)`
|
|
3341
|
-
);
|
|
3316
|
+
if (parsed.output !== "-") writeOut(`[wod download] note ${id} \u2192 ${target}`);
|
|
3342
3317
|
} else if (kind === "session") {
|
|
3343
3318
|
const bundle = await getSessionBundle(db.db, targetId);
|
|
3344
3319
|
if (!bundle) {
|
|
@@ -3352,7 +3327,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3352
3327
|
} else {
|
|
3353
3328
|
nodeFs().writeFileSync(target, csv, "utf-8");
|
|
3354
3329
|
}
|
|
3355
|
-
writeOut(`[wod download] session ${id} \u2192 ${target} (${bundle.events.length} events)`);
|
|
3330
|
+
if (parsed.output !== "-") writeOut(`[wod download] session ${id} \u2192 ${target} (${bundle.events.length} events)`);
|
|
3356
3331
|
} else if (kind === "effort") {
|
|
3357
3332
|
const effort = await getEffortBySlug(db.db, targetId);
|
|
3358
3333
|
if (!effort) {
|
|
@@ -3410,7 +3385,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3410
3385
|
tags
|
|
3411
3386
|
});
|
|
3412
3387
|
writeOut(
|
|
3413
|
-
`[wod import] note ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.
|
|
3388
|
+
`[wod import] note ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.blocks} blocks${persisted.tags > 0 ? `, ${persisted.tags} tags` : ""} \u2192 ${db2.path}`
|
|
3414
3389
|
);
|
|
3415
3390
|
} finally {
|
|
3416
3391
|
await db2.close();
|
|
@@ -3452,7 +3427,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3452
3427
|
try {
|
|
3453
3428
|
const result = await persistSessionBundle(db, session, events);
|
|
3454
3429
|
writeOut(
|
|
3455
|
-
`[wod import] session ${session.id} \u2014 ${result.events} events
|
|
3430
|
+
`[wod import] session ${session.id} \u2014 ${result.events} events \u2192 ${db.path}`
|
|
3456
3431
|
);
|
|
3457
3432
|
} finally {
|
|
3458
3433
|
await db.close();
|
|
@@ -3478,7 +3453,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
3478
3453
|
const totalBytes = result.manifest.chunks.reduce((s, c) => s + c.bytes, 0);
|
|
3479
3454
|
writeOut(`[wod build] schema v${result.manifest.schema} version ${result.manifest.version} (${result.manifest.versionLabel}) \u2192 ${result.outDir}`);
|
|
3480
3455
|
for (const c of result.manifest.chunks) {
|
|
3481
|
-
writeOut(` ${c.id.padEnd(48)} ${String(c.count).padStart(4)}
|
|
3456
|
+
writeOut(` ${c.id.padEnd(48)} ${String(c.count).padStart(4)} files ${kb(c.bytes).padStart(8)}`);
|
|
3482
3457
|
}
|
|
3483
3458
|
writeOut(`[wod build] ${result.manifest.chunks.length} chunks, ${result.totalFiles} files, ${kb(totalBytes)} total \u2014 scan ${result.passMs.scan}ms, block-index ${result.passMs.blockIndex}ms, block-efforts ${result.passMs.blockEfforts}ms, write ${result.passMs.write}ms`);
|
|
3484
3459
|
return 0;
|