@bitcobblers/wod-wiki-engine 0.11.43 → 0.35.2116
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/wod.cjs +2761 -232
- package/dist/bin/wod.cjs.map +1 -1
- package/dist/bin/wod.mjs +7 -1
- package/dist/bin/wod.mjs.map +1 -1
- package/dist/chunk-6L5ZDZJ7.mjs +15 -0
- package/dist/chunk-6L5ZDZJ7.mjs.map +1 -0
- package/dist/chunk-B2OYLNEZ.mjs +2722 -0
- package/dist/chunk-B2OYLNEZ.mjs.map +1 -0
- package/dist/compiler-KTBR6OEL.mjs +787 -0
- package/dist/compiler-KTBR6OEL.mjs.map +1 -0
- package/dist/index.cjs +2816 -251
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +146 -29
- package/dist/index.d.ts +146 -29
- package/dist/index.mjs +15 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -4
- package/dist/chunk-QFWJKM45.mjs +0 -1041
- package/dist/chunk-QFWJKM45.mjs.map +0 -1
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import './chunk-6L5ZDZJ7.mjs';
|
|
3
|
+
import { parseScript, isEffortDiscipline } from '@bitcobblers/wod-wiki-lang';
|
|
4
|
+
import { parseDocumentSections } from '@bitcobblers/wod-wiki-core';
|
|
5
|
+
import { parseDashboardNote, buildDashboardDocument, isDashboardWidgetType, resolveWidgetType, parseQuery } from '@bitcobblers/wod-wiki-wql';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, readdirSync } from 'fs';
|
|
8
|
+
import { join, relative } from 'path';
|
|
9
|
+
import { createHash } from 'crypto';
|
|
10
|
+
|
|
11
|
+
if (typeof globalThis.require === 'undefined') {
|
|
12
|
+
try { globalThis.require = createRequire(import.meta.url); } catch {}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/cli/seedbuild/contract.ts
|
|
16
|
+
var SEED_SCHEMA = 7;
|
|
17
|
+
var TEMPLATE_CHUNK_ID = "template";
|
|
18
|
+
var DASHBOARDS_CHUNK_ID = "dashboards";
|
|
19
|
+
var BLOCK_INDEX_CHUNK_PREFIX = "block-index.";
|
|
20
|
+
var BLOCK_EFFORTS_CHUNK_PREFIX = "block-efforts.";
|
|
21
|
+
function canvasRouteSlug(content) {
|
|
22
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
23
|
+
if (fm?.[1] && !/^template:\s*canvas\s*$/m.test(fm[1])) return null;
|
|
24
|
+
const route = fm?.[1]?.match(/^route:\s*(\S+)\s*$/m)?.[1];
|
|
25
|
+
if (!route) return null;
|
|
26
|
+
return route.replace(/^\//, "") || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/cli/seedbuild/effort-slugs.ts
|
|
30
|
+
function extractEffortSlugs(statements) {
|
|
31
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
32
|
+
for (const statement of statements) {
|
|
33
|
+
if (statement.exerciseId) {
|
|
34
|
+
slugs.add(statement.exerciseId);
|
|
35
|
+
} else {
|
|
36
|
+
const effortMetric = statement.metrics?.getMetric?.("effort");
|
|
37
|
+
const effort = effortMetric && typeof effortMetric.value === "string" ? effortMetric.value : void 0;
|
|
38
|
+
if (effort) {
|
|
39
|
+
const slug = effort.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
40
|
+
if (slug) slugs.add(slug);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return Array.from(slugs);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/cli/seedbuild/filename.ts
|
|
48
|
+
var FILE_TITLE_STOP_WORDS = {
|
|
49
|
+
and: true,
|
|
50
|
+
or: true,
|
|
51
|
+
of: true,
|
|
52
|
+
for: true,
|
|
53
|
+
to: true,
|
|
54
|
+
in: true,
|
|
55
|
+
on: true,
|
|
56
|
+
at: true,
|
|
57
|
+
by: true,
|
|
58
|
+
with: true,
|
|
59
|
+
the: true,
|
|
60
|
+
a: true,
|
|
61
|
+
an: true
|
|
62
|
+
};
|
|
63
|
+
function fileToDisplayName(filename) {
|
|
64
|
+
const base = filename.replace(/\.md$/, "");
|
|
65
|
+
if (base.toUpperCase() === "README") return "Overview";
|
|
66
|
+
const cleaned = base.replace(/^day-\d+-/, "");
|
|
67
|
+
const tokens = cleaned.split(/[-_]/);
|
|
68
|
+
const titled = [];
|
|
69
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
70
|
+
const token = tokens[i];
|
|
71
|
+
const next = tokens[i + 1];
|
|
72
|
+
const after = tokens[i + 2];
|
|
73
|
+
const prev = tokens[i - 1];
|
|
74
|
+
if (/^\d{1,2}$/.test(token) && /^\d{1,2}$/.test(next ?? "") && !/^\d+$/.test(prev ?? "") && !/^\d+$/.test(after ?? "")) {
|
|
75
|
+
titled.push(`${token}.${next}`);
|
|
76
|
+
i++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (titled.length > 0 && FILE_TITLE_STOP_WORDS[token.toLowerCase()]) {
|
|
80
|
+
titled.push(token.toLowerCase());
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
titled.push(token.charAt(0).toUpperCase() + token.slice(1));
|
|
84
|
+
}
|
|
85
|
+
return titled.join(" ");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/cli/seedbuild/frontmatter.ts
|
|
89
|
+
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
90
|
+
function unquote(value) {
|
|
91
|
+
return value.replace(
|
|
92
|
+
/^(['"])(.*)\1$/,
|
|
93
|
+
(_match, quote, inner) => quote === '"' ? inner.replace(/\\(["\\])/g, "$1") : inner
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
function parseFrontmatter(raw) {
|
|
97
|
+
const match = raw.match(FRONTMATTER_RE);
|
|
98
|
+
if (!match) return { meta: {}, body: raw };
|
|
99
|
+
return { meta: parseMetaLines(match[1].split(/\r?\n/)), body: raw.slice(match[0].length) };
|
|
100
|
+
}
|
|
101
|
+
function parseMetaLines(lines) {
|
|
102
|
+
const meta = {};
|
|
103
|
+
for (let i = 0; i < lines.length; i++) {
|
|
104
|
+
const keyMatch = lines[i].match(/^([A-Za-z][\w.-]*)\s*:(.*)$/);
|
|
105
|
+
if (!keyMatch) continue;
|
|
106
|
+
const key = keyMatch[1];
|
|
107
|
+
const rawVal = keyMatch[2].trim();
|
|
108
|
+
if (rawVal === "") {
|
|
109
|
+
const items = [];
|
|
110
|
+
while (i + 1 < lines.length) {
|
|
111
|
+
const itemMatch = lines[i + 1].match(/^\s+-\s+(.+)$/);
|
|
112
|
+
if (!itemMatch) break;
|
|
113
|
+
items.push(itemMatch[1].trim());
|
|
114
|
+
i++;
|
|
115
|
+
}
|
|
116
|
+
meta[key] = items.length > 0 ? items : "";
|
|
117
|
+
} else {
|
|
118
|
+
const wasQuoted = /^(['"])(.*)\1$/.test(rawVal);
|
|
119
|
+
const value = unquote(rawVal);
|
|
120
|
+
const num = Number(value);
|
|
121
|
+
meta[key] = !wasQuoted && value !== "" && !isNaN(num) ? num : value;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return meta;
|
|
125
|
+
}
|
|
126
|
+
function getScalar(meta, key) {
|
|
127
|
+
const value = meta[key];
|
|
128
|
+
return typeof value === "string" || typeof value === "number" ? value : void 0;
|
|
129
|
+
}
|
|
130
|
+
function getList(meta, key) {
|
|
131
|
+
const value = meta[key];
|
|
132
|
+
return Array.isArray(value) ? value : [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/cli/seedbuild/effort-file.ts
|
|
136
|
+
function parseDiscipline(val) {
|
|
137
|
+
if (!val) return void 0;
|
|
138
|
+
const normalized = unquoteYaml(val.trim()).toLowerCase();
|
|
139
|
+
if (isEffortDiscipline(normalized)) return normalized;
|
|
140
|
+
console.warn(`[effort-file] unknown discipline '${val}' \u2014 treating as unset`);
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
function unquoteYaml(val) {
|
|
144
|
+
if (val.startsWith('"') && val.endsWith('"')) {
|
|
145
|
+
return val.slice(1, -1).replace(/\\(["\\])/g, "$1");
|
|
146
|
+
}
|
|
147
|
+
if (val.startsWith("'") && val.endsWith("'")) {
|
|
148
|
+
return val.slice(1, -1).replace(/\\'/g, "'");
|
|
149
|
+
}
|
|
150
|
+
return val;
|
|
151
|
+
}
|
|
152
|
+
function coerceNestedScalar(raw) {
|
|
153
|
+
if (raw === "true") return true;
|
|
154
|
+
if (raw === "false") return false;
|
|
155
|
+
if (raw !== "" && !Number.isNaN(Number(raw))) return Number(raw);
|
|
156
|
+
return raw;
|
|
157
|
+
}
|
|
158
|
+
function readNestedSection(lines, key) {
|
|
159
|
+
const start = lines.findIndex((l) => new RegExp(`^${key}:\\s*$`).test(l));
|
|
160
|
+
if (start === -1) return void 0;
|
|
161
|
+
const result = {};
|
|
162
|
+
let subKey = null;
|
|
163
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
164
|
+
const line = lines[i];
|
|
165
|
+
if (/^\S/.test(line)) break;
|
|
166
|
+
const trimmed = line.trim();
|
|
167
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
168
|
+
const indent = line.length - line.trimStart().length;
|
|
169
|
+
const m = trimmed.match(/^([\w.-]+):\s*(.*)$/);
|
|
170
|
+
if (!m) continue;
|
|
171
|
+
const [, k, rawVal] = m;
|
|
172
|
+
if (indent === 2) {
|
|
173
|
+
if (rawVal === "") {
|
|
174
|
+
subKey = k;
|
|
175
|
+
result[k] = {};
|
|
176
|
+
} else {
|
|
177
|
+
subKey = null;
|
|
178
|
+
result[k] = coerceNestedScalar(rawVal);
|
|
179
|
+
}
|
|
180
|
+
} else if (indent >= 4 && subKey !== null) {
|
|
181
|
+
result[subKey][k] = coerceNestedScalar(rawVal);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
185
|
+
}
|
|
186
|
+
function parseEffortFile(raw) {
|
|
187
|
+
const blockMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
188
|
+
if (!blockMatch) return null;
|
|
189
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
190
|
+
const innerLines = blockMatch[1].split(/\r?\n/);
|
|
191
|
+
const id = getScalar(meta, "id");
|
|
192
|
+
const slug = getScalar(meta, "slug");
|
|
193
|
+
const label = getScalar(meta, "label");
|
|
194
|
+
const aliases = getList(meta, "aliases");
|
|
195
|
+
const baseAttrs = readNestedSection(innerLines, "baseAttributes");
|
|
196
|
+
const hints = readNestedSection(innerLines, "hints");
|
|
197
|
+
const derivation = readNestedSection(innerLines, "derivation");
|
|
198
|
+
const metRaw = baseAttrs?.met;
|
|
199
|
+
const met = typeof metRaw === "number" ? metRaw : Number(metRaw);
|
|
200
|
+
if (!id || !slug || !label || metRaw === void 0 || Number.isNaN(met)) return null;
|
|
201
|
+
const discipline = parseDiscipline(
|
|
202
|
+
typeof baseAttrs?.discipline === "string" ? baseAttrs.discipline : void 0
|
|
203
|
+
);
|
|
204
|
+
const intensityTier = baseAttrs?.intensityTier;
|
|
205
|
+
const trimmedBody = body.trim();
|
|
206
|
+
return {
|
|
207
|
+
id: String(id),
|
|
208
|
+
slug: String(slug),
|
|
209
|
+
label: String(label),
|
|
210
|
+
aliases,
|
|
211
|
+
baseAttributes: {
|
|
212
|
+
met,
|
|
213
|
+
...discipline ? { discipline } : {},
|
|
214
|
+
...intensityTier ? { intensityTier } : {}
|
|
215
|
+
},
|
|
216
|
+
registrySource: "bundled",
|
|
217
|
+
...trimmedBody ? { body: trimmedBody } : {},
|
|
218
|
+
...hints ? { hints } : {},
|
|
219
|
+
...derivation ? { derivation } : {}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
var UNKNOWN_GIT_VERSION = {
|
|
223
|
+
version: 0,
|
|
224
|
+
versionLabel: "unknown",
|
|
225
|
+
commit: "",
|
|
226
|
+
dirty: false
|
|
227
|
+
};
|
|
228
|
+
function resolveGitVersion(repoRoot) {
|
|
229
|
+
try {
|
|
230
|
+
const run = (args) => execSync(`git ${args}`, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
231
|
+
const count = Number(run("rev-list --count HEAD"));
|
|
232
|
+
return {
|
|
233
|
+
version: Number.isFinite(count) && count > 0 ? count : 0,
|
|
234
|
+
versionLabel: run("describe --tags --long --always --dirty"),
|
|
235
|
+
commit: run("rev-parse HEAD"),
|
|
236
|
+
dirty: run("status --porcelain") !== ""
|
|
237
|
+
};
|
|
238
|
+
} catch {
|
|
239
|
+
return UNKNOWN_GIT_VERSION;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function escapeHtml(s) {
|
|
243
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
244
|
+
}
|
|
245
|
+
function rowHtml(c) {
|
|
246
|
+
const kind = c.kind ?? "notes";
|
|
247
|
+
return `<tr><td>${escapeHtml(c.id)}</td><td>${kind}</td><td>${c.count}</td><td>${(c.bytes / 1024).toFixed(1)} KB</td><td><code>${c.sha256.slice(0, 8)}</code></td></tr>`;
|
|
248
|
+
}
|
|
249
|
+
function writeBuildReport(outDir, data) {
|
|
250
|
+
const { manifest, totalMs, passMs, fileCount } = data;
|
|
251
|
+
const totalBytes = manifest.chunks.reduce((s, c) => s + c.bytes, 0);
|
|
252
|
+
const dirtyBadge = manifest.dirty ? ' <strong style="color:#b45309">(dirty working tree \u2014 not reproducible from <code>' + escapeHtml(manifest.commit ?? "") + "</code>)</strong>" : "";
|
|
253
|
+
const html = `<!doctype html>
|
|
254
|
+
<html lang="en">
|
|
255
|
+
<head>
|
|
256
|
+
<meta charset="utf-8" />
|
|
257
|
+
<title>Seed build report</title>
|
|
258
|
+
<style>
|
|
259
|
+
body { font-family: system-ui, sans-serif; max-width: 960px; margin: 2rem auto; padding: 0 1rem; color: #1f2937; }
|
|
260
|
+
h1 { font-size: 1.4rem; } h2 { font-size: 1.1rem; margin-top: 2rem; }
|
|
261
|
+
table { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
|
|
262
|
+
th, td { text-align: left; padding: 0.3rem 0.6rem; border-bottom: 1px solid #e5e7eb; }
|
|
263
|
+
code { font-size: 0.85em; }
|
|
264
|
+
dl { display: grid; grid-template-columns: max-content 1fr; gap: 0.2rem 1rem; font-size: 0.95rem; }
|
|
265
|
+
dt { font-weight: 600; }
|
|
266
|
+
</style>
|
|
267
|
+
</head>
|
|
268
|
+
<body>
|
|
269
|
+
<h1>Seed build report</h1>
|
|
270
|
+
<h2>Version</h2>
|
|
271
|
+
<dl>
|
|
272
|
+
<dt>version</dt><dd>${manifest.version} <code>${escapeHtml(manifest.versionLabel ?? "")}</code>${dirtyBadge}</dd>
|
|
273
|
+
<dt>commit</dt><dd><code>${escapeHtml(manifest.commit ?? "unknown")}</code></dd>
|
|
274
|
+
<dt>built at</dt><dd>${escapeHtml(manifest.builtAt)}</dd>
|
|
275
|
+
<dt>schema</dt><dd>v${manifest.schema}</dd>
|
|
276
|
+
</dl>
|
|
277
|
+
<h2>Benchmarks</h2>
|
|
278
|
+
<dl>
|
|
279
|
+
<dt>total</dt><dd>${totalMs} ms</dd>
|
|
280
|
+
<dt>corpus scan + partition</dt><dd>${passMs.scan} ms (${fileCount} files)</dd>
|
|
281
|
+
<dt>block-index derivation</dt><dd>${passMs.blockIndex} ms</dd>
|
|
282
|
+
<dt>block-efforts derivation</dt><dd>${passMs.blockEfforts} ms</dd>
|
|
283
|
+
<dt>chunk writes</dt><dd>${passMs.write} ms</dd>
|
|
284
|
+
</dl>
|
|
285
|
+
<h2>Chunks (${manifest.chunks.length}, ${(totalBytes / 1024).toFixed(0)} KB total)</h2>
|
|
286
|
+
<table>
|
|
287
|
+
<thead><tr><th>id</th><th>kind</th><th>rows</th><th>size</th><th>sha256</th></tr></thead>
|
|
288
|
+
<tbody>
|
|
289
|
+
${manifest.chunks.map(rowHtml).join("\n")}
|
|
290
|
+
</tbody>
|
|
291
|
+
</table>
|
|
292
|
+
</body>
|
|
293
|
+
</html>
|
|
294
|
+
`;
|
|
295
|
+
const file = join(outDir, "report.html");
|
|
296
|
+
writeFileSync(file, html, "utf8");
|
|
297
|
+
return file;
|
|
298
|
+
}
|
|
299
|
+
function listMarkdownFiles(dir, prefix = "") {
|
|
300
|
+
if (!existsSync(dir)) return [];
|
|
301
|
+
const out = [];
|
|
302
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
303
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
304
|
+
if (entry.isDirectory()) out.push(...listMarkdownFiles(join(dir, entry.name), rel));
|
|
305
|
+
else if (entry.isFile() && entry.name.endsWith(".md")) out.push(rel);
|
|
306
|
+
}
|
|
307
|
+
return out.sort();
|
|
308
|
+
}
|
|
309
|
+
function chunkIdFor(relPath) {
|
|
310
|
+
const parts = relPath.split("/");
|
|
311
|
+
if (parts[0] !== "markdown" || parts.length < 2) return null;
|
|
312
|
+
const corpus = parts[1];
|
|
313
|
+
if (parts.length === 2) return null;
|
|
314
|
+
const slug = parts[2]?.replace(/\.md$/, "");
|
|
315
|
+
switch (corpus) {
|
|
316
|
+
case "canvas":
|
|
317
|
+
case "dashboards":
|
|
318
|
+
case "efforts":
|
|
319
|
+
return corpus;
|
|
320
|
+
case "collections":
|
|
321
|
+
return parts.length === 3 ? `collection._root` : `collection.${slug}`;
|
|
322
|
+
case "feeds":
|
|
323
|
+
return parts.length === 3 ? `feed._root` : `feed.${slug}`;
|
|
324
|
+
default:
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function collectCorpusMarkdown(markdownDir) {
|
|
329
|
+
return listMarkdownFiles(markdownDir).map((p) => `markdown/${p}`);
|
|
330
|
+
}
|
|
331
|
+
function partitionIntoChunks(paths) {
|
|
332
|
+
const byId = /* @__PURE__ */ new Map();
|
|
333
|
+
for (const p of paths) {
|
|
334
|
+
const id = chunkIdFor(p);
|
|
335
|
+
if (id === null) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`[generate-seed] Parity failure: '${p}' is not part of any known corpus (canvas | dashboards | efforts | collections | feeds). Extend chunkIdFor() if this is a new corpus.`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
const list = byId.get(id);
|
|
341
|
+
if (list) list.push(p);
|
|
342
|
+
else byId.set(id, [p]);
|
|
343
|
+
}
|
|
344
|
+
return [...byId.entries()].sort(([a], [b]) => a < b ? -1 : 1).map(([id, files]) => ({ id, files }));
|
|
345
|
+
}
|
|
346
|
+
function validateDashboardNote(raw) {
|
|
347
|
+
const problems = [];
|
|
348
|
+
const { meta } = parseFrontmatter(raw);
|
|
349
|
+
const { sections } = parseDashboardNote(raw);
|
|
350
|
+
const doc = buildDashboardDocument(sections, meta);
|
|
351
|
+
if (!doc.isDashboard) return ["file is not a dashboard note (missing dashboard frontmatter)"];
|
|
352
|
+
if (doc.title === void 0) problems.push("dashboard has no title");
|
|
353
|
+
if (doc.widgets.length < 3) problems.push(`dashboard has only ${doc.widgets.length} widgets (the locked format has \u2265 3)`);
|
|
354
|
+
const declared = new Set(doc.tokens.map((t) => t.name));
|
|
355
|
+
for (const widget of doc.widgets) {
|
|
356
|
+
const where = `widget '${widget.title ?? widget.body.slice(0, 40)}'`;
|
|
357
|
+
if (widget.title === void 0) problems.push(`${where}: missing title`);
|
|
358
|
+
if (!isDashboardWidgetType(resolveWidgetType(widget.type))) {
|
|
359
|
+
problems.push(`${where}: unknown widget type '${widget.type}'`);
|
|
360
|
+
}
|
|
361
|
+
const parsed = parseQuery(widget.body);
|
|
362
|
+
if (parsed.error !== void 0) problems.push(`${where}: WQL parse error: ${parsed.error}`);
|
|
363
|
+
for (const ref of [
|
|
364
|
+
...widget.body.matchAll(/\$([A-Za-z][\w-]*)/g),
|
|
365
|
+
...Object.values(widget.attributes).flatMap((v) => [...String(v).matchAll(/\$([A-Za-z][\w-]*)/g)])
|
|
366
|
+
]) {
|
|
367
|
+
if (!declared.has(ref[1])) problems.push(`${where}: undeclared token $${ref[1]}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return problems;
|
|
371
|
+
}
|
|
372
|
+
function gateDashboards(groups, corpusRoot) {
|
|
373
|
+
for (const group of groups) {
|
|
374
|
+
if (group.id !== DASHBOARDS_CHUNK_ID) continue;
|
|
375
|
+
for (const p of group.files) {
|
|
376
|
+
const problems = validateDashboardNote(readFileSync(join(corpusRoot, p), "utf8"));
|
|
377
|
+
if (problems.length > 0) {
|
|
378
|
+
throw new Error(`[generate-seed] Dashboard validation failed for '${p}':
|
|
379
|
+
- ${problems.join("\n - ")}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function toSegmentDataType(section) {
|
|
385
|
+
switch (section.type) {
|
|
386
|
+
case "time":
|
|
387
|
+
case "log":
|
|
388
|
+
return "wod";
|
|
389
|
+
case "title": {
|
|
390
|
+
const l = section.level ?? 1;
|
|
391
|
+
return `h${Math.min(Math.max(l, 1), 6)}`;
|
|
392
|
+
}
|
|
393
|
+
case "frontmatter":
|
|
394
|
+
return "frontmatter";
|
|
395
|
+
case "markdown":
|
|
396
|
+
case "widget":
|
|
397
|
+
case "embed":
|
|
398
|
+
case "code":
|
|
399
|
+
return "markdown";
|
|
400
|
+
default:
|
|
401
|
+
return "markdown";
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function getFileCreatedAt(file, repoRoot) {
|
|
405
|
+
try {
|
|
406
|
+
const out = execSync(`git log -1 --format=%ct -- '${file}'`, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
407
|
+
const ts = Number(out) * 1e3;
|
|
408
|
+
return Number.isFinite(ts) && ts > 0 ? ts : 0;
|
|
409
|
+
} catch {
|
|
410
|
+
return 0;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function feedDateToCreatedAt(dateKey) {
|
|
414
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateKey)) return 0;
|
|
415
|
+
const ts = Date.parse(`${dateKey}T00:00:00Z`);
|
|
416
|
+
return Number.isNaN(ts) ? 0 : ts;
|
|
417
|
+
}
|
|
418
|
+
var BLOCK_INDEX_ROWS_PER_CHUNK = 4e3;
|
|
419
|
+
function buildBlockIndexRows(markdownDir, corpusRoot) {
|
|
420
|
+
const index = [];
|
|
421
|
+
for (const file of listMarkdownFiles(join(markdownDir, "collections"))) {
|
|
422
|
+
const rel = `markdown/collections/${file}`;
|
|
423
|
+
const parts = rel.split("/");
|
|
424
|
+
if (parts.length < 4) continue;
|
|
425
|
+
const dirName = parts[parts.length - 2];
|
|
426
|
+
const fileNameExt = parts[parts.length - 1];
|
|
427
|
+
const isReadme = fileNameExt.toLowerCase() === "readme.md";
|
|
428
|
+
const fileName = fileNameExt.replace(/\.md$/, "");
|
|
429
|
+
const noteId = isReadme ? dirName : `${dirName}/${fileName}`;
|
|
430
|
+
const content = readFileSync(join(corpusRoot, rel), "utf8");
|
|
431
|
+
const headingMatch = isReadme ? content.match(/^#\s+(.+)$/m) : null;
|
|
432
|
+
const noteTitle = headingMatch?.[1]?.trim() || fileToDisplayName(isReadme ? dirName : fileNameExt);
|
|
433
|
+
const createdAt = getFileCreatedAt(rel, corpusRoot);
|
|
434
|
+
const sections = parseDocumentSections(content);
|
|
435
|
+
let position = 0;
|
|
436
|
+
for (const section of sections) {
|
|
437
|
+
const blockContentId = section.scriptBlock?.contentId ?? void 0;
|
|
438
|
+
index.push({
|
|
439
|
+
id: `static:${noteId}:${section.id}:1`,
|
|
440
|
+
noteId,
|
|
441
|
+
segmentId: section.id,
|
|
442
|
+
segmentVersion: 1,
|
|
443
|
+
position: position++,
|
|
444
|
+
dataType: toSegmentDataType(section),
|
|
445
|
+
blockContentId,
|
|
446
|
+
rawContent: section.displayContent,
|
|
447
|
+
noteTitle,
|
|
448
|
+
createdAt,
|
|
449
|
+
isStatic: true,
|
|
450
|
+
sourceId: isReadme ? `collection:${dirName}` : `collection:${noteId}`,
|
|
451
|
+
sourcePath: rel
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
for (const file of listMarkdownFiles(join(markdownDir, "feeds"))) {
|
|
456
|
+
const rel = `markdown/feeds/${file}`;
|
|
457
|
+
const parts = rel.split("/");
|
|
458
|
+
if (parts.length < 5) continue;
|
|
459
|
+
const dirName = parts[parts.length - 3];
|
|
460
|
+
const dateKey = parts[parts.length - 2];
|
|
461
|
+
const fileNameExt = parts[parts.length - 1];
|
|
462
|
+
if (fileNameExt.toLowerCase() === "readme.md") continue;
|
|
463
|
+
const fileName = fileNameExt.replace(/\.md$/, "");
|
|
464
|
+
const noteId = `feeds/${dirName}/${dateKey}/${fileName}`;
|
|
465
|
+
const noteTitle = fileToDisplayName(fileNameExt);
|
|
466
|
+
const content = readFileSync(join(corpusRoot, rel), "utf8");
|
|
467
|
+
const sections = parseDocumentSections(content);
|
|
468
|
+
let position = 0;
|
|
469
|
+
for (const section of sections) {
|
|
470
|
+
const blockContentId = section.scriptBlock?.contentId ?? void 0;
|
|
471
|
+
index.push({
|
|
472
|
+
id: `static:${noteId}:${section.id}:1`,
|
|
473
|
+
noteId,
|
|
474
|
+
segmentId: section.id,
|
|
475
|
+
segmentVersion: 1,
|
|
476
|
+
position: position++,
|
|
477
|
+
dataType: toSegmentDataType(section),
|
|
478
|
+
blockContentId,
|
|
479
|
+
rawContent: section.displayContent,
|
|
480
|
+
noteTitle,
|
|
481
|
+
createdAt: feedDateToCreatedAt(dateKey),
|
|
482
|
+
isStatic: true,
|
|
483
|
+
sourceId: `feed:${noteId}`,
|
|
484
|
+
sourcePath: rel
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
for (const file of listMarkdownFiles(join(markdownDir, "canvas"))) {
|
|
489
|
+
const rel = `markdown/canvas/${file}`;
|
|
490
|
+
const content = readFileSync(join(corpusRoot, rel), "utf8");
|
|
491
|
+
const noteId = canvasRouteSlug(content);
|
|
492
|
+
if (!noteId) continue;
|
|
493
|
+
const fileNameExt = file.split("/").pop() ?? file;
|
|
494
|
+
const noteTitle = fileToDisplayName(fileNameExt);
|
|
495
|
+
const createdAt = getFileCreatedAt(rel, corpusRoot);
|
|
496
|
+
const sections = parseDocumentSections(content);
|
|
497
|
+
let position = 0;
|
|
498
|
+
for (const section of sections) {
|
|
499
|
+
const blockContentId = section.scriptBlock?.contentId ?? void 0;
|
|
500
|
+
index.push({
|
|
501
|
+
id: `static:${noteId}:${section.id}:1`,
|
|
502
|
+
noteId,
|
|
503
|
+
segmentId: section.id,
|
|
504
|
+
segmentVersion: 1,
|
|
505
|
+
position: position++,
|
|
506
|
+
dataType: toSegmentDataType(section),
|
|
507
|
+
blockContentId,
|
|
508
|
+
rawContent: section.displayContent,
|
|
509
|
+
noteTitle,
|
|
510
|
+
createdAt,
|
|
511
|
+
isStatic: true,
|
|
512
|
+
sourceId: `guides:${noteId}`,
|
|
513
|
+
sourcePath: rel
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return index;
|
|
518
|
+
}
|
|
519
|
+
var NON_EFFORT_SLUGS = {
|
|
520
|
+
"amrap": true,
|
|
521
|
+
"emom": true,
|
|
522
|
+
"tabata": true,
|
|
523
|
+
"for-time": true,
|
|
524
|
+
"rounds": true,
|
|
525
|
+
"sets": true,
|
|
526
|
+
"repetitions": true,
|
|
527
|
+
"count-down": true,
|
|
528
|
+
"total-reps": true,
|
|
529
|
+
"warmup": true,
|
|
530
|
+
"cooldown": true,
|
|
531
|
+
"cool-down": true,
|
|
532
|
+
"transition": true,
|
|
533
|
+
"rest": true,
|
|
534
|
+
"break": true,
|
|
535
|
+
"work": true,
|
|
536
|
+
"i-suck-at-this": true,
|
|
537
|
+
"isuckatthis": true,
|
|
538
|
+
"movement": true,
|
|
539
|
+
"movement-you": true,
|
|
540
|
+
"selected-exercise": true,
|
|
541
|
+
"training-setup": true,
|
|
542
|
+
"grade-system": true,
|
|
543
|
+
"hangs": true,
|
|
544
|
+
"warm-light-wind": true,
|
|
545
|
+
"basin": true,
|
|
546
|
+
"rock-canyon": true,
|
|
547
|
+
"b-onsight": true,
|
|
548
|
+
"a-redpoint": true,
|
|
549
|
+
"d-redpoint": true,
|
|
550
|
+
"v0-flash": true,
|
|
551
|
+
"v2-flash": true,
|
|
552
|
+
"v7-redpoint": true,
|
|
553
|
+
"avgrpe-mean": true,
|
|
554
|
+
"calculate": true,
|
|
555
|
+
"totalload-sum": true,
|
|
556
|
+
"query": true,
|
|
557
|
+
"by": true,
|
|
558
|
+
"": true,
|
|
559
|
+
"-": true,
|
|
560
|
+
"...": true,
|
|
561
|
+
"x": true,
|
|
562
|
+
"s": true,
|
|
563
|
+
"bw": true
|
|
564
|
+
};
|
|
565
|
+
function formatDiscoveredLabel(raw) {
|
|
566
|
+
const clean = raw.replace(/[-_]+/g, " ").trim();
|
|
567
|
+
return clean.split(/\s+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(" ");
|
|
568
|
+
}
|
|
569
|
+
function loadExistingEffortMap(markdownDir) {
|
|
570
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
571
|
+
const aliasToCanonical = /* @__PURE__ */ new Map();
|
|
572
|
+
const effortsDir = join(markdownDir, "efforts");
|
|
573
|
+
for (const file of listMarkdownFiles(effortsDir)) {
|
|
574
|
+
const raw = readFileSync(join(effortsDir, file), "utf8");
|
|
575
|
+
const parsed = parseEffortFile(raw);
|
|
576
|
+
if (!parsed) continue;
|
|
577
|
+
slugs.add(parsed.slug);
|
|
578
|
+
aliasToCanonical.set(parsed.slug, parsed.slug);
|
|
579
|
+
aliasToCanonical.set(parsed.label.toLowerCase(), parsed.slug);
|
|
580
|
+
aliasToCanonical.set(parsed.slug.replace(/-/g, " "), parsed.slug);
|
|
581
|
+
for (const alias of parsed.aliases) {
|
|
582
|
+
aliasToCanonical.set(alias.toLowerCase(), parsed.slug);
|
|
583
|
+
aliasToCanonical.set(alias.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""), parsed.slug);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return { slugs, aliasToCanonical };
|
|
587
|
+
}
|
|
588
|
+
function discoverUncatalogedEfforts(markdownDir, corpusRoot) {
|
|
589
|
+
const { slugs, aliasToCanonical } = loadExistingEffortMap(markdownDir);
|
|
590
|
+
const blocks = buildBlockIndexRows(markdownDir, corpusRoot);
|
|
591
|
+
const discovered = /* @__PURE__ */ new Map();
|
|
592
|
+
for (const block of blocks) {
|
|
593
|
+
if (block.dataType !== "wod" || !block.rawContent) continue;
|
|
594
|
+
const script = parseScript(block.rawContent);
|
|
595
|
+
for (const statement of script.statements) {
|
|
596
|
+
const effortMetric = statement.metrics?.getMetric?.("effort");
|
|
597
|
+
const effortLabel = (effortMetric && typeof effortMetric.value === "string" ? effortMetric.value : void 0) ?? statement.exerciseId;
|
|
598
|
+
if (!effortLabel) continue;
|
|
599
|
+
const slugified = effortLabel.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
600
|
+
if (!slugified || NON_EFFORT_SLUGS[slugified]) continue;
|
|
601
|
+
const canonical = aliasToCanonical.get(slugified) ?? aliasToCanonical.get(effortLabel.toLowerCase());
|
|
602
|
+
if (canonical && slugs.has(canonical)) continue;
|
|
603
|
+
const existing = discovered.get(slugified);
|
|
604
|
+
if (existing) {
|
|
605
|
+
existing.rawLabels.add(effortLabel);
|
|
606
|
+
} else {
|
|
607
|
+
discovered.set(slugified, {
|
|
608
|
+
label: formatDiscoveredLabel(effortLabel),
|
|
609
|
+
rawLabels: /* @__PURE__ */ new Set([effortLabel])
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return Array.from(discovered.entries()).map(([slug, data]) => ({
|
|
615
|
+
slug,
|
|
616
|
+
label: data.label,
|
|
617
|
+
rawLabels: Array.from(data.rawLabels)
|
|
618
|
+
}));
|
|
619
|
+
}
|
|
620
|
+
function scaffoldDiscoveredEfforts(discovered, markdownDir) {
|
|
621
|
+
const targetDir = join(markdownDir, "efforts", "discovered");
|
|
622
|
+
if (!existsSync(targetDir)) {
|
|
623
|
+
mkdirSync(targetDir, { recursive: true });
|
|
624
|
+
}
|
|
625
|
+
const createdPaths = [];
|
|
626
|
+
for (const effort of discovered) {
|
|
627
|
+
const filePath = join(targetDir, `${effort.slug}.md`);
|
|
628
|
+
if (existsSync(filePath)) continue;
|
|
629
|
+
const aliases = effort.rawLabels.filter((l) => l.toLowerCase() !== effort.label.toLowerCase()).map((l) => ` - "${l.replace(/"/g, '\\"')}"`);
|
|
630
|
+
const content = [
|
|
631
|
+
"---",
|
|
632
|
+
`id: effort-bundled-${effort.slug}`,
|
|
633
|
+
`slug: ${effort.slug}`,
|
|
634
|
+
`label: "${effort.label.replace(/"/g, '\\"')}"`,
|
|
635
|
+
aliases.length > 0 ? `aliases:
|
|
636
|
+
${aliases.join("\n")}` : "aliases: []",
|
|
637
|
+
"baseAttributes:",
|
|
638
|
+
" met: 5.0",
|
|
639
|
+
" discipline: general",
|
|
640
|
+
" intensityTier: moderate",
|
|
641
|
+
"registrySource: bundled",
|
|
642
|
+
"---",
|
|
643
|
+
""
|
|
644
|
+
].join("\n");
|
|
645
|
+
writeFileSync(filePath, content, "utf8");
|
|
646
|
+
createdPaths.push(filePath);
|
|
647
|
+
}
|
|
648
|
+
return createdPaths;
|
|
649
|
+
}
|
|
650
|
+
function buildBlockEffortRows(markdownDir, corpusRoot) {
|
|
651
|
+
const efforts = [];
|
|
652
|
+
const { aliasToCanonical } = loadExistingEffortMap(markdownDir);
|
|
653
|
+
const blocks = buildBlockIndexRows(markdownDir, corpusRoot);
|
|
654
|
+
for (const block of blocks) {
|
|
655
|
+
if (block.dataType !== "wod" || !block.rawContent) continue;
|
|
656
|
+
const script = parseScript(block.rawContent);
|
|
657
|
+
const effortSlugs = extractEffortSlugs(script.statements);
|
|
658
|
+
for (const rawSlug of effortSlugs) {
|
|
659
|
+
const norm = rawSlug.toLowerCase().trim();
|
|
660
|
+
const canonicalSlug = aliasToCanonical.get(norm) ?? aliasToCanonical.get(norm.replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "")) ?? norm.replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
661
|
+
if (!canonicalSlug || NON_EFFORT_SLUGS[canonicalSlug] || NON_EFFORT_SLUGS[norm]) {
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
efforts.push({
|
|
665
|
+
id: `${block.id}:${canonicalSlug}`,
|
|
666
|
+
noteId: block.noteId,
|
|
667
|
+
blockId: block.segmentId,
|
|
668
|
+
blockContentId: block.blockContentId,
|
|
669
|
+
effortSlug: canonicalSlug,
|
|
670
|
+
isStatic: block.isStatic,
|
|
671
|
+
sourcePath: block.sourcePath,
|
|
672
|
+
createdAt: block.createdAt
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return efforts;
|
|
677
|
+
}
|
|
678
|
+
function buildStaticParentMap(blockRows) {
|
|
679
|
+
const map = {};
|
|
680
|
+
for (const row of blockRows) {
|
|
681
|
+
if (!row.sourcePath) continue;
|
|
682
|
+
const prev = map[row.noteId];
|
|
683
|
+
if (prev && prev.sourcePath !== row.sourcePath) {
|
|
684
|
+
throw new Error(
|
|
685
|
+
`[generate-seed] ambiguous static parent '${row.noteId}' maps to both '${prev.sourcePath}' and '${row.sourcePath}'`
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
map[row.noteId] = { sourcePath: row.sourcePath, sourceId: row.sourceId };
|
|
689
|
+
}
|
|
690
|
+
return map;
|
|
691
|
+
}
|
|
692
|
+
function sha256Hex(data) {
|
|
693
|
+
return createHash("sha256").update(data).digest("hex");
|
|
694
|
+
}
|
|
695
|
+
function generateSeed(options = {}) {
|
|
696
|
+
const t0 = Date.now();
|
|
697
|
+
const passMs = { scan: 0, blockIndex: 0, blockEfforts: 0, write: 0 };
|
|
698
|
+
const corpusRoot = options.corpusRoot ?? process.cwd();
|
|
699
|
+
const markdownDir = options.markdownDir ?? join(corpusRoot, "markdown");
|
|
700
|
+
const outDir = options.outDir ?? join(corpusRoot, "dist");
|
|
701
|
+
const gitVersion = options.version !== void 0 ? { ...resolveGitVersion(corpusRoot), version: options.version } : resolveGitVersion(corpusRoot);
|
|
702
|
+
if (options.scaffoldDiscoveredEfforts) {
|
|
703
|
+
const discovered = discoverUncatalogedEfforts(markdownDir, corpusRoot);
|
|
704
|
+
if (discovered.length > 0) {
|
|
705
|
+
scaffoldDiscoveredEfforts(discovered, markdownDir);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const tScan = Date.now();
|
|
709
|
+
const paths = collectCorpusMarkdown(markdownDir);
|
|
710
|
+
const groups = partitionIntoChunks(paths);
|
|
711
|
+
gateDashboards(groups, corpusRoot);
|
|
712
|
+
if (options.templateFile && existsSync(options.templateFile)) {
|
|
713
|
+
const templateRowPath = options.templateRowPath ?? relative(corpusRoot, options.templateFile).split("\\").join("/");
|
|
714
|
+
groups.push({ id: TEMPLATE_CHUNK_ID, files: [templateRowPath] });
|
|
715
|
+
}
|
|
716
|
+
passMs.scan = Date.now() - tScan;
|
|
717
|
+
const chunksDir = join(outDir, "chunks");
|
|
718
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
719
|
+
mkdirSync(chunksDir, { recursive: true });
|
|
720
|
+
const manifestChunks = [];
|
|
721
|
+
let totalFiles = 0;
|
|
722
|
+
const tWrite = Date.now();
|
|
723
|
+
for (const { id, files } of groups) {
|
|
724
|
+
const templateSource = id === TEMPLATE_CHUNK_ID ? options.templateFile : void 0;
|
|
725
|
+
const rows = files.map((p) => ({
|
|
726
|
+
path: p,
|
|
727
|
+
content: readFileSync(templateSource ?? join(corpusRoot, p), "utf8")
|
|
728
|
+
}));
|
|
729
|
+
const bytes = Buffer.from(JSON.stringify(rows), "utf8");
|
|
730
|
+
const sha = sha256Hex(bytes);
|
|
731
|
+
const file = `chunks/${id}.${sha.slice(0, 8)}.json`;
|
|
732
|
+
writeFileSync(join(outDir, file), bytes);
|
|
733
|
+
manifestChunks.push({ id, path: file, sha256: sha, bytes: bytes.length, count: rows.length });
|
|
734
|
+
totalFiles += rows.length;
|
|
735
|
+
}
|
|
736
|
+
const tBlockIndex = Date.now();
|
|
737
|
+
const blockRows = buildBlockIndexRows(markdownDir, corpusRoot);
|
|
738
|
+
passMs.blockIndex = Date.now() - tBlockIndex;
|
|
739
|
+
writeFileSync(
|
|
740
|
+
join(outDir, "block-parent-map.json"),
|
|
741
|
+
JSON.stringify(buildStaticParentMap(blockRows), null, 2) + "\n"
|
|
742
|
+
);
|
|
743
|
+
for (let i = 0; blockRows.length > 0 && i * BLOCK_INDEX_ROWS_PER_CHUNK < blockRows.length; i++) {
|
|
744
|
+
const part = blockRows.slice(i * BLOCK_INDEX_ROWS_PER_CHUNK, (i + 1) * BLOCK_INDEX_ROWS_PER_CHUNK);
|
|
745
|
+
if (part.length === 0) break;
|
|
746
|
+
const id = `${BLOCK_INDEX_CHUNK_PREFIX}${i}`;
|
|
747
|
+
const bytes = Buffer.from(JSON.stringify(part), "utf8");
|
|
748
|
+
const sha = sha256Hex(bytes);
|
|
749
|
+
const file = `chunks/${id}.${sha.slice(0, 8)}.json`;
|
|
750
|
+
writeFileSync(join(outDir, file), bytes);
|
|
751
|
+
manifestChunks.push({ id, path: file, sha256: sha, bytes: bytes.length, count: part.length, kind: "block-index" });
|
|
752
|
+
}
|
|
753
|
+
const tBlockEfforts = Date.now();
|
|
754
|
+
const effortRows = buildBlockEffortRows(markdownDir, corpusRoot);
|
|
755
|
+
passMs.blockEfforts = Date.now() - tBlockEfforts;
|
|
756
|
+
for (let i = 0; effortRows.length > 0 && i * BLOCK_INDEX_ROWS_PER_CHUNK < effortRows.length; i++) {
|
|
757
|
+
const part = effortRows.slice(i * BLOCK_INDEX_ROWS_PER_CHUNK, (i + 1) * BLOCK_INDEX_ROWS_PER_CHUNK);
|
|
758
|
+
if (part.length === 0) break;
|
|
759
|
+
const id = `${BLOCK_EFFORTS_CHUNK_PREFIX}${i}`;
|
|
760
|
+
const bytes = Buffer.from(JSON.stringify(part), "utf8");
|
|
761
|
+
const sha = sha256Hex(bytes);
|
|
762
|
+
const file = `chunks/${id}.${sha.slice(0, 8)}.json`;
|
|
763
|
+
writeFileSync(join(outDir, file), bytes);
|
|
764
|
+
manifestChunks.push({ id, path: file, sha256: sha, bytes: bytes.length, count: part.length, kind: "block-efforts" });
|
|
765
|
+
}
|
|
766
|
+
passMs.write = Date.now() - tWrite;
|
|
767
|
+
const manifest = {
|
|
768
|
+
schema: SEED_SCHEMA,
|
|
769
|
+
version: gitVersion.version,
|
|
770
|
+
versionLabel: gitVersion.versionLabel,
|
|
771
|
+
commit: gitVersion.commit,
|
|
772
|
+
dirty: gitVersion.dirty,
|
|
773
|
+
builtAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
774
|
+
chunks: manifestChunks
|
|
775
|
+
};
|
|
776
|
+
writeFileSync(join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
777
|
+
const reportPath = writeBuildReport(outDir, {
|
|
778
|
+
manifest,
|
|
779
|
+
totalMs: Date.now() - t0,
|
|
780
|
+
passMs,
|
|
781
|
+
fileCount: paths.length});
|
|
782
|
+
return { manifest, outDir, totalFiles, reportPath, passMs };
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export { buildBlockEffortRows, buildBlockIndexRows, buildStaticParentMap, chunkIdFor, collectCorpusMarkdown, discoverUncatalogedEfforts, feedDateToCreatedAt, generateSeed, loadExistingEffortMap, partitionIntoChunks, scaffoldDiscoveredEfforts, validateDashboardNote };
|
|
786
|
+
//# sourceMappingURL=compiler-KTBR6OEL.mjs.map
|
|
787
|
+
//# sourceMappingURL=compiler-KTBR6OEL.mjs.map
|