@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/index.mjs
CHANGED
|
@@ -1,25 +1,1100 @@
|
|
|
1
|
-
import {
|
|
2
|
-
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 } from './chunk-N4KFWY6Z.mjs';
|
|
3
|
-
export { CANVAS_CHUNK_ID, DASHBOARDS_CHUNK_ID, EFFORTS_CHUNK_ID, SEED_BROADCAST_CHANNEL, SEED_REGISTERED_WIDGETS, SEED_SCHEMA, TEMPLATE_CHUNK_ID, canvasRouteSlug, catalogForChunkId, emptySeedMeta, seedMetaKey, seedNoteSourceId } from './chunk-Y6WO6MXS.mjs';
|
|
1
|
+
import { KNOWN_SECTION_WIDGETS, MetricType, validateNoteBlocks, parseDocumentSections, blockContentId, reconcileNoteBlocks, mapNoteBlockChanges } from '@bitcobblers/wod-wiki-core';
|
|
4
2
|
export * from '@bitcobblers/wod-wiki-core';
|
|
3
|
+
import { dialectRegistry, realtimeProcessorRegistry, summaryProcessorRegistry, getHints, isEffortDiscipline } from '@bitcobblers/wod-wiki-lang';
|
|
5
4
|
export { ABSENT, AGGREGATE_BUILTINS, AUTHORITATIVE_CASTS, AmrapLogicStrategy, AnalyticsEngine, AnalyticsTransformer, BUILTIN_CALCS, BUNDLED_EFFORT_COUNT, BlockBuilder, ButtonBehavior, CONSUMED_HINTS, CONSUMED_HINT_KEYS, CONTEXT_ATOMS, CalcEngine, CalculationRegistry, CardioDialect, ChildSelectionBehavior, ChildrenStrategy, ChoiceGroupMetric, ClimbDialect, ClimbMetricType, CountdownTimerBehavior, CountupTimerBehavior, CrossFitDialect, DEFAULT_RESOLVER_OPTIONS, DEFAULT_RUNTIME_OPTIONS, DEFAULT_UNRESOLVED_EFFORT_MET, DIM_LENGTH, DIM_MASS, DIM_TIME, DIM_ZERO, DialectStack, EFFORT_DATA_METRIC_TYPE, EFFORT_REGISTRY_ORIGINS, EffortFallbackStrategy, EffortResolver, ErrorAction, EventBus, ExecutionContext, ExitBehavior, FixtureSyntaxError, GenericGroupStrategy, GenericLoopStrategy, GenericTimerStrategy, HabitsDialect, INTENSITY_TIERS, InMemoryEffortRegistry, IntervalLogicStrategy, JitCompiler, LabelingBehavior, LocalRuntimeSubscription, LookupRegistry, MdTimerRuntime, MemoryLocation, MemoryTypeEnum, NextEvent, OutputEmitter, PRODUCTION_STRATEGIES, PopBlockAction, PushBlockAction, RegisterEventHandlerAction, Registry, ReportOutputBehavior, ReportOutputStrategy, RoundsMetric, RuntimeAdapter, RuntimeClock, RuntimeFactory, RuntimeLogger, RuntimeMemory, RuntimeObservers, RuntimeStack, STORE_CALCS, STREAM_ATOMS, ScriptRuntime, SessionRootStrategy, SnapshotClock, SoundCueBehavior, SoundStrategy, StandardAnalyticsProfile, StartSessionAction, StartWorkoutAction, SubscriptionManager, TimeSpan, TypedMemoryReference, UNITS, UnitsDialect, UnregisterEventHandlerAction, VISIBILITY_ICONS, VISIBILITY_LABELS, WhiteboardScript, WodDialect, YogaDialect, buildTooltip, bundledEfforts, calculateElapsed, canonicalizeType, collapseUnresolvedChoices, compareStatement, compileLineForm, compoundName, computeColumnLabel, computeLabel, createAnalyticsEngineForBlock, createCalcEngine, createCompiler, createDialectStack, createMetricPresentationPolicy, createMockClock, createParser, dialectRegistry, dialectStack, diffStatement, evaluate, evaluateCalculateDefinitions, extractMetrics, extractStatements, findBestFuzzyMatch, findUnresolvedChoices, frozenNow, getAnalyticsFromLogs, getHints, getMetricVisibility, getRuntimeNowMs, hasHint, hintMetric, hintsToContainer, isCastSubscription, isChoiceResolved, isTimeLikeMetric, isWithinThreshold, levenshteinDistance, metricPresentation, normalizeForFuzzy, outputNodeId, outputNodeIds, parseCalcLine, parseCalculateBlock, parseExpression, parseMetricLine, parseScript, realtimeProcessorRegistry, renderMetric, strategyRegistry, summaryProcessorRegistry, toStoredOutputStatement, truthy, wallClockNow, whiteboardParser, whiteboardScript, whiteboardScriptLanguage, writeChoiceSelection } from '@bitcobblers/wod-wiki-lang';
|
|
6
5
|
export { BlockTimerDisplay, ScriptRuntimeProvider, useActiveControls, useBlockMemory, useLiveAnalytics, useMemorySubscription, useNextPreview, useOutputStatements, usePrimaryTimer, useRoundDisplay, useRuntimeExecution, useScriptRuntime, useSecondaryTimers, useSnapshotBlocks, useStackDisplayRows, useStackFragmentSources, useStackSnapshot, useStackTimers, useTimerDisplay, useTimerElapsed } from '@bitcobblers/wod-wiki-lang/react';
|
|
6
|
+
import { WQL_TYPED_TAG_KEYS } from '@bitcobblers/wod-wiki-wql';
|
|
7
7
|
export { DASHBOARD_GRID_MAX_COLS, DASHBOARD_WIDGET_TYPES, DEFAULT_DASHBOARD_TITLE, PLANNED_WIDGET_TYPES, QueryService, WQL_AGGREGATORS, WQL_CALC_TARGETS, WQL_CHART_HEADS, WQL_COMPARISON_OPS, WQL_CONTENT_FILTER_KEYS, WQL_DISPLAY_UNITS, WQL_FIND_TARGETS, WQL_FUNCTION_HEADS, WQL_GRAINS, WQL_INTENSITY_TIERS, WQL_KEYWORDS, WQL_METRIC_AGGREGATES, WQL_METRIC_FAMILIES, WQL_RESULT_PLANES, WQL_ROLLUP_PERIODS, WQL_SOURCES, WQL_SOURCE_HEADS, WQL_SOURCE_VALUES, WQL_STANDARD_DATASETS, WQL_TAG_KEYS, WQL_VIRTUAL_DIMS, buildDashboardDocument, buildDashboardScaffold, dashboardSlug, defaultTokenValues, extractDashboardTokens, isAggregateQuery, isDashboardMeta, isDashboardWidgetType, isFindQuery, isPipelineQuery, normalizeWql, parseQuery, parseQueryWidgetSuffix, parseWqlSuffixes, resolveWidgetType, serialize, splitAtWhere, substituteTokens, unknownTokensMessage, unknownWidgetTypeMessage, wql, wqlLanguage, wqlParser, wqlTerms } from '@bitcobblers/wod-wiki-wql';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// src/index.ts
|
|
10
|
+
var activePacks = /* @__PURE__ */ new Map();
|
|
11
|
+
function defineLanguagePack(pack) {
|
|
12
|
+
return pack;
|
|
13
|
+
}
|
|
14
|
+
function registerLanguagePack(pack) {
|
|
15
|
+
const packId = pack.identity?.id ?? pack.identity?.name ?? "anonymous-pack";
|
|
16
|
+
activePacks.set(packId, pack);
|
|
17
|
+
if (pack.lang?.analyzer) {
|
|
18
|
+
const AnalyzerCtor = pack.lang.analyzer;
|
|
19
|
+
const instance = new AnalyzerCtor();
|
|
20
|
+
dialectRegistry.register(instance);
|
|
21
|
+
}
|
|
22
|
+
if (pack.lang?.analytics) {
|
|
23
|
+
for (const ProcessorCtor of pack.lang.analytics) {
|
|
24
|
+
const instance = new ProcessorCtor();
|
|
25
|
+
if ("processOutput" in instance) {
|
|
26
|
+
realtimeProcessorRegistry.register(instance);
|
|
27
|
+
}
|
|
28
|
+
if ("processSummary" in instance) {
|
|
29
|
+
summaryProcessorRegistry.register(instance);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function unregisterLanguagePack(packOrId) {
|
|
35
|
+
const packId = typeof packOrId === "string" ? packOrId : packOrId.identity?.id ?? packOrId.identity?.name ?? "anonymous-pack";
|
|
36
|
+
const pack = activePacks.get(packId);
|
|
37
|
+
activePacks.delete(packId);
|
|
38
|
+
if (pack?.lang?.analyzer) {
|
|
39
|
+
const instance = new pack.lang.analyzer();
|
|
40
|
+
dialectRegistry.unregister(instance.id);
|
|
41
|
+
} else if (typeof packOrId === "string") {
|
|
42
|
+
dialectRegistry.unregister(packOrId);
|
|
43
|
+
}
|
|
44
|
+
if (pack?.lang?.analytics) {
|
|
45
|
+
for (const ProcessorCtor of pack.lang.analytics) {
|
|
46
|
+
const instance = new ProcessorCtor();
|
|
47
|
+
if ("processOutput" in instance) {
|
|
48
|
+
realtimeProcessorRegistry.unregister(instance.id);
|
|
49
|
+
}
|
|
50
|
+
if ("processSummary" in instance) {
|
|
51
|
+
summaryProcessorRegistry.unregister(instance.id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function listLanguagePacks() {
|
|
57
|
+
return Array.from(activePacks.values());
|
|
58
|
+
}
|
|
59
|
+
function getRegisteredLanguagePacks() {
|
|
60
|
+
return Array.from(activePacks.values());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/store.ts
|
|
64
|
+
function inMemoryEventStore(events) {
|
|
65
|
+
const rows = [...events];
|
|
66
|
+
return {
|
|
67
|
+
getEventsByTimeRange: async (start, end) => rows.filter((r) => r.timestamp >= start && r.timestamp <= end),
|
|
68
|
+
getEventsByResult: async (resultId) => rows.filter((r) => r.resultId === resultId),
|
|
69
|
+
getEventsForNote: async (noteId) => rows.filter((r) => r.noteId === noteId),
|
|
70
|
+
getEventsByContent: async (blockContentId2) => rows.filter((r) => r.blockContentId === blockContentId2),
|
|
71
|
+
scanAll: async () => rows,
|
|
72
|
+
appendEvents: async (appended) => {
|
|
73
|
+
rows.push(...appended);
|
|
74
|
+
},
|
|
75
|
+
finalizeSummaries: async (resultId, finals) => {
|
|
76
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
77
|
+
const row = rows[i];
|
|
78
|
+
if (row.resultId === resultId && row.grain === "summary" && row.origin !== "user") rows.splice(i, 1);
|
|
79
|
+
}
|
|
80
|
+
rows.push(...finals);
|
|
81
|
+
},
|
|
82
|
+
deleteEvents: async (ids) => {
|
|
83
|
+
const doomed = new Set(ids);
|
|
84
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
85
|
+
if (doomed.has(rows[i].id)) rows.splice(i, 1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function createIRFile(kind, data, optionsOrSource, generatedAt) {
|
|
91
|
+
let source = "wod:headless";
|
|
92
|
+
let genAt = Date.now();
|
|
93
|
+
if (typeof optionsOrSource === "string") {
|
|
94
|
+
source = optionsOrSource;
|
|
95
|
+
if (generatedAt !== void 0) genAt = generatedAt;
|
|
96
|
+
} else if (optionsOrSource && typeof optionsOrSource === "object") {
|
|
97
|
+
if (optionsOrSource.source) source = optionsOrSource.source;
|
|
98
|
+
if (optionsOrSource.generatedAt !== void 0) genAt = optionsOrSource.generatedAt;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
$schema: "https://wod-wiki.dev/ir/v1.json",
|
|
102
|
+
kind,
|
|
103
|
+
generatedAt: genAt,
|
|
104
|
+
source,
|
|
105
|
+
data
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function isIRFile(obj) {
|
|
109
|
+
if (!obj || typeof obj !== "object") return false;
|
|
110
|
+
const cand = obj;
|
|
111
|
+
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;
|
|
112
|
+
}
|
|
113
|
+
function statementToNode(stmt, idMap) {
|
|
114
|
+
const meta = stmt.meta;
|
|
115
|
+
const metrics = stmt.metrics.toArray().map((m) => ({
|
|
116
|
+
type: m.type,
|
|
117
|
+
...m.image !== void 0 ? { image: m.image } : {},
|
|
118
|
+
...m.value !== void 0 ? { value: m.value } : {},
|
|
119
|
+
...m.origin !== void 0 ? { origin: m.origin } : {}
|
|
120
|
+
}));
|
|
121
|
+
const raw = meta?.raw ?? "";
|
|
122
|
+
const from = meta?.startOffset ?? 0;
|
|
123
|
+
const to = meta?.endOffset ?? from + raw.length;
|
|
124
|
+
const durationFrag = stmt.metrics.find((m) => m.type === MetricType.Duration || m.type === "duration");
|
|
125
|
+
const roundsFrag = stmt.metrics.find((m) => m.type === MetricType.Rounds || m.type === "rounds");
|
|
126
|
+
const type = durationFrag ? "duration" : roundsFrag ? "rounds" : "statement";
|
|
127
|
+
const hints = getHints(stmt);
|
|
128
|
+
const childIds = (stmt.children ?? []).flat();
|
|
129
|
+
const children = childIds.map((cid) => idMap.get(cid)).filter((c) => c !== void 0).map((c) => statementToNode(c, idMap));
|
|
130
|
+
return {
|
|
131
|
+
id: stmt.id,
|
|
132
|
+
type,
|
|
133
|
+
raw,
|
|
134
|
+
from,
|
|
135
|
+
to,
|
|
136
|
+
metrics,
|
|
137
|
+
...hints.length > 0 ? { hints } : {},
|
|
138
|
+
...children.length > 0 ? { children } : {}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function buildStatementTree(script) {
|
|
142
|
+
const idMap = new Map(script.statements.map((s) => [s.id, s]));
|
|
143
|
+
const topLevel = script.statements.filter((s) => s.parent === void 0 || !idMap.has(s.parent));
|
|
144
|
+
if (topLevel.length === 1 && topLevel[0].children.length > 0) {
|
|
145
|
+
return statementToNode(topLevel[0], idMap);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
id: 0,
|
|
149
|
+
type: "root",
|
|
150
|
+
raw: script.source,
|
|
151
|
+
from: 0,
|
|
152
|
+
to: script.source.length,
|
|
153
|
+
metrics: [],
|
|
154
|
+
children: topLevel.map((s) => statementToNode(s, idMap))
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
var NOTE_ID_FRONTMATTER_KEY = "wod-note-id";
|
|
158
|
+
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;
|
|
159
|
+
function extractNoteIdentity(markdown) {
|
|
160
|
+
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
|
|
161
|
+
if (!block) return void 0;
|
|
162
|
+
let noteId;
|
|
163
|
+
let title;
|
|
164
|
+
for (const line of block[1].split(/\r?\n/)) {
|
|
165
|
+
const kv = line.match(/^([^:]+):\s*(.*)$/);
|
|
166
|
+
if (!kv) continue;
|
|
167
|
+
const key = kv[1].trim();
|
|
168
|
+
const value = kv[2].trim();
|
|
169
|
+
if (key === NOTE_ID_FRONTMATTER_KEY && UUID_RE.test(value)) noteId = value;
|
|
170
|
+
if (key === "title" && value) title = value;
|
|
171
|
+
}
|
|
172
|
+
return noteId ? { noteId, title } : void 0;
|
|
173
|
+
}
|
|
174
|
+
var FRONTMATTER_SAFE = /^[^:#\[\]{}'"*&,!?|>%@`\s][^:#\[\]{}"']*$/;
|
|
175
|
+
function injectIdentityFrontmatter(markdown, identity) {
|
|
176
|
+
const stamp = (lines2) => {
|
|
177
|
+
const replaceOrInsert = (key, value) => {
|
|
178
|
+
if (value === void 0 || !FRONTMATTER_SAFE.test(value)) return;
|
|
179
|
+
const idx = lines2.findIndex((l) => l.startsWith(`${key}:`));
|
|
180
|
+
if (idx >= 0) lines2[idx] = `${key}: ${value}`;
|
|
181
|
+
else lines2.push(`${key}: ${value}`);
|
|
182
|
+
};
|
|
183
|
+
replaceOrInsert("title", identity.title);
|
|
184
|
+
replaceOrInsert(NOTE_ID_FRONTMATTER_KEY, identity.noteId);
|
|
185
|
+
return lines2;
|
|
186
|
+
};
|
|
187
|
+
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
|
|
188
|
+
if (block) {
|
|
189
|
+
const newline = markdown.startsWith("---\r\n") ? "\r\n" : "\n";
|
|
190
|
+
const lines2 = stamp(block[1].split(/\r?\n/));
|
|
191
|
+
return `---${newline}${lines2.join(newline)}${newline}---${markdown.slice(block[0].length)}`;
|
|
192
|
+
}
|
|
193
|
+
const lines = stamp([]);
|
|
194
|
+
return `---
|
|
195
|
+
${lines.join("\n")}
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
${markdown}`;
|
|
199
|
+
}
|
|
200
|
+
function composeNoteMarkdown(note) {
|
|
201
|
+
return injectIdentityFrontmatter(note.rawContent, { noteId: note.id, title: note.title });
|
|
202
|
+
}
|
|
203
|
+
var FRONTMATTER_BLOCK_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
204
|
+
function readFrontmatterMeta(markdown) {
|
|
205
|
+
const match = FRONTMATTER_BLOCK_RE.exec(markdown);
|
|
206
|
+
if (!match) return {};
|
|
207
|
+
const meta = {};
|
|
208
|
+
const lines = match[1].split(/\r?\n/);
|
|
209
|
+
let currentListKey;
|
|
210
|
+
for (const rawLine of lines) {
|
|
211
|
+
const line = rawLine.trimEnd();
|
|
212
|
+
const item = line.match(/^\s+-\s+(.*)$/);
|
|
213
|
+
if (item && currentListKey) {
|
|
214
|
+
meta[currentListKey].push(item[1].trim());
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const kv = line.match(/^([^:]+):\s*(.*)$/);
|
|
218
|
+
if (!kv) continue;
|
|
219
|
+
const key = kv[1].trim().toLowerCase();
|
|
220
|
+
const value = kv[2].trim();
|
|
221
|
+
if (value === "") {
|
|
222
|
+
meta[key] = [];
|
|
223
|
+
currentListKey = key;
|
|
224
|
+
} else if (value.startsWith("[") && value.endsWith("]")) {
|
|
225
|
+
meta[key] = value.slice(1, -1).split(",").map((v) => v.trim().replace(/^['"]|['"]$/g, "")).filter((v) => v.length > 0);
|
|
226
|
+
currentListKey = void 0;
|
|
227
|
+
} else {
|
|
228
|
+
meta[key] = value;
|
|
229
|
+
currentListKey = void 0;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return meta;
|
|
233
|
+
}
|
|
234
|
+
var asList = (value) => Array.isArray(value) ? value : typeof value === "string" ? value.split(",").map((v) => v.trim()).filter((v) => v.length > 0) : [];
|
|
235
|
+
function extractTypedTags(markdown, typedKeys = WQL_TYPED_TAG_KEYS) {
|
|
236
|
+
const meta = readFrontmatterMeta(markdown);
|
|
237
|
+
const results = asList(meta["tags"]).map((label) => ({ label }));
|
|
238
|
+
const metaKeys = Object.keys(meta);
|
|
239
|
+
for (const typeName of typedKeys) {
|
|
240
|
+
if (typeName === "tags") continue;
|
|
241
|
+
const matchedKey = metaKeys.find((k) => k === typeName.toLowerCase());
|
|
242
|
+
if (!matchedKey) continue;
|
|
243
|
+
for (const label of asList(meta[matchedKey])) {
|
|
244
|
+
results.push({ label, type: typeName });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return results.filter((t) => t.label.length > 0);
|
|
248
|
+
}
|
|
249
|
+
function markdownToEffort(raw) {
|
|
250
|
+
const match = FRONTMATTER_BLOCK_RE.exec(raw);
|
|
251
|
+
if (!match) return null;
|
|
252
|
+
const meta = readFrontmatterMeta(raw);
|
|
253
|
+
const body = raw.slice(match[0].length).trim();
|
|
254
|
+
const id = scalarOf(meta["id"]);
|
|
255
|
+
const slug = scalarOf(meta["slug"]);
|
|
256
|
+
const label = scalarOf(meta["label"]);
|
|
257
|
+
if (!id || !slug || !label) return null;
|
|
258
|
+
const baseAttributes = {};
|
|
259
|
+
const blockRe = /^\s{2,}([^:]+):\s*(.+)$/;
|
|
260
|
+
let inAttrs = false;
|
|
261
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
262
|
+
if (/^baseAttributes:/.test(line)) {
|
|
263
|
+
inAttrs = true;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (inAttrs) {
|
|
267
|
+
const attr = blockRe.exec(line);
|
|
268
|
+
if (attr && /^ {2,}/.test(line)) {
|
|
269
|
+
const v = attr[2].trim();
|
|
270
|
+
baseAttributes[attr[1].trim()] = /^\d+(\.\d+)?$/.test(v) ? Number(v) : v;
|
|
271
|
+
} else if (/^\S/.test(line)) {
|
|
272
|
+
inAttrs = false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (typeof baseAttributes["met"] !== "number") return null;
|
|
277
|
+
return {
|
|
278
|
+
id,
|
|
279
|
+
slug,
|
|
280
|
+
label,
|
|
281
|
+
aliases: asList(meta["aliases"]),
|
|
282
|
+
baseAttributes,
|
|
283
|
+
...body ? { body } : {}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function effortToMarkdown(effort) {
|
|
287
|
+
const lines = ["---", `id: ${effort.id}`, `slug: ${effort.slug}`, `label: ${effort.label}`];
|
|
288
|
+
if (effort.aliases && effort.aliases.length > 0) {
|
|
289
|
+
lines.push("aliases:");
|
|
290
|
+
for (const alias of effort.aliases) lines.push(` - ${alias}`);
|
|
291
|
+
}
|
|
292
|
+
const attrs = Object.entries(effort.baseAttributes).filter(([, v]) => typeof v === "number" || typeof v === "string");
|
|
293
|
+
if (attrs.length > 0) {
|
|
294
|
+
lines.push("baseAttributes:");
|
|
295
|
+
for (const [k, v] of attrs) lines.push(` ${k}: ${String(v)}`);
|
|
296
|
+
}
|
|
297
|
+
lines.push("---", "");
|
|
298
|
+
return `${lines.join("\n")}
|
|
299
|
+
${(effort.body ?? "").trim()}`.trimEnd() + "\n";
|
|
300
|
+
}
|
|
301
|
+
function scalarOf(value) {
|
|
302
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
303
|
+
}
|
|
304
|
+
function equipmentToCsv(labels) {
|
|
305
|
+
return ["label", ...labels].map((l) => escapeCsvField(l)).join("\n") + "\n";
|
|
306
|
+
}
|
|
307
|
+
function csvToEquipmentLabels(csv) {
|
|
308
|
+
const lines = splitCsvRecords(csv);
|
|
309
|
+
if (lines.length === 0) return [];
|
|
310
|
+
const header = parseCsvLine(lines[0]);
|
|
311
|
+
const labelIdx = header[0] === "label" ? 0 : -1;
|
|
312
|
+
const start = labelIdx >= 0 ? 1 : 0;
|
|
313
|
+
const labels = [];
|
|
314
|
+
for (const line of lines.slice(start)) {
|
|
315
|
+
const cells = parseCsvLine(line);
|
|
316
|
+
const label = (labelIdx >= 0 ? cells[0] : cells.join(",")).trim();
|
|
317
|
+
if (label) labels.push(label);
|
|
318
|
+
}
|
|
319
|
+
return [...new Set(labels)];
|
|
320
|
+
}
|
|
321
|
+
var SESSION_HEADER = [
|
|
322
|
+
"sessionId",
|
|
323
|
+
"noteId",
|
|
324
|
+
"blockId",
|
|
325
|
+
"blockVersion",
|
|
326
|
+
"blockContentId",
|
|
327
|
+
"origin",
|
|
328
|
+
"status",
|
|
329
|
+
"startTime",
|
|
330
|
+
"endTime",
|
|
331
|
+
"duration",
|
|
332
|
+
"completed",
|
|
333
|
+
"sessionCreatedAt",
|
|
334
|
+
"sessionExtraJson"
|
|
335
|
+
];
|
|
336
|
+
var EVENT_HEADER = [
|
|
337
|
+
"eventId",
|
|
338
|
+
"grain",
|
|
339
|
+
"outputType",
|
|
340
|
+
"timestamp",
|
|
341
|
+
"pageId",
|
|
342
|
+
"effortSlug",
|
|
343
|
+
"eventBlockId",
|
|
344
|
+
"eventBlockVersion",
|
|
345
|
+
"eventBlockContentId",
|
|
346
|
+
"eventOrigin",
|
|
347
|
+
"timeSpan",
|
|
348
|
+
"sourceBlockKey",
|
|
349
|
+
"sourceStatementId",
|
|
350
|
+
"line",
|
|
351
|
+
"text",
|
|
352
|
+
"dialect",
|
|
353
|
+
"stackLevel",
|
|
354
|
+
"completionReason",
|
|
355
|
+
"representationKind",
|
|
356
|
+
"metricType",
|
|
357
|
+
"metricValue",
|
|
358
|
+
"metricUnit",
|
|
359
|
+
"metricImage",
|
|
360
|
+
"metricOrigin",
|
|
361
|
+
"metricMetadata",
|
|
362
|
+
"metricTemporal",
|
|
363
|
+
// Verbatim JSON backstops: make the machine round-trip lossless across
|
|
364
|
+
// schema fields that have no dedicated spreadsheet column. Explicit cells
|
|
365
|
+
// above still win on import, so hand-edited values are honored.
|
|
366
|
+
"metricJson",
|
|
367
|
+
"eventExtraJson"
|
|
368
|
+
];
|
|
369
|
+
var SESSION_CSV_HEADER = [...SESSION_HEADER, ...EVENT_HEADER];
|
|
370
|
+
function escapeCsvField(field) {
|
|
371
|
+
if (field === void 0 || field === null) return "";
|
|
372
|
+
const str = typeof field === "object" ? JSON.stringify(field) : String(field);
|
|
373
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
|
|
374
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
375
|
+
}
|
|
376
|
+
return str;
|
|
377
|
+
}
|
|
378
|
+
var metricValueToCell = (value) => {
|
|
379
|
+
if (value === void 0 || value === null) return "";
|
|
380
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
381
|
+
return String(value);
|
|
382
|
+
};
|
|
383
|
+
var metricValueFromCell = (cell) => {
|
|
384
|
+
if (cell === "") return void 0;
|
|
385
|
+
if (/^-?\d+(\.\d+)?$/.test(cell)) return Number(cell);
|
|
386
|
+
if (cell === "true" || cell === "false") return cell === "true";
|
|
387
|
+
if (cell.startsWith("{") || cell.startsWith("[")) {
|
|
388
|
+
try {
|
|
389
|
+
return JSON.parse(cell);
|
|
390
|
+
} catch {
|
|
391
|
+
return cell;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return cell;
|
|
395
|
+
};
|
|
396
|
+
function sessionCells(session) {
|
|
397
|
+
return [
|
|
398
|
+
session.id,
|
|
399
|
+
session.noteId,
|
|
400
|
+
session.blockId,
|
|
401
|
+
session.blockVersion,
|
|
402
|
+
session.blockContentId,
|
|
403
|
+
session.origin,
|
|
404
|
+
session.status,
|
|
405
|
+
session.startTime,
|
|
406
|
+
session.endTime,
|
|
407
|
+
session.duration,
|
|
408
|
+
session.completed,
|
|
409
|
+
session.createdAt,
|
|
410
|
+
JSON.stringify(session)
|
|
411
|
+
].map(escapeCsvField);
|
|
412
|
+
}
|
|
413
|
+
function metricCells(event, metric, index) {
|
|
414
|
+
return [
|
|
415
|
+
event.id,
|
|
416
|
+
event.grain,
|
|
417
|
+
event.outputType,
|
|
418
|
+
event.timestamp,
|
|
419
|
+
event.pageId,
|
|
420
|
+
event.effortSlug,
|
|
421
|
+
event.blockId,
|
|
422
|
+
event.blockVersion,
|
|
423
|
+
event.blockContentId,
|
|
424
|
+
event.origin,
|
|
425
|
+
event.timeSpan,
|
|
426
|
+
event.sourceBlockKey,
|
|
427
|
+
event.sourceStatementId,
|
|
428
|
+
event.line,
|
|
429
|
+
event.text,
|
|
430
|
+
event.dialect,
|
|
431
|
+
event.stackLevel,
|
|
432
|
+
event.completionReason,
|
|
433
|
+
event.representationKind,
|
|
434
|
+
metric.type,
|
|
435
|
+
metricValueToCell(metric.value),
|
|
436
|
+
metric.unit,
|
|
437
|
+
metric.image,
|
|
438
|
+
metric.origin,
|
|
439
|
+
metric.metadata === void 0 ? "" : JSON.stringify(metric.metadata),
|
|
440
|
+
event.metricTemporal?.[index] === void 0 ? "" : JSON.stringify(event.metricTemporal[index]),
|
|
441
|
+
JSON.stringify(metric)
|
|
442
|
+
].map(escapeCsvField);
|
|
443
|
+
}
|
|
444
|
+
function eventExtraCells(event) {
|
|
445
|
+
const { metrics: _metrics, ...rest } = event;
|
|
446
|
+
return escapeCsvField(JSON.stringify(rest));
|
|
447
|
+
}
|
|
448
|
+
function sessionToCsv(session, events) {
|
|
449
|
+
const lines = [SESSION_CSV_HEADER.join(",")];
|
|
450
|
+
lines.push([...sessionCells(session), ...EVENT_HEADER.map(() => "")].join(","));
|
|
451
|
+
const ordered = [...events].sort((a, b) => a.id.localeCompare(b.id));
|
|
452
|
+
for (const event of ordered) {
|
|
453
|
+
const extra = eventExtraCells(event);
|
|
454
|
+
const metrics = event.metrics ?? [];
|
|
455
|
+
if (metrics.length === 0) {
|
|
456
|
+
lines.push([...sessionCells(session), ...metricCells(event, { type: "" }, 0), extra].join(","));
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
for (let i = 0; i < metrics.length; i++) {
|
|
460
|
+
lines.push([...sessionCells(session), ...metricCells(event, metrics[i], i), extra].join(","));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return lines.join("\n") + "\n";
|
|
464
|
+
}
|
|
465
|
+
function parseCsvLine(line) {
|
|
466
|
+
const cells = [];
|
|
467
|
+
let current = "";
|
|
468
|
+
let quoted = false;
|
|
469
|
+
for (let i = 0; i < line.length; i++) {
|
|
470
|
+
const ch = line[i];
|
|
471
|
+
if (quoted) {
|
|
472
|
+
if (ch === '"') {
|
|
473
|
+
if (line[i + 1] === '"') {
|
|
474
|
+
current += '"';
|
|
475
|
+
i += 1;
|
|
476
|
+
} else {
|
|
477
|
+
quoted = false;
|
|
478
|
+
}
|
|
479
|
+
} else {
|
|
480
|
+
current += ch;
|
|
481
|
+
}
|
|
482
|
+
} else if (ch === '"' && current === "") {
|
|
483
|
+
quoted = true;
|
|
484
|
+
} else if (ch === ",") {
|
|
485
|
+
cells.push(current);
|
|
486
|
+
current = "";
|
|
487
|
+
} else {
|
|
488
|
+
current += ch;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
cells.push(current);
|
|
492
|
+
return cells;
|
|
493
|
+
}
|
|
494
|
+
function splitCsvRecords(csv) {
|
|
495
|
+
const records = [];
|
|
496
|
+
let current = "";
|
|
497
|
+
let quoted = false;
|
|
498
|
+
for (let i = 0; i < csv.length; i++) {
|
|
499
|
+
const ch = csv[i];
|
|
500
|
+
if (ch === '"') {
|
|
501
|
+
if (quoted && csv[i + 1] === '"') {
|
|
502
|
+
current += '""';
|
|
503
|
+
i += 1;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
quoted = !quoted;
|
|
507
|
+
current += ch;
|
|
508
|
+
} else if (!quoted && (ch === "\n" || ch === "\r")) {
|
|
509
|
+
if (ch === "\r" && csv[i + 1] === "\n") i += 1;
|
|
510
|
+
if (current.length > 0) records.push(current);
|
|
511
|
+
current = "";
|
|
512
|
+
} else {
|
|
513
|
+
current += ch;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (current.length > 0) records.push(current);
|
|
517
|
+
return records;
|
|
518
|
+
}
|
|
519
|
+
var num = (cell) => cell === "" ? void 0 : Number(cell);
|
|
520
|
+
var nonempty = (cell) => cell === "" ? void 0 : cell;
|
|
521
|
+
function jsonCell(cell) {
|
|
522
|
+
if (!cell) return void 0;
|
|
523
|
+
try {
|
|
524
|
+
return JSON.parse(cell);
|
|
525
|
+
} catch {
|
|
526
|
+
return void 0;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function csvToSessionRows(csv) {
|
|
530
|
+
const lines = splitCsvRecords(csv);
|
|
531
|
+
if (lines.length < 2) throw new Error("[wod import] Session CSV has no data rows");
|
|
532
|
+
const header = parseCsvLine(lines[0]);
|
|
533
|
+
if (header.some((name) => /segment/i.test(name))) {
|
|
534
|
+
throw new Error("[wod import] Incompatible Session CSV segment pins; blockId/blockVersion are required");
|
|
535
|
+
}
|
|
536
|
+
const col = (name) => {
|
|
537
|
+
const idx = header.indexOf(name);
|
|
538
|
+
if (idx < 0) throw new Error(`[wod import] Session CSV missing column "${name}"`);
|
|
539
|
+
return idx;
|
|
540
|
+
};
|
|
541
|
+
const at = (cells, name) => cells[col(name)] ?? "";
|
|
542
|
+
const eventRows = /* @__PURE__ */ new Map();
|
|
543
|
+
let session;
|
|
544
|
+
const ensureSession = (cells) => {
|
|
545
|
+
const id = at(cells, "sessionId");
|
|
546
|
+
const noteId = at(cells, "noteId");
|
|
547
|
+
const blockId = at(cells, "blockId");
|
|
548
|
+
const blockVersion = Number(at(cells, "blockVersion"));
|
|
549
|
+
if (!id || !noteId || !blockId || !Number.isSafeInteger(blockVersion) || blockVersion < 1) {
|
|
550
|
+
throw new Error("[wod import] Session CSV requires sessionId, noteId, blockId and positive blockVersion");
|
|
551
|
+
}
|
|
552
|
+
if (session) {
|
|
553
|
+
if (session.id !== id || session.noteId !== noteId || session.blockId !== blockId || session.blockVersion !== blockVersion) {
|
|
554
|
+
throw new Error("[wod import] Session CSV contains inconsistent source pins");
|
|
555
|
+
}
|
|
556
|
+
return session;
|
|
557
|
+
}
|
|
558
|
+
const extra = jsonCell(at(cells, "sessionExtraJson")) ?? {};
|
|
559
|
+
if ("segmentId" in extra || "segmentVersion" in extra || "version" in extra || "rawContent" in extra || "data" in extra) {
|
|
560
|
+
throw new Error("[wod import] Incompatible Session CSV source snapshot or segment pin");
|
|
561
|
+
}
|
|
562
|
+
session = {
|
|
563
|
+
...extra,
|
|
564
|
+
id,
|
|
565
|
+
noteId,
|
|
566
|
+
blockId,
|
|
567
|
+
blockVersion,
|
|
568
|
+
blockContentId: at(cells, "blockContentId") || void 0,
|
|
569
|
+
origin: at(cells, "origin") || void 0,
|
|
570
|
+
status: at(cells, "status") || void 0,
|
|
571
|
+
startTime: Number(at(cells, "startTime") || 0),
|
|
572
|
+
endTime: Number(at(cells, "endTime") || 0),
|
|
573
|
+
duration: Number(at(cells, "duration") || 0),
|
|
574
|
+
completed: at(cells, "completed") === "true",
|
|
575
|
+
createdAt: Number(at(cells, "sessionCreatedAt") || 0)
|
|
576
|
+
};
|
|
577
|
+
if (![session.startTime, session.endTime, session.duration, session.createdAt].every(Number.isFinite)) {
|
|
578
|
+
throw new Error("[wod import] Session CSV lifecycle values must be finite numbers");
|
|
579
|
+
}
|
|
580
|
+
return session;
|
|
581
|
+
};
|
|
582
|
+
for (const line of lines.slice(1)) {
|
|
583
|
+
const cells = parseCsvLine(line);
|
|
584
|
+
if (cells.length === 1 && cells[0] === "") continue;
|
|
585
|
+
const currentSession = ensureSession(cells);
|
|
586
|
+
const eventId = at(cells, "eventId");
|
|
587
|
+
if (!eventId) continue;
|
|
588
|
+
let event = eventRows.get(eventId);
|
|
589
|
+
if (!event) {
|
|
590
|
+
const extra = jsonCell(at(cells, "eventExtraJson")) ?? {};
|
|
591
|
+
if ("segmentId" in extra || "segmentVersion" in extra) {
|
|
592
|
+
throw new Error("[wod import] Incompatible Session CSV event segment pins");
|
|
593
|
+
}
|
|
594
|
+
const blockId = nonempty(at(cells, "eventBlockId"));
|
|
595
|
+
const blockVersion = num(at(cells, "eventBlockVersion"));
|
|
596
|
+
if (blockId !== currentSession.blockId || blockVersion !== currentSession.blockVersion) {
|
|
597
|
+
throw new Error("[wod import] Event source pin differs from Session source pin");
|
|
598
|
+
}
|
|
599
|
+
event = {
|
|
600
|
+
...extra,
|
|
601
|
+
id: eventId,
|
|
602
|
+
resultId: currentSession.id,
|
|
603
|
+
noteId: nonempty(at(cells, "noteId")) ?? extra.noteId ?? currentSession.noteId,
|
|
604
|
+
blockContentId: nonempty(at(cells, "eventBlockContentId")) ?? extra.blockContentId,
|
|
605
|
+
pageId: nonempty(at(cells, "pageId")) ?? extra.pageId,
|
|
606
|
+
origin: nonempty(at(cells, "eventOrigin")) ?? extra.origin,
|
|
607
|
+
timestamp: Number(at(cells, "timestamp") || extra.timestamp || 0),
|
|
608
|
+
grain: nonempty(at(cells, "grain")) ?? extra.grain ?? "event",
|
|
609
|
+
effortSlug: nonempty(at(cells, "effortSlug")) ?? extra.effortSlug,
|
|
610
|
+
outputType: nonempty(at(cells, "outputType")) ?? extra.outputType ?? "event",
|
|
611
|
+
blockId,
|
|
612
|
+
blockVersion,
|
|
613
|
+
timeSpan: jsonCell(at(cells, "timeSpan")) ?? extra.timeSpan,
|
|
614
|
+
sourceBlockKey: nonempty(at(cells, "sourceBlockKey")) ?? extra.sourceBlockKey,
|
|
615
|
+
sourceStatementId: num(at(cells, "sourceStatementId")) ?? extra.sourceStatementId,
|
|
616
|
+
line: num(at(cells, "line")) ?? extra.line,
|
|
617
|
+
text: nonempty(at(cells, "text")) ?? extra.text,
|
|
618
|
+
dialect: nonempty(at(cells, "dialect")) ?? extra.dialect,
|
|
619
|
+
stackLevel: num(at(cells, "stackLevel")) ?? extra.stackLevel,
|
|
620
|
+
completionReason: nonempty(at(cells, "completionReason")) ?? extra.completionReason,
|
|
621
|
+
representationKind: nonempty(at(cells, "representationKind")) ?? extra.representationKind,
|
|
622
|
+
metrics: [],
|
|
623
|
+
// Rebuild anchors from metric rows while preserving an authored empty array.
|
|
624
|
+
metricTemporal: Array.isArray(extra.metricTemporal) ? [] : void 0
|
|
625
|
+
};
|
|
626
|
+
eventRows.set(eventId, event);
|
|
627
|
+
}
|
|
628
|
+
const metricType = at(cells, "metricType");
|
|
629
|
+
if (!metricType) continue;
|
|
630
|
+
const baseMetric = jsonCell(at(cells, "metricJson")) ?? {};
|
|
631
|
+
const metric = {
|
|
632
|
+
...baseMetric,
|
|
633
|
+
type: metricType,
|
|
634
|
+
...at(cells, "metricValue") !== "" ? { value: metricValueFromCell(at(cells, "metricValue")) } : {},
|
|
635
|
+
...nonempty(at(cells, "metricUnit")) ? { unit: at(cells, "metricUnit") } : {},
|
|
636
|
+
...nonempty(at(cells, "metricImage")) ? { image: at(cells, "metricImage") } : {},
|
|
637
|
+
...nonempty(at(cells, "metricOrigin")) ? { origin: at(cells, "metricOrigin") } : {},
|
|
638
|
+
...at(cells, "metricMetadata") !== "" ? { metadata: jsonCell(at(cells, "metricMetadata")) } : {}
|
|
639
|
+
};
|
|
640
|
+
event.metrics.push(metric);
|
|
641
|
+
const temporal = jsonCell(at(cells, "metricTemporal"));
|
|
642
|
+
if (temporal !== void 0) {
|
|
643
|
+
(event.metricTemporal ??= []).push(temporal);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (!session) throw new Error("[wod import] Session CSV did not contain session columns");
|
|
647
|
+
return { session, events: [...eventRows.values()] };
|
|
11
648
|
}
|
|
12
649
|
|
|
13
650
|
// src/version.ts
|
|
14
|
-
var VERSION = "0.
|
|
15
|
-
var GIT_SHA = "
|
|
16
|
-
var BUILD_TIME = "2026-10-
|
|
651
|
+
var VERSION = "0.36.2133";
|
|
652
|
+
var GIT_SHA = "bb2644d";
|
|
653
|
+
var BUILD_TIME = "2026-10-15T01:00:35.539Z";
|
|
17
654
|
var SEMVER = {
|
|
18
|
-
version: "0.
|
|
19
|
-
gitSha: "
|
|
20
|
-
buildTime: "2026-10-
|
|
655
|
+
version: "0.36.2133",
|
|
656
|
+
gitSha: "bb2644d",
|
|
657
|
+
buildTime: "2026-10-15T01:00:35.539Z"
|
|
21
658
|
};
|
|
659
|
+
var SEED_SCHEMA = 8;
|
|
660
|
+
var SEED_REGISTERED_WIDGETS = KNOWN_SECTION_WIDGETS;
|
|
661
|
+
var SEED_BROADCAST_CHANNEL = "wodwiki.seed";
|
|
662
|
+
var EFFORTS_CHUNK_ID = "efforts";
|
|
663
|
+
var CANVAS_CHUNK_ID = "canvas";
|
|
664
|
+
var DASHBOARDS_CHUNK_ID = "dashboards";
|
|
665
|
+
var TEMPLATE_CHUNK_ID = "template";
|
|
666
|
+
function seedMetaKey(sourceId) {
|
|
667
|
+
return `seed:${sourceId}`;
|
|
668
|
+
}
|
|
669
|
+
function emptySeedMeta(key) {
|
|
670
|
+
return { key, schema: SEED_SCHEMA, seedVersion: 0, builtAt: "", importedAt: 0, chunks: {}, claim: null };
|
|
671
|
+
}
|
|
672
|
+
function catalogForChunkId(chunkId) {
|
|
673
|
+
const value = /^(?:collection|feed)\.(.+)$/.exec(chunkId)?.[1];
|
|
674
|
+
return value && value !== "_root" ? value : void 0;
|
|
675
|
+
}
|
|
676
|
+
function canvasRouteSlug(content) {
|
|
677
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
678
|
+
if (fm?.[1] && !/^template:\s*canvas\s*$/m.test(fm[1])) return null;
|
|
679
|
+
return fm?.[1]?.match(/^route:\s*(\S+)\s*$/m)?.[1]?.replace(/^\//, "") || null;
|
|
680
|
+
}
|
|
681
|
+
function seedNoteSourceId(path, content, chunkId) {
|
|
682
|
+
const parts = path.split("/");
|
|
683
|
+
const stem = (parts.at(-1) ?? "").replace(/\.md$/i, "");
|
|
684
|
+
const under = parts.slice(2).map((p) => p.replace(/\.md$/i, "")).join("/");
|
|
685
|
+
if (chunkId === CANVAS_CHUNK_ID) return `guides:${canvasRouteSlug(content) ?? under}`;
|
|
686
|
+
if (chunkId === DASHBOARDS_CHUNK_ID) return "dashboards";
|
|
687
|
+
if (chunkId === EFFORTS_CHUNK_ID) return "efforts";
|
|
688
|
+
if (chunkId === TEMPLATE_CHUNK_ID) return "playground";
|
|
689
|
+
if (chunkId.startsWith("collection.")) {
|
|
690
|
+
const catalog = catalogForChunkId(chunkId);
|
|
691
|
+
return !catalog ? "collection:_root" : stem.toLowerCase() === "readme" ? `collection:${catalog}` : `collection:${catalog}/${stem}`;
|
|
692
|
+
}
|
|
693
|
+
if (chunkId.startsWith("feed.")) return `feed:feeds/${under}`;
|
|
694
|
+
return void 0;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/seed/frontmatter.ts
|
|
698
|
+
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
699
|
+
function unquote(value) {
|
|
700
|
+
return value.replace(
|
|
701
|
+
/^(['"])(.*)\1$/,
|
|
702
|
+
(_match, quote, inner) => quote === '"' ? inner.replace(/\\(["\\])/g, "$1") : inner
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
function parseFrontmatter(raw) {
|
|
706
|
+
const match = raw.match(FRONTMATTER_RE);
|
|
707
|
+
if (!match) return { meta: {}, body: raw };
|
|
708
|
+
return { meta: parseMetaLines(match[1].split(/\r?\n/)), body: raw.slice(match[0].length) };
|
|
709
|
+
}
|
|
710
|
+
function parseMetaLines(lines) {
|
|
711
|
+
const meta = {};
|
|
712
|
+
for (let i = 0; i < lines.length; i++) {
|
|
713
|
+
const keyMatch = lines[i].match(/^([A-Za-z][\w.-]*)\s*:(.*)$/);
|
|
714
|
+
if (!keyMatch) continue;
|
|
715
|
+
const key = keyMatch[1];
|
|
716
|
+
const rawVal = keyMatch[2].trim();
|
|
717
|
+
if (rawVal === "") {
|
|
718
|
+
const items = [];
|
|
719
|
+
while (i + 1 < lines.length) {
|
|
720
|
+
const itemMatch = lines[i + 1].match(/^\s+-\s+(.+)$/);
|
|
721
|
+
if (!itemMatch) break;
|
|
722
|
+
items.push(itemMatch[1].trim());
|
|
723
|
+
i++;
|
|
724
|
+
}
|
|
725
|
+
meta[key] = items.length > 0 ? items : "";
|
|
726
|
+
} else {
|
|
727
|
+
const wasQuoted = /^(['"])(.*)\1$/.test(rawVal);
|
|
728
|
+
const value = unquote(rawVal);
|
|
729
|
+
const num2 = Number(value);
|
|
730
|
+
meta[key] = !wasQuoted && value !== "" && !isNaN(num2) ? num2 : value;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return meta;
|
|
734
|
+
}
|
|
735
|
+
function getScalar(meta, key) {
|
|
736
|
+
const value = meta[key];
|
|
737
|
+
return typeof value === "string" || typeof value === "number" ? value : void 0;
|
|
738
|
+
}
|
|
739
|
+
function getList(meta, key) {
|
|
740
|
+
const value = meta[key];
|
|
741
|
+
return Array.isArray(value) ? value : [];
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/seed/effort-file.ts
|
|
745
|
+
function parseDiscipline(val) {
|
|
746
|
+
if (!val) return void 0;
|
|
747
|
+
const normalized = unquoteYaml(val.trim()).toLowerCase();
|
|
748
|
+
if (isEffortDiscipline(normalized)) return normalized;
|
|
749
|
+
console.warn(`[effort-file] unknown discipline '${val}' \u2014 treating as unset`);
|
|
750
|
+
return void 0;
|
|
751
|
+
}
|
|
752
|
+
function unquoteYaml(val) {
|
|
753
|
+
if (val.startsWith('"') && val.endsWith('"')) {
|
|
754
|
+
return val.slice(1, -1).replace(/\\(["\\])/g, "$1");
|
|
755
|
+
}
|
|
756
|
+
if (val.startsWith("'") && val.endsWith("'")) {
|
|
757
|
+
return val.slice(1, -1).replace(/\\'/g, "'");
|
|
758
|
+
}
|
|
759
|
+
return val;
|
|
760
|
+
}
|
|
761
|
+
function coerceNestedScalar(raw) {
|
|
762
|
+
if (raw === "true") return true;
|
|
763
|
+
if (raw === "false") return false;
|
|
764
|
+
if (raw !== "" && !Number.isNaN(Number(raw))) return Number(raw);
|
|
765
|
+
return raw;
|
|
766
|
+
}
|
|
767
|
+
function readNestedSection(lines, key) {
|
|
768
|
+
const start = lines.findIndex((l) => new RegExp(`^${key}:\\s*$`).test(l));
|
|
769
|
+
if (start === -1) return void 0;
|
|
770
|
+
const result = {};
|
|
771
|
+
let subKey = null;
|
|
772
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
773
|
+
const line = lines[i];
|
|
774
|
+
if (/^\S/.test(line)) break;
|
|
775
|
+
const trimmed = line.trim();
|
|
776
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
777
|
+
const indent = line.length - line.trimStart().length;
|
|
778
|
+
const m = trimmed.match(/^([\w.-]+):\s*(.*)$/);
|
|
779
|
+
if (!m) continue;
|
|
780
|
+
const [, k, rawVal] = m;
|
|
781
|
+
if (indent === 2) {
|
|
782
|
+
if (rawVal === "") {
|
|
783
|
+
subKey = k;
|
|
784
|
+
result[k] = {};
|
|
785
|
+
} else {
|
|
786
|
+
subKey = null;
|
|
787
|
+
result[k] = coerceNestedScalar(rawVal);
|
|
788
|
+
}
|
|
789
|
+
} else if (indent >= 4 && subKey !== null) {
|
|
790
|
+
result[subKey][k] = coerceNestedScalar(rawVal);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
794
|
+
}
|
|
795
|
+
function parseEffortFile(raw) {
|
|
796
|
+
const blockMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
797
|
+
if (!blockMatch) return null;
|
|
798
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
799
|
+
const innerLines = blockMatch[1].split(/\r?\n/);
|
|
800
|
+
const id = getScalar(meta, "id");
|
|
801
|
+
const slug = getScalar(meta, "slug");
|
|
802
|
+
const label = getScalar(meta, "label");
|
|
803
|
+
const aliases = getList(meta, "aliases");
|
|
804
|
+
const baseAttrs = readNestedSection(innerLines, "baseAttributes");
|
|
805
|
+
const hints = readNestedSection(innerLines, "hints");
|
|
806
|
+
const derivation = readNestedSection(innerLines, "derivation");
|
|
807
|
+
const metRaw = baseAttrs?.met;
|
|
808
|
+
const met = typeof metRaw === "number" ? metRaw : Number(metRaw);
|
|
809
|
+
if (!id || !slug || !label || metRaw === void 0 || Number.isNaN(met)) return null;
|
|
810
|
+
const discipline = parseDiscipline(
|
|
811
|
+
typeof baseAttrs?.discipline === "string" ? baseAttrs.discipline : void 0
|
|
812
|
+
);
|
|
813
|
+
const intensityTier = baseAttrs?.intensityTier;
|
|
814
|
+
const trimmedBody = body.trim();
|
|
815
|
+
return {
|
|
816
|
+
id: String(id),
|
|
817
|
+
slug: String(slug),
|
|
818
|
+
label: String(label),
|
|
819
|
+
aliases,
|
|
820
|
+
baseAttributes: {
|
|
821
|
+
met,
|
|
822
|
+
...discipline ? { discipline } : {},
|
|
823
|
+
...intensityTier ? { intensityTier } : {}
|
|
824
|
+
},
|
|
825
|
+
registrySource: "bundled",
|
|
826
|
+
...trimmedBody ? { body: trimmedBody } : {},
|
|
827
|
+
...hints ? { hints } : {},
|
|
828
|
+
...derivation ? { derivation } : {}
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
function object(value) {
|
|
832
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
833
|
+
}
|
|
834
|
+
function positive(value) {
|
|
835
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
836
|
+
}
|
|
837
|
+
function nonnegative(value) {
|
|
838
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
839
|
+
}
|
|
840
|
+
function assertSeedManifest(value) {
|
|
841
|
+
if (!object(value) || value.schema !== SEED_SCHEMA) throw new Error("Incompatible seed schema; rebuild seed artifacts");
|
|
842
|
+
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");
|
|
843
|
+
for (const key of ["versionLabel", "commit"]) if (value[key] !== void 0 && typeof value[key] !== "string") throw new Error("invalid seed manifest metadata");
|
|
844
|
+
if (value.dirty !== void 0 && typeof value.dirty !== "boolean") throw new Error("invalid seed manifest metadata");
|
|
845
|
+
const ids = /* @__PURE__ */ new Set(), paths = /* @__PURE__ */ new Set();
|
|
846
|
+
for (const chunk of value.chunks) {
|
|
847
|
+
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");
|
|
848
|
+
ids.add(chunk.id);
|
|
849
|
+
paths.add(chunk.path);
|
|
850
|
+
}
|
|
851
|
+
return value;
|
|
852
|
+
}
|
|
853
|
+
function assertSeedRows(value) {
|
|
854
|
+
if (!Array.isArray(value)) throw new Error("seed chunk is not an array");
|
|
855
|
+
const paths = /* @__PURE__ */ new Set();
|
|
856
|
+
for (const row of value) {
|
|
857
|
+
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");
|
|
858
|
+
paths.add(row.path);
|
|
859
|
+
for (const [id, block] of Object.entries(row.blocks)) {
|
|
860
|
+
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");
|
|
861
|
+
}
|
|
862
|
+
validateNoteBlocks(row.content, row.blocks, 1, { registeredWidgets: SEED_REGISTERED_WIDGETS });
|
|
863
|
+
const indexed = /* @__PURE__ */ new Set();
|
|
864
|
+
const sections = parseDocumentSections(row.content, { registeredWidgets: SEED_REGISTERED_WIDGETS });
|
|
865
|
+
for (const projection of row.blockIndex) {
|
|
866
|
+
if (!object(projection) || typeof projection.blockId !== "string") throw new Error("invalid seed projection");
|
|
867
|
+
const block = row.blocks[projection.blockId];
|
|
868
|
+
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");
|
|
869
|
+
if ("segmentId" in projection || "segmentVersion" in projection) throw new Error("incompatible seed projection");
|
|
870
|
+
for (const key of ["sourceId", "routeId", "blockContentId"]) if (projection[key] !== void 0 && typeof projection[key] !== "string") throw new Error("invalid seed projection metadata");
|
|
871
|
+
if (projection.position !== void 0 && !nonnegative(projection.position)) throw new Error("invalid seed projection position");
|
|
872
|
+
if (block.kind === "wod") {
|
|
873
|
+
const section = sections.find((s) => s.from === block.from && s.to === block.to);
|
|
874
|
+
if (projection.blockContentId !== blockContentId(section?.scriptBlock?.content ?? row.content.slice(Number(block.from), Number(block.to)))) throw new Error("invalid seed workout fingerprint");
|
|
875
|
+
} else if (projection.blockContentId !== void 0) throw new Error("non-workout seed fingerprint");
|
|
876
|
+
indexed.add(projection.blockId);
|
|
877
|
+
}
|
|
878
|
+
if (indexed.size !== Object.keys(row.blocks).length) throw new Error("missing actionable seed projection");
|
|
879
|
+
const links = /* @__PURE__ */ new Set();
|
|
880
|
+
for (const link of row.blockEfforts) {
|
|
881
|
+
if (!object(link) || typeof link.blockId !== "string" || typeof link.noteId !== "string" || typeof link.effortSlug !== "string" || !link.effortSlug) throw new Error("invalid seed effort identity");
|
|
882
|
+
const block = row.blocks[link.blockId];
|
|
883
|
+
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");
|
|
884
|
+
const projection = row.blockIndex.find((p) => object(p) && p.blockId === link.blockId);
|
|
885
|
+
if (!object(projection) || link.noteId !== projection.noteId || link.blockContentId !== projection.blockContentId || !Number.isFinite(link.createdAt)) throw new Error("invalid seed effort metadata");
|
|
886
|
+
links.add(String(link.id));
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return value;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/seed/runtime.ts
|
|
893
|
+
var SEED_NAMESPACE = "1b671a64-40d5-491e-99b0-da01ff1f3341";
|
|
894
|
+
async function seedNoteId(path) {
|
|
895
|
+
const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(`${SEED_NAMESPACE}:${path}`));
|
|
896
|
+
const bytes = new Uint8Array(digest.slice(0, 16));
|
|
897
|
+
bytes[6] = bytes[6] & 15 | 80;
|
|
898
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
899
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
900
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
901
|
+
}
|
|
902
|
+
function equal(a, b) {
|
|
903
|
+
if (Object.is(a, b)) return true;
|
|
904
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
|
|
905
|
+
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);
|
|
906
|
+
return ak.length === bk.length && ak.every((key) => key in b && equal(Reflect.get(a, key), Reflect.get(b, key)));
|
|
907
|
+
}
|
|
908
|
+
function titleFromPath(path) {
|
|
909
|
+
return (path.split("/").at(-1) ?? path).replace(/\.md$/, "").replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
910
|
+
}
|
|
911
|
+
function refreshedBlocks(existing, rawContent, sourceRevision) {
|
|
912
|
+
const old = existing.rawContent;
|
|
913
|
+
let from = 0, endOld = old.length, endNew = rawContent.length;
|
|
914
|
+
while (from < endOld && from < endNew && old[from] === rawContent[from]) from++;
|
|
915
|
+
while (endOld > from && endNew > from && old[endOld - 1] === rawContent[endNew - 1]) {
|
|
916
|
+
endOld--;
|
|
917
|
+
endNew--;
|
|
918
|
+
}
|
|
919
|
+
const intersect = Object.values(existing.blocks).filter((b) => from < b.to && endOld > b.from);
|
|
920
|
+
const trusted = intersect.length <= 1 ? existing.blocks : Object.fromEntries(Object.entries(existing.blocks).filter(([, b]) => !intersect.includes(b)));
|
|
921
|
+
const mapped = mapNoteBlockChanges(trusted, [{ from, to: endOld, insert: rawContent.slice(from, endNew) }]);
|
|
922
|
+
let ordinal = 0;
|
|
923
|
+
return reconcileNoteBlocks(rawContent, { sourceRevision, blocks: mapped, registeredWidgets: SEED_REGISTERED_WIDGETS, createId: () => `seed:${existing.id}:r${sourceRevision}:${ordinal++}` });
|
|
924
|
+
}
|
|
925
|
+
async function putChanged(store, values, keys) {
|
|
926
|
+
if (!values.length) return;
|
|
927
|
+
const prior = await store.getMany(keys);
|
|
928
|
+
const changed = values.filter((value, i) => !equal(value, prior[i]));
|
|
929
|
+
if (changed.length) await store.putMany(changed);
|
|
930
|
+
}
|
|
931
|
+
async function applySeedChunk(storage, write) {
|
|
932
|
+
assertSeedManifest(write.manifest);
|
|
933
|
+
assertSeedRows(write.rows);
|
|
934
|
+
const result = { totalNotes: 0, totalEfforts: 0, totalBlocks: 0, skippedUserOwned: 0, deleted: 0, addedNotes: 0, updatedNotes: 0 };
|
|
935
|
+
const ids = await Promise.all(write.rows.map((row) => seedNoteId(row.path)));
|
|
936
|
+
const pagesBySlug = /* @__PURE__ */ new Map();
|
|
937
|
+
const linkIds = /* @__PURE__ */ new Map();
|
|
938
|
+
const tagIds = /* @__PURE__ */ new Map();
|
|
939
|
+
for (const [i, row] of write.rows.entries()) {
|
|
940
|
+
const slug = readFrontmatterMeta(row.content)["slug"];
|
|
941
|
+
if (typeof slug === "string" && slug.trim()) {
|
|
942
|
+
pagesBySlug.set(slug, await seedNoteId(`page:${slug}`));
|
|
943
|
+
linkIds.set(ids[i], await seedNoteId(`pagenote:${slug}:${row.path}`));
|
|
944
|
+
}
|
|
945
|
+
for (const { label } of extractTypedTags(row.content)) {
|
|
946
|
+
if (!tagIds.has(label)) tagIds.set(label, await seedNoteId(`tag:${label}`));
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return storage.transaction(["notes", "block_revisions", "block_index", "block_efforts", "page", "page_notes", "tags", "note_tags", "efforts", "meta"], "readwrite", async (tx) => {
|
|
950
|
+
const metaStore = tx.readwrite("meta");
|
|
951
|
+
const actualMeta = await metaStore.get(write.meta.key);
|
|
952
|
+
if (!equal(actualMeta, write.expectedMeta)) throw new Error("Seed checkpoint changed during import");
|
|
953
|
+
if (write.meta.schema !== SEED_SCHEMA || write.manifest.schema !== SEED_SCHEMA) throw new Error("Incompatible seed schema; recreate artifacts");
|
|
954
|
+
const noteStore = tx.readwrite("notes");
|
|
955
|
+
const priorIds = write.chunkId ? write.expectedMeta?.chunks[write.chunkId]?.noteIds ?? [] : [];
|
|
956
|
+
const declared = new Set(ids);
|
|
957
|
+
const goneIds = priorIds.filter((id) => !declared.has(id));
|
|
958
|
+
const allIds = [.../* @__PURE__ */ new Set([...ids, ...goneIds])];
|
|
959
|
+
const stored = await noteStore.getMany(allIds);
|
|
960
|
+
const existingById = new Map(allIds.map((id, i) => [id, stored[i]]));
|
|
961
|
+
const ownedIds = allIds.filter((id) => {
|
|
962
|
+
const note = existingById.get(id);
|
|
963
|
+
return !note || note.seedOrigin === "seed" && (declared.has(id) || note.seedChunkId === write.chunkId);
|
|
964
|
+
});
|
|
965
|
+
const owned = new Set(ownedIds);
|
|
966
|
+
const revisionStore = tx.readwrite("block_revisions");
|
|
967
|
+
const historyIds = ids.filter((id, i) => owned.has(id) && existingById.get(id)?.rawContent !== write.rows[i].content);
|
|
968
|
+
const histories = await revisionStore.getAllFromIndexMany("by-note", historyIds);
|
|
969
|
+
const history = new Map(historyIds.map((id, i) => [id, histories[i]]));
|
|
970
|
+
const indexStore = tx.readwrite("block_index"), effortLinks = tx.readwrite("block_efforts");
|
|
971
|
+
const tagLinks = tx.readwrite("note_tags"), pageLinks = tx.readwrite("page_notes");
|
|
972
|
+
const currentIndexes = await indexStore.getAllFromIndexMany("by-note", ownedIds);
|
|
973
|
+
const currentEfforts = await effortLinks.getAllFromIndexMany("by-note", ownedIds);
|
|
974
|
+
const currentTags = await tagLinks.getAllFromIndexMany("by-note", ownedIds);
|
|
975
|
+
const currentPages = await pageLinks.getAllFromIndexMany("by-note", ownedIds);
|
|
976
|
+
const slot = new Map(ownedIds.map((id, i) => [id, i]));
|
|
977
|
+
const tagsStore = tx.readwrite("tags");
|
|
978
|
+
const labels = [...tagIds.keys()];
|
|
979
|
+
const matches = await tagsStore.getAllFromIndexMany("by-label", labels);
|
|
980
|
+
const tagsByLabel = new Map(labels.map((label, i) => [label, matches[i][0]]));
|
|
981
|
+
const changedTags = [], changedNotes = [], revisions = [], pages = [], pageNotes = [], noteTags = [];
|
|
982
|
+
const blocks = [], blockEfforts = [];
|
|
983
|
+
for (const [i, row] of write.rows.entries()) {
|
|
984
|
+
const id = ids[i], existing = existingById.get(id);
|
|
985
|
+
if (!owned.has(id)) {
|
|
986
|
+
result.skippedUserOwned++;
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
if (!existing) result.addedNotes++;
|
|
990
|
+
else if (existing.rawContent !== row.content) result.updatedNotes++;
|
|
991
|
+
validateNoteBlocks(row.content, row.blocks, 1, { registeredWidgets: SEED_REGISTERED_WIDGETS });
|
|
992
|
+
const sourceRevision = existing ? existing.sourceRevision + (existing.rawContent === row.content ? 0 : 1) : 1;
|
|
993
|
+
let ordinal = 0;
|
|
994
|
+
const retainedHistory = history.get(id) ?? [];
|
|
995
|
+
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);
|
|
996
|
+
for (const block of Object.values(map)) {
|
|
997
|
+
const saved = (history.get(id) ?? []).filter((r) => r.blockId === block.id).sort((a, b) => b.version - a.version)[0];
|
|
998
|
+
const source = row.content.slice(block.from, block.to);
|
|
999
|
+
if (existing?.rawContent === row.content) continue;
|
|
1000
|
+
const same = saved && saved.kind === block.kind && saved.rawContent === source && saved.dialect === block.dialect && saved.sport === block.sport;
|
|
1001
|
+
block.version = !existing ? 1 : same ? saved.version : (saved?.version ?? 0) + 1;
|
|
1002
|
+
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 } : {} });
|
|
1003
|
+
}
|
|
1004
|
+
validateNoteBlocks(row.content, map, sourceRevision, { registeredWidgets: SEED_REGISTERED_WIDGETS });
|
|
1005
|
+
const fm = readFrontmatterMeta(row.content);
|
|
1006
|
+
const title = typeof fm["title"] === "string" && fm["title"].trim() ? fm["title"].trim() : titleFromPath(row.path);
|
|
1007
|
+
const sourceId = write.chunkId ? seedNoteSourceId(row.path, row.content, write.chunkId) : void 0;
|
|
1008
|
+
if (!sourceId) throw new Error(`Unknown seed source family ${write.chunkId}`);
|
|
1009
|
+
const date = typeof fm["date"] === "string" && /^\d{4}-\d{2}-\d{2}$/.test(fm["date"]) ? Date.parse(`${fm["date"]}T12:00:00Z`) : void 0;
|
|
1010
|
+
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 };
|
|
1011
|
+
if (date === void 0) delete note.date;
|
|
1012
|
+
if (!equal(note, existing)) changedNotes.push(note);
|
|
1013
|
+
const byRange = new Map(Object.values(map).map((b) => [`${b.from}:${b.to}`, b]));
|
|
1014
|
+
const compiledById = new Map(row.blockIndex.map((b) => [b.blockId, b]));
|
|
1015
|
+
const remap = /* @__PURE__ */ new Map();
|
|
1016
|
+
for (const compiled of row.blockIndex) {
|
|
1017
|
+
const block = byRange.get(`${compiled.from}:${compiled.to}`);
|
|
1018
|
+
if (!block || block.kind !== compiled.dataType) throw new Error("Orphan seed projection");
|
|
1019
|
+
remap.set(compiled.blockId, block.id);
|
|
1020
|
+
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) };
|
|
1021
|
+
const prior = currentIndexes[slot.get(id)].find((b) => b.id === projection.id);
|
|
1022
|
+
if (!prior || prior.isStatic === true) blocks.push(projection);
|
|
1023
|
+
}
|
|
1024
|
+
for (const compiled of row.blockEfforts) {
|
|
1025
|
+
const blockId = remap.get(compiled.blockId);
|
|
1026
|
+
if (!blockId || compiledById.get(compiled.blockId)?.dataType !== "wod") throw new Error("Orphan seed effort link");
|
|
1027
|
+
const link = { ...compiled, id: `${id}:${blockId}:${compiled.effortSlug}`, noteId: id, blockId, sourcePath: row.path };
|
|
1028
|
+
const prior = currentEfforts[slot.get(id)].find((b) => b.id === link.id);
|
|
1029
|
+
if (!prior || prior.isStatic === true) blockEfforts.push(prior ? { ...link, createdAt: prior.createdAt } : link);
|
|
1030
|
+
}
|
|
1031
|
+
const n = slot.get(id);
|
|
1032
|
+
const wantedTags = /* @__PURE__ */ new Set();
|
|
1033
|
+
for (const item of extractTypedTags(row.content)) {
|
|
1034
|
+
let tag = tagsByLabel.get(item.label);
|
|
1035
|
+
if (!tag) {
|
|
1036
|
+
tag = { id: tagIds.get(item.label), label: item.label, type: item.type, createdAt: write.meta.importedAt };
|
|
1037
|
+
changedTags.push(tag);
|
|
1038
|
+
tagsByLabel.set(item.label, tag);
|
|
1039
|
+
} else if (item.type && !tag.type) {
|
|
1040
|
+
tag = { ...tag, type: item.type };
|
|
1041
|
+
changedTags.push(tag);
|
|
1042
|
+
tagsByLabel.set(item.label, tag);
|
|
1043
|
+
}
|
|
1044
|
+
if (wantedTags.has(tag.id)) continue;
|
|
1045
|
+
wantedTags.add(tag.id);
|
|
1046
|
+
const link = currentTags[n].find((l) => l.tagId === tag.id);
|
|
1047
|
+
if (!link) noteTags.push({ id: `${id}:${tag.id}`, noteId: id, tagId: tag.id });
|
|
1048
|
+
}
|
|
1049
|
+
for (const link of currentTags[n]) if (!wantedTags.has(link.tagId)) await tagLinks.delete(link.id);
|
|
1050
|
+
const slug = fm["slug"];
|
|
1051
|
+
if (typeof slug === "string" && pagesBySlug.has(slug)) {
|
|
1052
|
+
const pageId = pagesBySlug.get(slug);
|
|
1053
|
+
pages.push({ id: pageId, slug, title: titleFromPath(row.path), createdAt: write.manifest.version });
|
|
1054
|
+
pageNotes.push({ id: linkIds.get(id), pageId, noteId: id, createdAt: write.manifest.version });
|
|
1055
|
+
}
|
|
1056
|
+
result.totalNotes++;
|
|
1057
|
+
result.totalBlocks += Object.keys(map).length;
|
|
1058
|
+
}
|
|
1059
|
+
const wantedBlocks = new Set(blocks.map((b) => b.id)), wantedEfforts = new Set(blockEfforts.map((b) => b.id));
|
|
1060
|
+
for (const rows of currentIndexes) for (const row of rows) if (row.isStatic === true && !wantedBlocks.has(row.id)) await indexStore.delete(row.id);
|
|
1061
|
+
for (const rows of currentEfforts) for (const row of rows) if (row.isStatic === true && !wantedEfforts.has(row.id)) await effortLinks.delete(row.id);
|
|
1062
|
+
for (const id of goneIds) {
|
|
1063
|
+
if (!owned.has(id) || !existingById.get(id)) continue;
|
|
1064
|
+
const n = slot.get(id);
|
|
1065
|
+
await noteStore.delete(id);
|
|
1066
|
+
for (const link of currentTags[n]) await tagLinks.delete(link.id);
|
|
1067
|
+
for (const link of currentPages[n]) await pageLinks.delete(link.id);
|
|
1068
|
+
result.deleted++;
|
|
1069
|
+
}
|
|
1070
|
+
const effortStore = tx.readwrite("efforts");
|
|
1071
|
+
if (write.chunkId === EFFORTS_CHUNK_ID) {
|
|
1072
|
+
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" }));
|
|
1073
|
+
const prior = await effortStore.getMany(efforts.map((e) => e.slug));
|
|
1074
|
+
const changed = efforts.filter((e, i) => (!prior[i] || prior[i]?.registrySource === "bundled") && !equal(e, prior[i]));
|
|
1075
|
+
if (changed.length) await effortStore.putMany(changed);
|
|
1076
|
+
result.totalEfforts = changed.length;
|
|
1077
|
+
const wantedSlugs = new Set(efforts.map((effort) => effort.slug));
|
|
1078
|
+
const removed = allIds.map((id) => {
|
|
1079
|
+
const note = existingById.get(id);
|
|
1080
|
+
return owned.has(id) && note ? parseEffortFile(note.rawContent)?.slug : void 0;
|
|
1081
|
+
}).filter((slug) => !!slug && !wantedSlugs.has(slug));
|
|
1082
|
+
const old = await effortStore.getMany(removed);
|
|
1083
|
+
for (const [i, effort] of old.entries()) if (effort?.registrySource === "bundled") await effortStore.delete(removed[i]);
|
|
1084
|
+
}
|
|
1085
|
+
if (changedNotes.length) await noteStore.putMany(changedNotes);
|
|
1086
|
+
if (revisions.length) await revisionStore.putMany(revisions);
|
|
1087
|
+
await putChanged(indexStore, blocks, blocks.map((b) => b.id));
|
|
1088
|
+
await putChanged(effortLinks, blockEfforts, blockEfforts.map((b) => b.id));
|
|
1089
|
+
if (changedTags.length) await tagsStore.putMany([...new Map(changedTags.map((t) => [t.id, t])).values()]);
|
|
1090
|
+
if (noteTags.length) await tagLinks.putMany(noteTags);
|
|
1091
|
+
await putChanged(tx.readwrite("page"), pages, pages.map((p) => p.id));
|
|
1092
|
+
await putChanged(pageLinks, pageNotes, pageNotes.map((p) => p.id));
|
|
1093
|
+
await metaStore.put(write.meta);
|
|
1094
|
+
return result;
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
22
1097
|
|
|
23
|
-
export { BUILD_TIME, GIT_SHA, SEMVER, VERSION };
|
|
1098
|
+
export { BUILD_TIME, CANVAS_CHUNK_ID, DASHBOARDS_CHUNK_ID, EFFORTS_CHUNK_ID, GIT_SHA, NOTE_ID_FRONTMATTER_KEY, SEED_BROADCAST_CHANNEL, SEED_REGISTERED_WIDGETS, SEED_SCHEMA, SEMVER, SESSION_CSV_HEADER, TEMPLATE_CHUNK_ID, VERSION, applySeedChunk, assertSeedManifest, assertSeedRows, buildStatementTree, canvasRouteSlug, catalogForChunkId, composeNoteMarkdown, createIRFile, csvToEquipmentLabels, csvToSessionRows, defineLanguagePack, effortToMarkdown, emptySeedMeta, equipmentToCsv, extractNoteIdentity, extractTypedTags, getRegisteredLanguagePacks, inMemoryEventStore, injectIdentityFrontmatter, isIRFile, listLanguagePacks, markdownToEffort, parseCsvLine, readFrontmatterMeta, registerLanguagePack, seedMetaKey, seedNoteId, seedNoteSourceId, sessionToCsv, splitCsvRecords, statementToNode, unregisterLanguagePack };
|
|
24
1099
|
//# sourceMappingURL=index.mjs.map
|
|
25
1100
|
//# sourceMappingURL=index.mjs.map
|