@bitcobblers/wod-wiki-engine 0.35.2129 → 0.36.2133
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +431 -2941
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -194
- package/dist/index.d.ts +11 -194
- package/dist/index.mjs +1087 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -11
- package/dist/bin/wod.cjs +0 -3561
- package/dist/bin/wod.cjs.map +0 -1
- package/dist/bin/wod.d.cts +0 -1
- package/dist/bin/wod.d.ts +0 -1
- package/dist/bin/wod.mjs +0 -16
- package/dist/bin/wod.mjs.map +0 -1
- package/dist/chunk-N4KFWY6Z.mjs +0 -2780
- package/dist/chunk-N4KFWY6Z.mjs.map +0 -1
- package/dist/chunk-Y6WO6MXS.mjs +0 -187
- package/dist/chunk-Y6WO6MXS.mjs.map +0 -1
- package/dist/compiler-SXUCF7AU.mjs +0 -565
- package/dist/compiler-SXUCF7AU.mjs.map +0 -1
package/dist/chunk-N4KFWY6Z.mjs
DELETED
|
@@ -1,2780 +0,0 @@
|
|
|
1
|
-
import { createRequire } from 'module';
|
|
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
|
-
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, 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';
|
|
7
|
-
|
|
8
|
-
if (typeof globalThis.require === 'undefined') {
|
|
9
|
-
try { globalThis.require = createRequire(import.meta.url); } catch {}
|
|
10
|
-
}
|
|
11
|
-
var activePacks = /* @__PURE__ */ new Map();
|
|
12
|
-
function defineLanguagePack(pack) {
|
|
13
|
-
return pack;
|
|
14
|
-
}
|
|
15
|
-
function registerLanguagePack(pack) {
|
|
16
|
-
const packId = pack.identity?.id ?? pack.identity?.name ?? "anonymous-pack";
|
|
17
|
-
activePacks.set(packId, pack);
|
|
18
|
-
if (pack.lang?.analyzer) {
|
|
19
|
-
const AnalyzerCtor = pack.lang.analyzer;
|
|
20
|
-
const instance = new AnalyzerCtor();
|
|
21
|
-
dialectRegistry.register(instance);
|
|
22
|
-
}
|
|
23
|
-
if (pack.lang?.analytics) {
|
|
24
|
-
for (const ProcessorCtor of pack.lang.analytics) {
|
|
25
|
-
const instance = new ProcessorCtor();
|
|
26
|
-
if ("processOutput" in instance) {
|
|
27
|
-
realtimeProcessorRegistry.register(instance);
|
|
28
|
-
}
|
|
29
|
-
if ("processSummary" in instance) {
|
|
30
|
-
summaryProcessorRegistry.register(instance);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function unregisterLanguagePack(packOrId) {
|
|
36
|
-
const packId = typeof packOrId === "string" ? packOrId : packOrId.identity?.id ?? packOrId.identity?.name ?? "anonymous-pack";
|
|
37
|
-
const pack = activePacks.get(packId);
|
|
38
|
-
activePacks.delete(packId);
|
|
39
|
-
if (pack?.lang?.analyzer) {
|
|
40
|
-
const instance = new pack.lang.analyzer();
|
|
41
|
-
dialectRegistry.unregister(instance.id);
|
|
42
|
-
} else if (typeof packOrId === "string") {
|
|
43
|
-
dialectRegistry.unregister(packOrId);
|
|
44
|
-
}
|
|
45
|
-
if (pack?.lang?.analytics) {
|
|
46
|
-
for (const ProcessorCtor of pack.lang.analytics) {
|
|
47
|
-
const instance = new ProcessorCtor();
|
|
48
|
-
if ("processOutput" in instance) {
|
|
49
|
-
realtimeProcessorRegistry.unregister(instance.id);
|
|
50
|
-
}
|
|
51
|
-
if ("processSummary" in instance) {
|
|
52
|
-
summaryProcessorRegistry.unregister(instance.id);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
function listLanguagePacks() {
|
|
58
|
-
return Array.from(activePacks.values());
|
|
59
|
-
}
|
|
60
|
-
function getRegisteredLanguagePacks() {
|
|
61
|
-
return Array.from(activePacks.values());
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// src/store.ts
|
|
65
|
-
function inMemoryEventStore(events) {
|
|
66
|
-
const rows = [...events];
|
|
67
|
-
return {
|
|
68
|
-
getEventsByTimeRange: async (start, end) => rows.filter((r) => r.timestamp >= start && r.timestamp <= end),
|
|
69
|
-
getEventsByResult: async (resultId) => rows.filter((r) => r.resultId === resultId),
|
|
70
|
-
getEventsForNote: async (noteId) => rows.filter((r) => r.noteId === noteId),
|
|
71
|
-
getEventsByContent: async (blockContentId4) => rows.filter((r) => r.blockContentId === blockContentId4),
|
|
72
|
-
scanAll: async () => rows,
|
|
73
|
-
appendEvents: async (appended) => {
|
|
74
|
-
rows.push(...appended);
|
|
75
|
-
},
|
|
76
|
-
finalizeSummaries: async (resultId, finals) => {
|
|
77
|
-
for (let i = rows.length - 1; i >= 0; i--) {
|
|
78
|
-
const row = rows[i];
|
|
79
|
-
if (row.resultId === resultId && row.grain === "summary" && row.origin !== "user") rows.splice(i, 1);
|
|
80
|
-
}
|
|
81
|
-
rows.push(...finals);
|
|
82
|
-
},
|
|
83
|
-
deleteEvents: async (ids) => {
|
|
84
|
-
const doomed = new Set(ids);
|
|
85
|
-
for (let i = rows.length - 1; i >= 0; i--) {
|
|
86
|
-
if (doomed.has(rows[i].id)) rows.splice(i, 1);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
function createIRFile(kind, data, optionsOrSource, generatedAt) {
|
|
92
|
-
let source = "wod:headless";
|
|
93
|
-
let genAt = Date.now();
|
|
94
|
-
if (typeof optionsOrSource === "string") {
|
|
95
|
-
source = optionsOrSource;
|
|
96
|
-
if (generatedAt !== void 0) genAt = generatedAt;
|
|
97
|
-
} else if (optionsOrSource && typeof optionsOrSource === "object") {
|
|
98
|
-
if (optionsOrSource.source) source = optionsOrSource.source;
|
|
99
|
-
if (optionsOrSource.generatedAt !== void 0) genAt = optionsOrSource.generatedAt;
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
$schema: "https://wod-wiki.dev/ir/v1.json",
|
|
103
|
-
kind,
|
|
104
|
-
generatedAt: genAt,
|
|
105
|
-
source,
|
|
106
|
-
data
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
function isIRFile(obj) {
|
|
110
|
-
if (!obj || typeof obj !== "object") return false;
|
|
111
|
-
const cand = obj;
|
|
112
|
-
return cand.$schema === "https://wod-wiki.dev/ir/v1.json" && typeof cand.kind === "string" && typeof cand.generatedAt === "number" && typeof cand.source === "string" && "data" in cand;
|
|
113
|
-
}
|
|
114
|
-
function statementToNode(stmt, idMap) {
|
|
115
|
-
const meta = stmt.meta;
|
|
116
|
-
const metrics = stmt.metrics.toArray().map((m) => ({
|
|
117
|
-
type: m.type,
|
|
118
|
-
...m.image !== void 0 ? { image: m.image } : {},
|
|
119
|
-
...m.value !== void 0 ? { value: m.value } : {},
|
|
120
|
-
...m.origin !== void 0 ? { origin: m.origin } : {}
|
|
121
|
-
}));
|
|
122
|
-
const raw = meta?.raw ?? "";
|
|
123
|
-
const from = meta?.startOffset ?? 0;
|
|
124
|
-
const to = meta?.endOffset ?? from + raw.length;
|
|
125
|
-
const durationFrag = stmt.metrics.find((m) => m.type === MetricType.Duration || m.type === "duration");
|
|
126
|
-
const roundsFrag = stmt.metrics.find((m) => m.type === MetricType.Rounds || m.type === "rounds");
|
|
127
|
-
const type = durationFrag ? "duration" : roundsFrag ? "rounds" : "statement";
|
|
128
|
-
const hints = getHints(stmt);
|
|
129
|
-
const childIds = (stmt.children ?? []).flat();
|
|
130
|
-
const children = childIds.map((cid) => idMap.get(cid)).filter((c) => c !== void 0).map((c) => statementToNode(c, idMap));
|
|
131
|
-
return {
|
|
132
|
-
id: stmt.id,
|
|
133
|
-
type,
|
|
134
|
-
raw,
|
|
135
|
-
from,
|
|
136
|
-
to,
|
|
137
|
-
metrics,
|
|
138
|
-
...hints.length > 0 ? { hints } : {},
|
|
139
|
-
...children.length > 0 ? { children } : {}
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
function buildStatementTree(script) {
|
|
143
|
-
const idMap = new Map(script.statements.map((s) => [s.id, s]));
|
|
144
|
-
const topLevel = script.statements.filter((s) => s.parent === void 0 || !idMap.has(s.parent));
|
|
145
|
-
if (topLevel.length === 1 && topLevel[0].children.length > 0) {
|
|
146
|
-
return statementToNode(topLevel[0], idMap);
|
|
147
|
-
}
|
|
148
|
-
return {
|
|
149
|
-
id: 0,
|
|
150
|
-
type: "root",
|
|
151
|
-
raw: script.source,
|
|
152
|
-
from: 0,
|
|
153
|
-
to: script.source.length,
|
|
154
|
-
metrics: [],
|
|
155
|
-
children: topLevel.map((s) => statementToNode(s, idMap))
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
var ParseSyntaxError = class extends Error {
|
|
159
|
-
constructor(errors, message = "Parse error in Whiteboard Script") {
|
|
160
|
-
const details = errors.map((e) => e.message).join("; ");
|
|
161
|
-
super(`${message}: ${details}`);
|
|
162
|
-
this.errors = errors;
|
|
163
|
-
this.name = "ParseSyntaxError";
|
|
164
|
-
}
|
|
165
|
-
errors;
|
|
166
|
-
};
|
|
167
|
-
function runParse(source, options = {}) {
|
|
168
|
-
const parser = createParser();
|
|
169
|
-
const script = parser.read(source, options.sport);
|
|
170
|
-
if (script.errors && script.errors.length > 0) {
|
|
171
|
-
throw new ParseSyntaxError(script.errors);
|
|
172
|
-
}
|
|
173
|
-
const tree = buildStatementTree(script);
|
|
174
|
-
return createIRFile("parse-tree", tree, { source: options.sourceLabel ?? "cli:wod parse" });
|
|
175
|
-
}
|
|
176
|
-
function buildWorkoutResults(outputs, options) {
|
|
177
|
-
return {
|
|
178
|
-
startTime: options.startTime ?? options.now.nowMs(),
|
|
179
|
-
endTime: options.now.nowMs(),
|
|
180
|
-
duration: options.elapsedTime,
|
|
181
|
-
completed: options.completed,
|
|
182
|
-
logs: outputs.map(toStoredOutputStatement)
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
async function runExecution(source, options = {}) {
|
|
186
|
-
const parser = createParser();
|
|
187
|
-
const script = parser.read(source, options.sport);
|
|
188
|
-
if (script.errors && script.errors.length > 0) {
|
|
189
|
-
throw new ParseSyntaxError(script.errors);
|
|
190
|
-
}
|
|
191
|
-
const startTime = options.startTime ?? Date.now();
|
|
192
|
-
const now = options.now ?? frozenNow(new Date(startTime));
|
|
193
|
-
const compiler = createCompiler();
|
|
194
|
-
const clock = createMockClock(new Date(startTime));
|
|
195
|
-
const stack = new RuntimeStack();
|
|
196
|
-
const eventBus = new EventBus();
|
|
197
|
-
const runtime = new ScriptRuntime(
|
|
198
|
-
script,
|
|
199
|
-
compiler,
|
|
200
|
-
{ stack, clock, eventBus },
|
|
201
|
-
{},
|
|
202
|
-
now
|
|
203
|
-
);
|
|
204
|
-
const { engine, analyticsContext } = createAnalyticsEngineForBlock({
|
|
205
|
-
id: options.blockId ?? "cli-block",
|
|
206
|
-
content: source,
|
|
207
|
-
statements: script.statements,
|
|
208
|
-
sport: options.sport,
|
|
209
|
-
version: options.blockVersion ?? 1,
|
|
210
|
-
startLine: 0,
|
|
211
|
-
endLine: 0,
|
|
212
|
-
dialect: options.dialect ?? "time",
|
|
213
|
-
state: "idle",
|
|
214
|
-
createdAt: Date.now()
|
|
215
|
-
});
|
|
216
|
-
runtime.analyticsContext = analyticsContext;
|
|
217
|
-
runtime.setAnalyticsEngine(engine);
|
|
218
|
-
const outputs = [];
|
|
219
|
-
runtime.subscribeToOutput((out) => {
|
|
220
|
-
outputs.push(out);
|
|
221
|
-
});
|
|
222
|
-
runtime.do(new StartSessionAction());
|
|
223
|
-
const maxSteps = options.maxIterations ?? 5e3;
|
|
224
|
-
let steps = 0;
|
|
225
|
-
while (stack.count > 0 && steps < maxSteps) {
|
|
226
|
-
runtime.handle(new NextEvent(void 0, runtime.nowProvider));
|
|
227
|
-
clock.advance(1e3);
|
|
228
|
-
steps++;
|
|
229
|
-
}
|
|
230
|
-
const workoutResults = buildWorkoutResults(outputs, {
|
|
231
|
-
startTime,
|
|
232
|
-
elapsedTime: steps * 1e3,
|
|
233
|
-
completed: stack.count === 0,
|
|
234
|
-
now
|
|
235
|
-
});
|
|
236
|
-
const logs = outputs.map(toStoredOutputStatement);
|
|
237
|
-
return createIRFile(
|
|
238
|
-
"execution-log",
|
|
239
|
-
{
|
|
240
|
-
results: workoutResults,
|
|
241
|
-
logs,
|
|
242
|
-
statements: logs,
|
|
243
|
-
sourceScript: source
|
|
244
|
-
},
|
|
245
|
-
{ source: options.sourceLabel ?? "cli:wod run" }
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
function getFs() {
|
|
249
|
-
if (typeof globalThis.process?.versions?.node === "undefined" && typeof globalThis.Bun === "undefined") {
|
|
250
|
-
throw new Error("File system operations are only supported in Node / Bun environments");
|
|
251
|
-
}
|
|
252
|
-
return __require("fs");
|
|
253
|
-
}
|
|
254
|
-
var WqlSyntaxError = class extends Error {
|
|
255
|
-
constructor(query, syntaxError) {
|
|
256
|
-
super(`WQL Syntax Error in "${query}": ${syntaxError}`);
|
|
257
|
-
this.query = query;
|
|
258
|
-
this.syntaxError = syntaxError;
|
|
259
|
-
this.name = "WqlSyntaxError";
|
|
260
|
-
}
|
|
261
|
-
query;
|
|
262
|
-
syntaxError;
|
|
263
|
-
};
|
|
264
|
-
function extractPayload(rawJson) {
|
|
265
|
-
try {
|
|
266
|
-
const parsed = JSON.parse(rawJson);
|
|
267
|
-
if (isIRFile(parsed)) {
|
|
268
|
-
return parsed.data;
|
|
269
|
-
}
|
|
270
|
-
return parsed;
|
|
271
|
-
} catch (err) {
|
|
272
|
-
throw new Error(`Failed to parse JSON input: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
function buildStoresFromData(data) {
|
|
276
|
-
const events = [
|
|
277
|
-
...data.eventRecords,
|
|
278
|
-
...data.facts.map((f, i) => ({
|
|
279
|
-
id: `fact:${f.resultId ?? "stdin"}:${f.metricKey ?? f.type}:${i}`,
|
|
280
|
-
resultId: f.resultId ?? "stdin-result-1",
|
|
281
|
-
noteId: f.noteId ?? "stdin-note-1",
|
|
282
|
-
blockContentId: f.blockContentId,
|
|
283
|
-
pageId: f.pageId,
|
|
284
|
-
origin: f.origin,
|
|
285
|
-
timestamp: f.timestamp,
|
|
286
|
-
grain: f.grain === "event" ? "event" : "summary",
|
|
287
|
-
outputType: "analytics",
|
|
288
|
-
effortSlug: f.effortSlug,
|
|
289
|
-
metrics: [{
|
|
290
|
-
type: f.metricKey ?? f.type,
|
|
291
|
-
value: f.value,
|
|
292
|
-
...f.unit ? { unit: f.unit } : {},
|
|
293
|
-
origin: "engine"
|
|
294
|
-
}],
|
|
295
|
-
blockId: f.blockId,
|
|
296
|
-
blockVersion: f.blockVersion
|
|
297
|
-
}))
|
|
298
|
-
];
|
|
299
|
-
const eventStore = inMemoryEventStore(events);
|
|
300
|
-
const noteStore = {
|
|
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
|
-
},
|
|
306
|
-
getNoteIdsForTag: async (tag) => {
|
|
307
|
-
const matching = /* @__PURE__ */ new Set();
|
|
308
|
-
for (const [noteId, tags] of Object.entries(data.noteTags)) {
|
|
309
|
-
if (tags.includes(tag)) matching.add(noteId);
|
|
310
|
-
}
|
|
311
|
-
return matching;
|
|
312
|
-
},
|
|
313
|
-
getNoteTagLabels: async (noteId) => [...data.noteTags[noteId] ?? []]
|
|
314
|
-
};
|
|
315
|
-
const blockStore = {
|
|
316
|
-
getAllBlocks: async () => [...data.blocks]
|
|
317
|
-
};
|
|
318
|
-
const effortStore = {
|
|
319
|
-
getAllEfforts: async () => [...data.efforts]
|
|
320
|
-
};
|
|
321
|
-
return { eventStore, noteStore, blockStore, effortStore };
|
|
322
|
-
}
|
|
323
|
-
function loadQueryData(options) {
|
|
324
|
-
const data = {
|
|
325
|
-
eventRecords: [],
|
|
326
|
-
facts: [],
|
|
327
|
-
results: [],
|
|
328
|
-
notes: [],
|
|
329
|
-
blocks: [],
|
|
330
|
-
revisions: [],
|
|
331
|
-
efforts: [...bundledEfforts],
|
|
332
|
-
noteTags: {}
|
|
333
|
-
};
|
|
334
|
-
if (options.corpusPath) {
|
|
335
|
-
const fs = getFs();
|
|
336
|
-
if (!fs.existsSync(options.corpusPath)) {
|
|
337
|
-
throw new Error(`Corpus file not found at path: ${options.corpusPath}`);
|
|
338
|
-
}
|
|
339
|
-
const raw = fs.readFileSync(options.corpusPath, "utf-8");
|
|
340
|
-
const payload = extractPayload(raw);
|
|
341
|
-
if (Array.isArray(payload)) {
|
|
342
|
-
data.facts = payload;
|
|
343
|
-
} else if (payload && typeof payload === "object") {
|
|
344
|
-
const obj = payload;
|
|
345
|
-
if ("segments" in obj) throw new Error("Incompatible corpus segments; whole Notes and block revisions are required");
|
|
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;
|
|
349
|
-
if (Array.isArray(obj.records)) {
|
|
350
|
-
data.eventRecords = obj.records;
|
|
351
|
-
}
|
|
352
|
-
if (Array.isArray(obj.notes)) {
|
|
353
|
-
data.notes = obj.notes;
|
|
354
|
-
for (const n of obj.notes) {
|
|
355
|
-
if (n && typeof n === "object" && n.id && Array.isArray(n.tags)) {
|
|
356
|
-
data.noteTags[n.id] = n.tags;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
} else if (obj.facts || obj.results || obj.notes || obj.blocks || obj.efforts || obj.tags || obj.logs) {
|
|
361
|
-
const corpus = payload;
|
|
362
|
-
if (corpus.facts) data.facts = corpus.facts;
|
|
363
|
-
if (corpus.results) data.results = corpus.results;
|
|
364
|
-
if (corpus.notes) data.notes = corpus.notes;
|
|
365
|
-
if (corpus.blocks) data.blocks = corpus.blocks;
|
|
366
|
-
if (corpus.efforts) data.efforts = corpus.efforts;
|
|
367
|
-
if (corpus.revisions) data.revisions = corpus.revisions;
|
|
368
|
-
if (corpus.tags) data.noteTags = corpus.tags;
|
|
369
|
-
if (corpus.logs) {
|
|
370
|
-
const identity = {
|
|
371
|
-
noteId: "corpus-note-1",
|
|
372
|
-
resultId: "corpus-result-1",
|
|
373
|
-
origin: "journal",
|
|
374
|
-
workoutTimestamp: corpus.logs[0]?.timeSpan?.started ?? Date.now()
|
|
375
|
-
};
|
|
376
|
-
data.eventRecords = [...toEventRows(corpus.logs, identity), ...toSummaryEventRows(corpus.logs, identity)];
|
|
377
|
-
}
|
|
378
|
-
} else {
|
|
379
|
-
throw new Error(`Unrecognized corpus payload shape in "${options.corpusPath}" (expected event-journal, fact-set, or CorpusIRData)`);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
} else if (options.stdinLog) {
|
|
383
|
-
const payload = extractPayload(options.stdinLog);
|
|
384
|
-
let executionLog;
|
|
385
|
-
const rawLogs = payload.logs ?? ("statements" in payload ? payload.statements : void 0) ?? [];
|
|
386
|
-
if ("results" in payload) {
|
|
387
|
-
executionLog = { results: payload.results, logs: rawLogs, statements: rawLogs };
|
|
388
|
-
} else {
|
|
389
|
-
executionLog = { results: payload, logs: rawLogs, statements: rawLogs };
|
|
390
|
-
}
|
|
391
|
-
const identity = {
|
|
392
|
-
noteId: "stdin-note-1",
|
|
393
|
-
resultId: "stdin-result-1",
|
|
394
|
-
origin: "journal",
|
|
395
|
-
workoutTimestamp: executionLog.results.startTime
|
|
396
|
-
};
|
|
397
|
-
data.eventRecords = [
|
|
398
|
-
...toEventRows(executionLog.logs, identity),
|
|
399
|
-
...toSummaryEventRows(executionLog.logs, identity)
|
|
400
|
-
];
|
|
401
|
-
} else if (options.stdinFacts) {
|
|
402
|
-
const payload = extractPayload(options.stdinFacts);
|
|
403
|
-
if (Array.isArray(payload)) {
|
|
404
|
-
data.facts = payload;
|
|
405
|
-
} else if (payload && typeof payload === "object" && "facts" in payload) {
|
|
406
|
-
data.facts = payload.facts;
|
|
407
|
-
}
|
|
408
|
-
} else {
|
|
409
|
-
throw new Error("No input dataset provided. Specify --corpus <path>, --stdin-log, or --stdin-facts.");
|
|
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
|
-
}
|
|
465
|
-
return data;
|
|
466
|
-
}
|
|
467
|
-
async function runQueryCli(wqlString, options = {}) {
|
|
468
|
-
const parsed = parseQuery(wqlString);
|
|
469
|
-
if (parsed.error) {
|
|
470
|
-
throw new WqlSyntaxError(wqlString, parsed.error);
|
|
471
|
-
}
|
|
472
|
-
const stores = options.stores ?? buildStoresFromData(loadQueryData(options));
|
|
473
|
-
const service = new QueryService(stores);
|
|
474
|
-
if (isPipelineQuery(parsed)) {
|
|
475
|
-
const result2 = await service.runPipeline(wqlString, {
|
|
476
|
-
preferredUnit: options.preferredUnit
|
|
477
|
-
});
|
|
478
|
-
return createIRFile("pipeline-result", result2, { source: options.sourceLabel ?? "cli:wod query" });
|
|
479
|
-
}
|
|
480
|
-
if (isFindQuery(parsed)) {
|
|
481
|
-
const result2 = await service.runFind(parsed);
|
|
482
|
-
const kind = parsed.target === "session" ? "rows-result" : "find-result";
|
|
483
|
-
return createIRFile(kind, result2, { source: options.sourceLabel ?? "cli:wod query" });
|
|
484
|
-
}
|
|
485
|
-
const result = await service.runQuery(wqlString, {
|
|
486
|
-
preferredUnit: options.preferredUnit
|
|
487
|
-
});
|
|
488
|
-
return createIRFile("query-result", result, { source: options.sourceLabel ?? "cli:wod query" });
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// src/cli/loader.ts
|
|
492
|
-
function getPathUtils() {
|
|
493
|
-
if (typeof globalThis.process?.versions?.node === "undefined" && !("Bun" in globalThis)) {
|
|
494
|
-
throw new Error("Path resolution is only supported in Node / Bun environments");
|
|
495
|
-
}
|
|
496
|
-
return {
|
|
497
|
-
// eslint-disable-next-line no-restricted-syntax -- lazy CJS require keeps node builtins out of browser bundles
|
|
498
|
-
resolve: __require("path").resolve,
|
|
499
|
-
// eslint-disable-next-line no-restricted-syntax -- lazy CJS require keeps node builtins out of browser bundles
|
|
500
|
-
pathToFileURL: __require("url").pathToFileURL
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
var PackLoadError = class extends Error {
|
|
504
|
-
constructor(spec, originalError) {
|
|
505
|
-
const msg = originalError instanceof Error ? originalError.message : String(originalError);
|
|
506
|
-
super(`Failed to load Language Pack "${spec}": ${msg}`);
|
|
507
|
-
this.spec = spec;
|
|
508
|
-
this.originalError = originalError;
|
|
509
|
-
this.name = "PackLoadError";
|
|
510
|
-
}
|
|
511
|
-
spec;
|
|
512
|
-
originalError;
|
|
513
|
-
};
|
|
514
|
-
async function loadLanguagePack(moduleSpec) {
|
|
515
|
-
try {
|
|
516
|
-
let importPath = moduleSpec;
|
|
517
|
-
if (moduleSpec.startsWith(".") || moduleSpec.startsWith("/") || moduleSpec.includes("\\") || moduleSpec.endsWith(".ts") || moduleSpec.endsWith(".js") || moduleSpec.endsWith(".mjs")) {
|
|
518
|
-
const { resolve, pathToFileURL } = getPathUtils();
|
|
519
|
-
const resolved = resolve(process.cwd(), moduleSpec);
|
|
520
|
-
importPath = pathToFileURL(resolved).href;
|
|
521
|
-
}
|
|
522
|
-
const imported = await import(
|
|
523
|
-
/* @vite-ignore */
|
|
524
|
-
importPath
|
|
525
|
-
);
|
|
526
|
-
const candidate = imported.default ?? imported.pack ?? imported;
|
|
527
|
-
if (!candidate || typeof candidate !== "object") {
|
|
528
|
-
throw new Error(`Module "${moduleSpec}" does not export a valid LanguagePack object`);
|
|
529
|
-
}
|
|
530
|
-
const pack = candidate;
|
|
531
|
-
registerLanguagePack(pack);
|
|
532
|
-
return pack;
|
|
533
|
-
} catch (error) {
|
|
534
|
-
if (error instanceof PackLoadError) throw error;
|
|
535
|
-
throw new PackLoadError(moduleSpec, error);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
var NOTE_ID_FRONTMATTER_KEY = "wod-note-id";
|
|
539
|
-
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
540
|
-
function extractNoteIdentity(markdown) {
|
|
541
|
-
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
|
|
542
|
-
if (!block) return void 0;
|
|
543
|
-
let noteId;
|
|
544
|
-
let title;
|
|
545
|
-
for (const line of block[1].split(/\r?\n/)) {
|
|
546
|
-
const kv = line.match(/^([^:]+):\s*(.*)$/);
|
|
547
|
-
if (!kv) continue;
|
|
548
|
-
const key = kv[1].trim();
|
|
549
|
-
const value = kv[2].trim();
|
|
550
|
-
if (key === NOTE_ID_FRONTMATTER_KEY && UUID_RE.test(value)) noteId = value;
|
|
551
|
-
if (key === "title" && value) title = value;
|
|
552
|
-
}
|
|
553
|
-
return noteId ? { noteId, title } : void 0;
|
|
554
|
-
}
|
|
555
|
-
var FRONTMATTER_SAFE = /^[^:#\[\]{}'"*&,!?|>%@`\s][^:#\[\]{}"']*$/;
|
|
556
|
-
function injectIdentityFrontmatter(markdown, identity) {
|
|
557
|
-
const stamp = (lines2) => {
|
|
558
|
-
const replaceOrInsert = (key, value) => {
|
|
559
|
-
if (value === void 0 || !FRONTMATTER_SAFE.test(value)) return;
|
|
560
|
-
const idx = lines2.findIndex((l) => l.startsWith(`${key}:`));
|
|
561
|
-
if (idx >= 0) lines2[idx] = `${key}: ${value}`;
|
|
562
|
-
else lines2.push(`${key}: ${value}`);
|
|
563
|
-
};
|
|
564
|
-
replaceOrInsert("title", identity.title);
|
|
565
|
-
replaceOrInsert(NOTE_ID_FRONTMATTER_KEY, identity.noteId);
|
|
566
|
-
return lines2;
|
|
567
|
-
};
|
|
568
|
-
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
|
|
569
|
-
if (block) {
|
|
570
|
-
const newline = markdown.startsWith("---\r\n") ? "\r\n" : "\n";
|
|
571
|
-
const lines2 = stamp(block[1].split(/\r?\n/));
|
|
572
|
-
return `---${newline}${lines2.join(newline)}${newline}---${markdown.slice(block[0].length)}`;
|
|
573
|
-
}
|
|
574
|
-
const lines = stamp([]);
|
|
575
|
-
return `---
|
|
576
|
-
${lines.join("\n")}
|
|
577
|
-
---
|
|
578
|
-
|
|
579
|
-
${markdown}`;
|
|
580
|
-
}
|
|
581
|
-
function composeNoteMarkdown(note) {
|
|
582
|
-
return injectIdentityFrontmatter(note.rawContent, { noteId: note.id, title: note.title });
|
|
583
|
-
}
|
|
584
|
-
var FRONTMATTER_BLOCK_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
585
|
-
function readFrontmatterMeta(markdown) {
|
|
586
|
-
const match = FRONTMATTER_BLOCK_RE.exec(markdown);
|
|
587
|
-
if (!match) return {};
|
|
588
|
-
const meta = {};
|
|
589
|
-
const lines = match[1].split(/\r?\n/);
|
|
590
|
-
let currentListKey;
|
|
591
|
-
for (const rawLine of lines) {
|
|
592
|
-
const line = rawLine.trimEnd();
|
|
593
|
-
const item = line.match(/^\s+-\s+(.*)$/);
|
|
594
|
-
if (item && currentListKey) {
|
|
595
|
-
meta[currentListKey].push(item[1].trim());
|
|
596
|
-
continue;
|
|
597
|
-
}
|
|
598
|
-
const kv = line.match(/^([^:]+):\s*(.*)$/);
|
|
599
|
-
if (!kv) continue;
|
|
600
|
-
const key = kv[1].trim().toLowerCase();
|
|
601
|
-
const value = kv[2].trim();
|
|
602
|
-
if (value === "") {
|
|
603
|
-
meta[key] = [];
|
|
604
|
-
currentListKey = key;
|
|
605
|
-
} else if (value.startsWith("[") && value.endsWith("]")) {
|
|
606
|
-
meta[key] = value.slice(1, -1).split(",").map((v) => v.trim().replace(/^['"]|['"]$/g, "")).filter((v) => v.length > 0);
|
|
607
|
-
currentListKey = void 0;
|
|
608
|
-
} else {
|
|
609
|
-
meta[key] = value;
|
|
610
|
-
currentListKey = void 0;
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
return meta;
|
|
614
|
-
}
|
|
615
|
-
var asList = (value) => Array.isArray(value) ? value : typeof value === "string" ? value.split(",").map((v) => v.trim()).filter((v) => v.length > 0) : [];
|
|
616
|
-
function extractTypedTags(markdown, typedKeys = WQL_TYPED_TAG_KEYS) {
|
|
617
|
-
const meta = readFrontmatterMeta(markdown);
|
|
618
|
-
const results = asList(meta["tags"]).map((label) => ({ label }));
|
|
619
|
-
const metaKeys = Object.keys(meta);
|
|
620
|
-
for (const typeName of typedKeys) {
|
|
621
|
-
if (typeName === "tags") continue;
|
|
622
|
-
const matchedKey = metaKeys.find((k) => k === typeName.toLowerCase());
|
|
623
|
-
if (!matchedKey) continue;
|
|
624
|
-
for (const label of asList(meta[matchedKey])) {
|
|
625
|
-
results.push({ label, type: typeName });
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
return results.filter((t) => t.label.length > 0);
|
|
629
|
-
}
|
|
630
|
-
function markdownToEffort(raw) {
|
|
631
|
-
const match = FRONTMATTER_BLOCK_RE.exec(raw);
|
|
632
|
-
if (!match) return null;
|
|
633
|
-
const meta = readFrontmatterMeta(raw);
|
|
634
|
-
const body = raw.slice(match[0].length).trim();
|
|
635
|
-
const id = scalarOf(meta["id"]);
|
|
636
|
-
const slug = scalarOf(meta["slug"]);
|
|
637
|
-
const label = scalarOf(meta["label"]);
|
|
638
|
-
if (!id || !slug || !label) return null;
|
|
639
|
-
const baseAttributes = {};
|
|
640
|
-
const blockRe = /^\s{2,}([^:]+):\s*(.+)$/;
|
|
641
|
-
let inAttrs = false;
|
|
642
|
-
for (const line of match[1].split(/\r?\n/)) {
|
|
643
|
-
if (/^baseAttributes:/.test(line)) {
|
|
644
|
-
inAttrs = true;
|
|
645
|
-
continue;
|
|
646
|
-
}
|
|
647
|
-
if (inAttrs) {
|
|
648
|
-
const attr = blockRe.exec(line);
|
|
649
|
-
if (attr && /^ {2,}/.test(line)) {
|
|
650
|
-
const v = attr[2].trim();
|
|
651
|
-
baseAttributes[attr[1].trim()] = /^\d+(\.\d+)?$/.test(v) ? Number(v) : v;
|
|
652
|
-
} else if (/^\S/.test(line)) {
|
|
653
|
-
inAttrs = false;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
if (typeof baseAttributes["met"] !== "number") return null;
|
|
658
|
-
return {
|
|
659
|
-
id,
|
|
660
|
-
slug,
|
|
661
|
-
label,
|
|
662
|
-
aliases: asList(meta["aliases"]),
|
|
663
|
-
baseAttributes,
|
|
664
|
-
...body ? { body } : {}
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
function effortToMarkdown(effort) {
|
|
668
|
-
const lines = ["---", `id: ${effort.id}`, `slug: ${effort.slug}`, `label: ${effort.label}`];
|
|
669
|
-
if (effort.aliases && effort.aliases.length > 0) {
|
|
670
|
-
lines.push("aliases:");
|
|
671
|
-
for (const alias of effort.aliases) lines.push(` - ${alias}`);
|
|
672
|
-
}
|
|
673
|
-
const attrs = Object.entries(effort.baseAttributes).filter(([, v]) => typeof v === "number" || typeof v === "string");
|
|
674
|
-
if (attrs.length > 0) {
|
|
675
|
-
lines.push("baseAttributes:");
|
|
676
|
-
for (const [k, v] of attrs) lines.push(` ${k}: ${String(v)}`);
|
|
677
|
-
}
|
|
678
|
-
lines.push("---", "");
|
|
679
|
-
return `${lines.join("\n")}
|
|
680
|
-
${(effort.body ?? "").trim()}`.trimEnd() + "\n";
|
|
681
|
-
}
|
|
682
|
-
function scalarOf(value) {
|
|
683
|
-
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
684
|
-
}
|
|
685
|
-
function equipmentToCsv(labels) {
|
|
686
|
-
return ["label", ...labels].map((l) => escapeCsvField(l)).join("\n") + "\n";
|
|
687
|
-
}
|
|
688
|
-
function csvToEquipmentLabels(csv) {
|
|
689
|
-
const lines = splitCsvRecords(csv);
|
|
690
|
-
if (lines.length === 0) return [];
|
|
691
|
-
const header = parseCsvLine(lines[0]);
|
|
692
|
-
const labelIdx = header[0] === "label" ? 0 : -1;
|
|
693
|
-
const start = labelIdx >= 0 ? 1 : 0;
|
|
694
|
-
const labels = [];
|
|
695
|
-
for (const line of lines.slice(start)) {
|
|
696
|
-
const cells = parseCsvLine(line);
|
|
697
|
-
const label = (labelIdx >= 0 ? cells[0] : cells.join(",")).trim();
|
|
698
|
-
if (label) labels.push(label);
|
|
699
|
-
}
|
|
700
|
-
return [...new Set(labels)];
|
|
701
|
-
}
|
|
702
|
-
var SESSION_HEADER = [
|
|
703
|
-
"sessionId",
|
|
704
|
-
"noteId",
|
|
705
|
-
"blockId",
|
|
706
|
-
"blockVersion",
|
|
707
|
-
"blockContentId",
|
|
708
|
-
"origin",
|
|
709
|
-
"status",
|
|
710
|
-
"startTime",
|
|
711
|
-
"endTime",
|
|
712
|
-
"duration",
|
|
713
|
-
"completed",
|
|
714
|
-
"sessionCreatedAt",
|
|
715
|
-
"sessionExtraJson"
|
|
716
|
-
];
|
|
717
|
-
var EVENT_HEADER = [
|
|
718
|
-
"eventId",
|
|
719
|
-
"grain",
|
|
720
|
-
"outputType",
|
|
721
|
-
"timestamp",
|
|
722
|
-
"pageId",
|
|
723
|
-
"effortSlug",
|
|
724
|
-
"eventBlockId",
|
|
725
|
-
"eventBlockVersion",
|
|
726
|
-
"eventBlockContentId",
|
|
727
|
-
"eventOrigin",
|
|
728
|
-
"timeSpan",
|
|
729
|
-
"sourceBlockKey",
|
|
730
|
-
"sourceStatementId",
|
|
731
|
-
"line",
|
|
732
|
-
"text",
|
|
733
|
-
"dialect",
|
|
734
|
-
"stackLevel",
|
|
735
|
-
"completionReason",
|
|
736
|
-
"representationKind",
|
|
737
|
-
"metricType",
|
|
738
|
-
"metricValue",
|
|
739
|
-
"metricUnit",
|
|
740
|
-
"metricImage",
|
|
741
|
-
"metricOrigin",
|
|
742
|
-
"metricMetadata",
|
|
743
|
-
"metricTemporal",
|
|
744
|
-
// Verbatim JSON backstops: make the machine round-trip lossless across
|
|
745
|
-
// schema fields that have no dedicated spreadsheet column. Explicit cells
|
|
746
|
-
// above still win on import, so hand-edited values are honored.
|
|
747
|
-
"metricJson",
|
|
748
|
-
"eventExtraJson"
|
|
749
|
-
];
|
|
750
|
-
var SESSION_CSV_HEADER = [...SESSION_HEADER, ...EVENT_HEADER];
|
|
751
|
-
function escapeCsvField(field) {
|
|
752
|
-
if (field === void 0 || field === null) return "";
|
|
753
|
-
const str = typeof field === "object" ? JSON.stringify(field) : String(field);
|
|
754
|
-
if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
|
|
755
|
-
return `"${str.replace(/"/g, '""')}"`;
|
|
756
|
-
}
|
|
757
|
-
return str;
|
|
758
|
-
}
|
|
759
|
-
var metricValueToCell = (value) => {
|
|
760
|
-
if (value === void 0 || value === null) return "";
|
|
761
|
-
if (typeof value === "object") return JSON.stringify(value);
|
|
762
|
-
return String(value);
|
|
763
|
-
};
|
|
764
|
-
var metricValueFromCell = (cell) => {
|
|
765
|
-
if (cell === "") return void 0;
|
|
766
|
-
if (/^-?\d+(\.\d+)?$/.test(cell)) return Number(cell);
|
|
767
|
-
if (cell === "true" || cell === "false") return cell === "true";
|
|
768
|
-
if (cell.startsWith("{") || cell.startsWith("[")) {
|
|
769
|
-
try {
|
|
770
|
-
return JSON.parse(cell);
|
|
771
|
-
} catch {
|
|
772
|
-
return cell;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
return cell;
|
|
776
|
-
};
|
|
777
|
-
function sessionCells(session) {
|
|
778
|
-
return [
|
|
779
|
-
session.id,
|
|
780
|
-
session.noteId,
|
|
781
|
-
session.blockId,
|
|
782
|
-
session.blockVersion,
|
|
783
|
-
session.blockContentId,
|
|
784
|
-
session.origin,
|
|
785
|
-
session.status,
|
|
786
|
-
session.startTime,
|
|
787
|
-
session.endTime,
|
|
788
|
-
session.duration,
|
|
789
|
-
session.completed,
|
|
790
|
-
session.createdAt,
|
|
791
|
-
JSON.stringify(session)
|
|
792
|
-
].map(escapeCsvField);
|
|
793
|
-
}
|
|
794
|
-
function metricCells(event, metric, index) {
|
|
795
|
-
return [
|
|
796
|
-
event.id,
|
|
797
|
-
event.grain,
|
|
798
|
-
event.outputType,
|
|
799
|
-
event.timestamp,
|
|
800
|
-
event.pageId,
|
|
801
|
-
event.effortSlug,
|
|
802
|
-
event.blockId,
|
|
803
|
-
event.blockVersion,
|
|
804
|
-
event.blockContentId,
|
|
805
|
-
event.origin,
|
|
806
|
-
event.timeSpan,
|
|
807
|
-
event.sourceBlockKey,
|
|
808
|
-
event.sourceStatementId,
|
|
809
|
-
event.line,
|
|
810
|
-
event.text,
|
|
811
|
-
event.dialect,
|
|
812
|
-
event.stackLevel,
|
|
813
|
-
event.completionReason,
|
|
814
|
-
event.representationKind,
|
|
815
|
-
metric.type,
|
|
816
|
-
metricValueToCell(metric.value),
|
|
817
|
-
metric.unit,
|
|
818
|
-
metric.image,
|
|
819
|
-
metric.origin,
|
|
820
|
-
metric.metadata === void 0 ? "" : JSON.stringify(metric.metadata),
|
|
821
|
-
event.metricTemporal?.[index] === void 0 ? "" : JSON.stringify(event.metricTemporal[index]),
|
|
822
|
-
JSON.stringify(metric)
|
|
823
|
-
].map(escapeCsvField);
|
|
824
|
-
}
|
|
825
|
-
function eventExtraCells(event) {
|
|
826
|
-
const { metrics: _metrics, ...rest } = event;
|
|
827
|
-
return escapeCsvField(JSON.stringify(rest));
|
|
828
|
-
}
|
|
829
|
-
function sessionToCsv(session, events) {
|
|
830
|
-
const lines = [SESSION_CSV_HEADER.join(",")];
|
|
831
|
-
lines.push([...sessionCells(session), ...EVENT_HEADER.map(() => "")].join(","));
|
|
832
|
-
const ordered = [...events].sort((a, b) => a.id.localeCompare(b.id));
|
|
833
|
-
for (const event of ordered) {
|
|
834
|
-
const extra = eventExtraCells(event);
|
|
835
|
-
const metrics = event.metrics ?? [];
|
|
836
|
-
if (metrics.length === 0) {
|
|
837
|
-
lines.push([...sessionCells(session), ...metricCells(event, { type: "" }, 0), extra].join(","));
|
|
838
|
-
continue;
|
|
839
|
-
}
|
|
840
|
-
for (let i = 0; i < metrics.length; i++) {
|
|
841
|
-
lines.push([...sessionCells(session), ...metricCells(event, metrics[i], i), extra].join(","));
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
return lines.join("\n") + "\n";
|
|
845
|
-
}
|
|
846
|
-
function parseCsvLine(line) {
|
|
847
|
-
const cells = [];
|
|
848
|
-
let current = "";
|
|
849
|
-
let quoted = false;
|
|
850
|
-
for (let i = 0; i < line.length; i++) {
|
|
851
|
-
const ch = line[i];
|
|
852
|
-
if (quoted) {
|
|
853
|
-
if (ch === '"') {
|
|
854
|
-
if (line[i + 1] === '"') {
|
|
855
|
-
current += '"';
|
|
856
|
-
i += 1;
|
|
857
|
-
} else {
|
|
858
|
-
quoted = false;
|
|
859
|
-
}
|
|
860
|
-
} else {
|
|
861
|
-
current += ch;
|
|
862
|
-
}
|
|
863
|
-
} else if (ch === '"' && current === "") {
|
|
864
|
-
quoted = true;
|
|
865
|
-
} else if (ch === ",") {
|
|
866
|
-
cells.push(current);
|
|
867
|
-
current = "";
|
|
868
|
-
} else {
|
|
869
|
-
current += ch;
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
cells.push(current);
|
|
873
|
-
return cells;
|
|
874
|
-
}
|
|
875
|
-
function splitCsvRecords(csv) {
|
|
876
|
-
const records = [];
|
|
877
|
-
let current = "";
|
|
878
|
-
let quoted = false;
|
|
879
|
-
for (let i = 0; i < csv.length; i++) {
|
|
880
|
-
const ch = csv[i];
|
|
881
|
-
if (ch === '"') {
|
|
882
|
-
if (quoted && csv[i + 1] === '"') {
|
|
883
|
-
current += '""';
|
|
884
|
-
i += 1;
|
|
885
|
-
continue;
|
|
886
|
-
}
|
|
887
|
-
quoted = !quoted;
|
|
888
|
-
current += ch;
|
|
889
|
-
} else if (!quoted && (ch === "\n" || ch === "\r")) {
|
|
890
|
-
if (ch === "\r" && csv[i + 1] === "\n") i += 1;
|
|
891
|
-
if (current.length > 0) records.push(current);
|
|
892
|
-
current = "";
|
|
893
|
-
} else {
|
|
894
|
-
current += ch;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
if (current.length > 0) records.push(current);
|
|
898
|
-
return records;
|
|
899
|
-
}
|
|
900
|
-
var num = (cell) => cell === "" ? void 0 : Number(cell);
|
|
901
|
-
var nonempty = (cell) => cell === "" ? void 0 : cell;
|
|
902
|
-
function jsonCell(cell) {
|
|
903
|
-
if (!cell) return void 0;
|
|
904
|
-
try {
|
|
905
|
-
return JSON.parse(cell);
|
|
906
|
-
} catch {
|
|
907
|
-
return void 0;
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
function csvToSessionRows(csv) {
|
|
911
|
-
const lines = splitCsvRecords(csv);
|
|
912
|
-
if (lines.length < 2) throw new Error("[wod import] Session CSV has no data rows");
|
|
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
|
-
}
|
|
917
|
-
const col = (name) => {
|
|
918
|
-
const idx = header.indexOf(name);
|
|
919
|
-
if (idx < 0) throw new Error(`[wod import] Session CSV missing column "${name}"`);
|
|
920
|
-
return idx;
|
|
921
|
-
};
|
|
922
|
-
const at = (cells, name) => cells[col(name)] ?? "";
|
|
923
|
-
const eventRows = /* @__PURE__ */ new Map();
|
|
924
|
-
let session;
|
|
925
|
-
const ensureSession = (cells) => {
|
|
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
|
-
}
|
|
943
|
-
session = {
|
|
944
|
-
...extra,
|
|
945
|
-
id,
|
|
946
|
-
noteId,
|
|
947
|
-
blockId,
|
|
948
|
-
blockVersion,
|
|
949
|
-
blockContentId: at(cells, "blockContentId") || void 0,
|
|
950
|
-
origin: at(cells, "origin") || void 0,
|
|
951
|
-
status: at(cells, "status") || void 0,
|
|
952
|
-
startTime: Number(at(cells, "startTime") || 0),
|
|
953
|
-
endTime: Number(at(cells, "endTime") || 0),
|
|
954
|
-
duration: Number(at(cells, "duration") || 0),
|
|
955
|
-
completed: at(cells, "completed") === "true",
|
|
956
|
-
createdAt: Number(at(cells, "sessionCreatedAt") || 0)
|
|
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
|
-
}
|
|
961
|
-
return session;
|
|
962
|
-
};
|
|
963
|
-
for (const line of lines.slice(1)) {
|
|
964
|
-
const cells = parseCsvLine(line);
|
|
965
|
-
if (cells.length === 1 && cells[0] === "") continue;
|
|
966
|
-
const currentSession = ensureSession(cells);
|
|
967
|
-
const eventId = at(cells, "eventId");
|
|
968
|
-
if (!eventId) continue;
|
|
969
|
-
let event = eventRows.get(eventId);
|
|
970
|
-
if (!event) {
|
|
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
|
-
}
|
|
980
|
-
event = {
|
|
981
|
-
...extra,
|
|
982
|
-
id: eventId,
|
|
983
|
-
resultId: currentSession.id,
|
|
984
|
-
noteId: nonempty(at(cells, "noteId")) ?? extra.noteId ?? currentSession.noteId,
|
|
985
|
-
blockContentId: nonempty(at(cells, "eventBlockContentId")) ?? extra.blockContentId,
|
|
986
|
-
pageId: nonempty(at(cells, "pageId")) ?? extra.pageId,
|
|
987
|
-
origin: nonempty(at(cells, "eventOrigin")) ?? extra.origin,
|
|
988
|
-
timestamp: Number(at(cells, "timestamp") || extra.timestamp || 0),
|
|
989
|
-
grain: nonempty(at(cells, "grain")) ?? extra.grain ?? "event",
|
|
990
|
-
effortSlug: nonempty(at(cells, "effortSlug")) ?? extra.effortSlug,
|
|
991
|
-
outputType: nonempty(at(cells, "outputType")) ?? extra.outputType ?? "event",
|
|
992
|
-
blockId,
|
|
993
|
-
blockVersion,
|
|
994
|
-
timeSpan: jsonCell(at(cells, "timeSpan")) ?? extra.timeSpan,
|
|
995
|
-
sourceBlockKey: nonempty(at(cells, "sourceBlockKey")) ?? extra.sourceBlockKey,
|
|
996
|
-
sourceStatementId: num(at(cells, "sourceStatementId")) ?? extra.sourceStatementId,
|
|
997
|
-
line: num(at(cells, "line")) ?? extra.line,
|
|
998
|
-
text: nonempty(at(cells, "text")) ?? extra.text,
|
|
999
|
-
dialect: nonempty(at(cells, "dialect")) ?? extra.dialect,
|
|
1000
|
-
stackLevel: num(at(cells, "stackLevel")) ?? extra.stackLevel,
|
|
1001
|
-
completionReason: nonempty(at(cells, "completionReason")) ?? extra.completionReason,
|
|
1002
|
-
representationKind: nonempty(at(cells, "representationKind")) ?? extra.representationKind,
|
|
1003
|
-
metrics: [],
|
|
1004
|
-
// Rebuild anchors from metric rows while preserving an authored empty array.
|
|
1005
|
-
metricTemporal: Array.isArray(extra.metricTemporal) ? [] : void 0
|
|
1006
|
-
};
|
|
1007
|
-
eventRows.set(eventId, event);
|
|
1008
|
-
}
|
|
1009
|
-
const metricType = at(cells, "metricType");
|
|
1010
|
-
if (!metricType) continue;
|
|
1011
|
-
const baseMetric = jsonCell(at(cells, "metricJson")) ?? {};
|
|
1012
|
-
const metric = {
|
|
1013
|
-
...baseMetric,
|
|
1014
|
-
type: metricType,
|
|
1015
|
-
...at(cells, "metricValue") !== "" ? { value: metricValueFromCell(at(cells, "metricValue")) } : {},
|
|
1016
|
-
...nonempty(at(cells, "metricUnit")) ? { unit: at(cells, "metricUnit") } : {},
|
|
1017
|
-
...nonempty(at(cells, "metricImage")) ? { image: at(cells, "metricImage") } : {},
|
|
1018
|
-
...nonempty(at(cells, "metricOrigin")) ? { origin: at(cells, "metricOrigin") } : {},
|
|
1019
|
-
...at(cells, "metricMetadata") !== "" ? { metadata: jsonCell(at(cells, "metricMetadata")) } : {}
|
|
1020
|
-
};
|
|
1021
|
-
event.metrics.push(metric);
|
|
1022
|
-
const temporal = jsonCell(at(cells, "metricTemporal"));
|
|
1023
|
-
if (temporal !== void 0) {
|
|
1024
|
-
(event.metricTemporal ??= []).push(temporal);
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
if (!session) throw new Error("[wod import] Session CSV did not contain session columns");
|
|
1028
|
-
return { session, events: [...eventRows.values()] };
|
|
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, addedNotes: 0, updatedNotes: 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
|
-
if (!existing) result.addedNotes++;
|
|
1187
|
-
else if (existing.rawContent !== row.content) result.updatedNotes++;
|
|
1188
|
-
validateNoteBlocks(row.content, row.blocks, 1, { registeredWidgets: SEED_REGISTERED_WIDGETS });
|
|
1189
|
-
const sourceRevision = existing ? existing.sourceRevision + (existing.rawContent === row.content ? 0 : 1) : 1;
|
|
1190
|
-
let ordinal = 0;
|
|
1191
|
-
const retainedHistory = history.get(id) ?? [];
|
|
1192
|
-
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);
|
|
1193
|
-
for (const block of Object.values(map)) {
|
|
1194
|
-
const saved = (history.get(id) ?? []).filter((r) => r.blockId === block.id).sort((a, b) => b.version - a.version)[0];
|
|
1195
|
-
const source = row.content.slice(block.from, block.to);
|
|
1196
|
-
if (existing?.rawContent === row.content) continue;
|
|
1197
|
-
const same = saved && saved.kind === block.kind && saved.rawContent === source && saved.dialect === block.dialect && saved.sport === block.sport;
|
|
1198
|
-
block.version = !existing ? 1 : same ? saved.version : (saved?.version ?? 0) + 1;
|
|
1199
|
-
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 } : {} });
|
|
1200
|
-
}
|
|
1201
|
-
validateNoteBlocks(row.content, map, sourceRevision, { registeredWidgets: SEED_REGISTERED_WIDGETS });
|
|
1202
|
-
const fm = readFrontmatterMeta(row.content);
|
|
1203
|
-
const title = typeof fm["title"] === "string" && fm["title"].trim() ? fm["title"].trim() : titleFromPath(row.path);
|
|
1204
|
-
const sourceId = write.chunkId ? seedNoteSourceId(row.path, row.content, write.chunkId) : void 0;
|
|
1205
|
-
if (!sourceId) throw new Error(`Unknown seed source family ${write.chunkId}`);
|
|
1206
|
-
const date = typeof fm["date"] === "string" && /^\d{4}-\d{2}-\d{2}$/.test(fm["date"]) ? Date.parse(`${fm["date"]}T12:00:00Z`) : void 0;
|
|
1207
|
-
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 };
|
|
1208
|
-
if (date === void 0) delete note.date;
|
|
1209
|
-
if (!equal(note, existing)) changedNotes.push(note);
|
|
1210
|
-
const byRange = new Map(Object.values(map).map((b) => [`${b.from}:${b.to}`, b]));
|
|
1211
|
-
const compiledById = new Map(row.blockIndex.map((b) => [b.blockId, b]));
|
|
1212
|
-
const remap = /* @__PURE__ */ new Map();
|
|
1213
|
-
for (const compiled of row.blockIndex) {
|
|
1214
|
-
const block = byRange.get(`${compiled.from}:${compiled.to}`);
|
|
1215
|
-
if (!block || block.kind !== compiled.dataType) throw new Error("Orphan seed projection");
|
|
1216
|
-
remap.set(compiled.blockId, block.id);
|
|
1217
|
-
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) };
|
|
1218
|
-
const prior = currentIndexes[slot.get(id)].find((b) => b.id === projection.id);
|
|
1219
|
-
if (!prior || prior.isStatic === true) blocks.push(projection);
|
|
1220
|
-
}
|
|
1221
|
-
for (const compiled of row.blockEfforts) {
|
|
1222
|
-
const blockId = remap.get(compiled.blockId);
|
|
1223
|
-
if (!blockId || compiledById.get(compiled.blockId)?.dataType !== "wod") throw new Error("Orphan seed effort link");
|
|
1224
|
-
const link = { ...compiled, id: `${id}:${blockId}:${compiled.effortSlug}`, noteId: id, blockId, sourcePath: row.path };
|
|
1225
|
-
const prior = currentEfforts[slot.get(id)].find((b) => b.id === link.id);
|
|
1226
|
-
if (!prior || prior.isStatic === true) blockEfforts.push(prior ? { ...link, createdAt: prior.createdAt } : link);
|
|
1227
|
-
}
|
|
1228
|
-
const n = slot.get(id);
|
|
1229
|
-
const wantedTags = /* @__PURE__ */ new Set();
|
|
1230
|
-
for (const item of extractTypedTags(row.content)) {
|
|
1231
|
-
let tag = tagsByLabel.get(item.label);
|
|
1232
|
-
if (!tag) {
|
|
1233
|
-
tag = { id: tagIds.get(item.label), label: item.label, type: item.type, createdAt: write.meta.importedAt };
|
|
1234
|
-
changedTags.push(tag);
|
|
1235
|
-
tagsByLabel.set(item.label, tag);
|
|
1236
|
-
} else if (item.type && !tag.type) {
|
|
1237
|
-
tag = { ...tag, type: item.type };
|
|
1238
|
-
changedTags.push(tag);
|
|
1239
|
-
tagsByLabel.set(item.label, tag);
|
|
1240
|
-
}
|
|
1241
|
-
if (wantedTags.has(tag.id)) continue;
|
|
1242
|
-
wantedTags.add(tag.id);
|
|
1243
|
-
const link = currentTags[n].find((l) => l.tagId === tag.id);
|
|
1244
|
-
if (!link) noteTags.push({ id: `${id}:${tag.id}`, noteId: id, tagId: tag.id });
|
|
1245
|
-
}
|
|
1246
|
-
for (const link of currentTags[n]) if (!wantedTags.has(link.tagId)) await tagLinks.delete(link.id);
|
|
1247
|
-
const slug = fm["slug"];
|
|
1248
|
-
if (typeof slug === "string" && pagesBySlug.has(slug)) {
|
|
1249
|
-
const pageId = pagesBySlug.get(slug);
|
|
1250
|
-
pages.push({ id: pageId, slug, title: titleFromPath(row.path), createdAt: write.manifest.version });
|
|
1251
|
-
pageNotes.push({ id: linkIds.get(id), pageId, noteId: id, createdAt: write.manifest.version });
|
|
1252
|
-
}
|
|
1253
|
-
result.totalNotes++;
|
|
1254
|
-
result.totalBlocks += Object.keys(map).length;
|
|
1255
|
-
}
|
|
1256
|
-
const wantedBlocks = new Set(blocks.map((b) => b.id)), wantedEfforts = new Set(blockEfforts.map((b) => b.id));
|
|
1257
|
-
for (const rows of currentIndexes) for (const row of rows) if (row.isStatic === true && !wantedBlocks.has(row.id)) await indexStore.delete(row.id);
|
|
1258
|
-
for (const rows of currentEfforts) for (const row of rows) if (row.isStatic === true && !wantedEfforts.has(row.id)) await effortLinks.delete(row.id);
|
|
1259
|
-
for (const id of goneIds) {
|
|
1260
|
-
if (!owned.has(id) || !existingById.get(id)) continue;
|
|
1261
|
-
const n = slot.get(id);
|
|
1262
|
-
await noteStore.delete(id);
|
|
1263
|
-
for (const link of currentTags[n]) await tagLinks.delete(link.id);
|
|
1264
|
-
for (const link of currentPages[n]) await pageLinks.delete(link.id);
|
|
1265
|
-
result.deleted++;
|
|
1266
|
-
}
|
|
1267
|
-
const effortStore = tx.readwrite("efforts");
|
|
1268
|
-
if (write.chunkId === EFFORTS_CHUNK_ID) {
|
|
1269
|
-
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" }));
|
|
1270
|
-
const prior = await effortStore.getMany(efforts.map((e) => e.slug));
|
|
1271
|
-
const changed = efforts.filter((e, i) => (!prior[i] || prior[i]?.registrySource === "bundled") && !equal(e, prior[i]));
|
|
1272
|
-
if (changed.length) await effortStore.putMany(changed);
|
|
1273
|
-
result.totalEfforts = changed.length;
|
|
1274
|
-
const wantedSlugs = new Set(efforts.map((effort) => effort.slug));
|
|
1275
|
-
const removed = allIds.map((id) => {
|
|
1276
|
-
const note = existingById.get(id);
|
|
1277
|
-
return owned.has(id) && note ? parseEffortFile(note.rawContent)?.slug : void 0;
|
|
1278
|
-
}).filter((slug) => !!slug && !wantedSlugs.has(slug));
|
|
1279
|
-
const old = await effortStore.getMany(removed);
|
|
1280
|
-
for (const [i, effort] of old.entries()) if (effort?.registrySource === "bundled") await effortStore.delete(removed[i]);
|
|
1281
|
-
}
|
|
1282
|
-
if (changedNotes.length) await noteStore.putMany(changedNotes);
|
|
1283
|
-
if (revisions.length) await revisionStore.putMany(revisions);
|
|
1284
|
-
await putChanged(indexStore, blocks, blocks.map((b) => b.id));
|
|
1285
|
-
await putChanged(effortLinks, blockEfforts, blockEfforts.map((b) => b.id));
|
|
1286
|
-
if (changedTags.length) await tagsStore.putMany([...new Map(changedTags.map((t) => [t.id, t])).values()]);
|
|
1287
|
-
if (noteTags.length) await tagLinks.putMany(noteTags);
|
|
1288
|
-
await putChanged(tx.readwrite("page"), pages, pages.map((p) => p.id));
|
|
1289
|
-
await putChanged(pageLinks, pageNotes, pageNotes.map((p) => p.id));
|
|
1290
|
-
await metaStore.put(write.meta);
|
|
1291
|
-
return result;
|
|
1292
|
-
});
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
// src/cli/formatters.ts
|
|
1296
|
-
function escapeCsvField2(field) {
|
|
1297
|
-
if (field === void 0 || field === null) return "";
|
|
1298
|
-
const str = String(field);
|
|
1299
|
-
if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
|
|
1300
|
-
return `"${str.replace(/"/g, '""')}"`;
|
|
1301
|
-
}
|
|
1302
|
-
return str;
|
|
1303
|
-
}
|
|
1304
|
-
function formatDate(ts) {
|
|
1305
|
-
if (!ts) return "-";
|
|
1306
|
-
try {
|
|
1307
|
-
return new Date(ts).toISOString();
|
|
1308
|
-
} catch {
|
|
1309
|
-
return String(ts);
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
function formatMetricValue(m) {
|
|
1313
|
-
if (m.image) return m.image;
|
|
1314
|
-
if (m.value === void 0 || m.value === null) return "";
|
|
1315
|
-
if (typeof m.value === "object") return JSON.stringify(m.value);
|
|
1316
|
-
return String(m.value);
|
|
1317
|
-
}
|
|
1318
|
-
function formatMetric(m) {
|
|
1319
|
-
const t = m.type || "metric";
|
|
1320
|
-
return `${t}:${formatMetricValue(m)}`;
|
|
1321
|
-
}
|
|
1322
|
-
function formatParseOutput(ir, format, pretty = true) {
|
|
1323
|
-
if (format === "json") {
|
|
1324
|
-
return JSON.stringify(ir, null, pretty ? 2 : void 0);
|
|
1325
|
-
}
|
|
1326
|
-
const flatNodes = [];
|
|
1327
|
-
const walk = (node) => {
|
|
1328
|
-
flatNodes.push(node);
|
|
1329
|
-
for (const child of node.children ?? []) {
|
|
1330
|
-
walk(child);
|
|
1331
|
-
}
|
|
1332
|
-
};
|
|
1333
|
-
walk(ir.data);
|
|
1334
|
-
if (format === "csv") {
|
|
1335
|
-
const header = "id,type,from,to,raw,metrics,hints";
|
|
1336
|
-
const rows = flatNodes.map(
|
|
1337
|
-
(n) => [
|
|
1338
|
-
n.id,
|
|
1339
|
-
n.type,
|
|
1340
|
-
n.from,
|
|
1341
|
-
n.to,
|
|
1342
|
-
escapeCsvField2(n.raw),
|
|
1343
|
-
escapeCsvField2(n.metrics.map(formatMetric).join("; ")),
|
|
1344
|
-
escapeCsvField2(n.hints?.join("; ") ?? "")
|
|
1345
|
-
].join(",")
|
|
1346
|
-
);
|
|
1347
|
-
return [header, ...rows].join("\n");
|
|
1348
|
-
}
|
|
1349
|
-
const lines = [];
|
|
1350
|
-
lines.push(`Parse Tree (${flatNodes.length} statements):`);
|
|
1351
|
-
lines.push("\u2500".repeat(80));
|
|
1352
|
-
lines.push(
|
|
1353
|
-
["ID".padEnd(6), "Type".padEnd(12), "Span".padEnd(12), "Hints".padEnd(20), "Raw Text"].join(" | ")
|
|
1354
|
-
);
|
|
1355
|
-
lines.push("\u2500".repeat(80));
|
|
1356
|
-
for (const n of flatNodes) {
|
|
1357
|
-
const span = `${n.from}-${n.to}`;
|
|
1358
|
-
const hints = (n.hints ?? []).join(", ");
|
|
1359
|
-
lines.push(
|
|
1360
|
-
[
|
|
1361
|
-
String(n.id).padEnd(6),
|
|
1362
|
-
n.type.padEnd(12),
|
|
1363
|
-
span.padEnd(12),
|
|
1364
|
-
(hints.length > 18 ? hints.slice(0, 15) + "..." : hints).padEnd(20),
|
|
1365
|
-
n.raw.replace(/\n/g, "\\n")
|
|
1366
|
-
].join(" | ")
|
|
1367
|
-
);
|
|
1368
|
-
}
|
|
1369
|
-
return lines.join("\n");
|
|
1370
|
-
}
|
|
1371
|
-
function formatExecutionOutput(ir, format, pretty = true) {
|
|
1372
|
-
if (format === "json") {
|
|
1373
|
-
return JSON.stringify(ir, null, pretty ? 2 : void 0);
|
|
1374
|
-
}
|
|
1375
|
-
const { results, logs } = ir.data;
|
|
1376
|
-
const statements = logs;
|
|
1377
|
-
if (format === "csv") {
|
|
1378
|
-
const header = "id,outputType,started,ended,sourceBlockKey,stackLevel,completionReason,metrics,hints";
|
|
1379
|
-
const rows = statements.map(
|
|
1380
|
-
(s) => [
|
|
1381
|
-
s.id,
|
|
1382
|
-
s.outputType,
|
|
1383
|
-
s.timeSpan?.started ?? s.timestamp ?? 0,
|
|
1384
|
-
s.timeSpan?.ended ?? "",
|
|
1385
|
-
escapeCsvField2(s.sourceBlockKey),
|
|
1386
|
-
s.stackLevel,
|
|
1387
|
-
escapeCsvField2(s.completionReason ?? ""),
|
|
1388
|
-
escapeCsvField2(s.metrics.map(formatMetric).join("; ")),
|
|
1389
|
-
escapeCsvField2(s.hints?.join("; ") ?? "")
|
|
1390
|
-
].join(",")
|
|
1391
|
-
);
|
|
1392
|
-
return [header, ...rows].join("\n");
|
|
1393
|
-
}
|
|
1394
|
-
const lines = [];
|
|
1395
|
-
lines.push(`Workout Execution Results:`);
|
|
1396
|
-
lines.push(` Duration: ${results.duration}ms`);
|
|
1397
|
-
lines.push(` Completed: ${results.completed ? "yes" : "no"}`);
|
|
1398
|
-
lines.push(` Start: ${formatDate(results.startTime)}`);
|
|
1399
|
-
lines.push(` End: ${formatDate(results.endTime)}`);
|
|
1400
|
-
lines.push(` Logs: ${statements.length} statements emitted`);
|
|
1401
|
-
lines.push("\u2500".repeat(80));
|
|
1402
|
-
lines.push(
|
|
1403
|
-
["ID".padEnd(8), "Type".padEnd(12), "Block".padEnd(18), "Reason".padEnd(15), "Metrics"].join(" | ")
|
|
1404
|
-
);
|
|
1405
|
-
lines.push("\u2500".repeat(80));
|
|
1406
|
-
for (const s of statements) {
|
|
1407
|
-
const metricsStr = s.metrics.map(formatMetric).join(", ");
|
|
1408
|
-
lines.push(
|
|
1409
|
-
[
|
|
1410
|
-
String(s.id).padEnd(8),
|
|
1411
|
-
(s.outputType ?? "segment").padEnd(12),
|
|
1412
|
-
(s.sourceBlockKey ?? "-").padEnd(18),
|
|
1413
|
-
(s.completionReason ?? "-").padEnd(15),
|
|
1414
|
-
metricsStr
|
|
1415
|
-
].join(" | ")
|
|
1416
|
-
);
|
|
1417
|
-
}
|
|
1418
|
-
return lines.join("\n");
|
|
1419
|
-
}
|
|
1420
|
-
function formatQueryOutput(ir, format, pretty = true) {
|
|
1421
|
-
if (format === "json") {
|
|
1422
|
-
return JSON.stringify(ir, null, pretty ? 2 : void 0);
|
|
1423
|
-
}
|
|
1424
|
-
const kind = ir.kind;
|
|
1425
|
-
if (kind === "rows-result") {
|
|
1426
|
-
const data2 = ir.data;
|
|
1427
|
-
if (format === "csv") {
|
|
1428
|
-
const header = "resultId,timestamp,outputType,eventId,metrics";
|
|
1429
|
-
const rows = [];
|
|
1430
|
-
for (const run of data2.runs) {
|
|
1431
|
-
for (const event of run.events) {
|
|
1432
|
-
rows.push(
|
|
1433
|
-
[
|
|
1434
|
-
run.resultId,
|
|
1435
|
-
run.timestamp,
|
|
1436
|
-
event.outputType,
|
|
1437
|
-
event.id,
|
|
1438
|
-
escapeCsvField2(event.metrics.map(formatMetric).join("; "))
|
|
1439
|
-
].join(",")
|
|
1440
|
-
);
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
return [header, ...rows].join("\n");
|
|
1444
|
-
}
|
|
1445
|
-
const lines2 = [];
|
|
1446
|
-
lines2.push(`Rows Query Result (${data2.runs.length} runs):`);
|
|
1447
|
-
if (data2.error) lines2.push(`Error: ${data2.error}`);
|
|
1448
|
-
lines2.push("\u2500".repeat(80));
|
|
1449
|
-
lines2.push(
|
|
1450
|
-
["Result ID".padEnd(20), "Date".padEnd(24), "Events"].join(" | ")
|
|
1451
|
-
);
|
|
1452
|
-
lines2.push("\u2500".repeat(80));
|
|
1453
|
-
for (const run of data2.runs) {
|
|
1454
|
-
lines2.push(
|
|
1455
|
-
[
|
|
1456
|
-
run.resultId.padEnd(20),
|
|
1457
|
-
formatDate(run.timestamp).padEnd(24),
|
|
1458
|
-
String(run.events.length)
|
|
1459
|
-
].join(" | ")
|
|
1460
|
-
);
|
|
1461
|
-
}
|
|
1462
|
-
return lines2.join("\n");
|
|
1463
|
-
}
|
|
1464
|
-
if (kind === "find-result") {
|
|
1465
|
-
const data2 = ir.data;
|
|
1466
|
-
if (format === "csv") {
|
|
1467
|
-
const header = "target,id,title_or_label,sourceId";
|
|
1468
|
-
const rows = [];
|
|
1469
|
-
for (const n of data2.notes) {
|
|
1470
|
-
rows.push(["note", n.id, escapeCsvField2(n.title), escapeCsvField2(n.sourceId ?? "")].join(","));
|
|
1471
|
-
}
|
|
1472
|
-
for (const b of data2.blocks) {
|
|
1473
|
-
rows.push(["block", b.id, escapeCsvField2(b.noteTitle), escapeCsvField2(b.sourceId ?? "")].join(","));
|
|
1474
|
-
}
|
|
1475
|
-
for (const e of data2.efforts ?? []) {
|
|
1476
|
-
rows.push(["effort", e.id, escapeCsvField2(e.label), escapeCsvField2(e.slug)].join(","));
|
|
1477
|
-
}
|
|
1478
|
-
return [header, ...rows].join("\n");
|
|
1479
|
-
}
|
|
1480
|
-
const lines2 = [];
|
|
1481
|
-
const matchedCount2 = data2.stages?.matched ?? 0;
|
|
1482
|
-
const selectedCount = data2.stages?.selected ?? 0;
|
|
1483
|
-
lines2.push(`Find Result (matched: ${matchedCount2} / selected: ${selectedCount}):`);
|
|
1484
|
-
lines2.push("\u2500".repeat(80));
|
|
1485
|
-
lines2.push(["Type".padEnd(8), "ID".padEnd(24), "Title / Label".padEnd(30), "Source ID"].join(" | "));
|
|
1486
|
-
lines2.push("\u2500".repeat(80));
|
|
1487
|
-
for (const n of data2.notes) {
|
|
1488
|
-
lines2.push(["note".padEnd(8), n.id.padEnd(24), n.title.padEnd(30), n.sourceId ?? "-"].join(" | "));
|
|
1489
|
-
}
|
|
1490
|
-
for (const b of data2.blocks) {
|
|
1491
|
-
lines2.push(["block".padEnd(8), b.id.padEnd(24), b.noteTitle.padEnd(30), b.sourceId ?? "-"].join(" | "));
|
|
1492
|
-
}
|
|
1493
|
-
for (const e of data2.efforts ?? []) {
|
|
1494
|
-
lines2.push(["effort".padEnd(8), e.id.padEnd(24), e.label.padEnd(30), e.slug].join(" | "));
|
|
1495
|
-
}
|
|
1496
|
-
return lines2.join("\n");
|
|
1497
|
-
}
|
|
1498
|
-
if (kind === "pipeline-result") {
|
|
1499
|
-
const data2 = ir.data;
|
|
1500
|
-
const sink = data2.chart ? `:${data2.chart.head}` : "data";
|
|
1501
|
-
if (format === "csv") {
|
|
1502
|
-
const header = "series,timestamp,date,value,unit";
|
|
1503
|
-
const rows = [];
|
|
1504
|
-
for (const s of data2.series ?? []) {
|
|
1505
|
-
for (const pt of s.points) {
|
|
1506
|
-
rows.push(
|
|
1507
|
-
[
|
|
1508
|
-
escapeCsvField2(s.label || s.key),
|
|
1509
|
-
pt.ts,
|
|
1510
|
-
formatDate(pt.ts),
|
|
1511
|
-
pt.value,
|
|
1512
|
-
escapeCsvField2(s.unit ?? data2.unit ?? "")
|
|
1513
|
-
].join(",")
|
|
1514
|
-
);
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
return [header, ...rows].join("\n");
|
|
1518
|
-
}
|
|
1519
|
-
const lines2 = [];
|
|
1520
|
-
if (data2.error) lines2.push(`Pipeline Error: ${data2.error}`);
|
|
1521
|
-
lines2.push(`Pipeline Result (\u2192 ${sink}):`);
|
|
1522
|
-
lines2.push("\u2500".repeat(80));
|
|
1523
|
-
if (data2.table) {
|
|
1524
|
-
lines2.push(`Table: ${data2.table.rows.length} of ${data2.table.totalCount} rows`);
|
|
1525
|
-
}
|
|
1526
|
-
for (const s of data2.series ?? []) {
|
|
1527
|
-
lines2.push(`${s.label || s.key}: ${s.points.length} points${s.unit ? ` (${s.unit})` : ""}`);
|
|
1528
|
-
}
|
|
1529
|
-
if (data2.notes?.length) lines2.push(`notes: ${data2.notes.length}`);
|
|
1530
|
-
if (data2.blocks?.length) lines2.push(`blocks: ${data2.blocks.length}`);
|
|
1531
|
-
if (data2.efforts?.length) lines2.push(`efforts: ${data2.efforts.length}`);
|
|
1532
|
-
if (data2.runs?.length) lines2.push(`runs: ${data2.runs.length}`);
|
|
1533
|
-
return lines2.join("\n");
|
|
1534
|
-
}
|
|
1535
|
-
const data = ir.data;
|
|
1536
|
-
if (data.scalar !== void 0 && data.series.length === 0) {
|
|
1537
|
-
if (format === "csv") {
|
|
1538
|
-
return `metric,scalar,unit
|
|
1539
|
-
${escapeCsvField2(data.parsed.metric)},${data.scalar},${escapeCsvField2(data.unit ?? "")}`;
|
|
1540
|
-
}
|
|
1541
|
-
return `Scalar Result: ${data.scalar}${data.unit ? " " + data.unit : ""}`;
|
|
1542
|
-
}
|
|
1543
|
-
if (format === "csv") {
|
|
1544
|
-
const header = "series,timestamp,date,value,unit";
|
|
1545
|
-
const rows = [];
|
|
1546
|
-
for (const s of data.series) {
|
|
1547
|
-
for (const pt of s.points) {
|
|
1548
|
-
rows.push(
|
|
1549
|
-
[
|
|
1550
|
-
escapeCsvField2(s.label || s.key),
|
|
1551
|
-
pt.ts,
|
|
1552
|
-
formatDate(pt.ts),
|
|
1553
|
-
pt.value,
|
|
1554
|
-
escapeCsvField2(s.unit ?? data.unit ?? "")
|
|
1555
|
-
].join(",")
|
|
1556
|
-
);
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
return [header, ...rows].join("\n");
|
|
1560
|
-
}
|
|
1561
|
-
const lines = [];
|
|
1562
|
-
const matchedCount = data.stages?.matched ?? data.stages?.selected ?? data.matched?.length ?? 0;
|
|
1563
|
-
lines.push(`Query Result (${data.series.length} series, ${matchedCount} matched facts):`);
|
|
1564
|
-
lines.push(
|
|
1565
|
-
["Series / Group".padEnd(20), "Timestamp".padEnd(16), "Date".padEnd(24), "Value".padEnd(12), "Unit"].join(" | ")
|
|
1566
|
-
);
|
|
1567
|
-
lines.push("\u2500".repeat(80));
|
|
1568
|
-
for (const s of data.series) {
|
|
1569
|
-
for (const pt of s.points) {
|
|
1570
|
-
lines.push(
|
|
1571
|
-
[
|
|
1572
|
-
(s.label || s.key).padEnd(20),
|
|
1573
|
-
String(pt.ts).padEnd(16),
|
|
1574
|
-
formatDate(pt.ts).padEnd(24),
|
|
1575
|
-
String(pt.value).padEnd(12),
|
|
1576
|
-
s.unit ?? data.unit ?? "-"
|
|
1577
|
-
].join(" | ")
|
|
1578
|
-
);
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
return lines.join("\n");
|
|
1582
|
-
}
|
|
1583
|
-
var nodePath = () => nodeRequire()("node:path");
|
|
1584
|
-
var nodeFs = () => nodeRequire()("node:fs");
|
|
1585
|
-
var nodeCrypto = () => nodeRequire()("node:crypto");
|
|
1586
|
-
function nodeRequire() {
|
|
1587
|
-
const g = globalThis;
|
|
1588
|
-
if (typeof g.process?.versions?.node === "undefined" && typeof g.Bun === "undefined") {
|
|
1589
|
-
throw new Error("SQLite operations are only supported in Node / Bun environments");
|
|
1590
|
-
}
|
|
1591
|
-
const req = g.require ?? (typeof __require === "function" ? __require : void 0);
|
|
1592
|
-
if (!req) throw new Error("SQLite operations require a CJS require shim (missing tsup banner?)");
|
|
1593
|
-
return req;
|
|
1594
|
-
}
|
|
1595
|
-
var sqlModulePromise;
|
|
1596
|
-
function loadSql() {
|
|
1597
|
-
sqlModulePromise ??= import(
|
|
1598
|
-
/* @vite-ignore */
|
|
1599
|
-
'@bitcobblers/wod-wiki-storage/sql'
|
|
1600
|
-
);
|
|
1601
|
-
return sqlModulePromise;
|
|
1602
|
-
}
|
|
1603
|
-
function resolveDbPath(dbPath) {
|
|
1604
|
-
const chosen = dbPath ?? process.env.WOD_WIKI_DB ?? "./wodwiki.db";
|
|
1605
|
-
return nodePath().resolve(process.cwd(), chosen);
|
|
1606
|
-
}
|
|
1607
|
-
async function openCliDb(dbPath) {
|
|
1608
|
-
const [{ createClient }, sql] = await Promise.all([
|
|
1609
|
-
import(
|
|
1610
|
-
/* @vite-ignore */
|
|
1611
|
-
'@libsql/client'
|
|
1612
|
-
),
|
|
1613
|
-
loadSql()
|
|
1614
|
-
]);
|
|
1615
|
-
const resolved = resolveDbPath(dbPath);
|
|
1616
|
-
nodeFs().mkdirSync(nodePath().dirname(resolved), { recursive: true });
|
|
1617
|
-
const db = sql.libsqlDatabase(createClient({ url: `file:${resolved}` }));
|
|
1618
|
-
try {
|
|
1619
|
-
await sql.initializeStore(db);
|
|
1620
|
-
await sql.initializeDomain(db);
|
|
1621
|
-
return { db, storage: new sql.SqlStorage(db), path: resolved, close: () => db.close() };
|
|
1622
|
-
} catch (error) {
|
|
1623
|
-
await db.close();
|
|
1624
|
-
throw error;
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
function formatV5Digest(digest) {
|
|
1628
|
-
const bytes = digest.slice(0, 16);
|
|
1629
|
-
bytes[6] = bytes[6] & 15 | 80;
|
|
1630
|
-
bytes[8] = bytes[8] & 63 | 128;
|
|
1631
|
-
const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0"));
|
|
1632
|
-
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
|
|
1633
|
-
}
|
|
1634
|
-
function uuid5(namespace, name) {
|
|
1635
|
-
return formatV5Digest(nodeCrypto().createHash("sha1").update(namespace).update(name).digest());
|
|
1636
|
-
}
|
|
1637
|
-
var CLI_UUID_NAMESPACE = "c0ffee00-8b1a-4e2c-9d3f-5a6b7c8d9e0f";
|
|
1638
|
-
function stableNoteId(sourceLabel, content) {
|
|
1639
|
-
if (sourceLabel && sourceLabel !== "-") {
|
|
1640
|
-
return uuid5(CLI_UUID_NAMESPACE, `wod-cli:${nodePath().resolve(process.cwd(), sourceLabel)}`);
|
|
1641
|
-
}
|
|
1642
|
-
return uuid5(CLI_UUID_NAMESPACE, `wod-cli:stdin:${content.length}:${content.slice(0, 256)}`);
|
|
1643
|
-
}
|
|
1644
|
-
function randomId() {
|
|
1645
|
-
return nodeCrypto().randomUUID();
|
|
1646
|
-
}
|
|
1647
|
-
async function allRows(sql, db, store) {
|
|
1648
|
-
return sql.listRows(db, sql.storeDefinition(store));
|
|
1649
|
-
}
|
|
1650
|
-
async function rowsByIndex(sql, db, store, index, only) {
|
|
1651
|
-
return sql.listRows(db, sql.storeDefinition(store), { only }, void 0, index);
|
|
1652
|
-
}
|
|
1653
|
-
function sqliteEventStore(db) {
|
|
1654
|
-
const all = async () => await allRows(await loadSql(), db, "events");
|
|
1655
|
-
return {
|
|
1656
|
-
getEventsByTimeRange: async (start, end) => (await all()).filter((r) => r.timestamp >= start && r.timestamp <= end),
|
|
1657
|
-
getEventsByResult: async (resultId) => (await all()).filter((r) => r.resultId === resultId),
|
|
1658
|
-
getEventsForNote: async (noteId) => (await all()).filter((r) => r.noteId === noteId),
|
|
1659
|
-
getEventsByContent: async (blockContentId4) => (await all()).filter((r) => r.blockContentId === blockContentId4),
|
|
1660
|
-
scanAll: all,
|
|
1661
|
-
appendEvents: async (appended) => {
|
|
1662
|
-
const sql = await loadSql();
|
|
1663
|
-
await db.transaction(async (tx) => {
|
|
1664
|
-
const eventsDef = sql.storeDefinition("events");
|
|
1665
|
-
for (const row of appended) await sql.putRow(tx, eventsDef, row);
|
|
1666
|
-
});
|
|
1667
|
-
},
|
|
1668
|
-
finalizeSummaries: async (resultId, finals) => {
|
|
1669
|
-
const sql = await loadSql();
|
|
1670
|
-
await db.transaction(async (tx) => {
|
|
1671
|
-
const eventsDef = sql.storeDefinition("events");
|
|
1672
|
-
const existing = (await sql.listRows(tx, eventsDef)).filter(
|
|
1673
|
-
(r) => r.resultId === resultId && r.grain === "summary" && r.origin !== "user"
|
|
1674
|
-
);
|
|
1675
|
-
for (const row of existing) await sql.deleteRows(tx, eventsDef, row.id);
|
|
1676
|
-
for (const row of finals) await sql.putRow(tx, eventsDef, row);
|
|
1677
|
-
});
|
|
1678
|
-
},
|
|
1679
|
-
deleteEvents: async (ids) => {
|
|
1680
|
-
const sql = await loadSql();
|
|
1681
|
-
await db.transaction(async (tx) => {
|
|
1682
|
-
const eventsDef = sql.storeDefinition("events");
|
|
1683
|
-
for (const id of ids) await sql.deleteRows(tx, eventsDef, id);
|
|
1684
|
-
});
|
|
1685
|
-
}
|
|
1686
|
-
};
|
|
1687
|
-
}
|
|
1688
|
-
function sqliteNoteStore(db) {
|
|
1689
|
-
return {
|
|
1690
|
-
getAllNotes: async () => await allRows(await loadSql(), db, "notes"),
|
|
1691
|
-
getNotesByIds: async (noteIds) => {
|
|
1692
|
-
const sql = await loadSql();
|
|
1693
|
-
const rows = await sql.getRows(db, sql.storeDefinition("notes"), [...noteIds]);
|
|
1694
|
-
return rows.filter((row) => row !== void 0);
|
|
1695
|
-
},
|
|
1696
|
-
getNoteIdsForTag: async (label) => {
|
|
1697
|
-
const sql = await loadSql();
|
|
1698
|
-
const tags = (await allRows(sql, db, "tags")).filter((t) => t.label === label);
|
|
1699
|
-
const ids = {};
|
|
1700
|
-
for (const tag of tags) {
|
|
1701
|
-
for (const row of await rowsByIndex(sql, db, "note_tags", "by-tag", tag.id)) {
|
|
1702
|
-
ids[row.noteId] = true;
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
return new Set(Object.keys(ids));
|
|
1706
|
-
},
|
|
1707
|
-
getNoteTagLabels: async (noteId) => {
|
|
1708
|
-
const sql = await loadSql();
|
|
1709
|
-
const tagRows = await allRows(sql, db, "tags");
|
|
1710
|
-
const labelById = {};
|
|
1711
|
-
for (const t of tagRows) labelById[t.id] = t.label;
|
|
1712
|
-
const links = await rowsByIndex(sql, db, "note_tags", "by-note", noteId);
|
|
1713
|
-
return links.map((l) => labelById[l.tagId]).filter((l) => typeof l === "string");
|
|
1714
|
-
},
|
|
1715
|
-
queryDomain: async (request) => {
|
|
1716
|
-
const result = await (await loadSql()).handleDomainQuery(db, request);
|
|
1717
|
-
return result;
|
|
1718
|
-
}
|
|
1719
|
-
};
|
|
1720
|
-
}
|
|
1721
|
-
function sqliteBlockStore(db) {
|
|
1722
|
-
return {
|
|
1723
|
-
getAllBlocks: async () => await allRows(await loadSql(), db, "block_index"),
|
|
1724
|
-
queryDomain: async (request) => {
|
|
1725
|
-
const result = await (await loadSql()).handleDomainQuery(db, { ...request, plan: "blocks" });
|
|
1726
|
-
return result;
|
|
1727
|
-
}
|
|
1728
|
-
};
|
|
1729
|
-
}
|
|
1730
|
-
function sqliteEffortStore(db) {
|
|
1731
|
-
return {
|
|
1732
|
-
getAllEfforts: async () => {
|
|
1733
|
-
const stored = await allRows(await loadSql(), db, "efforts");
|
|
1734
|
-
const bySlug = {};
|
|
1735
|
-
for (const effort of stored) bySlug[effort.slug] = effort;
|
|
1736
|
-
for (const effort of bundledEfforts) {
|
|
1737
|
-
bySlug[effort.slug] ??= effort;
|
|
1738
|
-
}
|
|
1739
|
-
return Object.values(bySlug);
|
|
1740
|
-
}
|
|
1741
|
-
};
|
|
1742
|
-
}
|
|
1743
|
-
function sqliteIndexStore(db, store) {
|
|
1744
|
-
return {
|
|
1745
|
-
getAllFromIndex: async (index, key) => rowsByIndex(await loadSql(), db, store, index, key)
|
|
1746
|
-
};
|
|
1747
|
-
}
|
|
1748
|
-
async function getNoteBundle(db, noteId) {
|
|
1749
|
-
const sql = await loadSql();
|
|
1750
|
-
const note = await sql.getRow(db, sql.storeDefinition("notes"), noteId);
|
|
1751
|
-
return { note };
|
|
1752
|
-
}
|
|
1753
|
-
async function getSessionBundle(db, resultId) {
|
|
1754
|
-
const sql = await loadSql();
|
|
1755
|
-
const session = await sql.getRow(db, sql.storeDefinition("sessions"), resultId);
|
|
1756
|
-
if (!session) return void 0;
|
|
1757
|
-
const events = await sql.listRows(db, sql.storeDefinition("events"), { only: resultId }, void 0, "by-result-grain");
|
|
1758
|
-
return { session, events };
|
|
1759
|
-
}
|
|
1760
|
-
async function getEffortBySlug(db, slug) {
|
|
1761
|
-
const sql = await loadSql();
|
|
1762
|
-
const efforts = await allRows(sql, db, "efforts");
|
|
1763
|
-
return efforts.find((e) => e.slug === slug);
|
|
1764
|
-
}
|
|
1765
|
-
async function listTagsByType(db, type) {
|
|
1766
|
-
const sql = await loadSql();
|
|
1767
|
-
const tags = await allRows(sql, db, "tags");
|
|
1768
|
-
return tags.filter((t) => t.type === type);
|
|
1769
|
-
}
|
|
1770
|
-
var registeredWidgets = KNOWN_SECTION_WIDGETS;
|
|
1771
|
-
var saveStores = ["notes", "block_revisions", "block_index", "block_efforts", "tags", "note_tags", "sessions"];
|
|
1772
|
-
function extractEffortSlugs(statements) {
|
|
1773
|
-
const slugs = /* @__PURE__ */ new Set();
|
|
1774
|
-
for (const statement of statements) {
|
|
1775
|
-
if (statement.exerciseId) {
|
|
1776
|
-
slugs.add(statement.exerciseId);
|
|
1777
|
-
} else {
|
|
1778
|
-
const effortMetric = statement.metrics?.getMetric?.("effort");
|
|
1779
|
-
const effort = effortMetric && "effort" in effortMetric ? effortMetric.effort : void 0;
|
|
1780
|
-
if (typeof effort === "string") {
|
|
1781
|
-
const slug = effort.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1782
|
-
if (slug) slugs.add(slug);
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
return [...slugs];
|
|
1787
|
-
}
|
|
1788
|
-
function titleFromLabel(sourceLabel) {
|
|
1789
|
-
if (!sourceLabel || sourceLabel === "-") return "stdin";
|
|
1790
|
-
const stem = sourceLabel.split("/").pop() ?? sourceLabel;
|
|
1791
|
-
return stem.replace(/\.[^.]+$/, "") || sourceLabel;
|
|
1792
|
-
}
|
|
1793
|
-
async function saveSource(db, options, startTime) {
|
|
1794
|
-
const noteId = options.noteId ?? stableNoteId(options.sourceLabel, options.source);
|
|
1795
|
-
const observed = await db.storage.readonly("notes").get(noteId);
|
|
1796
|
-
const expected = options.expectedSourceRevision ?? observed?.sourceRevision ?? 0;
|
|
1797
|
-
const now = options.now ?? Date.now();
|
|
1798
|
-
return db.storage.transaction([...saveStores], "readwrite", async (tx) => {
|
|
1799
|
-
const existing = await tx.readonly("notes").get(noteId);
|
|
1800
|
-
if ((existing?.sourceRevision ?? 0) !== expected) {
|
|
1801
|
-
throw new Error(`Stale Note source revision for ${noteId}; expected ${expected}`);
|
|
1802
|
-
}
|
|
1803
|
-
let sourceRevision = existing?.rawContent === options.source ? existing.sourceRevision : (existing?.sourceRevision ?? 0) + 1;
|
|
1804
|
-
const blocks = reconcileNoteBlocks(options.source, {
|
|
1805
|
-
sourceRevision,
|
|
1806
|
-
blocks: options.blocks ?? (existing?.rawContent === options.source ? existing.blocks : void 0),
|
|
1807
|
-
registeredWidgets,
|
|
1808
|
-
createId: randomId
|
|
1809
|
-
});
|
|
1810
|
-
validateNoteBlocks(options.source, blocks, sourceRevision, { registeredWidgets });
|
|
1811
|
-
const history = await tx.readonly("block_revisions").getAllFromIndex("by-note", noteId);
|
|
1812
|
-
const latest = /* @__PURE__ */ new Map();
|
|
1813
|
-
for (const row of history) {
|
|
1814
|
-
if ((latest.get(row.blockId)?.version ?? 0) < row.version) latest.set(row.blockId, row);
|
|
1815
|
-
}
|
|
1816
|
-
const revisions = [];
|
|
1817
|
-
for (const block of Object.values(blocks)) {
|
|
1818
|
-
const saved = latest.get(block.id);
|
|
1819
|
-
const rawContent = options.source.slice(block.from, block.to);
|
|
1820
|
-
const unchanged = saved && saved.rawContent === rawContent && saved.kind === block.kind && saved.dialect === block.dialect && saved.sport === block.sport;
|
|
1821
|
-
block.version = unchanged ? saved.version : (saved?.version ?? 0) + 1;
|
|
1822
|
-
if (!unchanged) revisions.push({
|
|
1823
|
-
noteId,
|
|
1824
|
-
blockId: block.id,
|
|
1825
|
-
version: block.version,
|
|
1826
|
-
kind: block.kind,
|
|
1827
|
-
rawContent,
|
|
1828
|
-
savedAt: now,
|
|
1829
|
-
...block.dialect ? { dialect: block.dialect } : {},
|
|
1830
|
-
...block.sport ? { sport: block.sport } : {}
|
|
1831
|
-
});
|
|
1832
|
-
}
|
|
1833
|
-
if (existing?.rawContent === options.source && !rowsEqual(existing.blocks, blocks)) {
|
|
1834
|
-
sourceRevision++;
|
|
1835
|
-
for (const block of Object.values(blocks)) block.sourceRevision = sourceRevision;
|
|
1836
|
-
}
|
|
1837
|
-
const note = {
|
|
1838
|
-
...existing,
|
|
1839
|
-
id: noteId,
|
|
1840
|
-
title: options.title ?? existing?.title ?? titleFromLabel(options.sourceLabel),
|
|
1841
|
-
type: existing?.type ?? "note",
|
|
1842
|
-
date: existing?.date ?? now,
|
|
1843
|
-
createdAt: existing?.createdAt ?? now,
|
|
1844
|
-
rawContent: options.source,
|
|
1845
|
-
sourceRevision,
|
|
1846
|
-
blocks
|
|
1847
|
-
};
|
|
1848
|
-
const priorRows = await tx.readonly("block_index").getAllFromIndex("by-note", noteId);
|
|
1849
|
-
const priorEfforts = await tx.readonly("block_efforts").getAllFromIndex("by-note", noteId);
|
|
1850
|
-
const priorById = new Map(priorRows.map((row) => [row.id, row]));
|
|
1851
|
-
const nextRows = [];
|
|
1852
|
-
const nextEfforts = [];
|
|
1853
|
-
for (const [position, block] of Object.values(blocks).sort((a, b) => a.from - b.from).entries()) {
|
|
1854
|
-
const id = `${noteId}:${block.id}`;
|
|
1855
|
-
const prior = priorById.get(id);
|
|
1856
|
-
const rawContent = options.source.slice(block.from, block.to);
|
|
1857
|
-
const body = parseDocumentSections(rawContent, { registeredWidgets })[0]?.displayContent ?? rawContent;
|
|
1858
|
-
const fingerprint = block.kind === "wod" ? blockContentId(body) : void 0;
|
|
1859
|
-
nextRows.push({
|
|
1860
|
-
...prior,
|
|
1861
|
-
id,
|
|
1862
|
-
noteId,
|
|
1863
|
-
blockId: block.id,
|
|
1864
|
-
blockVersion: block.version,
|
|
1865
|
-
dataType: block.kind,
|
|
1866
|
-
sourceRevision,
|
|
1867
|
-
from: block.from,
|
|
1868
|
-
to: block.to,
|
|
1869
|
-
position,
|
|
1870
|
-
rawContent,
|
|
1871
|
-
noteTitle: note.title,
|
|
1872
|
-
createdAt: prior?.createdAt ?? now,
|
|
1873
|
-
blockContentId: fingerprint,
|
|
1874
|
-
...note.sourceId ? { sourceId: note.sourceId } : {}
|
|
1875
|
-
});
|
|
1876
|
-
if (block.kind === "wod") {
|
|
1877
|
-
for (const effortSlug of extractEffortSlugs(parseScript(body, { sport: block.sport, dialect: block.dialect }).statements)) {
|
|
1878
|
-
const effortId = `${noteId}:${block.id}:${effortSlug}`;
|
|
1879
|
-
const previous = priorEfforts.find((row) => row.id === effortId);
|
|
1880
|
-
nextEfforts.push({
|
|
1881
|
-
...previous,
|
|
1882
|
-
id: effortId,
|
|
1883
|
-
noteId,
|
|
1884
|
-
blockId: block.id,
|
|
1885
|
-
blockContentId: fingerprint,
|
|
1886
|
-
effortSlug,
|
|
1887
|
-
createdAt: previous?.createdAt ?? now
|
|
1888
|
-
});
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
if (!rowsEqual(existing, note)) await tx.readwrite("notes").put(note);
|
|
1893
|
-
if (revisions.length) await tx.readwrite("block_revisions").putMany(revisions);
|
|
1894
|
-
await diffProjection(tx, "block_index", priorRows, nextRows);
|
|
1895
|
-
await diffProjection(tx, "block_efforts", priorEfforts, nextEfforts);
|
|
1896
|
-
const tags = await saveTags(tx, noteId, options.tags, now);
|
|
1897
|
-
let session;
|
|
1898
|
-
let revision;
|
|
1899
|
-
if (startTime !== void 0) {
|
|
1900
|
-
const block = Object.values(blocks).filter((row) => row.kind === "wod").sort((a, b) => a.from - b.from)[0];
|
|
1901
|
-
if (!block) throw new Error("[wod run] Note has no actionable workout block");
|
|
1902
|
-
revision = revisions.find((row) => row.blockId === block.id) ?? latest.get(block.id);
|
|
1903
|
-
if (!revision) throw new Error("[wod run] Saved workout revision is unavailable");
|
|
1904
|
-
session = {
|
|
1905
|
-
id: randomId(),
|
|
1906
|
-
noteId,
|
|
1907
|
-
blockId: block.id,
|
|
1908
|
-
blockVersion: block.version,
|
|
1909
|
-
blockContentId: blockContentId(parseDocumentSections(revision.rawContent)[0]?.displayContent ?? revision.rawContent),
|
|
1910
|
-
origin: "journal",
|
|
1911
|
-
status: "in-progress",
|
|
1912
|
-
startTime,
|
|
1913
|
-
endTime: startTime,
|
|
1914
|
-
duration: 0,
|
|
1915
|
-
completed: false,
|
|
1916
|
-
createdAt: startTime
|
|
1917
|
-
};
|
|
1918
|
-
await tx.readwrite("sessions").put(session);
|
|
1919
|
-
}
|
|
1920
|
-
return { noteId, note, blocks: nextRows.length, efforts: nextEfforts.length, tags, created: !existing, session, revision };
|
|
1921
|
-
});
|
|
1922
|
-
}
|
|
1923
|
-
async function diffProjection(tx, store, previous, next) {
|
|
1924
|
-
const nextIds = new Set(next.map((row) => row.id));
|
|
1925
|
-
for (const row of previous) if (!nextIds.has(row.id)) await tx.readwrite(store).delete(row.id);
|
|
1926
|
-
const old = new Map(previous.map((row) => [row.id, row]));
|
|
1927
|
-
for (const row of next) {
|
|
1928
|
-
if (!rowsEqual(old.get(row.id), row)) {
|
|
1929
|
-
if (store === "block_index" && "blockVersion" in row) await tx.readwrite("block_index").put(row);
|
|
1930
|
-
if (store === "block_efforts" && "effortSlug" in row) await tx.readwrite("block_efforts").put(row);
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
1934
|
-
async function saveTags(tx, noteId, items, now) {
|
|
1935
|
-
const previous = await tx.readonly("note_tags").getAllFromIndex("by-note", noteId);
|
|
1936
|
-
if (!items) return previous.length;
|
|
1937
|
-
const next = /* @__PURE__ */ new Map();
|
|
1938
|
-
for (const item of items) {
|
|
1939
|
-
const label = item.label.trim();
|
|
1940
|
-
if (!label) continue;
|
|
1941
|
-
const found = (await tx.readonly("tags").getAllFromIndex("by-label", label))[0];
|
|
1942
|
-
const tag = found ? { ...found, ...!found.type && item.type ? { type: item.type } : {} } : { id: uuid5(CLI_UUID_NAMESPACE, `tag:${label}`), label, type: item.type, createdAt: now };
|
|
1943
|
-
if (!rowsEqual(found, tag)) await tx.readwrite("tags").put(tag);
|
|
1944
|
-
const id = previous.find((link) => link.tagId === tag.id)?.id ?? uuid5(CLI_UUID_NAMESPACE, `note-tag:${noteId}:${tag.id}`);
|
|
1945
|
-
next.set(id, { ...previous.find((link) => link.id === id), id, noteId, tagId: tag.id });
|
|
1946
|
-
}
|
|
1947
|
-
for (const link of previous) if (!next.has(link.id)) await tx.readwrite("note_tags").delete(link.id);
|
|
1948
|
-
const old = new Map(previous.map((row) => [row.id, row]));
|
|
1949
|
-
for (const link of next.values()) if (!rowsEqual(old.get(link.id), link)) await tx.readwrite("note_tags").put(link);
|
|
1950
|
-
return next.size;
|
|
1951
|
-
}
|
|
1952
|
-
function persistNote(db, options) {
|
|
1953
|
-
return saveSource(db, options);
|
|
1954
|
-
}
|
|
1955
|
-
async function startRun(db, options, startTime = Date.now()) {
|
|
1956
|
-
const saved = await saveSource(db, options, startTime);
|
|
1957
|
-
if (!saved.session || !saved.revision) throw new Error("[wod run] Atomic run capture failed");
|
|
1958
|
-
return { session: saved.session, revision: saved.revision, noteCreated: saved.created };
|
|
1959
|
-
}
|
|
1960
|
-
async function persistRun(db, started, executionLog) {
|
|
1961
|
-
const results = executionLog.results;
|
|
1962
|
-
const session = {
|
|
1963
|
-
...started.session,
|
|
1964
|
-
status: results.completed ? "completed" : "in-progress",
|
|
1965
|
-
endTime: results.endTime,
|
|
1966
|
-
duration: results.duration ?? 0,
|
|
1967
|
-
completed: results.completed
|
|
1968
|
-
};
|
|
1969
|
-
const identity = {
|
|
1970
|
-
noteId: session.noteId,
|
|
1971
|
-
resultId: session.id,
|
|
1972
|
-
blockId: session.blockId,
|
|
1973
|
-
blockVersion: session.blockVersion,
|
|
1974
|
-
blockContentId: session.blockContentId,
|
|
1975
|
-
origin: session.origin,
|
|
1976
|
-
pageId: session.pageId,
|
|
1977
|
-
workoutTimestamp: session.createdAt
|
|
1978
|
-
};
|
|
1979
|
-
const events = toEventRows(executionLog.logs, identity);
|
|
1980
|
-
const summaries = toSummaryEventRows(executionLog.logs, identity);
|
|
1981
|
-
await db.storage.transaction(["sessions", "events", "block_revisions"], "readwrite", async (tx) => {
|
|
1982
|
-
const saved = await tx.readonly("sessions").get(session.id);
|
|
1983
|
-
if (!saved || saved.noteId !== session.noteId || saved.blockId !== session.blockId || saved.blockVersion !== session.blockVersion) {
|
|
1984
|
-
throw new Error("[wod run] Captured Session source pin changed");
|
|
1985
|
-
}
|
|
1986
|
-
const revision = await tx.readonly("block_revisions").get([session.noteId, session.blockId, session.blockVersion]);
|
|
1987
|
-
if (!revision || !rowsEqual(revision, started.revision)) throw new Error("[wod run] Captured revision is unavailable");
|
|
1988
|
-
await tx.readwrite("sessions").put(session);
|
|
1989
|
-
await tx.readwrite("events").putMany([...events, ...summaries]);
|
|
1990
|
-
});
|
|
1991
|
-
return { noteId: session.noteId, resultId: session.id, session, events: events.length, summaries: summaries.length, noteCreated: started.noteCreated };
|
|
1992
|
-
}
|
|
1993
|
-
async function persistSessionBundle(db, session, events) {
|
|
1994
|
-
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) {
|
|
1995
|
-
throw new Error("[wod import] Session requires an exact block revision pin");
|
|
1996
|
-
}
|
|
1997
|
-
await db.storage.transaction(["sessions", "events", "block_revisions"], "readwrite", async (tx) => {
|
|
1998
|
-
const revision = await tx.readonly("block_revisions").get([session.noteId, session.blockId, session.blockVersion]);
|
|
1999
|
-
if (!revision || revision.kind !== "wod") throw new Error("[wod import] Exact immutable workout revision is unavailable");
|
|
2000
|
-
for (const event of events) {
|
|
2001
|
-
if (event.resultId !== session.id || event.noteId !== session.noteId || event.blockId !== session.blockId || event.blockVersion !== session.blockVersion) {
|
|
2002
|
-
throw new Error("[wod import] Event source pin differs from Session source pin");
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
const existing = await tx.readonly("sessions").get(session.id);
|
|
2006
|
-
if (existing && !rowsEqual(existing, session)) throw new Error("[wod import] Session identity conflicts with existing row");
|
|
2007
|
-
if (!existing) await tx.readwrite("sessions").put(session);
|
|
2008
|
-
for (const event of events) {
|
|
2009
|
-
const old = await tx.readonly("events").get(event.id);
|
|
2010
|
-
if (old && !rowsEqual(old, event)) throw new Error("[wod import] Event identity conflicts with existing row");
|
|
2011
|
-
if (!old) await tx.readwrite("events").put(event);
|
|
2012
|
-
}
|
|
2013
|
-
});
|
|
2014
|
-
return { events: events.length };
|
|
2015
|
-
}
|
|
2016
|
-
async function persistEffort(db, effort) {
|
|
2017
|
-
const sql = await loadSql();
|
|
2018
|
-
await db.db.transaction(async (tx) => {
|
|
2019
|
-
await sql.putRow(tx, sql.storeDefinition("efforts"), effort);
|
|
2020
|
-
});
|
|
2021
|
-
}
|
|
2022
|
-
async function persistTypedTags(db, labels, type) {
|
|
2023
|
-
return db.storage.transaction(["tags"], "readwrite", async (tx) => {
|
|
2024
|
-
let added = 0;
|
|
2025
|
-
for (const raw of labels) {
|
|
2026
|
-
const label = raw.trim();
|
|
2027
|
-
if (!label) continue;
|
|
2028
|
-
const tag = (await tx.readonly("tags").getAllFromIndex("by-label", label))[0];
|
|
2029
|
-
if (!tag) {
|
|
2030
|
-
await tx.readwrite("tags").put({ id: uuid5(CLI_UUID_NAMESPACE, `tag:${label}`), label, type, createdAt: Date.now() });
|
|
2031
|
-
added++;
|
|
2032
|
-
} else if (!tag.type) {
|
|
2033
|
-
await tx.readwrite("tags").put({ ...tag, type });
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
return added;
|
|
2037
|
-
});
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
// src/cli/seed.ts
|
|
2041
|
-
async function importSeedDir(db, dir, sourceId) {
|
|
2042
|
-
const fs = nodeFs(), path = nodePath();
|
|
2043
|
-
const source = sourceId || path.basename(dir);
|
|
2044
|
-
const manifest = assertSeedManifest(JSON.parse(fs.readFileSync(path.resolve(dir, "manifest.json"), "utf-8")));
|
|
2045
|
-
const key = seedMetaKey(source);
|
|
2046
|
-
let expected = await db.storage.readonly("meta").get(key);
|
|
2047
|
-
if (expected && expected.schema !== SEED_SCHEMA) throw new Error("Incompatible seed checkpoint; recreate storage");
|
|
2048
|
-
let meta = expected ?? emptySeedMeta(key);
|
|
2049
|
-
if (manifest.version < meta.seedVersion) throw new Error("Seed manifest is older than stored checkpoint");
|
|
2050
|
-
const summary = { sourceId: source, version: manifest.version, notes: 0, efforts: 0, blocks: 0, blockEfforts: 0, deleted: 0, skippedUserOwned: 0 };
|
|
2051
|
-
const metadataChanged = manifest.version !== meta.seedVersion || manifest.builtAt !== meta.builtAt;
|
|
2052
|
-
const plan = manifest.chunks.filter((c) => metadataChanged || meta.chunks[c.id]?.sha256 !== c.sha256);
|
|
2053
|
-
plan.sort((a, b) => Number(b.id === CANVAS_CHUNK_ID) - Number(a.id === CANVAS_CHUNK_ID));
|
|
2054
|
-
const accumulate = (result) => {
|
|
2055
|
-
summary.notes += result.totalNotes;
|
|
2056
|
-
summary.efforts += result.totalEfforts;
|
|
2057
|
-
summary.blocks += result.totalBlocks;
|
|
2058
|
-
summary.deleted += result.deleted;
|
|
2059
|
-
summary.skippedUserOwned += result.skippedUserOwned;
|
|
2060
|
-
};
|
|
2061
|
-
for (const chunk of plan) {
|
|
2062
|
-
const rows = assertSeedRows(JSON.parse(fs.readFileSync(path.resolve(dir, chunk.path), "utf-8")));
|
|
2063
|
-
if (rows.length !== chunk.count) throw new Error("Seed chunk count mismatch");
|
|
2064
|
-
const next = { ...meta, chunks: { ...meta.chunks, [chunk.id]: { sha256: chunk.sha256, noteIds: await Promise.all(rows.map((r) => seedNoteId(r.path))) } }, importedAt: Date.now() };
|
|
2065
|
-
accumulate(await applySeedChunk(db.storage, { chunkId: chunk.id, rows, manifest, expectedMeta: expected, meta: next }));
|
|
2066
|
-
summary.blockEfforts += rows.reduce((count, row) => count + row.blockEfforts.length, 0);
|
|
2067
|
-
meta = next;
|
|
2068
|
-
expected = next;
|
|
2069
|
-
}
|
|
2070
|
-
for (const chunkId of Object.keys(meta.chunks)) {
|
|
2071
|
-
if (manifest.chunks.some((c) => c.id === chunkId)) continue;
|
|
2072
|
-
const chunks = { ...meta.chunks };
|
|
2073
|
-
delete chunks[chunkId];
|
|
2074
|
-
const next = { ...meta, chunks, importedAt: Date.now() };
|
|
2075
|
-
accumulate(await applySeedChunk(db.storage, { chunkId, rows: [], manifest, expectedMeta: expected, meta: next }));
|
|
2076
|
-
meta = next;
|
|
2077
|
-
expected = next;
|
|
2078
|
-
}
|
|
2079
|
-
if (plan.length || metadataChanged || Object.keys(meta.chunks).length !== Object.keys(expected?.chunks ?? {}).length) {
|
|
2080
|
-
const next = { ...meta, schema: SEED_SCHEMA, seedVersion: manifest.version, builtAt: manifest.builtAt, importedAt: Date.now() };
|
|
2081
|
-
await applySeedChunk(db.storage, { chunkId: null, rows: [], manifest, expectedMeta: expected, meta: next });
|
|
2082
|
-
}
|
|
2083
|
-
return summary;
|
|
2084
|
-
}
|
|
2085
|
-
function defaultSeedDirs(cwd) {
|
|
2086
|
-
const base = nodePath().resolve(cwd, "apps", "playground", "public", "seed");
|
|
2087
|
-
return ["core", "community"].map((id) => nodePath().resolve(base, id)).filter((dir) => nodeFs().existsSync(nodePath().resolve(dir, "manifest.json")));
|
|
2088
|
-
}
|
|
2089
|
-
function getFs2() {
|
|
2090
|
-
if (typeof globalThis.process?.versions?.node === "undefined" && typeof globalThis.Bun === "undefined") {
|
|
2091
|
-
throw new Error("File system operations are only supported in Node / Bun environments");
|
|
2092
|
-
}
|
|
2093
|
-
return __require("fs");
|
|
2094
|
-
}
|
|
2095
|
-
var VERSION = "0.6.0";
|
|
2096
|
-
function cliNoteSource(source, label, sport) {
|
|
2097
|
-
if (/\.(?:md|markdown)$/i.test(label) || parseDocumentSections(source).some((section) => section.scriptBlock)) return source;
|
|
2098
|
-
let length = 3;
|
|
2099
|
-
for (const match of source.matchAll(/`+/g)) length = Math.max(length, match[0].length + 1);
|
|
2100
|
-
const fence = "`".repeat(length);
|
|
2101
|
-
return `${fence}time${sport ? `:${sport}` : ""}
|
|
2102
|
-
${source}
|
|
2103
|
-
${fence}
|
|
2104
|
-
`;
|
|
2105
|
-
}
|
|
2106
|
-
function parseCliArgs(args) {
|
|
2107
|
-
const result = {
|
|
2108
|
-
format: "json",
|
|
2109
|
-
packs: [],
|
|
2110
|
-
sources: []
|
|
2111
|
-
};
|
|
2112
|
-
let i = 0;
|
|
2113
|
-
while (i < args.length) {
|
|
2114
|
-
const arg = args[i];
|
|
2115
|
-
if (arg === "--at" || arg.startsWith("--at=")) throw new Error("Unknown option --at; whole Note exports have no as-of version");
|
|
2116
|
-
if (arg === "--help" || arg === "-h") {
|
|
2117
|
-
result.help = true;
|
|
2118
|
-
i++;
|
|
2119
|
-
} else if (arg === "--version" || arg === "-v") {
|
|
2120
|
-
result.version = true;
|
|
2121
|
-
i++;
|
|
2122
|
-
} else if (arg === "--corpus" || arg === "-c") {
|
|
2123
|
-
result.corpus = args[++i];
|
|
2124
|
-
i++;
|
|
2125
|
-
} else if (arg === "--stdin-log") {
|
|
2126
|
-
result.stdinLog = true;
|
|
2127
|
-
i++;
|
|
2128
|
-
} else if (arg === "--stdin-facts") {
|
|
2129
|
-
result.stdinFacts = true;
|
|
2130
|
-
i++;
|
|
2131
|
-
} else if (arg === "--format") {
|
|
2132
|
-
const fmt = args[++i];
|
|
2133
|
-
if (fmt === "json" || fmt === "table" || fmt === "csv") {
|
|
2134
|
-
result.format = fmt;
|
|
2135
|
-
}
|
|
2136
|
-
i++;
|
|
2137
|
-
} else if (arg.startsWith("--format=")) {
|
|
2138
|
-
const fmt = arg.slice("--format=".length);
|
|
2139
|
-
if (fmt === "json" || fmt === "table" || fmt === "csv") {
|
|
2140
|
-
result.format = fmt;
|
|
2141
|
-
}
|
|
2142
|
-
i++;
|
|
2143
|
-
} else if (arg === "-o" || arg === "--output") {
|
|
2144
|
-
result.output = args[++i];
|
|
2145
|
-
i++;
|
|
2146
|
-
} else if (arg.startsWith("--output=")) {
|
|
2147
|
-
result.output = arg.slice("--output=".length);
|
|
2148
|
-
i++;
|
|
2149
|
-
} else if (arg === "--pack") {
|
|
2150
|
-
const packSpec = args[++i];
|
|
2151
|
-
if (packSpec) result.packs.push(packSpec);
|
|
2152
|
-
i++;
|
|
2153
|
-
} else if (arg.startsWith("--pack=")) {
|
|
2154
|
-
result.packs.push(arg.slice("--pack=".length));
|
|
2155
|
-
i++;
|
|
2156
|
-
} else if (arg === "--sport") {
|
|
2157
|
-
result.sport = args[++i];
|
|
2158
|
-
i++;
|
|
2159
|
-
} else if (arg === "--unit") {
|
|
2160
|
-
result.unit = args[++i];
|
|
2161
|
-
i++;
|
|
2162
|
-
} else if (arg === "--db") {
|
|
2163
|
-
result.db = args[++i];
|
|
2164
|
-
i++;
|
|
2165
|
-
} else if (arg.startsWith("--db=")) {
|
|
2166
|
-
result.db = arg.slice("--db=".length);
|
|
2167
|
-
i++;
|
|
2168
|
-
} else if (arg === "--debug") {
|
|
2169
|
-
result.debug = "";
|
|
2170
|
-
i++;
|
|
2171
|
-
} else if (arg.startsWith("--debug=")) {
|
|
2172
|
-
result.debug = arg.slice("--debug=".length);
|
|
2173
|
-
i++;
|
|
2174
|
-
} else if (arg === "--source") {
|
|
2175
|
-
const source = args[++i];
|
|
2176
|
-
if (source) result.sources.push(source);
|
|
2177
|
-
i++;
|
|
2178
|
-
} else if (arg.startsWith("--source=")) {
|
|
2179
|
-
result.sources.push(arg.slice("--source=".length));
|
|
2180
|
-
i++;
|
|
2181
|
-
} else if (arg === "--scaffold") {
|
|
2182
|
-
result.scaffold = true;
|
|
2183
|
-
i++;
|
|
2184
|
-
} else if (arg === "--template") {
|
|
2185
|
-
result.template = args[++i];
|
|
2186
|
-
i++;
|
|
2187
|
-
} else if (arg.startsWith("--template=")) {
|
|
2188
|
-
result.template = arg.slice("--template=".length);
|
|
2189
|
-
i++;
|
|
2190
|
-
} else if (arg === "--out") {
|
|
2191
|
-
result.output = args[++i];
|
|
2192
|
-
i++;
|
|
2193
|
-
} else if (arg.startsWith("--out=")) {
|
|
2194
|
-
result.output = arg.slice("--out=".length);
|
|
2195
|
-
i++;
|
|
2196
|
-
} else if (arg === "--dir") {
|
|
2197
|
-
result.dir = args[++i];
|
|
2198
|
-
i++;
|
|
2199
|
-
} else if (arg.startsWith("--dir=")) {
|
|
2200
|
-
result.dir = arg.slice("--dir=".length);
|
|
2201
|
-
i++;
|
|
2202
|
-
} else if (!result.subcommand) {
|
|
2203
|
-
if (arg === "parse" || arg === "run" || arg === "query" || arg === "download" || arg === "import" || arg === "build" || arg === "seed" || arg === "reset" || arg === "help" || arg === "version") {
|
|
2204
|
-
result.subcommand = arg;
|
|
2205
|
-
} else {
|
|
2206
|
-
result.target = arg;
|
|
2207
|
-
}
|
|
2208
|
-
i++;
|
|
2209
|
-
} else if (!result.target) {
|
|
2210
|
-
result.target = arg;
|
|
2211
|
-
i++;
|
|
2212
|
-
} else if (!result.target2) {
|
|
2213
|
-
result.target2 = arg;
|
|
2214
|
-
i++;
|
|
2215
|
-
} else {
|
|
2216
|
-
i++;
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
2219
|
-
return result;
|
|
2220
|
-
}
|
|
2221
|
-
function readBunStdinText() {
|
|
2222
|
-
const g = globalThis;
|
|
2223
|
-
if (g && typeof g === "object" && "Bun" in g) {
|
|
2224
|
-
const bun = g.Bun;
|
|
2225
|
-
if (bun && typeof bun === "object" && "stdin" in bun) {
|
|
2226
|
-
const stdin = bun.stdin;
|
|
2227
|
-
if (stdin && typeof stdin === "object" && "text" in stdin) {
|
|
2228
|
-
const text = stdin.text;
|
|
2229
|
-
if (typeof text === "function") return text;
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
}
|
|
2233
|
-
return void 0;
|
|
2234
|
-
}
|
|
2235
|
-
async function readStdin() {
|
|
2236
|
-
const bunText = readBunStdinText();
|
|
2237
|
-
if (bunText) return bunText();
|
|
2238
|
-
return new Promise((resolve, reject) => {
|
|
2239
|
-
const chunks = [];
|
|
2240
|
-
process.stdin.on("data", (chunk) => {
|
|
2241
|
-
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
2242
|
-
});
|
|
2243
|
-
process.stdin.on("end", () => {
|
|
2244
|
-
resolve(Buffer.concat(chunks).toString("utf-8"));
|
|
2245
|
-
});
|
|
2246
|
-
process.stdin.on("error", (err) => {
|
|
2247
|
-
reject(err);
|
|
2248
|
-
});
|
|
2249
|
-
});
|
|
2250
|
-
}
|
|
2251
|
-
function getUsageText() {
|
|
2252
|
-
return `
|
|
2253
|
-
wod CLI \u2014 Headless Whiteboard Language & WQL Engine runner (v${VERSION})
|
|
2254
|
-
|
|
2255
|
-
USAGE:
|
|
2256
|
-
wod parse <file | -> [options]
|
|
2257
|
-
wod run <file | -> [options]
|
|
2258
|
-
wod query '<wql-string>' [options]
|
|
2259
|
-
wod download <note|session|effort|equipment> <id> [options]
|
|
2260
|
-
wod import <note|session|effort|equipment> <file> [options]
|
|
2261
|
-
wod build [dir] [options]
|
|
2262
|
-
wod seed [options]
|
|
2263
|
-
wod reset [options]
|
|
2264
|
-
|
|
2265
|
-
SUBCOMMANDS:
|
|
2266
|
-
parse <file | -> Parse Whiteboard Script, create a note in the database
|
|
2267
|
-
(whole source + saved revisions + sparse block index), optionally emit parse-tree IR.
|
|
2268
|
-
Markdown carrying a 'wod-note-id' frontmatter stamp
|
|
2269
|
-
(see download) imports back into that same note.
|
|
2270
|
-
run <file | -> Execute the script state machine, record a session +
|
|
2271
|
-
event/summary rows in the database, optionally emit
|
|
2272
|
-
execution-log IR
|
|
2273
|
-
query '<wql>' Evaluate WQL against the database (or an explicit
|
|
2274
|
-
dataset) and emit QueryResult / RowsQueryResult IR
|
|
2275
|
-
download note <id> Export the note's authored markdown to <id>.md.
|
|
2276
|
-
The frontmatter identity stamp allows re-import.
|
|
2277
|
-
Frontmatter tags (general + typed equipment/quality/\u2026)
|
|
2278
|
-
are materialized into the tag graph on parse/import.
|
|
2279
|
-
download session <id>
|
|
2280
|
-
Write the session's generated metrics (event + summary
|
|
2281
|
-
rows, one CSV row per metric) to <id>.csv. Import back
|
|
2282
|
-
with 'wod import session'.
|
|
2283
|
-
download effort <slug>
|
|
2284
|
-
Write the effort registry row as corpus-style markdown
|
|
2285
|
-
(frontmatter id/slug/label/aliases/baseAttributes + body)
|
|
2286
|
-
to <slug>.md. Import back with 'wod import effort'.
|
|
2287
|
-
download equipment
|
|
2288
|
-
Write every equipment tag as a one-column CSV to
|
|
2289
|
-
equipment.csv. Import back with 'wod import equipment'.
|
|
2290
|
-
import note <file> Re-import exported note markdown (same as parse with
|
|
2291
|
-
the stamped identity).
|
|
2292
|
-
import session <file>
|
|
2293
|
-
Import a session + its events from a downloaded CSV.
|
|
2294
|
-
The exact saved block revision must already exist.
|
|
2295
|
-
import effort <file>
|
|
2296
|
-
Upsert an effort registry row from corpus-style markdown.
|
|
2297
|
-
import equipment <file>
|
|
2298
|
-
Create/upgrade equipment tags (CSV, one label per row).
|
|
2299
|
-
build [dir] Compile a seed corpus: validate every .md under
|
|
2300
|
-
<dir>/markdown (parity gate + dashboard format gate) and
|
|
2301
|
-
write manifest.json + content-hashed chunks/ +
|
|
2302
|
-
report.html to <dir>/dist with source/maps/projections bundled
|
|
2303
|
-
(override with -o/--output). Version stamps derive from
|
|
2304
|
-
<dir>'s git history (full checkout required).
|
|
2305
|
-
|
|
2306
|
-
BUILD OPTIONS (for 'build'):
|
|
2307
|
-
--scaffold Also scaffold markdown/efforts/discovered/*.md stubs for
|
|
2308
|
-
workout effort labels missing from the registry.
|
|
2309
|
-
--template <file> Compile an extra markdown file as the 'template' chunk
|
|
2310
|
-
(the host's playground intake template).
|
|
2311
|
-
|
|
2312
|
-
INPUT SOURCING (for 'query'; without these, the database is queried):
|
|
2313
|
-
-c, --corpus <path> Load historical corpus dataset from a versioned JSON file
|
|
2314
|
-
--stdin-log Read kind: 'execution-log' JSON payload from stdin
|
|
2315
|
-
--stdin-facts Read raw kind: 'fact-set' JSON payload from stdin
|
|
2316
|
-
|
|
2317
|
-
DATABASE (parse / run / query / download / import / seed / reset):
|
|
2318
|
-
--db <path> SQLite database file (default: $WOD_WIKI_DB or
|
|
2319
|
-
./wodwiki.db). Created on demand by parse/run/seed.
|
|
2320
|
-
|
|
2321
|
-
SEED SOURCING (for 'seed'):
|
|
2322
|
-
--source <dir> Extra compiled seed directory (manifest.json + chunks);
|
|
2323
|
-
repeatable. Defaults to the bundled core + community
|
|
2324
|
-
artifacts under apps/playground/public/seed.
|
|
2325
|
-
|
|
2326
|
-
OPTIONS:
|
|
2327
|
-
--format <fmt> Output format: json (default), table, csv
|
|
2328
|
-
-o, --output <path> Write the IR payload (or download) to a path instead of
|
|
2329
|
-
the default; '-' writes to stdout
|
|
2330
|
-
--debug[=name] Also write the IR payload to <name> (default
|
|
2331
|
-
wod-<subcommand>-debug.json); without it, parse/run
|
|
2332
|
-
print a one-line summary of what was persisted
|
|
2333
|
-
--pack <module> Dynamically load and register a Language Pack (repeatable)
|
|
2334
|
-
--sport <sport> Target sport fence dialect (e.g. climbing, crossfit)
|
|
2335
|
-
--unit <unit> Preferred display unit for queries (e.g. lb, kg)
|
|
2336
|
-
-h, --help Show help message
|
|
2337
|
-
-v, --version Show version number
|
|
2338
|
-
|
|
2339
|
-
EXIT CODES:
|
|
2340
|
-
0 - OK / Success
|
|
2341
|
-
1 - General / Runtime error
|
|
2342
|
-
2 - Parse / Syntax error
|
|
2343
|
-
3 - Language Pack load error
|
|
2344
|
-
`;
|
|
2345
|
-
}
|
|
2346
|
-
function printUsage(writer) {
|
|
2347
|
-
const text = getUsageText();
|
|
2348
|
-
if (writer) {
|
|
2349
|
-
writer(text);
|
|
2350
|
-
} else {
|
|
2351
|
-
process.stdout.write(text.endsWith("\n") ? text : text + "\n");
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
2355
|
-
const writeOut = io.stdout ?? ((t) => process.stdout.write(t + "\n"));
|
|
2356
|
-
const writeErr = io.stderr ?? ((t) => process.stderr.write(t + "\n"));
|
|
2357
|
-
const getStdin = io.readStdinFn ?? readStdin;
|
|
2358
|
-
let parsed;
|
|
2359
|
-
try {
|
|
2360
|
-
parsed = parseCliArgs(argv);
|
|
2361
|
-
} catch (error) {
|
|
2362
|
-
writeErr(error instanceof Error ? error.message : String(error));
|
|
2363
|
-
return 1;
|
|
2364
|
-
}
|
|
2365
|
-
if (parsed.version || parsed.subcommand === "version") {
|
|
2366
|
-
writeOut(`wod v${VERSION}`);
|
|
2367
|
-
return 0;
|
|
2368
|
-
}
|
|
2369
|
-
if (parsed.help || parsed.subcommand === "help") {
|
|
2370
|
-
printUsage(writeOut);
|
|
2371
|
-
return 0;
|
|
2372
|
-
}
|
|
2373
|
-
if (!parsed.subcommand) {
|
|
2374
|
-
printUsage(writeErr);
|
|
2375
|
-
return 1;
|
|
2376
|
-
}
|
|
2377
|
-
for (const packSpec of parsed.packs) {
|
|
2378
|
-
try {
|
|
2379
|
-
await loadLanguagePack(packSpec);
|
|
2380
|
-
} catch (err) {
|
|
2381
|
-
if (err instanceof PackLoadError) {
|
|
2382
|
-
writeErr(err.message);
|
|
2383
|
-
} else {
|
|
2384
|
-
writeErr(`[wod] Language pack load error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2385
|
-
}
|
|
2386
|
-
return 3;
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
const deliverOutput = (formattedText) => {
|
|
2390
|
-
if (parsed.output && parsed.output !== "-") {
|
|
2391
|
-
getFs2().writeFileSync(parsed.output, formattedText, "utf-8");
|
|
2392
|
-
} else {
|
|
2393
|
-
writeOut(formattedText);
|
|
2394
|
-
}
|
|
2395
|
-
};
|
|
2396
|
-
const debugTarget = (subcommand) => parsed.debug === void 0 ? void 0 : parsed.debug || `wod-${subcommand}-debug.json`;
|
|
2397
|
-
const writeDebugFile = (subcommand, ir) => {
|
|
2398
|
-
const target = debugTarget(subcommand);
|
|
2399
|
-
if (target !== void 0) {
|
|
2400
|
-
getFs2().writeFileSync(target, JSON.stringify(ir, null, 2) + "\n", "utf-8");
|
|
2401
|
-
}
|
|
2402
|
-
};
|
|
2403
|
-
try {
|
|
2404
|
-
if (parsed.subcommand === "parse") {
|
|
2405
|
-
if (!parsed.target) {
|
|
2406
|
-
writeErr('[wod parse] Error: Missing input file or "-" for stdin');
|
|
2407
|
-
return 1;
|
|
2408
|
-
}
|
|
2409
|
-
let source;
|
|
2410
|
-
if (parsed.target === "-") {
|
|
2411
|
-
source = await getStdin();
|
|
2412
|
-
} else {
|
|
2413
|
-
const fs = getFs2();
|
|
2414
|
-
if (!fs.existsSync(parsed.target)) {
|
|
2415
|
-
writeErr(`[wod parse] File not found: ${parsed.target}`);
|
|
2416
|
-
return 1;
|
|
2417
|
-
}
|
|
2418
|
-
source = fs.readFileSync(parsed.target, "utf-8");
|
|
2419
|
-
}
|
|
2420
|
-
const noteSource = cliNoteSource(source, parsed.target, parsed.sport);
|
|
2421
|
-
const ir = runParse(source, { sport: parsed.sport, sourceLabel: `cli:wod parse ${parsed.target}` });
|
|
2422
|
-
const identity = extractNoteIdentity(source);
|
|
2423
|
-
const tags = extractTypedTags(source);
|
|
2424
|
-
const db = await openCliDb(parsed.db);
|
|
2425
|
-
try {
|
|
2426
|
-
const persisted = await persistNote(db, {
|
|
2427
|
-
source: noteSource,
|
|
2428
|
-
sourceLabel: parsed.target,
|
|
2429
|
-
noteId: identity?.noteId,
|
|
2430
|
-
title: identity?.title,
|
|
2431
|
-
tags
|
|
2432
|
-
});
|
|
2433
|
-
writeOut(
|
|
2434
|
-
`[wod parse] note ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.blocks} blocks, ${persisted.efforts} efforts${persisted.tags > 0 ? `, ${persisted.tags} tags` : ""} \u2192 ${db.path}`
|
|
2435
|
-
);
|
|
2436
|
-
} finally {
|
|
2437
|
-
await db.close();
|
|
2438
|
-
}
|
|
2439
|
-
if (parsed.output) {
|
|
2440
|
-
const pretty = Boolean(process.stdout.isTTY);
|
|
2441
|
-
deliverOutput(formatParseOutput(ir, parsed.format, pretty));
|
|
2442
|
-
}
|
|
2443
|
-
writeDebugFile("parse", ir);
|
|
2444
|
-
return 0;
|
|
2445
|
-
}
|
|
2446
|
-
if (parsed.subcommand === "run") {
|
|
2447
|
-
if (!parsed.target) {
|
|
2448
|
-
writeErr('[wod run] Error: Missing input file or "-" for stdin');
|
|
2449
|
-
return 1;
|
|
2450
|
-
}
|
|
2451
|
-
let source;
|
|
2452
|
-
if (parsed.target === "-") {
|
|
2453
|
-
source = await getStdin();
|
|
2454
|
-
} else {
|
|
2455
|
-
const fs = getFs2();
|
|
2456
|
-
if (!fs.existsSync(parsed.target)) {
|
|
2457
|
-
writeErr(`[wod run] File not found: ${parsed.target}`);
|
|
2458
|
-
return 1;
|
|
2459
|
-
}
|
|
2460
|
-
source = fs.readFileSync(parsed.target, "utf-8");
|
|
2461
|
-
}
|
|
2462
|
-
const identity = extractNoteIdentity(source);
|
|
2463
|
-
const db = await openCliDb(parsed.db);
|
|
2464
|
-
let ir;
|
|
2465
|
-
try {
|
|
2466
|
-
const started = await startRun(db, {
|
|
2467
|
-
source: cliNoteSource(source, parsed.target, parsed.sport),
|
|
2468
|
-
sourceLabel: parsed.target,
|
|
2469
|
-
noteId: identity?.noteId,
|
|
2470
|
-
title: identity?.title,
|
|
2471
|
-
tags: extractTypedTags(source)
|
|
2472
|
-
});
|
|
2473
|
-
const section = parseDocumentSections(started.revision.rawContent).find((row) => row.scriptBlock);
|
|
2474
|
-
if (!section?.scriptBlock) throw new Error("[wod run] Captured workout source cannot be parsed");
|
|
2475
|
-
const bare = !/\.(?:md|markdown)$/i.test(parsed.target) && !parseDocumentSections(source).some((row) => row.scriptBlock);
|
|
2476
|
-
const frozenBody = bare ? section.scriptBlock.content.slice(0, -1) : section.scriptBlock.content;
|
|
2477
|
-
ir = await runExecution(frozenBody, {
|
|
2478
|
-
sport: started.revision.sport,
|
|
2479
|
-
dialect: started.revision.dialect,
|
|
2480
|
-
blockId: started.session.blockId,
|
|
2481
|
-
blockVersion: started.session.blockVersion,
|
|
2482
|
-
startTime: started.session.startTime,
|
|
2483
|
-
sourceLabel: `cli:wod run ${parsed.target}`
|
|
2484
|
-
});
|
|
2485
|
-
const persisted = await persistRun(db, started, ir.data);
|
|
2486
|
-
writeOut(
|
|
2487
|
-
`[wod run] session ${persisted.resultId} \u2014 ${persisted.events} events, ${persisted.summaries} summaries on note ${persisted.noteId} \u2192 ${db.path}`
|
|
2488
|
-
);
|
|
2489
|
-
} finally {
|
|
2490
|
-
await db.close();
|
|
2491
|
-
}
|
|
2492
|
-
if (parsed.output) {
|
|
2493
|
-
const pretty = Boolean(process.stdout.isTTY);
|
|
2494
|
-
deliverOutput(formatExecutionOutput(ir, parsed.format, pretty));
|
|
2495
|
-
}
|
|
2496
|
-
writeDebugFile("run", ir);
|
|
2497
|
-
return 0;
|
|
2498
|
-
}
|
|
2499
|
-
if (parsed.subcommand === "query") {
|
|
2500
|
-
if (!parsed.target) {
|
|
2501
|
-
writeErr("[wod query] Error: Missing WQL query argument");
|
|
2502
|
-
return 1;
|
|
2503
|
-
}
|
|
2504
|
-
const useDb = !parsed.corpus && !parsed.stdinLog && !parsed.stdinFacts;
|
|
2505
|
-
let stdinLogContent;
|
|
2506
|
-
let stdinFactsContent;
|
|
2507
|
-
if (parsed.stdinLog) {
|
|
2508
|
-
stdinLogContent = await getStdin();
|
|
2509
|
-
} else if (parsed.stdinFacts) {
|
|
2510
|
-
stdinFactsContent = await getStdin();
|
|
2511
|
-
}
|
|
2512
|
-
let ir;
|
|
2513
|
-
if (useDb) {
|
|
2514
|
-
const dbPath = resolveDbPath(parsed.db);
|
|
2515
|
-
if (!nodeFs().existsSync(dbPath)) {
|
|
2516
|
-
writeErr(
|
|
2517
|
-
`[wod query] No input dataset and no database at ${dbPath}. Specify --corpus <file>, --stdin-log, --stdin-facts, or run \`wod seed\` first.`
|
|
2518
|
-
);
|
|
2519
|
-
return 1;
|
|
2520
|
-
}
|
|
2521
|
-
const db = await openCliDb(parsed.db);
|
|
2522
|
-
try {
|
|
2523
|
-
ir = await runQueryCli(parsed.target, {
|
|
2524
|
-
preferredUnit: parsed.unit,
|
|
2525
|
-
sourceLabel: `cli:wod query "${parsed.target}"`,
|
|
2526
|
-
stores: {
|
|
2527
|
-
eventStore: sqliteEventStore(db.db),
|
|
2528
|
-
noteStore: sqliteNoteStore(db.db),
|
|
2529
|
-
blockStore: sqliteBlockStore(db.db),
|
|
2530
|
-
effortStore: sqliteEffortStore(db.db),
|
|
2531
|
-
blockEffortsStore: sqliteIndexStore(db.db, "block_efforts"),
|
|
2532
|
-
tagsStore: sqliteIndexStore(db.db, "tags"),
|
|
2533
|
-
noteTagsStore: sqliteIndexStore(db.db, "note_tags")
|
|
2534
|
-
}
|
|
2535
|
-
});
|
|
2536
|
-
} finally {
|
|
2537
|
-
await db.close();
|
|
2538
|
-
}
|
|
2539
|
-
} else {
|
|
2540
|
-
if (!parsed.corpus && !stdinLogContent && !stdinFactsContent) {
|
|
2541
|
-
writeErr("[wod query] Error: Query requires input data. Specify --corpus <file>, --stdin-log, or --stdin-facts.");
|
|
2542
|
-
return 1;
|
|
2543
|
-
}
|
|
2544
|
-
ir = await runQueryCli(parsed.target, {
|
|
2545
|
-
corpusPath: parsed.corpus,
|
|
2546
|
-
stdinLog: stdinLogContent,
|
|
2547
|
-
stdinFacts: stdinFactsContent,
|
|
2548
|
-
preferredUnit: parsed.unit,
|
|
2549
|
-
sourceLabel: `cli:wod query "${parsed.target}"`
|
|
2550
|
-
});
|
|
2551
|
-
}
|
|
2552
|
-
const pretty = !parsed.output && Boolean(process.stdout.isTTY);
|
|
2553
|
-
const formatted = formatQueryOutput(ir, parsed.format, pretty);
|
|
2554
|
-
deliverOutput(formatted);
|
|
2555
|
-
writeDebugFile("query", ir);
|
|
2556
|
-
return 0;
|
|
2557
|
-
}
|
|
2558
|
-
if (parsed.subcommand === "download") {
|
|
2559
|
-
const kind = parsed.target;
|
|
2560
|
-
const id = parsed.target2;
|
|
2561
|
-
const DOWNLOAD_KINDS = ["note", "session", "effort", "equipment"];
|
|
2562
|
-
if (!id && kind !== "equipment" || !DOWNLOAD_KINDS.includes(kind ?? "")) {
|
|
2563
|
-
writeErr("[wod download] Usage: wod download <note|session|effort|equipment> <id> [-o <path>]");
|
|
2564
|
-
return 1;
|
|
2565
|
-
}
|
|
2566
|
-
const targetId = id;
|
|
2567
|
-
const db = await openCliDb(parsed.db);
|
|
2568
|
-
try {
|
|
2569
|
-
if (kind === "note") {
|
|
2570
|
-
const bundle = await getNoteBundle(db.db, targetId);
|
|
2571
|
-
if (!bundle.note) {
|
|
2572
|
-
writeErr(`[wod download] Note not found in ${db.path}: ${id}`);
|
|
2573
|
-
return 1;
|
|
2574
|
-
}
|
|
2575
|
-
const markdown = composeNoteMarkdown(bundle.note);
|
|
2576
|
-
const target = parsed.output && parsed.output !== "-" ? parsed.output : `${id}.md`;
|
|
2577
|
-
if (parsed.output === "-") {
|
|
2578
|
-
writeOut(markdown);
|
|
2579
|
-
} else {
|
|
2580
|
-
nodeFs().writeFileSync(target, markdown, "utf-8");
|
|
2581
|
-
}
|
|
2582
|
-
if (parsed.output !== "-") writeOut(`[wod download] note ${id} \u2192 ${target}`);
|
|
2583
|
-
} else if (kind === "session") {
|
|
2584
|
-
const bundle = await getSessionBundle(db.db, targetId);
|
|
2585
|
-
if (!bundle) {
|
|
2586
|
-
writeErr(`[wod download] Session not found in ${db.path}: ${id}`);
|
|
2587
|
-
return 1;
|
|
2588
|
-
}
|
|
2589
|
-
const csv = sessionToCsv(bundle.session, bundle.events);
|
|
2590
|
-
const target = parsed.output && parsed.output !== "-" ? parsed.output : `${id}.csv`;
|
|
2591
|
-
if (parsed.output === "-") {
|
|
2592
|
-
writeOut(csv);
|
|
2593
|
-
} else {
|
|
2594
|
-
nodeFs().writeFileSync(target, csv, "utf-8");
|
|
2595
|
-
}
|
|
2596
|
-
if (parsed.output !== "-") writeOut(`[wod download] session ${id} \u2192 ${target} (${bundle.events.length} events)`);
|
|
2597
|
-
} else if (kind === "effort") {
|
|
2598
|
-
const effort = await getEffortBySlug(db.db, targetId);
|
|
2599
|
-
if (!effort) {
|
|
2600
|
-
writeErr(`[wod download] Effort not found in ${db.path}: ${id}`);
|
|
2601
|
-
return 1;
|
|
2602
|
-
}
|
|
2603
|
-
const markdown = effortToMarkdown(effort);
|
|
2604
|
-
const target = parsed.output && parsed.output !== "-" ? parsed.output : `${id}.md`;
|
|
2605
|
-
if (parsed.output === "-") {
|
|
2606
|
-
writeOut(markdown);
|
|
2607
|
-
} else {
|
|
2608
|
-
nodeFs().writeFileSync(target, markdown, "utf-8");
|
|
2609
|
-
}
|
|
2610
|
-
writeOut(`[wod download] effort ${id} \u2192 ${target}`);
|
|
2611
|
-
} else {
|
|
2612
|
-
const labels = (await listTagsByType(db.db, "equipment")).map((t) => t.label).sort();
|
|
2613
|
-
const csv = equipmentToCsv(labels);
|
|
2614
|
-
const target = parsed.output && parsed.output !== "-" ? parsed.output : "equipment.csv";
|
|
2615
|
-
if (parsed.output === "-") {
|
|
2616
|
-
writeOut(csv);
|
|
2617
|
-
} else {
|
|
2618
|
-
nodeFs().writeFileSync(target, csv, "utf-8");
|
|
2619
|
-
}
|
|
2620
|
-
writeOut(`[wod download] equipment \u2192 ${target} (${labels.length} labels)`);
|
|
2621
|
-
}
|
|
2622
|
-
} finally {
|
|
2623
|
-
await db.close();
|
|
2624
|
-
}
|
|
2625
|
-
return 0;
|
|
2626
|
-
}
|
|
2627
|
-
if (parsed.subcommand === "import") {
|
|
2628
|
-
const kind = parsed.target;
|
|
2629
|
-
const file = parsed.target2;
|
|
2630
|
-
const IMPORT_KINDS = ["note", "session", "effort", "equipment"];
|
|
2631
|
-
if (!file || !IMPORT_KINDS.includes(kind ?? "")) {
|
|
2632
|
-
writeErr("[wod import] Usage: wod import <note|session|effort|equipment> <file>");
|
|
2633
|
-
return 1;
|
|
2634
|
-
}
|
|
2635
|
-
const fs = nodeFs();
|
|
2636
|
-
if (!fs.existsSync(file)) {
|
|
2637
|
-
writeErr(`[wod import] File not found: ${file}`);
|
|
2638
|
-
return 1;
|
|
2639
|
-
}
|
|
2640
|
-
if (kind === "note") {
|
|
2641
|
-
const source = fs.readFileSync(file, "utf-8");
|
|
2642
|
-
const identity = extractNoteIdentity(source);
|
|
2643
|
-
const tags = extractTypedTags(source);
|
|
2644
|
-
const db2 = await openCliDb(parsed.db);
|
|
2645
|
-
try {
|
|
2646
|
-
const persisted = await persistNote(db2, {
|
|
2647
|
-
source,
|
|
2648
|
-
sourceLabel: file,
|
|
2649
|
-
noteId: identity?.noteId,
|
|
2650
|
-
title: identity?.title,
|
|
2651
|
-
tags
|
|
2652
|
-
});
|
|
2653
|
-
writeOut(
|
|
2654
|
-
`[wod import] note ${persisted.noteId}${persisted.created ? "" : " (updated)"} \u2014 ${persisted.blocks} blocks${persisted.tags > 0 ? `, ${persisted.tags} tags` : ""} \u2192 ${db2.path}`
|
|
2655
|
-
);
|
|
2656
|
-
} finally {
|
|
2657
|
-
await db2.close();
|
|
2658
|
-
}
|
|
2659
|
-
return 0;
|
|
2660
|
-
}
|
|
2661
|
-
if (kind === "effort") {
|
|
2662
|
-
const effort = markdownToEffort(fs.readFileSync(file, "utf-8"));
|
|
2663
|
-
if (!effort) {
|
|
2664
|
-
writeErr(`[wod import] Unparseable effort file (need frontmatter id/slug/label + numeric baseAttributes.met): ${file}`);
|
|
2665
|
-
return 1;
|
|
2666
|
-
}
|
|
2667
|
-
const db2 = await openCliDb(parsed.db);
|
|
2668
|
-
try {
|
|
2669
|
-
await persistEffort(db2, { ...effort, registrySource: "user" });
|
|
2670
|
-
writeOut(`[wod import] effort ${effort.slug} \u2014 ${effort.label} \u2192 ${db2.path}`);
|
|
2671
|
-
} finally {
|
|
2672
|
-
await db2.close();
|
|
2673
|
-
}
|
|
2674
|
-
return 0;
|
|
2675
|
-
}
|
|
2676
|
-
if (kind === "equipment") {
|
|
2677
|
-
const labels = csvToEquipmentLabels(fs.readFileSync(file, "utf-8"));
|
|
2678
|
-
if (labels.length === 0) {
|
|
2679
|
-
writeErr(`[wod import] No equipment labels found in ${file}`);
|
|
2680
|
-
return 1;
|
|
2681
|
-
}
|
|
2682
|
-
const db2 = await openCliDb(parsed.db);
|
|
2683
|
-
try {
|
|
2684
|
-
const added = await persistTypedTags(db2, labels, "equipment");
|
|
2685
|
-
writeOut(`[wod import] equipment \u2014 ${labels.length} labels (${added} new) \u2192 ${db2.path}`);
|
|
2686
|
-
} finally {
|
|
2687
|
-
await db2.close();
|
|
2688
|
-
}
|
|
2689
|
-
return 0;
|
|
2690
|
-
}
|
|
2691
|
-
const { session, events } = csvToSessionRows(fs.readFileSync(file, "utf-8"));
|
|
2692
|
-
const db = await openCliDb(parsed.db);
|
|
2693
|
-
try {
|
|
2694
|
-
const result = await persistSessionBundle(db, session, events);
|
|
2695
|
-
writeOut(
|
|
2696
|
-
`[wod import] session ${session.id} \u2014 ${result.events} events \u2192 ${db.path}`
|
|
2697
|
-
);
|
|
2698
|
-
} finally {
|
|
2699
|
-
await db.close();
|
|
2700
|
-
}
|
|
2701
|
-
return 0;
|
|
2702
|
-
}
|
|
2703
|
-
if (parsed.subcommand === "build") {
|
|
2704
|
-
const cwd = process.cwd();
|
|
2705
|
-
const root = nodePath().resolve(cwd, parsed.dir ?? parsed.target ?? ".");
|
|
2706
|
-
if (!nodeFs().existsSync(nodePath().resolve(root, "markdown"))) {
|
|
2707
|
-
writeErr(`[wod build] No markdown/ corpus under ${root} \u2014 run from the content repo root (or pass its directory).`);
|
|
2708
|
-
return 1;
|
|
2709
|
-
}
|
|
2710
|
-
const { generateSeed } = await import('./compiler-SXUCF7AU.mjs');
|
|
2711
|
-
try {
|
|
2712
|
-
const result = generateSeed({
|
|
2713
|
-
corpusRoot: root,
|
|
2714
|
-
...parsed.output ? { outDir: nodePath().resolve(cwd, parsed.output) } : {},
|
|
2715
|
-
...parsed.template ? { templateFile: nodePath().resolve(cwd, parsed.template) } : {},
|
|
2716
|
-
scaffoldDiscoveredEfforts: parsed.scaffold === true
|
|
2717
|
-
});
|
|
2718
|
-
const kb = (n) => `${(n / 1024).toFixed(0)} KB`;
|
|
2719
|
-
const totalBytes = result.manifest.chunks.reduce((s, c) => s + c.bytes, 0);
|
|
2720
|
-
writeOut(`[wod build] schema v${result.manifest.schema} version ${result.manifest.version} (${result.manifest.versionLabel}) \u2192 ${result.outDir}`);
|
|
2721
|
-
for (const c of result.manifest.chunks) {
|
|
2722
|
-
writeOut(` ${c.id.padEnd(48)} ${String(c.count).padStart(4)} files ${kb(c.bytes).padStart(8)}`);
|
|
2723
|
-
}
|
|
2724
|
-
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`);
|
|
2725
|
-
return 0;
|
|
2726
|
-
} catch (error) {
|
|
2727
|
-
writeErr(`[wod build] ${error.message}`);
|
|
2728
|
-
return 1;
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
if (parsed.subcommand === "seed") {
|
|
2732
|
-
const dirs = parsed.sources.length > 0 ? parsed.sources.map((s) => nodePath().resolve(process.cwd(), s)) : defaultSeedDirs(process.cwd());
|
|
2733
|
-
if (dirs.length === 0) {
|
|
2734
|
-
writeErr(
|
|
2735
|
-
"[wod seed] No seed sources found. Run `bun scripts/sync-seed.ts` to build the bundled artifacts, or pass --source <dir> pointing at a compiled seed directory (manifest.json + chunks)."
|
|
2736
|
-
);
|
|
2737
|
-
return 1;
|
|
2738
|
-
}
|
|
2739
|
-
const db = await openCliDb(parsed.db);
|
|
2740
|
-
try {
|
|
2741
|
-
for (const dir of dirs) {
|
|
2742
|
-
const summary = await importSeedDir(db, dir, "");
|
|
2743
|
-
writeOut(
|
|
2744
|
-
`[wod seed] ${summary.sourceId}: v${summary.version} \u2014 ${summary.notes} notes, ${summary.efforts} efforts, ${summary.blocks} blocks, ${summary.blockEfforts} block-efforts (${summary.deleted} deleted, ${summary.skippedUserOwned} user-owned skipped) \u2192 ${db.path}`
|
|
2745
|
-
);
|
|
2746
|
-
}
|
|
2747
|
-
} finally {
|
|
2748
|
-
await db.close();
|
|
2749
|
-
}
|
|
2750
|
-
return 0;
|
|
2751
|
-
}
|
|
2752
|
-
if (parsed.subcommand === "reset") {
|
|
2753
|
-
const target = resolveDbPath(parsed.db);
|
|
2754
|
-
if (nodeFs().existsSync(target)) {
|
|
2755
|
-
nodeFs().unlinkSync(target);
|
|
2756
|
-
writeOut(`[wod reset] removed ${target}`);
|
|
2757
|
-
} else {
|
|
2758
|
-
writeOut(`[wod reset] nothing to reset \u2014 ${target} does not exist`);
|
|
2759
|
-
}
|
|
2760
|
-
return 0;
|
|
2761
|
-
}
|
|
2762
|
-
writeErr(`[wod] Unknown subcommand "${parsed.subcommand}"`);
|
|
2763
|
-
return 1;
|
|
2764
|
-
} catch (error) {
|
|
2765
|
-
if (error instanceof ParseSyntaxError || error instanceof WqlSyntaxError) {
|
|
2766
|
-
writeErr(error.message);
|
|
2767
|
-
return 2;
|
|
2768
|
-
}
|
|
2769
|
-
if (error instanceof PackLoadError) {
|
|
2770
|
-
writeErr(error.message);
|
|
2771
|
-
return 3;
|
|
2772
|
-
}
|
|
2773
|
-
writeErr(`[wod] Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
2774
|
-
return 1;
|
|
2775
|
-
}
|
|
2776
|
-
}
|
|
2777
|
-
|
|
2778
|
-
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 };
|
|
2779
|
-
//# sourceMappingURL=chunk-N4KFWY6Z.mjs.map
|
|
2780
|
-
//# sourceMappingURL=chunk-N4KFWY6Z.mjs.map
|