@bitcobblers/wod-wiki-engine 0.35.2115 → 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 +10 -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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createRequire } from 'module';
|
|
2
|
-
import { __require } from './chunk-
|
|
2
|
+
import { SEED_SCHEMA, SEED_REGISTERED_WIDGETS, seedNoteSourceId, catalogForChunkId, DASHBOARDS_CHUNK_ID, EFFORTS_CHUNK_ID, parseEffortFile, __require, seedMetaKey, emptySeedMeta, CANVAS_CHUNK_ID } from './chunk-Y6WO6MXS.mjs';
|
|
3
3
|
import { dialectRegistry, realtimeProcessorRegistry, summaryProcessorRegistry, getHints, createParser, toStoredOutputStatement, frozenNow, createCompiler, createMockClock, RuntimeStack, EventBus, ScriptRuntime, createAnalyticsEngineForBlock, StartSessionAction, NextEvent, bundledEfforts, parseScript } from '@bitcobblers/wod-wiki-lang';
|
|
4
|
-
import { MetricType, parseDocumentSections } from '@bitcobblers/wod-wiki-core';
|
|
5
|
-
import { WQL_TYPED_TAG_KEYS, toEventRows, parseQuery, QueryService, isPipelineQuery, isFindQuery
|
|
4
|
+
import { MetricType, validateNoteBlocks, parseDocumentSections, blockContentId, reconcileNoteBlocks, mapNoteBlockChanges, KNOWN_SECTION_WIDGETS } from '@bitcobblers/wod-wiki-core';
|
|
5
|
+
import { WQL_TYPED_TAG_KEYS, toEventRows, toSummaryEventRows, parseQuery, QueryService, isPipelineQuery, isFindQuery } from '@bitcobblers/wod-wiki-wql';
|
|
6
|
+
import { rowsEqual } from '@bitcobblers/wod-wiki-storage';
|
|
6
7
|
|
|
7
8
|
if (typeof globalThis.require === 'undefined') {
|
|
8
9
|
try { globalThis.require = createRequire(import.meta.url); } catch {}
|
|
@@ -67,7 +68,7 @@ function inMemoryEventStore(events) {
|
|
|
67
68
|
getEventsByTimeRange: async (start, end) => rows.filter((r) => r.timestamp >= start && r.timestamp <= end),
|
|
68
69
|
getEventsByResult: async (resultId) => rows.filter((r) => r.resultId === resultId),
|
|
69
70
|
getEventsForNote: async (noteId) => rows.filter((r) => r.noteId === noteId),
|
|
70
|
-
getEventsByContent: async (
|
|
71
|
+
getEventsByContent: async (blockContentId4) => rows.filter((r) => r.blockContentId === blockContentId4),
|
|
71
72
|
scanAll: async () => rows,
|
|
72
73
|
appendEvents: async (appended) => {
|
|
73
74
|
rows.push(...appended);
|
|
@@ -201,14 +202,14 @@ async function runExecution(source, options = {}) {
|
|
|
201
202
|
now
|
|
202
203
|
);
|
|
203
204
|
const { engine, analyticsContext } = createAnalyticsEngineForBlock({
|
|
204
|
-
id: "cli-block",
|
|
205
|
+
id: options.blockId ?? "cli-block",
|
|
205
206
|
content: source,
|
|
206
207
|
statements: script.statements,
|
|
207
208
|
sport: options.sport,
|
|
208
|
-
version: 1,
|
|
209
|
+
version: options.blockVersion ?? 1,
|
|
209
210
|
startLine: 0,
|
|
210
211
|
endLine: 0,
|
|
211
|
-
dialect: "time",
|
|
212
|
+
dialect: options.dialect ?? "time",
|
|
212
213
|
state: "idle",
|
|
213
214
|
createdAt: Date.now()
|
|
214
215
|
});
|
|
@@ -238,7 +239,8 @@ async function runExecution(source, options = {}) {
|
|
|
238
239
|
{
|
|
239
240
|
results: workoutResults,
|
|
240
241
|
logs,
|
|
241
|
-
statements: logs
|
|
242
|
+
statements: logs,
|
|
243
|
+
sourceScript: source
|
|
242
244
|
},
|
|
243
245
|
{ source: options.sourceLabel ?? "cli:wod run" }
|
|
244
246
|
);
|
|
@@ -259,10 +261,6 @@ var WqlSyntaxError = class extends Error {
|
|
|
259
261
|
query;
|
|
260
262
|
syntaxError;
|
|
261
263
|
};
|
|
262
|
-
function legacyLogsOf(result) {
|
|
263
|
-
const legacy = result;
|
|
264
|
-
return legacy.data?.logs ?? [];
|
|
265
|
-
}
|
|
266
264
|
function extractPayload(rawJson) {
|
|
267
265
|
try {
|
|
268
266
|
const parsed = JSON.parse(rawJson);
|
|
@@ -275,16 +273,6 @@ function extractPayload(rawJson) {
|
|
|
275
273
|
}
|
|
276
274
|
}
|
|
277
275
|
function buildStoresFromData(data) {
|
|
278
|
-
const identity = (r) => ({
|
|
279
|
-
noteId: r.noteId,
|
|
280
|
-
resultId: r.id,
|
|
281
|
-
segmentId: r.segmentId,
|
|
282
|
-
segmentVersion: r.segmentVersion,
|
|
283
|
-
blockContentId: r.blockContentId,
|
|
284
|
-
origin: r.origin,
|
|
285
|
-
pageId: r.pageId,
|
|
286
|
-
workoutTimestamp: r.createdAt
|
|
287
|
-
});
|
|
288
276
|
const events = [
|
|
289
277
|
...data.eventRecords,
|
|
290
278
|
...data.facts.map((f, i) => ({
|
|
@@ -304,17 +292,17 @@ function buildStoresFromData(data) {
|
|
|
304
292
|
...f.unit ? { unit: f.unit } : {},
|
|
305
293
|
origin: "engine"
|
|
306
294
|
}],
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}))
|
|
310
|
-
...data.results.flatMap((r) => {
|
|
311
|
-
const logs = legacyLogsOf(r);
|
|
312
|
-
return [...toEventRows(logs, identity(r)), ...toSummaryEventRows(logs, identity(r))];
|
|
313
|
-
})
|
|
295
|
+
blockId: f.blockId,
|
|
296
|
+
blockVersion: f.blockVersion
|
|
297
|
+
}))
|
|
314
298
|
];
|
|
315
299
|
const eventStore = inMemoryEventStore(events);
|
|
316
300
|
const noteStore = {
|
|
317
301
|
getAllNotes: async () => [...data.notes],
|
|
302
|
+
getNotesByIds: async (ids) => {
|
|
303
|
+
const selected = new Set(ids);
|
|
304
|
+
return data.notes.filter((note) => selected.has(note.id));
|
|
305
|
+
},
|
|
318
306
|
getNoteIdsForTag: async (tag) => {
|
|
319
307
|
const matching = /* @__PURE__ */ new Set();
|
|
320
308
|
for (const [noteId, tags] of Object.entries(data.noteTags)) {
|
|
@@ -339,6 +327,7 @@ function loadQueryData(options) {
|
|
|
339
327
|
results: [],
|
|
340
328
|
notes: [],
|
|
341
329
|
blocks: [],
|
|
330
|
+
revisions: [],
|
|
342
331
|
efforts: [...bundledEfforts],
|
|
343
332
|
noteTags: {}
|
|
344
333
|
};
|
|
@@ -353,7 +342,10 @@ function loadQueryData(options) {
|
|
|
353
342
|
data.facts = payload;
|
|
354
343
|
} else if (payload && typeof payload === "object") {
|
|
355
344
|
const obj = payload;
|
|
345
|
+
if ("segments" in obj) throw new Error("Incompatible corpus segments; whole Notes and block revisions are required");
|
|
356
346
|
if (obj.kind === "event-journal" || Array.isArray(obj.records)) {
|
|
347
|
+
if (Array.isArray(obj.blockRevisions)) data.revisions = obj.blockRevisions;
|
|
348
|
+
if (Array.isArray(obj.sessions)) data.results = obj.sessions;
|
|
357
349
|
if (Array.isArray(obj.records)) {
|
|
358
350
|
data.eventRecords = obj.records;
|
|
359
351
|
}
|
|
@@ -372,20 +364,16 @@ function loadQueryData(options) {
|
|
|
372
364
|
if (corpus.notes) data.notes = corpus.notes;
|
|
373
365
|
if (corpus.blocks) data.blocks = corpus.blocks;
|
|
374
366
|
if (corpus.efforts) data.efforts = corpus.efforts;
|
|
367
|
+
if (corpus.revisions) data.revisions = corpus.revisions;
|
|
375
368
|
if (corpus.tags) data.noteTags = corpus.tags;
|
|
376
|
-
if (corpus.logs
|
|
377
|
-
const
|
|
378
|
-
id: "corpus-result-1",
|
|
369
|
+
if (corpus.logs) {
|
|
370
|
+
const identity = {
|
|
379
371
|
noteId: "corpus-note-1",
|
|
380
|
-
|
|
372
|
+
resultId: "corpus-result-1",
|
|
381
373
|
origin: "journal",
|
|
382
|
-
|
|
383
|
-
startTime: corpus.logs[0]?.timeSpan?.started ?? Date.now(),
|
|
384
|
-
endTime: corpus.logs[corpus.logs.length - 1]?.timeSpan?.ended ?? Date.now(),
|
|
385
|
-
duration: 0,
|
|
386
|
-
completed: true
|
|
374
|
+
workoutTimestamp: corpus.logs[0]?.timeSpan?.started ?? Date.now()
|
|
387
375
|
};
|
|
388
|
-
data.
|
|
376
|
+
data.eventRecords = [...toEventRows(corpus.logs, identity), ...toSummaryEventRows(corpus.logs, identity)];
|
|
389
377
|
}
|
|
390
378
|
} else {
|
|
391
379
|
throw new Error(`Unrecognized corpus payload shape in "${options.corpusPath}" (expected event-journal, fact-set, or CorpusIRData)`);
|
|
@@ -400,29 +388,16 @@ function loadQueryData(options) {
|
|
|
400
388
|
} else {
|
|
401
389
|
executionLog = { results: payload, logs: rawLogs, statements: rawLogs };
|
|
402
390
|
}
|
|
403
|
-
const
|
|
404
|
-
const workoutResult = {
|
|
405
|
-
id: resultId,
|
|
391
|
+
const identity = {
|
|
406
392
|
noteId: "stdin-note-1",
|
|
407
|
-
|
|
393
|
+
resultId: "stdin-result-1",
|
|
408
394
|
origin: "journal",
|
|
409
|
-
|
|
410
|
-
startTime: executionLog.results.startTime,
|
|
411
|
-
endTime: executionLog.results.endTime,
|
|
412
|
-
duration: executionLog.results.duration ?? 0,
|
|
413
|
-
completed: executionLog.results.completed
|
|
395
|
+
workoutTimestamp: executionLog.results.startTime
|
|
414
396
|
};
|
|
415
|
-
data.
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
segmentId: workoutResult.segmentId,
|
|
420
|
-
segmentVersion: workoutResult.segmentVersion,
|
|
421
|
-
blockContentId: workoutResult.blockContentId,
|
|
422
|
-
origin: workoutResult.origin,
|
|
423
|
-
pageId: workoutResult.pageId,
|
|
424
|
-
workoutTimestamp: workoutResult.createdAt
|
|
425
|
-
});
|
|
397
|
+
data.eventRecords = [
|
|
398
|
+
...toEventRows(executionLog.logs, identity),
|
|
399
|
+
...toSummaryEventRows(executionLog.logs, identity)
|
|
400
|
+
];
|
|
426
401
|
} else if (options.stdinFacts) {
|
|
427
402
|
const payload = extractPayload(options.stdinFacts);
|
|
428
403
|
if (Array.isArray(payload)) {
|
|
@@ -433,6 +408,60 @@ function loadQueryData(options) {
|
|
|
433
408
|
} else {
|
|
434
409
|
throw new Error("No input dataset provided. Specify --corpus <path>, --stdin-log, or --stdin-facts.");
|
|
435
410
|
}
|
|
411
|
+
for (const note of data.notes) {
|
|
412
|
+
if (typeof note.rawContent !== "string" || !Number.isSafeInteger(note.sourceRevision) || note.sourceRevision < 1) {
|
|
413
|
+
throw new Error("Incompatible corpus Note; exact rawContent and sourceRevision are required");
|
|
414
|
+
}
|
|
415
|
+
validateNoteBlocks(note.rawContent, note.blocks, note.sourceRevision);
|
|
416
|
+
if (!data.blocks.some((row) => row.noteId === note.id)) {
|
|
417
|
+
for (const [position, block] of Object.values(note.blocks).sort((a, b) => a.from - b.from).entries()) {
|
|
418
|
+
const rawContent = note.rawContent.slice(block.from, block.to);
|
|
419
|
+
const body = parseDocumentSections(rawContent)[0]?.displayContent ?? rawContent;
|
|
420
|
+
data.blocks.push({
|
|
421
|
+
id: `${note.id}:${block.id}`,
|
|
422
|
+
noteId: note.id,
|
|
423
|
+
blockId: block.id,
|
|
424
|
+
blockVersion: block.version,
|
|
425
|
+
sourceRevision: note.sourceRevision,
|
|
426
|
+
from: block.from,
|
|
427
|
+
to: block.to,
|
|
428
|
+
position,
|
|
429
|
+
dataType: block.kind,
|
|
430
|
+
rawContent,
|
|
431
|
+
noteTitle: note.title,
|
|
432
|
+
createdAt: note.createdAt,
|
|
433
|
+
...block.kind === "wod" ? { blockContentId: blockContentId(body) } : {}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
for (const session of data.results) {
|
|
439
|
+
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) {
|
|
440
|
+
throw new Error("Incompatible corpus Session; exact blockId/blockVersion pin required");
|
|
441
|
+
}
|
|
442
|
+
if (!data.revisions.some((row) => row.noteId === session.noteId && row.blockId === session.blockId && row.version === session.blockVersion && row.kind === "wod")) {
|
|
443
|
+
throw new Error("Corpus Session immutable revision is unavailable");
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
for (const row of [...data.eventRecords, ...data.facts, ...data.blocks]) {
|
|
447
|
+
if ("segmentId" in row || "segmentVersion" in row) throw new Error("Incompatible corpus segment pin");
|
|
448
|
+
}
|
|
449
|
+
const revisions = new Map(data.revisions.map((row) => [JSON.stringify([row.noteId, row.blockId, row.version]), row]));
|
|
450
|
+
for (const row of data.eventRecords) {
|
|
451
|
+
if (row.blockId !== void 0 || row.blockVersion !== void 0) {
|
|
452
|
+
if (!row.blockId || !Number.isSafeInteger(row.blockVersion) || !revisions.has(JSON.stringify([row.noteId, row.blockId, row.blockVersion]))) {
|
|
453
|
+
throw new Error("Corpus Event immutable revision is unavailable");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
for (const row of data.blocks) {
|
|
458
|
+
const note = data.notes.find((candidate) => candidate.id === row.noteId);
|
|
459
|
+
const block = note?.blocks[row.blockId];
|
|
460
|
+
if (!note || !block || row.blockVersion !== block.version || row.sourceRevision !== note.sourceRevision || row.from !== block.from || row.to !== block.to || row.dataType !== block.kind) {
|
|
461
|
+
throw new Error("Corpus block projection does not match current Note source");
|
|
462
|
+
}
|
|
463
|
+
row.rawContent = note.rawContent.slice(block.from, block.to);
|
|
464
|
+
}
|
|
436
465
|
return data;
|
|
437
466
|
}
|
|
438
467
|
async function runQueryCli(wqlString, options = {}) {
|
|
@@ -538,10 +567,9 @@ function injectIdentityFrontmatter(markdown, identity) {
|
|
|
538
567
|
};
|
|
539
568
|
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
|
|
540
569
|
if (block) {
|
|
570
|
+
const newline = markdown.startsWith("---\r\n") ? "\r\n" : "\n";
|
|
541
571
|
const lines2 = stamp(block[1].split(/\r?\n/));
|
|
542
|
-
return
|
|
543
|
-
${lines2.join("\n")}
|
|
544
|
-
---${markdown.slice(block[0].length)}`;
|
|
572
|
+
return `---${newline}${lines2.join(newline)}${newline}---${markdown.slice(block[0].length)}`;
|
|
545
573
|
}
|
|
546
574
|
const lines = stamp([]);
|
|
547
575
|
return `---
|
|
@@ -550,35 +578,8 @@ ${lines.join("\n")}
|
|
|
550
578
|
|
|
551
579
|
${markdown}`;
|
|
552
580
|
}
|
|
553
|
-
function
|
|
554
|
-
|
|
555
|
-
if (segment.dataType === "wod") {
|
|
556
|
-
const block = segment.data;
|
|
557
|
-
const dialect = block?.dialect ?? "time";
|
|
558
|
-
const fenceTag = block?.sport ? `${dialect}:${block.sport}` : dialect;
|
|
559
|
-
return `\`\`\`${fenceTag}
|
|
560
|
-
${segment.rawContent}
|
|
561
|
-
\`\`\``;
|
|
562
|
-
}
|
|
563
|
-
if (segment.dataType === "frontmatter") {
|
|
564
|
-
return `---
|
|
565
|
-
${segment.rawContent}
|
|
566
|
-
---`;
|
|
567
|
-
}
|
|
568
|
-
return segment.rawContent;
|
|
569
|
-
}
|
|
570
|
-
function composeNoteMarkdown(note, segments, version) {
|
|
571
|
-
const pool = version === void 0 ? segments.filter((s) => s.isHistory !== true) : segments;
|
|
572
|
-
const cap = version ?? Number.MAX_SAFE_INTEGER;
|
|
573
|
-
const bySegmentId = {};
|
|
574
|
-
for (const segment of pool) {
|
|
575
|
-
if (segment.version > cap) continue;
|
|
576
|
-
const current = bySegmentId[segment.id];
|
|
577
|
-
if (!current || segment.version > current.version) bySegmentId[segment.id] = segment;
|
|
578
|
-
}
|
|
579
|
-
const picked = Object.values(bySegmentId).sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
|
|
580
|
-
const markdown = picked.map(segmentToFragment).join("");
|
|
581
|
-
return injectIdentityFrontmatter(markdown, { noteId: note.id, title: note.title });
|
|
581
|
+
function composeNoteMarkdown(note) {
|
|
582
|
+
return injectIdentityFrontmatter(note.rawContent, { noteId: note.id, title: note.title });
|
|
582
583
|
}
|
|
583
584
|
var FRONTMATTER_BLOCK_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
584
585
|
function readFrontmatterMeta(markdown) {
|
|
@@ -701,9 +702,8 @@ function csvToEquipmentLabels(csv) {
|
|
|
701
702
|
var SESSION_HEADER = [
|
|
702
703
|
"sessionId",
|
|
703
704
|
"noteId",
|
|
704
|
-
"segmentId",
|
|
705
|
-
"segmentVersion",
|
|
706
705
|
"blockId",
|
|
706
|
+
"blockVersion",
|
|
707
707
|
"blockContentId",
|
|
708
708
|
"origin",
|
|
709
709
|
"status",
|
|
@@ -711,7 +711,8 @@ var SESSION_HEADER = [
|
|
|
711
711
|
"endTime",
|
|
712
712
|
"duration",
|
|
713
713
|
"completed",
|
|
714
|
-
"sessionCreatedAt"
|
|
714
|
+
"sessionCreatedAt",
|
|
715
|
+
"sessionExtraJson"
|
|
715
716
|
];
|
|
716
717
|
var EVENT_HEADER = [
|
|
717
718
|
"eventId",
|
|
@@ -720,8 +721,8 @@ var EVENT_HEADER = [
|
|
|
720
721
|
"timestamp",
|
|
721
722
|
"pageId",
|
|
722
723
|
"effortSlug",
|
|
723
|
-
"
|
|
724
|
-
"
|
|
724
|
+
"eventBlockId",
|
|
725
|
+
"eventBlockVersion",
|
|
725
726
|
"eventBlockContentId",
|
|
726
727
|
"eventOrigin",
|
|
727
728
|
"timeSpan",
|
|
@@ -777,9 +778,8 @@ function sessionCells(session) {
|
|
|
777
778
|
return [
|
|
778
779
|
session.id,
|
|
779
780
|
session.noteId,
|
|
780
|
-
session.segmentId,
|
|
781
|
-
session.segmentVersion,
|
|
782
781
|
session.blockId,
|
|
782
|
+
session.blockVersion,
|
|
783
783
|
session.blockContentId,
|
|
784
784
|
session.origin,
|
|
785
785
|
session.status,
|
|
@@ -787,7 +787,8 @@ function sessionCells(session) {
|
|
|
787
787
|
session.endTime,
|
|
788
788
|
session.duration,
|
|
789
789
|
session.completed,
|
|
790
|
-
session.createdAt
|
|
790
|
+
session.createdAt,
|
|
791
|
+
JSON.stringify(session)
|
|
791
792
|
].map(escapeCsvField);
|
|
792
793
|
}
|
|
793
794
|
function metricCells(event, metric, index) {
|
|
@@ -798,8 +799,8 @@ function metricCells(event, metric, index) {
|
|
|
798
799
|
event.timestamp,
|
|
799
800
|
event.pageId,
|
|
800
801
|
event.effortSlug,
|
|
801
|
-
event.
|
|
802
|
-
event.
|
|
802
|
+
event.blockId,
|
|
803
|
+
event.blockVersion,
|
|
803
804
|
event.blockContentId,
|
|
804
805
|
event.origin,
|
|
805
806
|
event.timeSpan,
|
|
@@ -910,6 +911,9 @@ function csvToSessionRows(csv) {
|
|
|
910
911
|
const lines = splitCsvRecords(csv);
|
|
911
912
|
if (lines.length < 2) throw new Error("[wod import] Session CSV has no data rows");
|
|
912
913
|
const header = parseCsvLine(lines[0]);
|
|
914
|
+
if (header.some((name) => /segment/i.test(name))) {
|
|
915
|
+
throw new Error("[wod import] Incompatible Session CSV segment pins; blockId/blockVersion are required");
|
|
916
|
+
}
|
|
913
917
|
const col = (name) => {
|
|
914
918
|
const idx = header.indexOf(name);
|
|
915
919
|
if (idx < 0) throw new Error(`[wod import] Session CSV missing column "${name}"`);
|
|
@@ -919,13 +923,29 @@ function csvToSessionRows(csv) {
|
|
|
919
923
|
const eventRows = /* @__PURE__ */ new Map();
|
|
920
924
|
let session;
|
|
921
925
|
const ensureSession = (cells) => {
|
|
922
|
-
|
|
926
|
+
const id = at(cells, "sessionId");
|
|
927
|
+
const noteId = at(cells, "noteId");
|
|
928
|
+
const blockId = at(cells, "blockId");
|
|
929
|
+
const blockVersion = Number(at(cells, "blockVersion"));
|
|
930
|
+
if (!id || !noteId || !blockId || !Number.isSafeInteger(blockVersion) || blockVersion < 1) {
|
|
931
|
+
throw new Error("[wod import] Session CSV requires sessionId, noteId, blockId and positive blockVersion");
|
|
932
|
+
}
|
|
933
|
+
if (session) {
|
|
934
|
+
if (session.id !== id || session.noteId !== noteId || session.blockId !== blockId || session.blockVersion !== blockVersion) {
|
|
935
|
+
throw new Error("[wod import] Session CSV contains inconsistent source pins");
|
|
936
|
+
}
|
|
937
|
+
return session;
|
|
938
|
+
}
|
|
939
|
+
const extra = jsonCell(at(cells, "sessionExtraJson")) ?? {};
|
|
940
|
+
if ("segmentId" in extra || "segmentVersion" in extra || "version" in extra || "rawContent" in extra || "data" in extra) {
|
|
941
|
+
throw new Error("[wod import] Incompatible Session CSV source snapshot or segment pin");
|
|
942
|
+
}
|
|
923
943
|
session = {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
944
|
+
...extra,
|
|
945
|
+
id,
|
|
946
|
+
noteId,
|
|
947
|
+
blockId,
|
|
948
|
+
blockVersion,
|
|
929
949
|
blockContentId: at(cells, "blockContentId") || void 0,
|
|
930
950
|
origin: at(cells, "origin") || void 0,
|
|
931
951
|
status: at(cells, "status") || void 0,
|
|
@@ -935,6 +955,9 @@ function csvToSessionRows(csv) {
|
|
|
935
955
|
completed: at(cells, "completed") === "true",
|
|
936
956
|
createdAt: Number(at(cells, "sessionCreatedAt") || 0)
|
|
937
957
|
};
|
|
958
|
+
if (![session.startTime, session.endTime, session.duration, session.createdAt].every(Number.isFinite)) {
|
|
959
|
+
throw new Error("[wod import] Session CSV lifecycle values must be finite numbers");
|
|
960
|
+
}
|
|
938
961
|
return session;
|
|
939
962
|
};
|
|
940
963
|
for (const line of lines.slice(1)) {
|
|
@@ -946,6 +969,14 @@ function csvToSessionRows(csv) {
|
|
|
946
969
|
let event = eventRows.get(eventId);
|
|
947
970
|
if (!event) {
|
|
948
971
|
const extra = jsonCell(at(cells, "eventExtraJson")) ?? {};
|
|
972
|
+
if ("segmentId" in extra || "segmentVersion" in extra) {
|
|
973
|
+
throw new Error("[wod import] Incompatible Session CSV event segment pins");
|
|
974
|
+
}
|
|
975
|
+
const blockId = nonempty(at(cells, "eventBlockId"));
|
|
976
|
+
const blockVersion = num(at(cells, "eventBlockVersion"));
|
|
977
|
+
if (blockId !== currentSession.blockId || blockVersion !== currentSession.blockVersion) {
|
|
978
|
+
throw new Error("[wod import] Event source pin differs from Session source pin");
|
|
979
|
+
}
|
|
949
980
|
event = {
|
|
950
981
|
...extra,
|
|
951
982
|
id: eventId,
|
|
@@ -958,8 +989,8 @@ function csvToSessionRows(csv) {
|
|
|
958
989
|
grain: nonempty(at(cells, "grain")) ?? extra.grain ?? "event",
|
|
959
990
|
effortSlug: nonempty(at(cells, "effortSlug")) ?? extra.effortSlug,
|
|
960
991
|
outputType: nonempty(at(cells, "outputType")) ?? extra.outputType ?? "event",
|
|
961
|
-
|
|
962
|
-
|
|
992
|
+
blockId,
|
|
993
|
+
blockVersion,
|
|
963
994
|
timeSpan: jsonCell(at(cells, "timeSpan")) ?? extra.timeSpan,
|
|
964
995
|
sourceBlockKey: nonempty(at(cells, "sourceBlockKey")) ?? extra.sourceBlockKey,
|
|
965
996
|
sourceStatementId: num(at(cells, "sourceStatementId")) ?? extra.sourceStatementId,
|
|
@@ -970,9 +1001,8 @@ function csvToSessionRows(csv) {
|
|
|
970
1001
|
completionReason: nonempty(at(cells, "completionReason")) ?? extra.completionReason,
|
|
971
1002
|
representationKind: nonempty(at(cells, "representationKind")) ?? extra.representationKind,
|
|
972
1003
|
metrics: [],
|
|
973
|
-
//
|
|
974
|
-
|
|
975
|
-
metricTemporal: void 0
|
|
1004
|
+
// Rebuild anchors from metric rows while preserving an authored empty array.
|
|
1005
|
+
metricTemporal: Array.isArray(extra.metricTemporal) ? [] : void 0
|
|
976
1006
|
};
|
|
977
1007
|
eventRows.set(eventId, event);
|
|
978
1008
|
}
|
|
@@ -997,6 +1027,268 @@ function csvToSessionRows(csv) {
|
|
|
997
1027
|
if (!session) throw new Error("[wod import] Session CSV did not contain session columns");
|
|
998
1028
|
return { session, events: [...eventRows.values()] };
|
|
999
1029
|
}
|
|
1030
|
+
function object(value) {
|
|
1031
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1032
|
+
}
|
|
1033
|
+
function positive(value) {
|
|
1034
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
1035
|
+
}
|
|
1036
|
+
function nonnegative(value) {
|
|
1037
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
1038
|
+
}
|
|
1039
|
+
function assertSeedManifest(value) {
|
|
1040
|
+
if (!object(value) || value.schema !== SEED_SCHEMA) throw new Error("Incompatible seed schema; rebuild seed artifacts");
|
|
1041
|
+
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");
|
|
1042
|
+
for (const key of ["versionLabel", "commit"]) if (value[key] !== void 0 && typeof value[key] !== "string") throw new Error("invalid seed manifest metadata");
|
|
1043
|
+
if (value.dirty !== void 0 && typeof value.dirty !== "boolean") throw new Error("invalid seed manifest metadata");
|
|
1044
|
+
const ids = /* @__PURE__ */ new Set(), paths = /* @__PURE__ */ new Set();
|
|
1045
|
+
for (const chunk of value.chunks) {
|
|
1046
|
+
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");
|
|
1047
|
+
ids.add(chunk.id);
|
|
1048
|
+
paths.add(chunk.path);
|
|
1049
|
+
}
|
|
1050
|
+
return value;
|
|
1051
|
+
}
|
|
1052
|
+
function assertSeedRows(value) {
|
|
1053
|
+
if (!Array.isArray(value)) throw new Error("seed chunk is not an array");
|
|
1054
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1055
|
+
for (const row of value) {
|
|
1056
|
+
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");
|
|
1057
|
+
paths.add(row.path);
|
|
1058
|
+
for (const [id, block] of Object.entries(row.blocks)) {
|
|
1059
|
+
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");
|
|
1060
|
+
}
|
|
1061
|
+
validateNoteBlocks(row.content, row.blocks, 1, { registeredWidgets: SEED_REGISTERED_WIDGETS });
|
|
1062
|
+
const indexed = /* @__PURE__ */ new Set();
|
|
1063
|
+
const sections = parseDocumentSections(row.content, { registeredWidgets: SEED_REGISTERED_WIDGETS });
|
|
1064
|
+
for (const projection of row.blockIndex) {
|
|
1065
|
+
if (!object(projection) || typeof projection.blockId !== "string") throw new Error("invalid seed projection");
|
|
1066
|
+
const block = row.blocks[projection.blockId];
|
|
1067
|
+
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");
|
|
1068
|
+
if ("segmentId" in projection || "segmentVersion" in projection) throw new Error("incompatible seed projection");
|
|
1069
|
+
for (const key of ["sourceId", "routeId", "blockContentId"]) if (projection[key] !== void 0 && typeof projection[key] !== "string") throw new Error("invalid seed projection metadata");
|
|
1070
|
+
if (projection.position !== void 0 && !nonnegative(projection.position)) throw new Error("invalid seed projection position");
|
|
1071
|
+
if (block.kind === "wod") {
|
|
1072
|
+
const section = sections.find((s) => s.from === block.from && s.to === block.to);
|
|
1073
|
+
if (projection.blockContentId !== blockContentId(section?.scriptBlock?.content ?? row.content.slice(Number(block.from), Number(block.to)))) throw new Error("invalid seed workout fingerprint");
|
|
1074
|
+
} else if (projection.blockContentId !== void 0) throw new Error("non-workout seed fingerprint");
|
|
1075
|
+
indexed.add(projection.blockId);
|
|
1076
|
+
}
|
|
1077
|
+
if (indexed.size !== Object.keys(row.blocks).length) throw new Error("missing actionable seed projection");
|
|
1078
|
+
const links = /* @__PURE__ */ new Set();
|
|
1079
|
+
for (const link of row.blockEfforts) {
|
|
1080
|
+
if (!object(link) || typeof link.blockId !== "string" || typeof link.noteId !== "string" || typeof link.effortSlug !== "string" || !link.effortSlug) throw new Error("invalid seed effort identity");
|
|
1081
|
+
const block = row.blocks[link.blockId];
|
|
1082
|
+
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");
|
|
1083
|
+
const projection = row.blockIndex.find((p) => object(p) && p.blockId === link.blockId);
|
|
1084
|
+
if (!object(projection) || link.noteId !== projection.noteId || link.blockContentId !== projection.blockContentId || !Number.isFinite(link.createdAt)) throw new Error("invalid seed effort metadata");
|
|
1085
|
+
links.add(String(link.id));
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return value;
|
|
1089
|
+
}
|
|
1090
|
+
var SEED_NAMESPACE = "1b671a64-40d5-491e-99b0-da01ff1f3341";
|
|
1091
|
+
async function seedNoteId(path) {
|
|
1092
|
+
const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(`${SEED_NAMESPACE}:${path}`));
|
|
1093
|
+
const bytes = new Uint8Array(digest.slice(0, 16));
|
|
1094
|
+
bytes[6] = bytes[6] & 15 | 80;
|
|
1095
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
1096
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1097
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
1098
|
+
}
|
|
1099
|
+
function equal(a, b) {
|
|
1100
|
+
if (Object.is(a, b)) return true;
|
|
1101
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
|
|
1102
|
+
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);
|
|
1103
|
+
return ak.length === bk.length && ak.every((key) => key in b && equal(Reflect.get(a, key), Reflect.get(b, key)));
|
|
1104
|
+
}
|
|
1105
|
+
function titleFromPath(path) {
|
|
1106
|
+
return (path.split("/").at(-1) ?? path).replace(/\.md$/, "").replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1107
|
+
}
|
|
1108
|
+
function refreshedBlocks(existing, rawContent, sourceRevision) {
|
|
1109
|
+
const old = existing.rawContent;
|
|
1110
|
+
let from = 0, endOld = old.length, endNew = rawContent.length;
|
|
1111
|
+
while (from < endOld && from < endNew && old[from] === rawContent[from]) from++;
|
|
1112
|
+
while (endOld > from && endNew > from && old[endOld - 1] === rawContent[endNew - 1]) {
|
|
1113
|
+
endOld--;
|
|
1114
|
+
endNew--;
|
|
1115
|
+
}
|
|
1116
|
+
const intersect = Object.values(existing.blocks).filter((b) => from < b.to && endOld > b.from);
|
|
1117
|
+
const trusted = intersect.length <= 1 ? existing.blocks : Object.fromEntries(Object.entries(existing.blocks).filter(([, b]) => !intersect.includes(b)));
|
|
1118
|
+
const mapped = mapNoteBlockChanges(trusted, [{ from, to: endOld, insert: rawContent.slice(from, endNew) }]);
|
|
1119
|
+
let ordinal = 0;
|
|
1120
|
+
return reconcileNoteBlocks(rawContent, { sourceRevision, blocks: mapped, registeredWidgets: SEED_REGISTERED_WIDGETS, createId: () => `seed:${existing.id}:r${sourceRevision}:${ordinal++}` });
|
|
1121
|
+
}
|
|
1122
|
+
async function putChanged(store, values, keys) {
|
|
1123
|
+
if (!values.length) return;
|
|
1124
|
+
const prior = await store.getMany(keys);
|
|
1125
|
+
const changed = values.filter((value, i) => !equal(value, prior[i]));
|
|
1126
|
+
if (changed.length) await store.putMany(changed);
|
|
1127
|
+
}
|
|
1128
|
+
async function applySeedChunk(storage, write) {
|
|
1129
|
+
assertSeedManifest(write.manifest);
|
|
1130
|
+
assertSeedRows(write.rows);
|
|
1131
|
+
const result = { totalNotes: 0, totalEfforts: 0, totalBlocks: 0, skippedUserOwned: 0, deleted: 0 };
|
|
1132
|
+
const ids = await Promise.all(write.rows.map((row) => seedNoteId(row.path)));
|
|
1133
|
+
const pagesBySlug = /* @__PURE__ */ new Map();
|
|
1134
|
+
const linkIds = /* @__PURE__ */ new Map();
|
|
1135
|
+
const tagIds = /* @__PURE__ */ new Map();
|
|
1136
|
+
for (const [i, row] of write.rows.entries()) {
|
|
1137
|
+
const slug = readFrontmatterMeta(row.content)["slug"];
|
|
1138
|
+
if (typeof slug === "string" && slug.trim()) {
|
|
1139
|
+
pagesBySlug.set(slug, await seedNoteId(`page:${slug}`));
|
|
1140
|
+
linkIds.set(ids[i], await seedNoteId(`pagenote:${slug}:${row.path}`));
|
|
1141
|
+
}
|
|
1142
|
+
for (const { label } of extractTypedTags(row.content)) {
|
|
1143
|
+
if (!tagIds.has(label)) tagIds.set(label, await seedNoteId(`tag:${label}`));
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return storage.transaction(["notes", "block_revisions", "block_index", "block_efforts", "page", "page_notes", "tags", "note_tags", "efforts", "meta"], "readwrite", async (tx) => {
|
|
1147
|
+
const metaStore = tx.readwrite("meta");
|
|
1148
|
+
const actualMeta = await metaStore.get(write.meta.key);
|
|
1149
|
+
if (!equal(actualMeta, write.expectedMeta)) throw new Error("Seed checkpoint changed during import");
|
|
1150
|
+
if (write.meta.schema !== SEED_SCHEMA || write.manifest.schema !== SEED_SCHEMA) throw new Error("Incompatible seed schema; recreate artifacts");
|
|
1151
|
+
const noteStore = tx.readwrite("notes");
|
|
1152
|
+
const priorIds = write.chunkId ? write.expectedMeta?.chunks[write.chunkId]?.noteIds ?? [] : [];
|
|
1153
|
+
const declared = new Set(ids);
|
|
1154
|
+
const goneIds = priorIds.filter((id) => !declared.has(id));
|
|
1155
|
+
const allIds = [.../* @__PURE__ */ new Set([...ids, ...goneIds])];
|
|
1156
|
+
const stored = await noteStore.getMany(allIds);
|
|
1157
|
+
const existingById = new Map(allIds.map((id, i) => [id, stored[i]]));
|
|
1158
|
+
const ownedIds = allIds.filter((id) => {
|
|
1159
|
+
const note = existingById.get(id);
|
|
1160
|
+
return !note || note.seedOrigin === "seed" && (declared.has(id) || note.seedChunkId === write.chunkId);
|
|
1161
|
+
});
|
|
1162
|
+
const owned = new Set(ownedIds);
|
|
1163
|
+
const revisionStore = tx.readwrite("block_revisions");
|
|
1164
|
+
const historyIds = ids.filter((id, i) => owned.has(id) && existingById.get(id)?.rawContent !== write.rows[i].content);
|
|
1165
|
+
const histories = await revisionStore.getAllFromIndexMany("by-note", historyIds);
|
|
1166
|
+
const history = new Map(historyIds.map((id, i) => [id, histories[i]]));
|
|
1167
|
+
const indexStore = tx.readwrite("block_index"), effortLinks = tx.readwrite("block_efforts");
|
|
1168
|
+
const tagLinks = tx.readwrite("note_tags"), pageLinks = tx.readwrite("page_notes");
|
|
1169
|
+
const currentIndexes = await indexStore.getAllFromIndexMany("by-note", ownedIds);
|
|
1170
|
+
const currentEfforts = await effortLinks.getAllFromIndexMany("by-note", ownedIds);
|
|
1171
|
+
const currentTags = await tagLinks.getAllFromIndexMany("by-note", ownedIds);
|
|
1172
|
+
const currentPages = await pageLinks.getAllFromIndexMany("by-note", ownedIds);
|
|
1173
|
+
const slot = new Map(ownedIds.map((id, i) => [id, i]));
|
|
1174
|
+
const tagsStore = tx.readwrite("tags");
|
|
1175
|
+
const labels = [...tagIds.keys()];
|
|
1176
|
+
const matches = await tagsStore.getAllFromIndexMany("by-label", labels);
|
|
1177
|
+
const tagsByLabel = new Map(labels.map((label, i) => [label, matches[i][0]]));
|
|
1178
|
+
const changedTags = [], changedNotes = [], revisions = [], pages = [], pageNotes = [], noteTags = [];
|
|
1179
|
+
const blocks = [], blockEfforts = [];
|
|
1180
|
+
for (const [i, row] of write.rows.entries()) {
|
|
1181
|
+
const id = ids[i], existing = existingById.get(id);
|
|
1182
|
+
if (!owned.has(id)) {
|
|
1183
|
+
result.skippedUserOwned++;
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
validateNoteBlocks(row.content, row.blocks, 1, { registeredWidgets: SEED_REGISTERED_WIDGETS });
|
|
1187
|
+
const sourceRevision = existing ? existing.sourceRevision + (existing.rawContent === row.content ? 0 : 1) : 1;
|
|
1188
|
+
let ordinal = 0;
|
|
1189
|
+
const retainedHistory = history.get(id) ?? [];
|
|
1190
|
+
const map = !existing ? retainedHistory.length ? 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);
|
|
1191
|
+
for (const block of Object.values(map)) {
|
|
1192
|
+
const saved = (history.get(id) ?? []).filter((r) => r.blockId === block.id).sort((a, b) => b.version - a.version)[0];
|
|
1193
|
+
const source = row.content.slice(block.from, block.to);
|
|
1194
|
+
if (existing?.rawContent === row.content) continue;
|
|
1195
|
+
const same = saved && saved.kind === block.kind && saved.rawContent === source && saved.dialect === block.dialect && saved.sport === block.sport;
|
|
1196
|
+
block.version = !existing ? 1 : same ? saved.version : (saved?.version ?? 0) + 1;
|
|
1197
|
+
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 } : {} });
|
|
1198
|
+
}
|
|
1199
|
+
validateNoteBlocks(row.content, map, sourceRevision, { registeredWidgets: SEED_REGISTERED_WIDGETS });
|
|
1200
|
+
const fm = readFrontmatterMeta(row.content);
|
|
1201
|
+
const title = typeof fm["title"] === "string" && fm["title"].trim() ? fm["title"].trim() : titleFromPath(row.path);
|
|
1202
|
+
const sourceId = write.chunkId ? seedNoteSourceId(row.path, row.content, write.chunkId) : void 0;
|
|
1203
|
+
if (!sourceId) throw new Error(`Unknown seed source family ${write.chunkId}`);
|
|
1204
|
+
const date = typeof fm["date"] === "string" && /^\d{4}-\d{2}-\d{2}$/.test(fm["date"]) ? Date.parse(`${fm["date"]}T12:00:00Z`) : void 0;
|
|
1205
|
+
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 };
|
|
1206
|
+
if (date === void 0) delete note.date;
|
|
1207
|
+
if (!equal(note, existing)) changedNotes.push(note);
|
|
1208
|
+
const byRange = new Map(Object.values(map).map((b) => [`${b.from}:${b.to}`, b]));
|
|
1209
|
+
const compiledById = new Map(row.blockIndex.map((b) => [b.blockId, b]));
|
|
1210
|
+
const remap = /* @__PURE__ */ new Map();
|
|
1211
|
+
for (const compiled of row.blockIndex) {
|
|
1212
|
+
const block = byRange.get(`${compiled.from}:${compiled.to}`);
|
|
1213
|
+
if (!block || block.kind !== compiled.dataType) throw new Error("Orphan seed projection");
|
|
1214
|
+
remap.set(compiled.blockId, block.id);
|
|
1215
|
+
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) };
|
|
1216
|
+
const prior = currentIndexes[slot.get(id)].find((b) => b.id === projection.id);
|
|
1217
|
+
if (!prior || prior.isStatic === true) blocks.push(projection);
|
|
1218
|
+
}
|
|
1219
|
+
for (const compiled of row.blockEfforts) {
|
|
1220
|
+
const blockId = remap.get(compiled.blockId);
|
|
1221
|
+
if (!blockId || compiledById.get(compiled.blockId)?.dataType !== "wod") throw new Error("Orphan seed effort link");
|
|
1222
|
+
const link = { ...compiled, id: `${id}:${blockId}:${compiled.effortSlug}`, noteId: id, blockId, sourcePath: row.path };
|
|
1223
|
+
const prior = currentEfforts[slot.get(id)].find((b) => b.id === link.id);
|
|
1224
|
+
if (!prior || prior.isStatic === true) blockEfforts.push(prior ? { ...link, createdAt: prior.createdAt } : link);
|
|
1225
|
+
}
|
|
1226
|
+
const n = slot.get(id);
|
|
1227
|
+
const wantedTags = /* @__PURE__ */ new Set();
|
|
1228
|
+
for (const item of extractTypedTags(row.content)) {
|
|
1229
|
+
let tag = tagsByLabel.get(item.label);
|
|
1230
|
+
if (!tag) {
|
|
1231
|
+
tag = { id: tagIds.get(item.label), label: item.label, type: item.type, createdAt: write.meta.importedAt };
|
|
1232
|
+
changedTags.push(tag);
|
|
1233
|
+
tagsByLabel.set(item.label, tag);
|
|
1234
|
+
} else if (item.type && !tag.type) {
|
|
1235
|
+
tag = { ...tag, type: item.type };
|
|
1236
|
+
changedTags.push(tag);
|
|
1237
|
+
tagsByLabel.set(item.label, tag);
|
|
1238
|
+
}
|
|
1239
|
+
if (wantedTags.has(tag.id)) continue;
|
|
1240
|
+
wantedTags.add(tag.id);
|
|
1241
|
+
const link = currentTags[n].find((l) => l.tagId === tag.id);
|
|
1242
|
+
if (!link) noteTags.push({ id: `${id}:${tag.id}`, noteId: id, tagId: tag.id });
|
|
1243
|
+
}
|
|
1244
|
+
for (const link of currentTags[n]) if (!wantedTags.has(link.tagId)) await tagLinks.delete(link.id);
|
|
1245
|
+
const slug = fm["slug"];
|
|
1246
|
+
if (typeof slug === "string" && pagesBySlug.has(slug)) {
|
|
1247
|
+
const pageId = pagesBySlug.get(slug);
|
|
1248
|
+
pages.push({ id: pageId, slug, title: titleFromPath(row.path), createdAt: write.manifest.version });
|
|
1249
|
+
pageNotes.push({ id: linkIds.get(id), pageId, noteId: id, createdAt: write.manifest.version });
|
|
1250
|
+
}
|
|
1251
|
+
result.totalNotes++;
|
|
1252
|
+
result.totalBlocks += Object.keys(map).length;
|
|
1253
|
+
}
|
|
1254
|
+
const wantedBlocks = new Set(blocks.map((b) => b.id)), wantedEfforts = new Set(blockEfforts.map((b) => b.id));
|
|
1255
|
+
for (const rows of currentIndexes) for (const row of rows) if (row.isStatic === true && !wantedBlocks.has(row.id)) await indexStore.delete(row.id);
|
|
1256
|
+
for (const rows of currentEfforts) for (const row of rows) if (row.isStatic === true && !wantedEfforts.has(row.id)) await effortLinks.delete(row.id);
|
|
1257
|
+
for (const id of goneIds) {
|
|
1258
|
+
if (!owned.has(id) || !existingById.get(id)) continue;
|
|
1259
|
+
const n = slot.get(id);
|
|
1260
|
+
await noteStore.delete(id);
|
|
1261
|
+
for (const link of currentTags[n]) await tagLinks.delete(link.id);
|
|
1262
|
+
for (const link of currentPages[n]) await pageLinks.delete(link.id);
|
|
1263
|
+
result.deleted++;
|
|
1264
|
+
}
|
|
1265
|
+
const effortStore = tx.readwrite("efforts");
|
|
1266
|
+
if (write.chunkId === EFFORTS_CHUNK_ID) {
|
|
1267
|
+
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" }));
|
|
1268
|
+
const prior = await effortStore.getMany(efforts.map((e) => e.slug));
|
|
1269
|
+
const changed = efforts.filter((e, i) => (!prior[i] || prior[i]?.registrySource === "bundled") && !equal(e, prior[i]));
|
|
1270
|
+
if (changed.length) await effortStore.putMany(changed);
|
|
1271
|
+
result.totalEfforts = changed.length;
|
|
1272
|
+
const wantedSlugs = new Set(efforts.map((effort) => effort.slug));
|
|
1273
|
+
const removed = allIds.map((id) => {
|
|
1274
|
+
const note = existingById.get(id);
|
|
1275
|
+
return owned.has(id) && note ? parseEffortFile(note.rawContent)?.slug : void 0;
|
|
1276
|
+
}).filter((slug) => !!slug && !wantedSlugs.has(slug));
|
|
1277
|
+
const old = await effortStore.getMany(removed);
|
|
1278
|
+
for (const [i, effort] of old.entries()) if (effort?.registrySource === "bundled") await effortStore.delete(removed[i]);
|
|
1279
|
+
}
|
|
1280
|
+
if (changedNotes.length) await noteStore.putMany(changedNotes);
|
|
1281
|
+
if (revisions.length) await revisionStore.putMany(revisions);
|
|
1282
|
+
await putChanged(indexStore, blocks, blocks.map((b) => b.id));
|
|
1283
|
+
await putChanged(effortLinks, blockEfforts, blockEfforts.map((b) => b.id));
|
|
1284
|
+
if (changedTags.length) await tagsStore.putMany([...new Map(changedTags.map((t) => [t.id, t])).values()]);
|
|
1285
|
+
if (noteTags.length) await tagLinks.putMany(noteTags);
|
|
1286
|
+
await putChanged(tx.readwrite("page"), pages, pages.map((p) => p.id));
|
|
1287
|
+
await putChanged(pageLinks, pageNotes, pageNotes.map((p) => p.id));
|
|
1288
|
+
await metaStore.put(write.meta);
|
|
1289
|
+
return result;
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1000
1292
|
|
|
1001
1293
|
// src/cli/formatters.ts
|
|
1002
1294
|
function escapeCsvField2(field) {
|
|
@@ -1321,9 +1613,14 @@ async function openCliDb(dbPath) {
|
|
|
1321
1613
|
const resolved = resolveDbPath(dbPath);
|
|
1322
1614
|
nodeFs().mkdirSync(nodePath().dirname(resolved), { recursive: true });
|
|
1323
1615
|
const db = sql.libsqlDatabase(createClient({ url: `file:${resolved}` }));
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1616
|
+
try {
|
|
1617
|
+
await sql.initializeStore(db);
|
|
1618
|
+
await sql.initializeDomain(db);
|
|
1619
|
+
return { db, storage: new sql.SqlStorage(db), path: resolved, close: () => db.close() };
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
await db.close();
|
|
1622
|
+
throw error;
|
|
1623
|
+
}
|
|
1327
1624
|
}
|
|
1328
1625
|
function formatV5Digest(digest) {
|
|
1329
1626
|
const bytes = digest.slice(0, 16);
|
|
@@ -1357,7 +1654,7 @@ function sqliteEventStore(db) {
|
|
|
1357
1654
|
getEventsByTimeRange: async (start, end) => (await all()).filter((r) => r.timestamp >= start && r.timestamp <= end),
|
|
1358
1655
|
getEventsByResult: async (resultId) => (await all()).filter((r) => r.resultId === resultId),
|
|
1359
1656
|
getEventsForNote: async (noteId) => (await all()).filter((r) => r.noteId === noteId),
|
|
1360
|
-
getEventsByContent: async (
|
|
1657
|
+
getEventsByContent: async (blockContentId4) => (await all()).filter((r) => r.blockContentId === blockContentId4),
|
|
1361
1658
|
scanAll: all,
|
|
1362
1659
|
appendEvents: async (appended) => {
|
|
1363
1660
|
const sql = await loadSql();
|
|
@@ -1389,6 +1686,11 @@ function sqliteEventStore(db) {
|
|
|
1389
1686
|
function sqliteNoteStore(db) {
|
|
1390
1687
|
return {
|
|
1391
1688
|
getAllNotes: async () => await allRows(await loadSql(), db, "notes"),
|
|
1689
|
+
getNotesByIds: async (noteIds) => {
|
|
1690
|
+
const sql = await loadSql();
|
|
1691
|
+
const rows = await sql.getRows(db, sql.storeDefinition("notes"), [...noteIds]);
|
|
1692
|
+
return rows.filter((row) => row !== void 0);
|
|
1693
|
+
},
|
|
1392
1694
|
getNoteIdsForTag: async (label) => {
|
|
1393
1695
|
const sql = await loadSql();
|
|
1394
1696
|
const tags = (await allRows(sql, db, "tags")).filter((t) => t.label === label);
|
|
@@ -1443,17 +1745,14 @@ function sqliteIndexStore(db, store) {
|
|
|
1443
1745
|
}
|
|
1444
1746
|
async function getNoteBundle(db, noteId) {
|
|
1445
1747
|
const sql = await loadSql();
|
|
1446
|
-
const
|
|
1447
|
-
|
|
1448
|
-
const segments = await rowsByIndex(sql, db, "segments", "by-note", noteId);
|
|
1449
|
-
return { note, segments };
|
|
1748
|
+
const note = await sql.getRow(db, sql.storeDefinition("notes"), noteId);
|
|
1749
|
+
return { note };
|
|
1450
1750
|
}
|
|
1451
1751
|
async function getSessionBundle(db, resultId) {
|
|
1452
1752
|
const sql = await loadSql();
|
|
1453
|
-
const
|
|
1454
|
-
const session = sessions.find((s) => s.id === resultId);
|
|
1753
|
+
const session = await sql.getRow(db, sql.storeDefinition("sessions"), resultId);
|
|
1455
1754
|
if (!session) return void 0;
|
|
1456
|
-
const events =
|
|
1755
|
+
const events = await sql.listRows(db, sql.storeDefinition("events"), { only: resultId }, void 0, "by-result-grain");
|
|
1457
1756
|
return { session, events };
|
|
1458
1757
|
}
|
|
1459
1758
|
async function getEffortBySlug(db, slug) {
|
|
@@ -1466,38 +1765,8 @@ async function listTagsByType(db, type) {
|
|
|
1466
1765
|
const tags = await allRows(sql, db, "tags");
|
|
1467
1766
|
return tags.filter((t) => t.type === type);
|
|
1468
1767
|
}
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
case "time":
|
|
1472
|
-
case "log":
|
|
1473
|
-
return "wod";
|
|
1474
|
-
case "title": {
|
|
1475
|
-
const level = Math.min(6, Math.max(1, section.level ?? 1));
|
|
1476
|
-
return `h${level}`;
|
|
1477
|
-
}
|
|
1478
|
-
case "frontmatter":
|
|
1479
|
-
return "frontmatter";
|
|
1480
|
-
case "markdown":
|
|
1481
|
-
default:
|
|
1482
|
-
return "markdown";
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
function sectionSourceFragments(content, sections) {
|
|
1486
|
-
const starts = [0];
|
|
1487
|
-
for (let i = 0; i < content.length; i++) {
|
|
1488
|
-
if (content[i] === "\n") starts.push(i + 1);
|
|
1489
|
-
}
|
|
1490
|
-
return sections.map((section, index) => content.slice(
|
|
1491
|
-
starts[section.startLine],
|
|
1492
|
-
index + 1 < sections.length ? starts[sections[index + 1].startLine] : content.length
|
|
1493
|
-
));
|
|
1494
|
-
}
|
|
1495
|
-
function scriptBlockContentId(data) {
|
|
1496
|
-
if (data && typeof data === "object" && "contentId" in data && typeof data.contentId === "string") {
|
|
1497
|
-
return data.contentId;
|
|
1498
|
-
}
|
|
1499
|
-
return void 0;
|
|
1500
|
-
}
|
|
1768
|
+
var registeredWidgets = KNOWN_SECTION_WIDGETS;
|
|
1769
|
+
var saveStores = ["notes", "block_revisions", "block_index", "block_efforts", "tags", "note_tags", "sessions"];
|
|
1501
1770
|
function extractEffortSlugs(statements) {
|
|
1502
1771
|
const slugs = /* @__PURE__ */ new Set();
|
|
1503
1772
|
for (const statement of statements) {
|
|
@@ -1514,227 +1783,233 @@ function extractEffortSlugs(statements) {
|
|
|
1514
1783
|
}
|
|
1515
1784
|
return [...slugs];
|
|
1516
1785
|
}
|
|
1517
|
-
async function rowsByNote(sql, db, store, noteId) {
|
|
1518
|
-
return sql.listRows(db, sql.storeDefinition(store), { only: noteId }, void 0, "by-note");
|
|
1519
|
-
}
|
|
1520
1786
|
function titleFromLabel(sourceLabel) {
|
|
1521
1787
|
if (!sourceLabel || sourceLabel === "-") return "stdin";
|
|
1522
1788
|
const stem = sourceLabel.split("/").pop() ?? sourceLabel;
|
|
1523
1789
|
return stem.replace(/\.[^.]+$/, "") || sourceLabel;
|
|
1524
1790
|
}
|
|
1525
|
-
async function
|
|
1526
|
-
const sql = await loadSql();
|
|
1527
|
-
const now = options.now ?? Date.now();
|
|
1791
|
+
async function saveSource(db, options, startTime) {
|
|
1528
1792
|
const noteId = options.noteId ?? stableNoteId(options.sourceLabel, options.source);
|
|
1529
|
-
const
|
|
1530
|
-
const
|
|
1531
|
-
const
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1793
|
+
const observed = await db.storage.readonly("notes").get(noteId);
|
|
1794
|
+
const expected = options.expectedSourceRevision ?? observed?.sourceRevision ?? 0;
|
|
1795
|
+
const now = options.now ?? Date.now();
|
|
1796
|
+
return db.storage.transaction([...saveStores], "readwrite", async (tx) => {
|
|
1797
|
+
const existing = await tx.readonly("notes").get(noteId);
|
|
1798
|
+
if ((existing?.sourceRevision ?? 0) !== expected) {
|
|
1799
|
+
throw new Error(`Stale Note source revision for ${noteId}; expected ${expected}`);
|
|
1800
|
+
}
|
|
1801
|
+
let sourceRevision = existing?.rawContent === options.source ? existing.sourceRevision : (existing?.sourceRevision ?? 0) + 1;
|
|
1802
|
+
const blocks = reconcileNoteBlocks(options.source, {
|
|
1803
|
+
sourceRevision,
|
|
1804
|
+
blocks: options.blocks ?? (existing?.rawContent === options.source ? existing.blocks : void 0),
|
|
1805
|
+
registeredWidgets,
|
|
1806
|
+
createId: randomId
|
|
1807
|
+
});
|
|
1808
|
+
validateNoteBlocks(options.source, blocks, sourceRevision, { registeredWidgets });
|
|
1809
|
+
const history = await tx.readonly("block_revisions").getAllFromIndex("by-note", noteId);
|
|
1810
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1811
|
+
for (const row of history) {
|
|
1812
|
+
if ((latest.get(row.blockId)?.version ?? 0) < row.version) latest.set(row.blockId, row);
|
|
1813
|
+
}
|
|
1814
|
+
const revisions = [];
|
|
1815
|
+
for (const block of Object.values(blocks)) {
|
|
1816
|
+
const saved = latest.get(block.id);
|
|
1817
|
+
const rawContent = options.source.slice(block.from, block.to);
|
|
1818
|
+
const unchanged = saved && saved.rawContent === rawContent && saved.kind === block.kind && saved.dialect === block.dialect && saved.sport === block.sport;
|
|
1819
|
+
block.version = unchanged ? saved.version : (saved?.version ?? 0) + 1;
|
|
1820
|
+
if (!unchanged) revisions.push({
|
|
1821
|
+
noteId,
|
|
1822
|
+
blockId: block.id,
|
|
1823
|
+
version: block.version,
|
|
1824
|
+
kind: block.kind,
|
|
1825
|
+
rawContent,
|
|
1826
|
+
savedAt: now,
|
|
1827
|
+
...block.dialect ? { dialect: block.dialect } : {},
|
|
1828
|
+
...block.sport ? { sport: block.sport } : {}
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
if (existing?.rawContent === options.source && !rowsEqual(existing.blocks, blocks)) {
|
|
1832
|
+
sourceRevision++;
|
|
1833
|
+
for (const block of Object.values(blocks)) block.sourceRevision = sourceRevision;
|
|
1834
|
+
}
|
|
1835
|
+
const note = {
|
|
1836
|
+
...existing,
|
|
1837
|
+
id: noteId,
|
|
1838
|
+
title: options.title ?? existing?.title ?? titleFromLabel(options.sourceLabel),
|
|
1839
|
+
type: existing?.type ?? "note",
|
|
1840
|
+
date: existing?.date ?? now,
|
|
1841
|
+
createdAt: existing?.createdAt ?? now,
|
|
1842
|
+
rawContent: options.source,
|
|
1843
|
+
sourceRevision,
|
|
1844
|
+
blocks
|
|
1562
1845
|
};
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
for (const segment of segments) {
|
|
1578
|
-
const blockContentId = segment.dataType === "wod" ? scriptBlockContentId(segment.data) : void 0;
|
|
1579
|
-
ops.push({
|
|
1580
|
-
op: "put",
|
|
1581
|
-
store: "block_index",
|
|
1582
|
-
value: {
|
|
1583
|
-
id: `${noteId}:${segment.id}:${segment.version}`,
|
|
1846
|
+
const priorRows = await tx.readonly("block_index").getAllFromIndex("by-note", noteId);
|
|
1847
|
+
const priorEfforts = await tx.readonly("block_efforts").getAllFromIndex("by-note", noteId);
|
|
1848
|
+
const priorById = new Map(priorRows.map((row) => [row.id, row]));
|
|
1849
|
+
const nextRows = [];
|
|
1850
|
+
const nextEfforts = [];
|
|
1851
|
+
for (const [position, block] of Object.values(blocks).sort((a, b) => a.from - b.from).entries()) {
|
|
1852
|
+
const id = `${noteId}:${block.id}`;
|
|
1853
|
+
const prior = priorById.get(id);
|
|
1854
|
+
const rawContent = options.source.slice(block.from, block.to);
|
|
1855
|
+
const body = parseDocumentSections(rawContent, { registeredWidgets })[0]?.displayContent ?? rawContent;
|
|
1856
|
+
const fingerprint = block.kind === "wod" ? blockContentId(body) : void 0;
|
|
1857
|
+
nextRows.push({
|
|
1858
|
+
...prior,
|
|
1859
|
+
id,
|
|
1584
1860
|
noteId,
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1861
|
+
blockId: block.id,
|
|
1862
|
+
blockVersion: block.version,
|
|
1863
|
+
dataType: block.kind,
|
|
1864
|
+
sourceRevision,
|
|
1865
|
+
from: block.from,
|
|
1866
|
+
to: block.to,
|
|
1867
|
+
position,
|
|
1868
|
+
rawContent,
|
|
1869
|
+
noteTitle: note.title,
|
|
1870
|
+
createdAt: prior?.createdAt ?? now,
|
|
1871
|
+
blockContentId: fingerprint,
|
|
1872
|
+
...note.sourceId ? { sourceId: note.sourceId } : {}
|
|
1873
|
+
});
|
|
1874
|
+
if (block.kind === "wod") {
|
|
1875
|
+
for (const effortSlug of extractEffortSlugs(parseScript(body, { sport: block.sport, dialect: block.dialect }).statements)) {
|
|
1876
|
+
const effortId = `${noteId}:${block.id}:${effortSlug}`;
|
|
1877
|
+
const previous = priorEfforts.find((row) => row.id === effortId);
|
|
1878
|
+
nextEfforts.push({
|
|
1879
|
+
...previous,
|
|
1880
|
+
id: effortId,
|
|
1604
1881
|
noteId,
|
|
1605
|
-
blockId:
|
|
1606
|
-
blockContentId,
|
|
1882
|
+
blockId: block.id,
|
|
1883
|
+
blockContentId: fingerprint,
|
|
1607
1884
|
effortSlug,
|
|
1608
|
-
createdAt:
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
effortCount += 1;
|
|
1885
|
+
createdAt: previous?.createdAt ?? now
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1612
1888
|
}
|
|
1613
1889
|
}
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
)
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
});
|
|
1890
|
+
if (!rowsEqual(existing, note)) await tx.readwrite("notes").put(note);
|
|
1891
|
+
if (revisions.length) await tx.readwrite("block_revisions").putMany(revisions);
|
|
1892
|
+
await diffProjection(tx, "block_index", priorRows, nextRows);
|
|
1893
|
+
await diffProjection(tx, "block_efforts", priorEfforts, nextEfforts);
|
|
1894
|
+
const tags = await saveTags(tx, noteId, options.tags, now);
|
|
1895
|
+
let session;
|
|
1896
|
+
let revision;
|
|
1897
|
+
if (startTime !== void 0) {
|
|
1898
|
+
const block = Object.values(blocks).filter((row) => row.kind === "wod").sort((a, b) => a.from - b.from)[0];
|
|
1899
|
+
if (!block) throw new Error("[wod run] Note has no actionable workout block");
|
|
1900
|
+
revision = revisions.find((row) => row.blockId === block.id) ?? latest.get(block.id);
|
|
1901
|
+
if (!revision) throw new Error("[wod run] Saved workout revision is unavailable");
|
|
1902
|
+
session = {
|
|
1903
|
+
id: randomId(),
|
|
1904
|
+
noteId,
|
|
1905
|
+
blockId: block.id,
|
|
1906
|
+
blockVersion: block.version,
|
|
1907
|
+
blockContentId: blockContentId(parseDocumentSections(revision.rawContent)[0]?.displayContent ?? revision.rawContent),
|
|
1908
|
+
origin: "journal",
|
|
1909
|
+
status: "in-progress",
|
|
1910
|
+
startTime,
|
|
1911
|
+
endTime: startTime,
|
|
1912
|
+
duration: 0,
|
|
1913
|
+
completed: false,
|
|
1914
|
+
createdAt: startTime
|
|
1915
|
+
};
|
|
1916
|
+
await tx.readwrite("sessions").put(session);
|
|
1642
1917
|
}
|
|
1643
|
-
|
|
1644
|
-
await db.db.transaction(async (tx) => {
|
|
1645
|
-
await sql.refreshDomain(tx, ops);
|
|
1918
|
+
return { noteId, note, blocks: nextRows.length, efforts: nextEfforts.length, tags, created: !existing, session, revision };
|
|
1646
1919
|
});
|
|
1647
|
-
return {
|
|
1648
|
-
noteId,
|
|
1649
|
-
note,
|
|
1650
|
-
segments: segments.length,
|
|
1651
|
-
blocks: blockCount,
|
|
1652
|
-
efforts: effortCount,
|
|
1653
|
-
tags: tagCount,
|
|
1654
|
-
created: !existingNote
|
|
1655
|
-
};
|
|
1656
1920
|
}
|
|
1657
|
-
async function
|
|
1658
|
-
const
|
|
1659
|
-
|
|
1921
|
+
async function diffProjection(tx, store, previous, next) {
|
|
1922
|
+
const nextIds = new Set(next.map((row) => row.id));
|
|
1923
|
+
for (const row of previous) if (!nextIds.has(row.id)) await tx.readwrite(store).delete(row.id);
|
|
1924
|
+
const old = new Map(previous.map((row) => [row.id, row]));
|
|
1925
|
+
for (const row of next) {
|
|
1926
|
+
if (!rowsEqual(old.get(row.id), row)) {
|
|
1927
|
+
if (store === "block_index" && "blockVersion" in row) await tx.readwrite("block_index").put(row);
|
|
1928
|
+
if (store === "block_efforts" && "effortSlug" in row) await tx.readwrite("block_efforts").put(row);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1660
1931
|
}
|
|
1661
|
-
async function
|
|
1662
|
-
const
|
|
1663
|
-
|
|
1664
|
-
const
|
|
1665
|
-
const
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
const
|
|
1932
|
+
async function saveTags(tx, noteId, items, now) {
|
|
1933
|
+
const previous = await tx.readonly("note_tags").getAllFromIndex("by-note", noteId);
|
|
1934
|
+
if (!items) return previous.length;
|
|
1935
|
+
const next = /* @__PURE__ */ new Map();
|
|
1936
|
+
for (const item of items) {
|
|
1937
|
+
const label = item.label.trim();
|
|
1938
|
+
if (!label) continue;
|
|
1939
|
+
const found = (await tx.readonly("tags").getAllFromIndex("by-label", label))[0];
|
|
1940
|
+
const tag = found ? { ...found, ...!found.type && item.type ? { type: item.type } : {} } : { id: uuid5(CLI_UUID_NAMESPACE, `tag:${label}`), label, type: item.type, createdAt: now };
|
|
1941
|
+
if (!rowsEqual(found, tag)) await tx.readwrite("tags").put(tag);
|
|
1942
|
+
const id = previous.find((link) => link.tagId === tag.id)?.id ?? uuid5(CLI_UUID_NAMESPACE, `note-tag:${noteId}:${tag.id}`);
|
|
1943
|
+
next.set(id, { ...previous.find((link) => link.id === id), id, noteId, tagId: tag.id });
|
|
1944
|
+
}
|
|
1945
|
+
for (const link of previous) if (!next.has(link.id)) await tx.readwrite("note_tags").delete(link.id);
|
|
1946
|
+
const old = new Map(previous.map((row) => [row.id, row]));
|
|
1947
|
+
for (const link of next.values()) if (!rowsEqual(old.get(link.id), link)) await tx.readwrite("note_tags").put(link);
|
|
1948
|
+
return next.size;
|
|
1949
|
+
}
|
|
1950
|
+
function persistNote(db, options) {
|
|
1951
|
+
return saveSource(db, options);
|
|
1952
|
+
}
|
|
1953
|
+
async function startRun(db, options, startTime = Date.now()) {
|
|
1954
|
+
const saved = await saveSource(db, options, startTime);
|
|
1955
|
+
if (!saved.session || !saved.revision) throw new Error("[wod run] Atomic run capture failed");
|
|
1956
|
+
return { session: saved.session, revision: saved.revision, noteCreated: saved.created };
|
|
1957
|
+
}
|
|
1958
|
+
async function persistRun(db, started, executionLog) {
|
|
1959
|
+
const results = executionLog.results;
|
|
1675
1960
|
const session = {
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
segmentVersion: runSegment?.version,
|
|
1679
|
-
noteId: noteState.noteId,
|
|
1680
|
-
blockId: runSegment?.id,
|
|
1681
|
-
blockContentId,
|
|
1682
|
-
origin: "journal",
|
|
1683
|
-
status: results.completed === false ? "in-progress" : "completed",
|
|
1684
|
-
startTime: results.startTime,
|
|
1961
|
+
...started.session,
|
|
1962
|
+
status: results.completed ? "completed" : "in-progress",
|
|
1685
1963
|
endTime: results.endTime,
|
|
1686
1964
|
duration: results.duration ?? 0,
|
|
1687
|
-
completed: results.completed
|
|
1688
|
-
createdAt: results.endTime || Date.now()
|
|
1965
|
+
completed: results.completed
|
|
1689
1966
|
};
|
|
1690
1967
|
const identity = {
|
|
1691
|
-
noteId:
|
|
1692
|
-
resultId,
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
blockContentId,
|
|
1968
|
+
noteId: session.noteId,
|
|
1969
|
+
resultId: session.id,
|
|
1970
|
+
blockId: session.blockId,
|
|
1971
|
+
blockVersion: session.blockVersion,
|
|
1972
|
+
blockContentId: session.blockContentId,
|
|
1696
1973
|
origin: session.origin,
|
|
1697
1974
|
pageId: session.pageId,
|
|
1698
1975
|
workoutTimestamp: session.createdAt
|
|
1699
1976
|
};
|
|
1700
|
-
const
|
|
1701
|
-
const
|
|
1702
|
-
await db.
|
|
1703
|
-
await
|
|
1704
|
-
|
|
1705
|
-
|
|
1977
|
+
const events = toEventRows(executionLog.logs, identity);
|
|
1978
|
+
const summaries = toSummaryEventRows(executionLog.logs, identity);
|
|
1979
|
+
await db.storage.transaction(["sessions", "events", "block_revisions"], "readwrite", async (tx) => {
|
|
1980
|
+
const saved = await tx.readonly("sessions").get(session.id);
|
|
1981
|
+
if (!saved || saved.noteId !== session.noteId || saved.blockId !== session.blockId || saved.blockVersion !== session.blockVersion) {
|
|
1982
|
+
throw new Error("[wod run] Captured Session source pin changed");
|
|
1983
|
+
}
|
|
1984
|
+
const revision = await tx.readonly("block_revisions").get([session.noteId, session.blockId, session.blockVersion]);
|
|
1985
|
+
if (!revision || !rowsEqual(revision, started.revision)) throw new Error("[wod run] Captured revision is unavailable");
|
|
1986
|
+
await tx.readwrite("sessions").put(session);
|
|
1987
|
+
await tx.readwrite("events").putMany([...events, ...summaries]);
|
|
1706
1988
|
});
|
|
1707
|
-
return {
|
|
1708
|
-
noteId: noteState.noteId,
|
|
1709
|
-
resultId,
|
|
1710
|
-
session,
|
|
1711
|
-
events: eventRows.length,
|
|
1712
|
-
summaries: summaryRows.length,
|
|
1713
|
-
noteCreated: noteState.created
|
|
1714
|
-
};
|
|
1989
|
+
return { noteId: session.noteId, resultId: session.id, session, events: events.length, summaries: summaries.length, noteCreated: started.noteCreated };
|
|
1715
1990
|
}
|
|
1716
1991
|
async function persistSessionBundle(db, session, events) {
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1992
|
+
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) {
|
|
1993
|
+
throw new Error("[wod import] Session requires an exact block revision pin");
|
|
1994
|
+
}
|
|
1995
|
+
await db.storage.transaction(["sessions", "events", "block_revisions"], "readwrite", async (tx) => {
|
|
1996
|
+
const revision = await tx.readonly("block_revisions").get([session.noteId, session.blockId, session.blockVersion]);
|
|
1997
|
+
if (!revision || revision.kind !== "wod") throw new Error("[wod import] Exact immutable workout revision is unavailable");
|
|
1998
|
+
for (const event of events) {
|
|
1999
|
+
if (event.resultId !== session.id || event.noteId !== session.noteId || event.blockId !== session.blockId || event.blockVersion !== session.blockVersion) {
|
|
2000
|
+
throw new Error("[wod import] Event source pin differs from Session source pin");
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
const existing = await tx.readonly("sessions").get(session.id);
|
|
2004
|
+
if (existing && !rowsEqual(existing, session)) throw new Error("[wod import] Session identity conflicts with existing row");
|
|
2005
|
+
if (!existing) await tx.readwrite("sessions").put(session);
|
|
2006
|
+
for (const event of events) {
|
|
2007
|
+
const old = await tx.readonly("events").get(event.id);
|
|
2008
|
+
if (old && !rowsEqual(old, event)) throw new Error("[wod import] Event identity conflicts with existing row");
|
|
2009
|
+
if (!old) await tx.readwrite("events").put(event);
|
|
2010
|
+
}
|
|
1736
2011
|
});
|
|
1737
|
-
return { events: events.length
|
|
2012
|
+
return { events: events.length };
|
|
1738
2013
|
}
|
|
1739
2014
|
async function persistEffort(db, effort) {
|
|
1740
2015
|
const sql = await loadSql();
|
|
@@ -1743,308 +2018,72 @@ async function persistEffort(db, effort) {
|
|
|
1743
2018
|
});
|
|
1744
2019
|
}
|
|
1745
2020
|
async function persistTypedTags(db, labels, type) {
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
ops.push({ op: "put", store: "tags", value: { id: randomId(), label, type, createdAt: now } });
|
|
1759
|
-
added += 1;
|
|
1760
|
-
} else if (!tag.type) {
|
|
1761
|
-
ops.push({ op: "put", store: "tags", value: { ...tag, type } });
|
|
2021
|
+
return db.storage.transaction(["tags"], "readwrite", async (tx) => {
|
|
2022
|
+
let added = 0;
|
|
2023
|
+
for (const raw of labels) {
|
|
2024
|
+
const label = raw.trim();
|
|
2025
|
+
if (!label) continue;
|
|
2026
|
+
const tag = (await tx.readonly("tags").getAllFromIndex("by-label", label))[0];
|
|
2027
|
+
if (!tag) {
|
|
2028
|
+
await tx.readwrite("tags").put({ id: uuid5(CLI_UUID_NAMESPACE, `tag:${label}`), label, type, createdAt: Date.now() });
|
|
2029
|
+
added++;
|
|
2030
|
+
} else if (!tag.type) {
|
|
2031
|
+
await tx.readwrite("tags").put({ ...tag, type });
|
|
2032
|
+
}
|
|
1762
2033
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
await db.db.transaction(async (tx) => {
|
|
1766
|
-
await sql.refreshDomain(tx, ops);
|
|
1767
|
-
});
|
|
1768
|
-
}
|
|
1769
|
-
return added;
|
|
2034
|
+
return added;
|
|
2035
|
+
});
|
|
1770
2036
|
}
|
|
1771
2037
|
|
|
1772
2038
|
// src/cli/seed.ts
|
|
1773
|
-
var SEED_NAMESPACE = "1b671a64-40d5-491e-99b0-da01ff1f3341";
|
|
1774
|
-
var SEED_SCHEMA = 7;
|
|
1775
|
-
var CANVAS_CHUNK_ID = "canvas";
|
|
1776
|
-
var DASHBOARDS_CHUNK_ID = "dashboards";
|
|
1777
|
-
var EFFORTS_CHUNK_ID = "efforts";
|
|
1778
|
-
var TEMPLATE_CHUNK_ID = "template";
|
|
1779
|
-
var scalarOf2 = (value) => typeof value === "string" ? value : void 0;
|
|
1780
|
-
function seedNoteId(path) {
|
|
1781
|
-
return formatV5Digest(nodeCrypto().createHash("sha1").update(`${SEED_NAMESPACE}:${path}`).digest());
|
|
1782
|
-
}
|
|
1783
|
-
function titleFromPath(path) {
|
|
1784
|
-
const stem = path.split("/").pop() ?? path;
|
|
1785
|
-
return stem.replace(/\.md$/, "").replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1786
|
-
}
|
|
1787
|
-
function catalogForChunkId(chunkId) {
|
|
1788
|
-
const match = /^(?:collection|feed)\.(.+)$/.exec(chunkId);
|
|
1789
|
-
const value = match?.[1];
|
|
1790
|
-
return value && value !== "_root" ? value : void 0;
|
|
1791
|
-
}
|
|
1792
|
-
function canvasRouteSlug(content) {
|
|
1793
|
-
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
1794
|
-
if (fm?.[1] && !/^template:\s*canvas\s*$/m.test(fm[1])) return null;
|
|
1795
|
-
const route = fm?.[1]?.match(/^route:\s*(\S+)\s*$/m)?.[1];
|
|
1796
|
-
if (!route) return null;
|
|
1797
|
-
return route.replace(/^\//, "") || null;
|
|
1798
|
-
}
|
|
1799
|
-
function seedNoteSourceId(path, content, chunkId) {
|
|
1800
|
-
const parts = path.split("/");
|
|
1801
|
-
const stem = (parts[parts.length - 1] ?? "").replace(/\.md$/i, "");
|
|
1802
|
-
const under = parts.slice(2).map((p) => p.replace(/\.md$/i, "")).join("/");
|
|
1803
|
-
if (chunkId === CANVAS_CHUNK_ID) {
|
|
1804
|
-
const route = canvasRouteSlug(content);
|
|
1805
|
-
return route ? `guides:${route}` : `guides:${under}`;
|
|
1806
|
-
}
|
|
1807
|
-
if (chunkId === DASHBOARDS_CHUNK_ID) return "dashboards";
|
|
1808
|
-
if (chunkId === EFFORTS_CHUNK_ID) return "efforts";
|
|
1809
|
-
if (chunkId === TEMPLATE_CHUNK_ID) return "playground";
|
|
1810
|
-
if (chunkId.startsWith("collection.")) {
|
|
1811
|
-
const catalog = catalogForChunkId(chunkId);
|
|
1812
|
-
if (!catalog) return "collection:_root";
|
|
1813
|
-
return stem.toLowerCase() === "readme" ? `collection:${catalog}` : `collection:${catalog}/${stem}`;
|
|
1814
|
-
}
|
|
1815
|
-
if (chunkId.startsWith("feed.")) return `feed:feeds/${under}`;
|
|
1816
|
-
return void 0;
|
|
1817
|
-
}
|
|
1818
|
-
function buildNoteRecord(row, chunkId, seedVersion) {
|
|
1819
|
-
const id = seedNoteId(row.path);
|
|
1820
|
-
const createdAt = seedVersion;
|
|
1821
|
-
const meta = readFrontmatterMeta(row.content);
|
|
1822
|
-
const tags = extractTypedTags(row.content);
|
|
1823
|
-
let date;
|
|
1824
|
-
let page;
|
|
1825
|
-
let pageNote;
|
|
1826
|
-
const rawDate = scalarOf2(meta["date"]);
|
|
1827
|
-
if (rawDate && /^\d{4}-\d{2}-\d{2}$/.test(rawDate)) {
|
|
1828
|
-
date = (/* @__PURE__ */ new Date(`${rawDate}T12:00:00Z`)).getTime();
|
|
1829
|
-
}
|
|
1830
|
-
const slug = scalarOf2(meta["slug"]);
|
|
1831
|
-
if (slug) {
|
|
1832
|
-
const pageId = seedNoteId(`page:${slug}`);
|
|
1833
|
-
page = { id: pageId, slug, title: titleFromPath(row.path), createdAt };
|
|
1834
|
-
pageNote = { id: seedNoteId(`pagenote:${slug}:${row.path}`), pageId, noteId: id, createdAt };
|
|
1835
|
-
}
|
|
1836
|
-
const sourceId = seedNoteSourceId(row.path, row.content, chunkId);
|
|
1837
|
-
if (!sourceId) {
|
|
1838
|
-
throw new Error(`[wod seed] chunk "${chunkId}" row "${row.path}" has no source family \u2014 refusing to import it as journal`);
|
|
1839
|
-
}
|
|
1840
|
-
const metaTitle = scalarOf2(meta["title"]);
|
|
1841
|
-
return {
|
|
1842
|
-
note: {
|
|
1843
|
-
id,
|
|
1844
|
-
title: metaTitle?.trim() || titleFromPath(row.path),
|
|
1845
|
-
date,
|
|
1846
|
-
createdAt,
|
|
1847
|
-
type: chunkId === DASHBOARDS_CHUNK_ID ? "dashboard" : "note",
|
|
1848
|
-
sourceId,
|
|
1849
|
-
catalog: catalogForChunkId(chunkId),
|
|
1850
|
-
seedOrigin: "seed",
|
|
1851
|
-
seedVersion,
|
|
1852
|
-
seedChunkId: chunkId,
|
|
1853
|
-
sourcePath: row.path
|
|
1854
|
-
},
|
|
1855
|
-
segment: {
|
|
1856
|
-
id: `seed:${id}`,
|
|
1857
|
-
version: 1,
|
|
1858
|
-
noteId: id,
|
|
1859
|
-
position: 0,
|
|
1860
|
-
dataType: "markdown",
|
|
1861
|
-
data: null,
|
|
1862
|
-
rawContent: row.content,
|
|
1863
|
-
createdAt
|
|
1864
|
-
},
|
|
1865
|
-
tags,
|
|
1866
|
-
page,
|
|
1867
|
-
pageNote,
|
|
1868
|
-
rawContent: row.content,
|
|
1869
|
-
sourcePath: row.path
|
|
1870
|
-
};
|
|
1871
|
-
}
|
|
1872
|
-
async function metaRecord(sql, db, sourceId) {
|
|
1873
|
-
const rows = await sql.listRows(db, sql.storeDefinition("meta"));
|
|
1874
|
-
const existing = rows.find((r) => r.key === `seed:${sourceId}`);
|
|
1875
|
-
return existing ?? {
|
|
1876
|
-
key: `seed:${sourceId}`,
|
|
1877
|
-
schema: SEED_SCHEMA,
|
|
1878
|
-
seedVersion: 0,
|
|
1879
|
-
builtAt: "",
|
|
1880
|
-
importedAt: 0,
|
|
1881
|
-
chunks: {},
|
|
1882
|
-
claim: null
|
|
1883
|
-
};
|
|
1884
|
-
}
|
|
1885
|
-
async function applyOps(db, ops) {
|
|
1886
|
-
if (ops.length === 0) return;
|
|
1887
|
-
const sql = await loadSql();
|
|
1888
|
-
await db.db.transaction(async (tx) => {
|
|
1889
|
-
await sql.refreshDomain(tx, ops);
|
|
1890
|
-
});
|
|
1891
|
-
}
|
|
1892
2039
|
async function importSeedDir(db, dir, sourceId) {
|
|
1893
|
-
const
|
|
1894
|
-
const
|
|
1895
|
-
const
|
|
1896
|
-
const
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
);
|
|
1901
|
-
const
|
|
1902
|
-
|
|
1903
|
-
);
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
deleted: 0,
|
|
1912
|
-
skippedUserOwned: 0
|
|
2040
|
+
const fs = nodeFs(), path = nodePath();
|
|
2041
|
+
const source = sourceId || path.basename(dir);
|
|
2042
|
+
const manifest = assertSeedManifest(JSON.parse(fs.readFileSync(path.resolve(dir, "manifest.json"), "utf-8")));
|
|
2043
|
+
const key = seedMetaKey(source);
|
|
2044
|
+
let expected = await db.storage.readonly("meta").get(key);
|
|
2045
|
+
if (expected && expected.schema !== SEED_SCHEMA) throw new Error("Incompatible seed checkpoint; recreate storage");
|
|
2046
|
+
let meta = expected ?? emptySeedMeta(key);
|
|
2047
|
+
if (manifest.version < meta.seedVersion) throw new Error("Seed manifest is older than stored checkpoint");
|
|
2048
|
+
const summary = { sourceId: source, version: manifest.version, notes: 0, efforts: 0, blocks: 0, blockEfforts: 0, deleted: 0, skippedUserOwned: 0 };
|
|
2049
|
+
const metadataChanged = manifest.version !== meta.seedVersion || manifest.builtAt !== meta.builtAt;
|
|
2050
|
+
const plan = manifest.chunks.filter((c) => metadataChanged || meta.chunks[c.id]?.sha256 !== c.sha256);
|
|
2051
|
+
plan.sort((a, b) => Number(b.id === CANVAS_CHUNK_ID) - Number(a.id === CANVAS_CHUNK_ID));
|
|
2052
|
+
const accumulate = (result) => {
|
|
2053
|
+
summary.notes += result.totalNotes;
|
|
2054
|
+
summary.efforts += result.totalEfforts;
|
|
2055
|
+
summary.blocks += result.totalBlocks;
|
|
2056
|
+
summary.deleted += result.deleted;
|
|
2057
|
+
summary.skippedUserOwned += result.skippedUserOwned;
|
|
1913
2058
|
};
|
|
1914
|
-
const
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
const built = rows2.map((row) => buildNoteRecord(row, chunk.id, manifest.version));
|
|
1936
|
-
const noteIds = [];
|
|
1937
|
-
for (const record of built) {
|
|
1938
|
-
bySourcePath.set(record.sourcePath, record.note.id);
|
|
1939
|
-
const existing = notesById.get(record.note.id);
|
|
1940
|
-
if (existing && existing.seedOrigin !== "seed") {
|
|
1941
|
-
summary.skippedUserOwned += 1;
|
|
1942
|
-
continue;
|
|
1943
|
-
}
|
|
1944
|
-
noteIds.push(record.note.id);
|
|
1945
|
-
ops.push({ op: "put", store: "notes", value: record.note });
|
|
1946
|
-
ops.push({ op: "put", store: "segments", value: record.segment });
|
|
1947
|
-
if (record.page) ops.push({ op: "put", store: "page", value: record.page });
|
|
1948
|
-
if (record.pageNote) ops.push({ op: "put", store: "page_notes", value: record.pageNote });
|
|
1949
|
-
for (const item of record.tags) {
|
|
1950
|
-
const label = item.label;
|
|
1951
|
-
let tag = tagRowByLabel.get(label);
|
|
1952
|
-
if (!tag) {
|
|
1953
|
-
tag = { id: randomId(), label, type: item.type, createdAt: Date.now() };
|
|
1954
|
-
tagRowByLabel.set(label, tag);
|
|
1955
|
-
ops.push({ op: "put", store: "tags", value: tag });
|
|
1956
|
-
} else if (item.type && !tag.type) {
|
|
1957
|
-
tag = { ...tag, type: item.type };
|
|
1958
|
-
tagRowByLabel.set(label, tag);
|
|
1959
|
-
ops.push({ op: "put", store: "tags", value: tag });
|
|
1960
|
-
}
|
|
1961
|
-
const tagId = tag.id;
|
|
1962
|
-
ops.push({
|
|
1963
|
-
op: "put",
|
|
1964
|
-
store: "note_tags",
|
|
1965
|
-
// Deterministic junction id — re-seeds must upsert the same row
|
|
1966
|
-
// (note_tags has a unique [noteId, tagId] index).
|
|
1967
|
-
value: { id: uuid5(CLI_UUID_NAMESPACE, `note-tag:${record.note.id}:${tagId}`), noteId: record.note.id, tagId }
|
|
1968
|
-
});
|
|
1969
|
-
}
|
|
1970
|
-
if (chunk.id === EFFORTS_CHUNK_ID) {
|
|
1971
|
-
const effort = markdownToEffort(record.rawContent);
|
|
1972
|
-
if (effort) {
|
|
1973
|
-
ops.push({ op: "put", store: "efforts", value: { ...effort, registrySource: "bundled" } });
|
|
1974
|
-
summary.efforts += 1;
|
|
1975
|
-
}
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
const priorIds = prior.chunks[chunk.id]?.noteIds ?? [];
|
|
1979
|
-
const goneIds = priorIds.filter((id) => !noteIds.includes(id) && notesById.get(id)?.seedOrigin === "seed");
|
|
1980
|
-
for (const id of goneIds) {
|
|
1981
|
-
ops.push(...await noteCascadeOps(sql, db, id));
|
|
1982
|
-
summary.deleted += 1;
|
|
1983
|
-
}
|
|
1984
|
-
noteIdsByChunk[chunk.id] = rows2.map((row) => seedNoteId(row.path));
|
|
1985
|
-
nextMeta.chunks[chunk.id] = { sha256: chunk.sha256, noteIds: noteIdsByChunk[chunk.id] };
|
|
1986
|
-
summary.notes += noteIds.length;
|
|
1987
|
-
await applyOps(db, ops);
|
|
1988
|
-
continue;
|
|
1989
|
-
}
|
|
1990
|
-
const rows = payload.filter((row) => row.isStatic === true);
|
|
1991
|
-
const rekeyed = rows.map((row) => {
|
|
1992
|
-
const noteId = row.sourcePath ? bySourcePath.get(row.sourcePath) : void 0;
|
|
1993
|
-
if (!noteId) {
|
|
1994
|
-
throw new Error(
|
|
1995
|
-
`[wod seed] static row ${row.id}: sourcePath "${row.sourcePath ?? ""}" has no imported note \u2014 chunk aborted`
|
|
1996
|
-
);
|
|
1997
|
-
}
|
|
1998
|
-
return chunk.kind === "block-efforts" ? { ...row, noteId } : { ...row, noteId, routeId: row.noteId };
|
|
1999
|
-
});
|
|
2000
|
-
const store = chunk.kind === "block-index" ? "block_index" : "block_efforts";
|
|
2001
|
-
for (const row of rekeyed) ops.push({ op: "put", store, value: row });
|
|
2002
|
-
const priorBlockIds = prior.chunks[chunk.id]?.blockIds ?? [];
|
|
2003
|
-
const writtenIds = new Set(rekeyed.map((row) => row.id));
|
|
2004
|
-
const goneBlocks = priorBlockIds.filter((id) => !writtenIds.has(id));
|
|
2005
|
-
for (const id of goneBlocks) ops.push({ op: "delete", store, key: id });
|
|
2006
|
-
summary.deleted += goneBlocks.length;
|
|
2007
|
-
blockIdsByChunk[chunk.id] = payload.map((row) => row.id);
|
|
2008
|
-
nextMeta.chunks[chunk.id] = { sha256: chunk.sha256, blockIds: blockIdsByChunk[chunk.id] };
|
|
2009
|
-
if (chunk.kind === "block-index") summary.blocks += rekeyed.length;
|
|
2010
|
-
else summary.blockEfforts += rekeyed.length;
|
|
2011
|
-
await applyOps(db, ops);
|
|
2012
|
-
}
|
|
2013
|
-
const manifestIds = new Set(manifest.chunks.map((c) => c.id));
|
|
2014
|
-
for (const [chunkId, checkpoint] of Object.entries(prior.chunks)) {
|
|
2015
|
-
if (manifestIds.has(chunkId)) continue;
|
|
2016
|
-
const sweep = [];
|
|
2017
|
-
for (const id of checkpoint.noteIds ?? []) {
|
|
2018
|
-
if (notesById.get(id)?.seedOrigin === "seed") {
|
|
2019
|
-
sweep.push(...await noteCascadeOps(sql, db, id));
|
|
2020
|
-
summary.deleted += 1;
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
for (const id of checkpoint.blockIds ?? []) {
|
|
2024
|
-
sweep.push({ op: "delete", store: "block_index", key: id });
|
|
2025
|
-
summary.deleted += 1;
|
|
2026
|
-
}
|
|
2027
|
-
await applyOps(db, sweep);
|
|
2059
|
+
for (const chunk of plan) {
|
|
2060
|
+
const rows = assertSeedRows(JSON.parse(fs.readFileSync(path.resolve(dir, chunk.path), "utf-8")));
|
|
2061
|
+
if (rows.length !== chunk.count) throw new Error("Seed chunk count mismatch");
|
|
2062
|
+
const next = { ...meta, chunks: { ...meta.chunks, [chunk.id]: { sha256: chunk.sha256, noteIds: await Promise.all(rows.map((r) => seedNoteId(r.path))) } }, importedAt: Date.now() };
|
|
2063
|
+
accumulate(await applySeedChunk(db.storage, { chunkId: chunk.id, rows, manifest, expectedMeta: expected, meta: next }));
|
|
2064
|
+
summary.blockEfforts += rows.reduce((count, row) => count + row.blockEfforts.length, 0);
|
|
2065
|
+
meta = next;
|
|
2066
|
+
expected = next;
|
|
2067
|
+
}
|
|
2068
|
+
for (const chunkId of Object.keys(meta.chunks)) {
|
|
2069
|
+
if (manifest.chunks.some((c) => c.id === chunkId)) continue;
|
|
2070
|
+
const chunks = { ...meta.chunks };
|
|
2071
|
+
delete chunks[chunkId];
|
|
2072
|
+
const next = { ...meta, chunks, importedAt: Date.now() };
|
|
2073
|
+
accumulate(await applySeedChunk(db.storage, { chunkId, rows: [], manifest, expectedMeta: expected, meta: next }));
|
|
2074
|
+
meta = next;
|
|
2075
|
+
expected = next;
|
|
2076
|
+
}
|
|
2077
|
+
if (plan.length || metadataChanged || Object.keys(meta.chunks).length !== Object.keys(expected?.chunks ?? {}).length) {
|
|
2078
|
+
const next = { ...meta, schema: SEED_SCHEMA, seedVersion: manifest.version, builtAt: manifest.builtAt, importedAt: Date.now() };
|
|
2079
|
+
await applySeedChunk(db.storage, { chunkId: null, rows: [], manifest, expectedMeta: expected, meta: next });
|
|
2028
2080
|
}
|
|
2029
|
-
await applyOps(db, [{ op: "put", store: "meta", value: nextMeta }]);
|
|
2030
2081
|
return summary;
|
|
2031
2082
|
}
|
|
2032
|
-
async function noteCascadeOps(sql, db, noteId) {
|
|
2033
|
-
const ops = [{ op: "delete", store: "notes", key: noteId }];
|
|
2034
|
-
for (const store of ["segments", "block_index", "block_efforts", "note_tags"]) {
|
|
2035
|
-
const rows = await sql.listRows(db.db, sql.storeDefinition(store), { only: noteId }, void 0, "by-note");
|
|
2036
|
-
for (const row of rows) {
|
|
2037
|
-
ops.push({ op: "delete", store, key: store === "segments" ? [row.id, row.version ?? 1] : row.id });
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
return ops;
|
|
2041
|
-
}
|
|
2042
2083
|
function defaultSeedDirs(cwd) {
|
|
2043
2084
|
const base = nodePath().resolve(cwd, "apps", "playground", "public", "seed");
|
|
2044
2085
|
return ["core", "community"].map((id) => nodePath().resolve(base, id)).filter((dir) => nodeFs().existsSync(nodePath().resolve(dir, "manifest.json")));
|
|
2045
2086
|
}
|
|
2046
|
-
|
|
2047
|
-
// src/cli/runner.ts
|
|
2048
2087
|
function getFs2() {
|
|
2049
2088
|
if (typeof globalThis.process?.versions?.node === "undefined" && typeof globalThis.Bun === "undefined") {
|
|
2050
2089
|
throw new Error("File system operations are only supported in Node / Bun environments");
|
|
@@ -2052,6 +2091,16 @@ function getFs2() {
|
|
|
2052
2091
|
return __require("fs");
|
|
2053
2092
|
}
|
|
2054
2093
|
var VERSION = "0.6.0";
|
|
2094
|
+
function cliNoteSource(source, label, sport) {
|
|
2095
|
+
if (/\.(?:md|markdown)$/i.test(label) || parseDocumentSections(source).some((section) => section.scriptBlock)) return source;
|
|
2096
|
+
let length = 3;
|
|
2097
|
+
for (const match of source.matchAll(/`+/g)) length = Math.max(length, match[0].length + 1);
|
|
2098
|
+
const fence = "`".repeat(length);
|
|
2099
|
+
return `${fence}time${sport ? `:${sport}` : ""}
|
|
2100
|
+
${source}
|
|
2101
|
+
${fence}
|
|
2102
|
+
`;
|
|
2103
|
+
}
|
|
2055
2104
|
function parseCliArgs(args) {
|
|
2056
2105
|
const result = {
|
|
2057
2106
|
format: "json",
|
|
@@ -2061,6 +2110,7 @@ function parseCliArgs(args) {
|
|
|
2061
2110
|
let i = 0;
|
|
2062
2111
|
while (i < args.length) {
|
|
2063
2112
|
const arg = args[i];
|
|
2113
|
+
if (arg === "--at" || arg.startsWith("--at=")) throw new Error("Unknown option --at; whole Note exports have no as-of version");
|
|
2064
2114
|
if (arg === "--help" || arg === "-h") {
|
|
2065
2115
|
result.help = true;
|
|
2066
2116
|
i++;
|
|
@@ -2119,12 +2169,6 @@ function parseCliArgs(args) {
|
|
|
2119
2169
|
} else if (arg.startsWith("--debug=")) {
|
|
2120
2170
|
result.debug = arg.slice("--debug=".length);
|
|
2121
2171
|
i++;
|
|
2122
|
-
} else if (arg === "--at") {
|
|
2123
|
-
result.at = args[++i];
|
|
2124
|
-
i++;
|
|
2125
|
-
} else if (arg.startsWith("--at=")) {
|
|
2126
|
-
result.at = arg.slice("--at=".length);
|
|
2127
|
-
i++;
|
|
2128
2172
|
} else if (arg === "--source") {
|
|
2129
2173
|
const source = args[++i];
|
|
2130
2174
|
if (source) result.sources.push(source);
|
|
@@ -2218,7 +2262,7 @@ USAGE:
|
|
|
2218
2262
|
|
|
2219
2263
|
SUBCOMMANDS:
|
|
2220
2264
|
parse <file | -> Parse Whiteboard Script, create a note in the database
|
|
2221
|
-
(
|
|
2265
|
+
(whole source + saved revisions + sparse block index), optionally emit parse-tree IR.
|
|
2222
2266
|
Markdown carrying a 'wod-note-id' frontmatter stamp
|
|
2223
2267
|
(see download) imports back into that same note.
|
|
2224
2268
|
run <file | -> Execute the script state machine, record a session +
|
|
@@ -2226,10 +2270,8 @@ SUBCOMMANDS:
|
|
|
2226
2270
|
execution-log IR
|
|
2227
2271
|
query '<wql>' Evaluate WQL against the database (or an explicit
|
|
2228
2272
|
dataset) and emit QueryResult / RowsQueryResult IR
|
|
2229
|
-
download note <id>
|
|
2230
|
-
|
|
2231
|
-
as-of a segment version (default: latest). The stamp
|
|
2232
|
-
in the frontmatter makes 'wod parse' re-import it.
|
|
2273
|
+
download note <id> Export the note's authored markdown to <id>.md.
|
|
2274
|
+
The frontmatter identity stamp allows re-import.
|
|
2233
2275
|
Frontmatter tags (general + typed equipment/quality/\u2026)
|
|
2234
2276
|
are materialized into the tag graph on parse/import.
|
|
2235
2277
|
download session <id>
|
|
@@ -2246,8 +2288,8 @@ SUBCOMMANDS:
|
|
|
2246
2288
|
import note <file> Re-import exported note markdown (same as parse with
|
|
2247
2289
|
the stamped identity).
|
|
2248
2290
|
import session <file>
|
|
2249
|
-
|
|
2250
|
-
|
|
2291
|
+
Import a session + its events from a downloaded CSV.
|
|
2292
|
+
The exact saved block revision must already exist.
|
|
2251
2293
|
import effort <file>
|
|
2252
2294
|
Upsert an effort registry row from corpus-style markdown.
|
|
2253
2295
|
import equipment <file>
|
|
@@ -2255,7 +2297,7 @@ SUBCOMMANDS:
|
|
|
2255
2297
|
build [dir] Compile a seed corpus: validate every .md under
|
|
2256
2298
|
<dir>/markdown (parity gate + dashboard format gate) and
|
|
2257
2299
|
write manifest.json + content-hashed chunks/ +
|
|
2258
|
-
|
|
2300
|
+
report.html to <dir>/dist with source/maps/projections bundled
|
|
2259
2301
|
(override with -o/--output). Version stamps derive from
|
|
2260
2302
|
<dir>'s git history (full checkout required).
|
|
2261
2303
|
|
|
@@ -2283,8 +2325,6 @@ OPTIONS:
|
|
|
2283
2325
|
--format <fmt> Output format: json (default), table, csv
|
|
2284
2326
|
-o, --output <path> Write the IR payload (or download) to a path instead of
|
|
2285
2327
|
the default; '-' writes to stdout
|
|
2286
|
-
--at <version> With 'download note': compose the markdown as-of the
|
|
2287
|
-
given segment version (default: latest)
|
|
2288
2328
|
--debug[=name] Also write the IR payload to <name> (default
|
|
2289
2329
|
wod-<subcommand>-debug.json); without it, parse/run
|
|
2290
2330
|
print a one-line summary of what was persisted
|
|
@@ -2313,7 +2353,13 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
2313
2353
|
const writeOut = io.stdout ?? ((t) => process.stdout.write(t + "\n"));
|
|
2314
2354
|
const writeErr = io.stderr ?? ((t) => process.stderr.write(t + "\n"));
|
|
2315
2355
|
const getStdin = io.readStdinFn ?? readStdin;
|
|
2316
|
-
|
|
2356
|
+
let parsed;
|
|
2357
|
+
try {
|
|
2358
|
+
parsed = parseCliArgs(argv);
|
|
2359
|
+
} catch (error) {
|
|
2360
|
+
writeErr(error instanceof Error ? error.message : String(error));
|
|
2361
|
+
return 1;
|
|
2362
|
+
}
|
|
2317
2363
|
if (parsed.version || parsed.subcommand === "version") {
|
|
2318
2364
|
writeOut(`wod v${VERSION}`);
|
|
2319
2365
|
return 0;
|
|
@@ -2339,7 +2385,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
2339
2385
|
}
|
|
2340
2386
|
}
|
|
2341
2387
|
const deliverOutput = (formattedText) => {
|
|
2342
|
-
if (parsed.output) {
|
|
2388
|
+
if (parsed.output && parsed.output !== "-") {
|
|
2343
2389
|
getFs2().writeFileSync(parsed.output, formattedText, "utf-8");
|
|
2344
2390
|
} else {
|
|
2345
2391
|
writeOut(formattedText);
|
|
@@ -2369,20 +2415,21 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
2369
2415
|
}
|
|
2370
2416
|
source = fs.readFileSync(parsed.target, "utf-8");
|
|
2371
2417
|
}
|
|
2418
|
+
const noteSource = cliNoteSource(source, parsed.target, parsed.sport);
|
|
2372
2419
|
const ir = runParse(source, { sport: parsed.sport, sourceLabel: `cli:wod parse ${parsed.target}` });
|
|
2373
2420
|
const identity = extractNoteIdentity(source);
|
|
2374
2421
|
const tags = extractTypedTags(source);
|
|
2375
2422
|
const db = await openCliDb(parsed.db);
|
|
2376
2423
|
try {
|
|
2377
2424
|
const persisted = await persistNote(db, {
|
|
2378
|
-
source,
|
|
2425
|
+
source: noteSource,
|
|
2379
2426
|
sourceLabel: parsed.target,
|
|
2380
2427
|
noteId: identity?.noteId,
|
|
2381
2428
|
title: identity?.title,
|
|
2382
2429
|
tags
|
|
2383
2430
|
});
|
|
2384
2431
|
writeOut(
|
|
2385
|
-
`[wod parse] note ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.
|
|
2432
|
+
`[wod parse] note ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.blocks} blocks, ${persisted.efforts} efforts${persisted.tags > 0 ? `, ${persisted.tags} tags` : ""} \u2192 ${db.path}`
|
|
2386
2433
|
);
|
|
2387
2434
|
} finally {
|
|
2388
2435
|
await db.close();
|
|
@@ -2410,14 +2457,30 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
2410
2457
|
}
|
|
2411
2458
|
source = fs.readFileSync(parsed.target, "utf-8");
|
|
2412
2459
|
}
|
|
2413
|
-
const
|
|
2460
|
+
const identity = extractNoteIdentity(source);
|
|
2414
2461
|
const db = await openCliDb(parsed.db);
|
|
2462
|
+
let ir;
|
|
2415
2463
|
try {
|
|
2416
|
-
const
|
|
2417
|
-
source,
|
|
2464
|
+
const started = await startRun(db, {
|
|
2465
|
+
source: cliNoteSource(source, parsed.target, parsed.sport),
|
|
2418
2466
|
sourceLabel: parsed.target,
|
|
2419
|
-
|
|
2467
|
+
noteId: identity?.noteId,
|
|
2468
|
+
title: identity?.title,
|
|
2469
|
+
tags: extractTypedTags(source)
|
|
2470
|
+
});
|
|
2471
|
+
const section = parseDocumentSections(started.revision.rawContent).find((row) => row.scriptBlock);
|
|
2472
|
+
if (!section?.scriptBlock) throw new Error("[wod run] Captured workout source cannot be parsed");
|
|
2473
|
+
const bare = !/\.(?:md|markdown)$/i.test(parsed.target) && !parseDocumentSections(source).some((row) => row.scriptBlock);
|
|
2474
|
+
const frozenBody = bare ? section.scriptBlock.content.slice(0, -1) : section.scriptBlock.content;
|
|
2475
|
+
ir = await runExecution(frozenBody, {
|
|
2476
|
+
sport: started.revision.sport,
|
|
2477
|
+
dialect: started.revision.dialect,
|
|
2478
|
+
blockId: started.session.blockId,
|
|
2479
|
+
blockVersion: started.session.blockVersion,
|
|
2480
|
+
startTime: started.session.startTime,
|
|
2481
|
+
sourceLabel: `cli:wod run ${parsed.target}`
|
|
2420
2482
|
});
|
|
2483
|
+
const persisted = await persistRun(db, started, ir.data);
|
|
2421
2484
|
writeOut(
|
|
2422
2485
|
`[wod run] session ${persisted.resultId} \u2014 ${persisted.events} events, ${persisted.summaries} summaries on note ${persisted.noteId} \u2192 ${db.path}`
|
|
2423
2486
|
);
|
|
@@ -2495,7 +2558,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
2495
2558
|
const id = parsed.target2;
|
|
2496
2559
|
const DOWNLOAD_KINDS = ["note", "session", "effort", "equipment"];
|
|
2497
2560
|
if (!id && kind !== "equipment" || !DOWNLOAD_KINDS.includes(kind ?? "")) {
|
|
2498
|
-
writeErr("[wod download] Usage: wod download <note|session|effort|equipment> <id> [
|
|
2561
|
+
writeErr("[wod download] Usage: wod download <note|session|effort|equipment> <id> [-o <path>]");
|
|
2499
2562
|
return 1;
|
|
2500
2563
|
}
|
|
2501
2564
|
const targetId = id;
|
|
@@ -2507,21 +2570,14 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
2507
2570
|
writeErr(`[wod download] Note not found in ${db.path}: ${id}`);
|
|
2508
2571
|
return 1;
|
|
2509
2572
|
}
|
|
2510
|
-
const
|
|
2511
|
-
|
|
2512
|
-
writeErr(`[wod download] Invalid --at version: ${parsed.at}`);
|
|
2513
|
-
return 1;
|
|
2514
|
-
}
|
|
2515
|
-
const markdown = composeNoteMarkdown(bundle.note, bundle.segments, at);
|
|
2516
|
-
const target = parsed.output && parsed.output !== "-" ? parsed.output : `${id}${at ? `.v${at}` : ""}.md`;
|
|
2573
|
+
const markdown = composeNoteMarkdown(bundle.note);
|
|
2574
|
+
const target = parsed.output && parsed.output !== "-" ? parsed.output : `${id}.md`;
|
|
2517
2575
|
if (parsed.output === "-") {
|
|
2518
2576
|
writeOut(markdown);
|
|
2519
2577
|
} else {
|
|
2520
2578
|
nodeFs().writeFileSync(target, markdown, "utf-8");
|
|
2521
2579
|
}
|
|
2522
|
-
writeOut(
|
|
2523
|
-
`[wod download] note ${id}${at ? ` v${at}` : ""} \u2192 ${target} (${bundle.segments.length} segment rows)`
|
|
2524
|
-
);
|
|
2580
|
+
if (parsed.output !== "-") writeOut(`[wod download] note ${id} \u2192 ${target}`);
|
|
2525
2581
|
} else if (kind === "session") {
|
|
2526
2582
|
const bundle = await getSessionBundle(db.db, targetId);
|
|
2527
2583
|
if (!bundle) {
|
|
@@ -2535,7 +2591,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
2535
2591
|
} else {
|
|
2536
2592
|
nodeFs().writeFileSync(target, csv, "utf-8");
|
|
2537
2593
|
}
|
|
2538
|
-
writeOut(`[wod download] session ${id} \u2192 ${target} (${bundle.events.length} events)`);
|
|
2594
|
+
if (parsed.output !== "-") writeOut(`[wod download] session ${id} \u2192 ${target} (${bundle.events.length} events)`);
|
|
2539
2595
|
} else if (kind === "effort") {
|
|
2540
2596
|
const effort = await getEffortBySlug(db.db, targetId);
|
|
2541
2597
|
if (!effort) {
|
|
@@ -2593,7 +2649,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
2593
2649
|
tags
|
|
2594
2650
|
});
|
|
2595
2651
|
writeOut(
|
|
2596
|
-
`[wod import] note ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.
|
|
2652
|
+
`[wod import] note ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.blocks} blocks${persisted.tags > 0 ? `, ${persisted.tags} tags` : ""} \u2192 ${db2.path}`
|
|
2597
2653
|
);
|
|
2598
2654
|
} finally {
|
|
2599
2655
|
await db2.close();
|
|
@@ -2635,7 +2691,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
2635
2691
|
try {
|
|
2636
2692
|
const result = await persistSessionBundle(db, session, events);
|
|
2637
2693
|
writeOut(
|
|
2638
|
-
`[wod import] session ${session.id} \u2014 ${result.events} events
|
|
2694
|
+
`[wod import] session ${session.id} \u2014 ${result.events} events \u2192 ${db.path}`
|
|
2639
2695
|
);
|
|
2640
2696
|
} finally {
|
|
2641
2697
|
await db.close();
|
|
@@ -2649,7 +2705,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
2649
2705
|
writeErr(`[wod build] No markdown/ corpus under ${root} \u2014 run from the content repo root (or pass its directory).`);
|
|
2650
2706
|
return 1;
|
|
2651
2707
|
}
|
|
2652
|
-
const { generateSeed } = await import('./compiler-
|
|
2708
|
+
const { generateSeed } = await import('./compiler-MQRZVZQW.mjs');
|
|
2653
2709
|
try {
|
|
2654
2710
|
const result = generateSeed({
|
|
2655
2711
|
corpusRoot: root,
|
|
@@ -2661,7 +2717,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
2661
2717
|
const totalBytes = result.manifest.chunks.reduce((s, c) => s + c.bytes, 0);
|
|
2662
2718
|
writeOut(`[wod build] schema v${result.manifest.schema} version ${result.manifest.version} (${result.manifest.versionLabel}) \u2192 ${result.outDir}`);
|
|
2663
2719
|
for (const c of result.manifest.chunks) {
|
|
2664
|
-
writeOut(` ${c.id.padEnd(48)} ${String(c.count).padStart(4)}
|
|
2720
|
+
writeOut(` ${c.id.padEnd(48)} ${String(c.count).padStart(4)} files ${kb(c.bytes).padStart(8)}`);
|
|
2665
2721
|
}
|
|
2666
2722
|
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`);
|
|
2667
2723
|
return 0;
|
|
@@ -2717,6 +2773,6 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
2717
2773
|
}
|
|
2718
2774
|
}
|
|
2719
2775
|
|
|
2720
|
-
export { NOTE_ID_FRONTMATTER_KEY, PackLoadError, ParseSyntaxError, SESSION_CSV_HEADER, WqlSyntaxError, buildStatementTree, buildWorkoutResults, cliMain, composeNoteMarkdown, createIRFile, csvToEquipmentLabels, csvToSessionRows, defineLanguagePack, effortToMarkdown, equipmentToCsv, extractNoteIdentity, extractTypedTags, formatExecutionOutput, formatParseOutput, formatQueryOutput, getRegisteredLanguagePacks, inMemoryEventStore, injectIdentityFrontmatter, isIRFile, listLanguagePacks, loadLanguagePack, loadQueryData, markdownToEffort, parseCliArgs, parseCsvLine, readFrontmatterMeta, readStdin, registerLanguagePack, runExecution, runParse, runQueryCli, sessionToCsv, splitCsvRecords, statementToNode, unregisterLanguagePack };
|
|
2721
|
-
//# sourceMappingURL=chunk-
|
|
2722
|
-
//# sourceMappingURL=chunk-
|
|
2776
|
+
export { NOTE_ID_FRONTMATTER_KEY, PackLoadError, ParseSyntaxError, SESSION_CSV_HEADER, WqlSyntaxError, applySeedChunk, assertSeedManifest, assertSeedRows, buildStatementTree, buildWorkoutResults, cliMain, composeNoteMarkdown, createIRFile, csvToEquipmentLabels, csvToSessionRows, defineLanguagePack, effortToMarkdown, equipmentToCsv, extractNoteIdentity, extractTypedTags, formatExecutionOutput, formatParseOutput, formatQueryOutput, getRegisteredLanguagePacks, inMemoryEventStore, injectIdentityFrontmatter, isIRFile, listLanguagePacks, loadLanguagePack, loadQueryData, markdownToEffort, parseCliArgs, parseCsvLine, readFrontmatterMeta, readStdin, registerLanguagePack, runExecution, runParse, runQueryCli, seedNoteId, sessionToCsv, splitCsvRecords, statementToNode, unregisterLanguagePack };
|
|
2777
|
+
//# sourceMappingURL=chunk-AKNXDA75.mjs.map
|
|
2778
|
+
//# sourceMappingURL=chunk-AKNXDA75.mjs.map
|