isorite 0.10.0 → 0.11.0

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.
Files changed (59) hide show
  1. package/dist/core.js +94 -7
  2. package/dist/main.js +864 -30
  3. package/package.json +2 -1
  4. package/web/_app/immutable/assets/{0.CPW9fjPu.css → 0.DcsDj0O1.css} +1 -1
  5. package/web/_app/immutable/assets/5.DbDKXCoO.css +1 -0
  6. package/web/_app/immutable/assets/NoteEditor.S3q5lmKq.css +1 -0
  7. package/web/_app/immutable/assets/drawn.CHbcoujQ.css +1 -0
  8. package/web/_app/immutable/chunks/{6RXH8WsD.js → BgT6ZdzK.js} +1 -1
  9. package/web/_app/immutable/chunks/{CsRPJjQS.js → CMeq8te_.js} +1 -1
  10. package/web/_app/immutable/chunks/CODXTnUW.js +523 -0
  11. package/web/_app/immutable/chunks/{C5MPRibb.js → CfL6O_ia.js} +1 -1
  12. package/web/_app/immutable/chunks/DDVefAkE.js +7 -0
  13. package/web/_app/immutable/chunks/DGmSCGD3.js +1 -0
  14. package/web/_app/immutable/chunks/{DQjIi5Jv.js → DNjLZcwq.js} +1 -1
  15. package/web/_app/immutable/chunks/{DCD8_jrW.js → DaulBP5z.js} +1 -1
  16. package/web/_app/immutable/chunks/{B8Qvvm53.js → Db_gdTNI.js} +1 -1
  17. package/web/_app/immutable/chunks/{CBkjDkEN.js → DktGnzDy.js} +14 -14
  18. package/web/_app/immutable/chunks/{CiU6tYJv.js → DuLysVkb.js} +1 -1
  19. package/web/_app/immutable/chunks/{DlKQFacy.js → F7HCRtmu.js} +1 -1
  20. package/web/_app/immutable/chunks/{BcgO-fBX.js → mLFVM17H.js} +1 -1
  21. package/web/_app/immutable/chunks/wxBqjpRy.js +1 -0
  22. package/web/_app/immutable/entry/app.wp4GfiBT.js +2 -0
  23. package/web/_app/immutable/entry/start.PPXyd7_0.js +1 -0
  24. package/web/_app/immutable/nodes/0.JerS9yOA.js +26 -0
  25. package/web/_app/immutable/nodes/{1.rzuq9VRh.js → 1.C-RxYNx3.js} +1 -1
  26. package/web/_app/immutable/nodes/2.Bbkruoyi.js +1 -0
  27. package/web/_app/immutable/nodes/3.BkpGKa9k.js +1 -0
  28. package/web/_app/immutable/nodes/{4.DP2MGav1.js → 4.Bj25nsxG.js} +1 -1
  29. package/web/_app/immutable/nodes/5.D9sfzyBX.js +9 -0
  30. package/web/_app/immutable/nodes/6.Di1re0aR.js +1 -0
  31. package/web/_app/version.json +1 -1
  32. package/web/index.html +16 -15
  33. package/web/kit/chart.js +55 -0
  34. package/web/kit/isorite.css +32 -0
  35. package/web/kit/kit.css +80 -0
  36. package/web/kit/kit.js +543 -0
  37. package/web/kit/licenses/d3.txt +13 -0
  38. package/web/kit/licenses/interval-tree-1d.txt +22 -0
  39. package/web/kit/licenses/isoformat.txt +13 -0
  40. package/web/kit/licenses/katex.txt +21 -0
  41. package/web/kit/licenses/observablehq-plot.txt +13 -0
  42. package/web/kit/licenses/three.txt +21 -0
  43. package/web/kit/math.css +1 -0
  44. package/web/kit/math.js +257 -0
  45. package/web/kit/model.js +4236 -0
  46. package/web/kit/plain.css +16 -0
  47. package/web/_app/immutable/assets/5.TsqJFEFF.css +0 -1
  48. package/web/_app/immutable/assets/NoteEditor.Wt5vU9Tb.css +0 -1
  49. package/web/_app/immutable/chunks/B0A4Snxv.js +0 -6
  50. package/web/_app/immutable/chunks/BJo70SOc.js +0 -1
  51. package/web/_app/immutable/chunks/BimZp6R0.js +0 -523
  52. package/web/_app/immutable/chunks/c7PFFMl4.js +0 -1
  53. package/web/_app/immutable/entry/app.iw6ZIOh2.js +0 -2
  54. package/web/_app/immutable/entry/start.CFnKMSWs.js +0 -1
  55. package/web/_app/immutable/nodes/0.BzEgwgsa.js +0 -26
  56. package/web/_app/immutable/nodes/2.D_jWsQLW.js +0 -1
  57. package/web/_app/immutable/nodes/3.BzRx3SiD.js +0 -1
  58. package/web/_app/immutable/nodes/5.BPS-CCQT.js +0 -5
  59. package/web/_app/immutable/nodes/6.DQMCyNJ3.js +0 -1
package/dist/main.js CHANGED
@@ -43,6 +43,342 @@ var __exportAll = (all, no_symbols) => {
43
43
  return target;
44
44
  };
45
45
  //#endregion
46
+ //#region packages/kit/src/assemble.ts
47
+ function meta(html, name) {
48
+ for (const m of html.matchAll(/<meta\b[^>]*>/gi)) {
49
+ const tag = m[0];
50
+ if ((tag.match(/\bname\s*=\s*["']([^"']*)["']/i)?.[1])?.toLowerCase() === name) return unescape(tag.match(/\bcontent\s*=\s*"([^"]*)"|\bcontent\s*=\s*'([^']*)'/i)?.slice(1).find((x) => x !== void 0) ?? "");
51
+ }
52
+ return null;
53
+ }
54
+ function manifestOf(html) {
55
+ return {
56
+ title: unescape(html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1]?.trim() ?? ""),
57
+ description: meta(html, "description") ?? "",
58
+ author: meta(html, "author") ?? "",
59
+ date: meta(html, "date") ?? "",
60
+ kit: meta(html, "isorite:kit"),
61
+ uses: (meta(html, "isorite:uses") ?? "").split(/[\s,]+/).filter(Boolean)
62
+ };
63
+ }
64
+ /** The artifact with its manifest as given (write_artifact, plan-m23 §3.5): its title, description, author, date, kit
65
+ * and uses set in its head, put in where missing, so they're always right whatever the file said. A file with no
66
+ * head or no document around it is given one. */
67
+ function withManifest(html, m) {
68
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
69
+ let doc = html.trim();
70
+ if (!/<html\b/i.test(doc)) doc = /<head\b/i.test(doc) ? `<!doctype html>\n<html lang="en">\n${doc}\n</html>` : `<!doctype html>\n<html lang="en">\n<head>\n</head>\n<body>\n${doc.replace(/<\/?body\b[^>]*>/gi, "")}\n</body>\n</html>`;
71
+ if (!/<head\b/i.test(doc)) doc = doc.replace(/<html\b[^>]*>/i, (h) => `${h}\n<head>\n</head>`);
72
+ if (!/^<!doctype/i.test(doc)) doc = `<!doctype html>\n${doc}`;
73
+ const metas = [
74
+ ["description", m.description],
75
+ ["author", m.author],
76
+ ["date", m.date],
77
+ ["isorite:kit", m.kit ?? void 0],
78
+ ["isorite:uses", m.uses?.join(" ")]
79
+ ];
80
+ for (const [name, value] of metas) {
81
+ if (value === void 0) continue;
82
+ const tag = `<meta name="${name}" content="${esc(value)}">`, re = new RegExp(`<meta\\b[^>]*\\bname\\s*=\\s*["']${name.replace(":", "\\:")}["'][^>]*>`, "i");
83
+ doc = re.test(doc) ? doc.replace(re, () => tag) : doc.replace(/<head\b[^>]*>/i, (h) => `${h}\n${tag}`);
84
+ }
85
+ if (m.title !== void 0) {
86
+ const t = `<title>${esc(m.title)}</title>`;
87
+ doc = /<title\b[^>]*>[\s\S]*?<\/title>/i.test(doc) ? doc.replace(/<title\b[^>]*>[\s\S]*?<\/title>/i, () => t) : doc.replace(/<head\b[^>]*>/i, (h) => `${h}\n${t}`);
88
+ }
89
+ if (!/<meta\b[^>]*charset/i.test(doc)) doc = doc.replace(/<head\b[^>]*>/i, (h) => `${h}\n<meta charset="utf-8">`);
90
+ return doc.endsWith("\n") ? doc : doc + "\n";
91
+ }
92
+ /** The kit's parts an artifact uses beyond the core: Plot with a chart, three.js with 3D. */
93
+ function partsUsed(html) {
94
+ const parts = [];
95
+ if (/<iso-chart[\s>]|\bPlot\.\w/.test(html)) parts.push("chart");
96
+ if (/<iso-model[\s>]|<iso-chart3d[\s>]|\bTHREE\.\w/.test(html)) parts.push("model");
97
+ const text = html.replace(/<script\b[\s\S]*?<\/script>/gi, "").replace(/<style\b[\s\S]*?<\/style>/gi, "");
98
+ if (/<iso-math[\s>]|\biso\.math\(|\$\$[\s\S]+?\$\$|\\\(|\$(?=\S)[^$\n]+?(?<=\S)\$(?!\d)/.test(text) || /\biso\.math\(/.test(html)) parts.push("math");
99
+ return parts;
100
+ }
101
+ /** The artifact as one document: the CSP, the kit's themes, components and the parts it uses, inlined at the top of
102
+ * its head (any kit <link> or <script src> it wrote is taken out), with its data too for a standalone export. */
103
+ function assemble(html, kit, opts = {}) {
104
+ const look = opts.look ?? "isorite", ground = opts.ground ?? "lapis";
105
+ const body = html.replace(/<link\b[^>]*\bhref\s*=\s*["'](?:\.\/)?kit\/[^"']*["'][^>]*>\s*/gi, "").replace(/<script\b[^>]*\bsrc\s*=\s*["'](?:\.\/)?kit\/[^"']*["'][^>]*>\s*<\/script>\s*/gi, "");
106
+ const data = Object.entries(opts.data ?? {}).map(([p, v]) => typeof v === "string" ? `<script type="text/plain" data-path="${attr(p)}">${safe(v)}<\/script>` : `<script type="application/octet-stream" data-encoding="base64" data-path="${attr(p)}">${base64(v)}<\/script>`);
107
+ const parts = partsUsed(body).map((p) => `${p === "math" ? `<style>\n${safe(kit["math.css"])}\n</style>\n` : ""}<script>\n${safe(kit[`${p}.js`])}\n<\/script>`);
108
+ const head = [
109
+ CSP,
110
+ `<style data-look="isorite">\n${safe(kit["isorite.css"])}\n</style>`,
111
+ `<style data-look="plain">\n${safe(kit["plain.css"])}\n</style>`,
112
+ `<style>\n${safe(kit["kit.css"])}\n</style>`,
113
+ ...data,
114
+ `<script>\n${safe(kit["kit.js"])}\n<\/script>`,
115
+ ...parts
116
+ ].join("\n");
117
+ return (/<head\b[^>]*>/i.test(body) ? body.replace(/<head\b[^>]*>/i, (h) => `${h}\n${head}\n`) : `<!doctype html><html><head><meta charset="utf-8">\n${head}\n</head><body>${body}</body></html>`).replace(/<html\b([^>]*)>/i, (_, a) => `<html${a.replace(/\s+data-(?:look|ground)\s*=\s*("[^"]*"|'[^']*'|\S+)/gi, "")} data-look="${look}" data-ground="${ground}">`);
118
+ }
119
+ function base64(bytes) {
120
+ let s = "";
121
+ for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode(...bytes.subarray(i, i + 32768));
122
+ return btoa(s);
123
+ }
124
+ var KIT_FILES, unescape, CSP, safe, attr;
125
+ var init_assemble = __esmMin((() => {
126
+ KIT_FILES = [
127
+ "kit.js",
128
+ "chart.js",
129
+ "model.js",
130
+ "math.js",
131
+ "math.css",
132
+ "isorite.css",
133
+ "plain.css",
134
+ "kit.css"
135
+ ];
136
+ unescape = (s) => s.replace(/&quot;/g, "\"").replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
137
+ CSP = `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; font-src data:; media-src data: blob:; connect-src 'none'">`;
138
+ safe = (t) => t.replace(/<\/(script|style)/gi, "<\\/$1");
139
+ attr = (s) => s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
140
+ }));
141
+ //#endregion
142
+ //#region packages/kit/src/rules.ts
143
+ /** The files an artifact reads, from its markup and script: data="…", an iso-model's src, iso.data/text/bytes("…"). */
144
+ function readsOf(html) {
145
+ const own = html.replace(/<!--[\s\S]*?-->/g, ""), reads = /* @__PURE__ */ new Set();
146
+ for (const r of own.matchAll(/iso\.(?:data|text|bytes)\(\s*["'`]([^"'`$]+)["'`]/g)) reads.add(r[1]);
147
+ for (const t of own.matchAll(TAG)) {
148
+ const a = attrsOf(t[2]);
149
+ (a.data ?? "").split(/[\s,]+/).filter(Boolean).forEach((f) => reads.add(f));
150
+ if (t[1].toLowerCase() === "iso-model" && a.src) reads.add(a.src);
151
+ }
152
+ return [...reads];
153
+ }
154
+ function checkArtifact(html, ctx = {}) {
155
+ const out = [], m = manifestOf(html);
156
+ const err = (rule, message) => out.push({
157
+ rule,
158
+ level: "error",
159
+ message
160
+ });
161
+ const warn = (rule, message) => out.push({
162
+ rule,
163
+ level: "warning",
164
+ message
165
+ });
166
+ const own = html.replace(/<!--[\s\S]*?-->/g, "");
167
+ if (m.kit === null) err(1, "no <meta name=\"isorite:kit\" content=\"1\">: without it isorite gives it no kit");
168
+ if (!m.title) err(1, "no <title>");
169
+ if (!m.description) err(1, "no <meta name=\"description\" content=\"…\">: one sentence on what it shows");
170
+ const reads = /* @__PURE__ */ new Set();
171
+ for (const r of own.matchAll(/iso\.(?:data|text|bytes)\(\s*["'`]([^"'`$]+)["'`]/g)) reads.add(r[1]);
172
+ const files = (a) => (a.data ?? "").split(/[\s,]+/).filter(Boolean);
173
+ for (const t of own.matchAll(TAG)) {
174
+ const a = attrsOf(t[2]);
175
+ files(a).forEach((f) => reads.add(f));
176
+ if (t[1].toLowerCase() === "iso-model" && a.src) reads.add(a.src);
177
+ }
178
+ for (const r of reads) if (!m.uses.includes(r)) err(2, `it reads ${r}, which isn't in <meta name="isorite:uses">`);
179
+ if (ctx.files) {
180
+ for (const u of m.uses) if (!ctx.files.includes(u)) err(2, `${u} is in isorite:uses but there's no such file beside it${ctx.files.length ? ` (there are: ${ctx.files.join(", ")})` : ""}`);
181
+ }
182
+ if (ctx.uses && [...ctx.uses].sort().join() !== [...m.uses].sort().join()) err(2, `isorite:uses (${m.uses.join(" ") || "none"}) and the uses given (${ctx.uses.join(" ") || "none"}) differ`);
183
+ if (ctx.columns) for (const t of own.matchAll(TAG)) {
184
+ const a = attrsOf(t[2]), fs = files(a), known = fs.map((f) => ctx.columns[f]);
185
+ if (!fs.length || known.some((k) => !k)) continue;
186
+ const have = [...new Set(known.flat()), ...fs.length > 1 ? ["file"] : []];
187
+ for (const k of COLUMN_ATTRS) for (const c of (a[k] ?? "").split(/[\s,]+/).filter(Boolean)) {
188
+ const name = c.replace(/^abs\((.+)\)$/, "$1");
189
+ if (k === "at" && t[1].toLowerCase() !== "iso-chart3d" && !have.includes(name)) continue;
190
+ if (!have.includes(name)) err(3, `<${t[1]} data="${a.data}" ${k}="${a[k]}">: ${a.data} has no column "${name}" (it has ${have.join(", ")})`);
191
+ }
192
+ }
193
+ const code = own.replace(/<meta\b[^>]*>/gi, "").replace(/\btone\s*=\s*"[^"]*"/g, "");
194
+ const colour = code.match(/#[0-9a-fA-F]{3,8}\b(?![-\w])|\b(?:rgba?|hsla?|oklch|oklab|lab|lch)\(\s*[\d.]/i) ?? code.match(/\b(?:fill|stroke|fillStyle|strokeStyle|color|background(?:-color)?|shadowColor|border(?:-color)?)\s*[:=]\s*["'`]?(?:white|black|red|blue|green|gray|grey|orange|yellow|purple|pink|navy|teal|silver)\b/i);
195
+ if (colour) err(4, `a colour of its own (${colour[0].trim().slice(0, 40)}): use the theme's, var(--lattice) in CSS or iso.tokens().lattice in script`);
196
+ const font = code.match(/font-family\s*:\s*[^;"}]*|\bfont\s*[:=]\s*["'`]?[^;"'`}]*(?<![.\w-])(?:sans-serif|serif|monospace|Arial|Helvetica|system-ui|Georgia|Times|Courier|Inter|Roboto)\b/i);
197
+ if (font && !/var\(--font|\$\{/.test(font[0])) err(4, `a font of its own (${font[0].slice(0, 50)}): use var(--font-ui), var(--font-text) or iso.tokens().ui`);
198
+ if (/border-radius\s*:/i.test(code)) err(4, "border-radius: the theme decides corners");
199
+ const urls = code.replace(/http:\/\/www\.w3\.org\/[\w/.-]+/g, "").match(/\bhttps?:\/\/[^\s"'`)<]+/);
200
+ if (urls) err(5, `a web address (${urls[0].slice(0, 60)}): an artifact reaches nothing outside the file`);
201
+ if (/<script\b[^>]*\bsrc\s*=/i.test(code.replace(/<script\b[^>]*\bsrc\s*=\s*["'](?:\.\/)?kit\/[^"']*["'][^>]*>/gi, ""))) err(5, "a <script src>: everything is in the file, and the kit is given");
202
+ if (/<link\b[^>]*\bhref\s*=\s*["'](?!(?:\.\/)?kit\/)/i.test(code)) err(5, "a <link href>: everything is in the file, and the kit is given");
203
+ if (/\b(?:fetch|XMLHttpRequest|WebSocket|EventSource|importScripts)\s*\(|\bimport\s*\(/.test(code)) err(5, "fetch, XMLHttpRequest or import(): read files with iso.data(path)");
204
+ if (html.length > 2e5) err(6, `${Math.round(html.length / 1024)} kB, over 200 kB: keep data in a data file, not in the page`);
205
+ const directive = own.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]*>/g, " ").match(/\b(?:drag(?:ging)? to|click (?:here|to|on)|tap to|scroll to|hover (?:to|over)|use the slider|press (?:the|play)|try (?:dragging|moving|clicking))\b[^.]*/i);
206
+ if (directive) warn(7, `text that explains how to use it ("${directive[0].trim().slice(0, 60)}"): say what a thing is, not what to do with it`);
207
+ return out;
208
+ }
209
+ var COLUMN_ATTRS, TAG, attrsOf;
210
+ var init_rules = __esmMin((() => {
211
+ init_assemble();
212
+ COLUMN_ATTRS = [
213
+ "x",
214
+ "y",
215
+ "z",
216
+ "by",
217
+ "column",
218
+ "columns",
219
+ "at"
220
+ ];
221
+ TAG = /<(iso-[\w-]+)\b([^>]*)>/gi;
222
+ attrsOf = (s) => Object.fromEntries([...s.matchAll(/([\w-]+)\s*=\s*"([^"]*)"/g)].map((m) => [m[1].toLowerCase(), m[2]]));
223
+ }));
224
+ //#endregion
225
+ //#region packages/kit/src/data.ts
226
+ /** One line of delimited text into fields; quotes may hold the delimiter, newlines and doubled quotes. */
227
+ function* records(text, sep) {
228
+ let field = "", row = [], quoted = false;
229
+ for (let i = 0; i < text.length; i++) {
230
+ const c = text[i];
231
+ if (quoted) {
232
+ if (c === "\"") {
233
+ if (text[i + 1] === "\"") {
234
+ field += "\"";
235
+ i++;
236
+ } else quoted = false;
237
+ } else field += c;
238
+ } else if (c === "\"" && field === "") quoted = true;
239
+ else if (c === sep) {
240
+ row.push(field);
241
+ field = "";
242
+ } else if (c === "\n" || c === "\r") {
243
+ if (c === "\r" && text[i + 1] === "\n") i++;
244
+ row.push(field);
245
+ field = "";
246
+ if (row.length > 1 || row[0] !== "") yield row;
247
+ row = [];
248
+ } else field += c;
249
+ }
250
+ row.push(field);
251
+ if (row.length > 1 || row[0] !== "") yield row;
252
+ }
253
+ function parseDelimited(text, sep = ",") {
254
+ const it = records(text.replace(/^/, ""), sep), head = it.next();
255
+ if (head.done) return [];
256
+ const cols = head.value.map((c) => c.trim());
257
+ const out = [];
258
+ for (const r of it) out.push(Object.fromEntries(cols.map((c, i) => [c, value(r[i] ?? "")])));
259
+ return out;
260
+ }
261
+ function parseData(path, text) {
262
+ const ext = path.toLowerCase().split(".").pop();
263
+ if (ext === "json") {
264
+ const j = JSON.parse(text);
265
+ if (Array.isArray(j)) return j;
266
+ const inner = j && typeof j === "object" ? Object.values(j).find(Array.isArray) : null;
267
+ if (inner) return inner;
268
+ throw new Error(`${path}: not an array of rows`);
269
+ }
270
+ if (ext === "jsonl" || ext === "ndjson") return text.split(/\r?\n/).filter((l) => l.trim()).map((l) => JSON.parse(l));
271
+ if (ext === "tsv") return parseDelimited(text, " ");
272
+ if (ext === "csv") return parseDelimited(text, ",");
273
+ throw new Error(`${path}: the kit reads CSV, TSV, JSON and JSON lines`);
274
+ }
275
+ /** Every column any row has, in the order they first appear. */
276
+ function columnsOf(rows) {
277
+ const seen = /* @__PURE__ */ new Set();
278
+ for (const r of rows.slice(0, 1e3)) for (const k of Object.keys(r)) seen.add(k);
279
+ return [...seen];
280
+ }
281
+ /** What read_data tells Iso: enough to build on a file without reading it. */
282
+ function shapeOf(path, rows, head = 5) {
283
+ return {
284
+ path,
285
+ rows: rows.length,
286
+ head: rows.slice(0, head),
287
+ columns: columnsOf(rows).map((name) => {
288
+ const vs = rows.map((r) => r[name]), present = vs.filter((v) => v !== null && v !== void 0 && v !== ""), empty = vs.length - present.length;
289
+ if (!present.length) return {
290
+ name,
291
+ type: "empty",
292
+ empty
293
+ };
294
+ if (present.every((v) => typeof v === "number")) {
295
+ let min = Infinity, max = -Infinity, sum = 0;
296
+ for (const v of present) {
297
+ if (v < min) min = v;
298
+ if (v > max) max = v;
299
+ sum += v;
300
+ }
301
+ return {
302
+ name,
303
+ type: "number",
304
+ min,
305
+ max,
306
+ mean: sum / present.length,
307
+ empty
308
+ };
309
+ }
310
+ if (present.every((v) => typeof v === "boolean")) return {
311
+ name,
312
+ type: "boolean",
313
+ empty
314
+ };
315
+ const distinct = [...new Set(present.map(String))];
316
+ return {
317
+ name,
318
+ type: "text",
319
+ distinct: distinct.length,
320
+ values: distinct.slice(0, 12),
321
+ empty
322
+ };
323
+ })
324
+ };
325
+ }
326
+ /** The shape as text, the way Iso reads it. */
327
+ function describeShape(s) {
328
+ const g = (x) => Number.isInteger(x) ? String(x) : String(+x.toPrecision(4));
329
+ const lines = [`${s.path}: ${s.rows} rows, ${s.columns.length} columns`];
330
+ for (const c of s.columns) {
331
+ const empty = c.empty ? `, ${c.empty} empty` : "";
332
+ if (c.type === "number") lines.push(`- ${c.name}: number, ${g(c.min)} to ${g(c.max)} (mean ${g(c.mean)})${empty}`);
333
+ else if (c.type === "text") lines.push(`- ${c.name}: text, ${c.distinct} distinct: ${c.values.join(", ")}${c.distinct > c.values.length ? ", …" : ""}${empty}`);
334
+ else lines.push(`- ${c.name}: ${c.type}${empty}`);
335
+ }
336
+ const cols = s.columns.map((c) => c.name);
337
+ lines.push("First rows:", cols.join(","), ...s.head.map((r) => cols.map((c) => r[c] ?? "").join(",")));
338
+ return lines.join("\n");
339
+ }
340
+ var NUMBER, value;
341
+ var init_data = __esmMin((() => {
342
+ NUMBER = /^[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?$/;
343
+ value = (s) => s === "" ? null : NUMBER.test(s.trim()) ? +s : s;
344
+ }));
345
+ //#endregion
346
+ //#region packages/kit/src/context.gen.ts
347
+ var REFERENCE, RECIPES;
348
+ var init_context_gen = __esmMin((() => {
349
+ REFERENCE = "# The kit (isorite kit 1)\n\nAn artifact is one HTML file: markup, perhaps a script, and a manifest in its `<head>`. isorite gives it the kit:\nthe look (every colour, font and corner), the elements below, and `window.iso`. Write what it shows and does, never\nhow it looks: no colour, font family or border-radius of your own.\n\n## The file\n\n```html\n<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Title</title>\n<meta name=\"description\" content=\"One sentence: what it shows, and what can be done with it.\">\n<meta name=\"isorite:kit\" content=\"1\">\n<meta name=\"isorite:uses\" content=\"runs.csv other.csv\">\n<style>/* layout only, if needed */</style>\n</head>\n<body>\n<iso-page>\n <header><h1>Title</h1><p>The question it answers, in a sentence or two.</p></header>\n <iso-grid>\n <iso-panel class=\"wide\"><h2>Name <span>a short reading</span></h2> … </iso-panel>\n <iso-panel><h2>…</h2> … </iso-panel>\n </iso-grid>\n</iso-page>\n</body>\n</html>\n```\n\n`write_artifact` fills in the title, description and manifest from its arguments, so they are always right.\n\n## Layout\n\n- `<iso-grid>`: panels in two columns (`style=\"--cols: 3\"` for three); `<iso-panel class=\"wide\">` spans the row.\n- `<iso-panel>`: its first child is `<h2>`; a `<span>` in the `h2` is a soft aside. A `<p>` in a panel is a\n caption: what the panel shows, in a sentence.\n- `<iso-row>`: controls side by side. `<iso-stats>`: big numbers side by side.\n\n## Data, with no script\n\nEvery element with `data=\"file.csv\"` reads it itself. Several files are one table:\n`data=\"euler.csv leapfrog.csv\"` adds a column `file` (`euler`, `leapfrog`) to compare them with `by=\"file\"`.\n`where=\"integrator=leapfrog\"` keeps some rows; `where=\"t<=$t\"` compares with a control's value. `abs(col)` is a\ncolumn's size.\n\n- `<iso-chart data=\"f.csv\" x=\"t\" y=\"energy\" by=\"body\">`: a chart.\n - `type`: `line` (default), `dot`, `bar`, `area`, `step`.\n - `y` may name several columns: `y=\"kinetic potential total\"`, each its own line.\n - `by`: a column whose values are the series.\n - `x-scale`, `y-scale`: `linear` or `log`, or `$name` to follow a control (`y-scale=\"$scale\"`).\n - `x-label`, `y-label`: the axis with its unit (`\"time (s)\"`). `height` in pixels (260).\n - `at=\"t\"`: follows the control `t`. A rule where x is its value; or, when the data has a column `t` and x is\n another column (a path in x and y over time), a dot on each series at the row whose `t` is nearest it.\n- `<iso-table data=\"f.csv\" columns=\"a b c\" limit=\"50\">`: the rows.\n- `<iso-stat data=\"f.csv\" column=\"err\" of=\"max\" label=\"worst error\" tone=\"gold\">`: one number. `of`: `min`, `max`,\n `mean`, `sum`, `count`, `first`, `last`. Tones: `gold`, `blue`, `green`, `rose`. Without `data`, set it from a\n script: `el.value = 0.0023`.\n- `<iso-chart3d data=\"f.csv\" x=\"x\" y=\"y\" z=\"z\" by=\"body\" at=\"t\">`: points or paths in 3D, turned by dragging.\n- `<iso-model src=\"part.glb\">`: a 3D model (glTF, GLB, STL, OBJ), turned by dragging. List it in isorite:uses.\n\n## Text and maths\n\n- A `<p>` in a panel is its caption. Maths is written as in a note: `$E = \\tfrac12 m v^2$` inline, `$$…$$` on a\n line of its own; the kit sets it. `<iso-math>\\frac{a}{b}</iso-math>` is a formula alone.\n- Never name the theme's colours in text (\"the lattice line\"): a chart's legend says which series is which.\n\n## Controls\n\nEach writes its value to `iso.state[name]`; elements that name it (`at=\"t\"`, `$t`) follow it.\n\n- `<iso-slider name=\"dt\" label=\"time step\" min=\"0.001\" max=\"0.05\" step=\"0.001\" value=\"0.01\" digits=\"3\">`\n- `<iso-select name=\"scale\" label=\"scale\" options=\"linear log\">`: one of several.\n- `<iso-toggle name=\"softened\" label=\"softened\" value=\"true\">`: on or off.\n- `<iso-play name=\"t\" min=\"0\" max=\"12\" speed=\"1.5\">`: plays `iso.state.t` from min to max, and pauses.\n- `<iso-note href=\"physics/softening\">softening</iso-note>`: opens a note in isorite.\n\n## Script, when markup isn't enough\n\nFor a simulation or anything computed. Put it at the end of the body.\n\n- `const rows = await iso.data(\"f.csv\")`: the rows as objects, numbers as numbers.\n- `iso.state.name`, `iso.watch((name) => …)`: the shared state; `watch` runs after any change.\n- `iso.loop((dt, t) => …)`: runs every frame (dt in seconds), paused when hidden. Returns a stop function.\n- `<iso-canvas height=\"320\" id=\"sim\">` and `document.getElementById(\"sim\").draw = (ctx, w, h, t) => { … }`: draw on\n a canvas sized to its panel; call `.redraw()` after a change. `t` is the theme: `t.text`, `t.soft`, `t.faint`,\n `t.line`, `t.lattice` (blue), `t.growth` (gold), `t.malachite` (green), `t.rose`, `t.series[i]`, `t.ui` (the font).\n- A chart of what the script computes: `chart.rows = [{ t: 0, energy: -1.3 }, …]` on an `<iso-chart>` with no\n `data`.\n- `iso.format(x)`: a number to read (3 figures, 2.3×10⁻³). `iso.tokens()`: the theme, as `t` above.\n- In CSS: `var(--text)`, `var(--text-soft)`, `var(--text-faint)`, `var(--line)`, `var(--lattice)`, `var(--growth)`,\n `var(--malachite)`, `var(--rose)`, `var(--font-ui)`, `var(--font-text)`.\n\n## Rules (write_artifact checks them, and refuses a file that breaks one)\n\n1. Every file read (`data=\"…\"`, `src=\"…\"`, `iso.data(\"…\")`) is in `uses`, and exists: call `read_data` first.\n2. Every column named is in its file, spelt as `read_data` gives it.\n3. No colour, font family or border-radius of its own: no `#hex`, `rgb(…)`, `white`, `Arial`. Use the tokens.\n4. No network: no `http…` address, no `<script src>`, no `fetch`. The kit is given; everything else is in the file.\n5. Under 200 kB: data stays in data files.\n6. No text that tells how to use it (\"drag to rotate\", \"click here\"): a label says what a thing is.\n\n## What good is\n\nEvery panel answers a question about the data, with a caption saying what to see. Numbers are read out. Axes say\nwhat they measure, with units. Controls change what is drawn at once. Nothing is decoration.\n";
350
+ RECIPES = {
351
+ "dashboard": {
352
+ "about": "a dashboard of one data file: numbers, charts and a table, in markup only",
353
+ "html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>The pendulum's period</title>\n<meta name=\"description\" content=\"Forty timed swings at eight lengths: how the period grows with length, how close it comes to 2π√(L/g), and how much the trials scatter.\">\n<meta name=\"isorite:kit\" content=\"1\">\n<meta name=\"isorite:uses\" content=\"pendulum.csv\">\n</head>\n<body>\n<!-- Recipe 1: a dashboard of one data file, in markup only. Change: the file, the numbers, the charts' columns, the\n captions. Every column named here must be one read_data gave. -->\n<iso-page>\n <header>\n <h1>The pendulum's period</h1>\n <p>Does a longer pendulum swing slower, and by as much as theory says?</p>\n </header>\n <iso-grid>\n <iso-panel class=\"wide\">\n <h2>In numbers <span>40 swings</span></h2>\n <iso-stats>\n <iso-stat data=\"pendulum.csv\" column=\"period_s\" of=\"min\" label=\"shortest period (s)\" tone=\"blue\"></iso-stat>\n <iso-stat data=\"pendulum.csv\" column=\"period_s\" of=\"max\" label=\"longest period (s)\" tone=\"gold\"></iso-stat>\n <iso-stat data=\"pendulum.csv\" column=\"temp_c\" of=\"mean\" label=\"mean temperature (°C)\"></iso-stat>\n <iso-stat data=\"pendulum.csv\" column=\"trial\" of=\"count\" label=\"swings timed\"></iso-stat>\n </iso-stats>\n </iso-panel>\n <iso-panel>\n <h2>Period against length</h2>\n <iso-chart data=\"pendulum.csv\" type=\"dot\" x=\"length_m\" y=\"period_s\" x-label=\"length (m)\" y-label=\"period (s)\"></iso-chart>\n <p>The period rises with length, more slowly as the pendulum gets longer: a square root.</p>\n </iso-panel>\n <iso-panel>\n <h2>On log axes <span>slope ½ is √L</span></h2>\n <iso-chart data=\"pendulum.csv\" type=\"dot\" x=\"length_m\" y=\"period_s\" x-scale=\"log\" y-scale=\"log\" x-label=\"length (m)\" y-label=\"period (s)\"></iso-chart>\n <p>A straight line here is a power law; its slope, near one half, is the exponent.</p>\n </iso-panel>\n <iso-panel class=\"wide\">\n <h2>Each trial <span>the scatter is the stopwatch</span></h2>\n <iso-chart data=\"pendulum.csv\" type=\"line\" x=\"length_m\" y=\"period_s\" by=\"trial\" height=\"200\" x-label=\"length (m)\" y-label=\"period (s)\"></iso-chart>\n </iso-panel>\n <iso-panel class=\"wide\">\n <h2>The measurements</h2>\n <iso-table data=\"pendulum.csv\" columns=\"length_m trial period_s temp_c\" limit=\"40\"></iso-table>\n </iso-panel>\n </iso-grid>\n</iso-page>\n</body>\n</html>\n"
354
+ },
355
+ "compare": {
356
+ "about": "two data files with the same columns, compared: together on one chart, and side by side",
357
+ "html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>A lid against an open mug</title>\n<meta name=\"description\" content=\"Two mugs of the same coffee cooling for an hour, one with a lid: their temperature together, the gap between them, and where each stands at a chosen minute.\">\n<meta name=\"isorite:kit\" content=\"1\">\n<meta name=\"isorite:uses\" content=\"mug-open.csv mug-lid.csv\">\n</head>\n<body>\n<!-- Recipe 2: comparing two data files with the same columns. data=\"a.csv b.csv\" reads both as one table with a\n column `file` (\"mug-open\", \"mug-lid\"), so by=\"file\" draws one series each and where=\"file=…\" picks one.\n Change: the files, the columns, the captions. -->\n<iso-page>\n <header>\n <h1>A lid against an open mug</h1>\n <p>How much longer does a lid keep coffee hot?</p>\n </header>\n <iso-grid>\n <iso-panel class=\"wide\">\n <h2>At one minute</h2>\n <iso-row>\n <iso-slider name=\"t\" label=\"minute\" min=\"0\" max=\"60\" step=\"2\" value=\"20\" digits=\"0\"></iso-slider>\n <iso-select name=\"scale\" label=\"temperature scale\" options=\"linear log\"></iso-select>\n </iso-row>\n <iso-stats>\n <iso-stat data=\"mug-open.csv\" column=\"temp_c\" of=\"last\" where=\"t_min<=$t\" label=\"open (°C)\" tone=\"blue\"></iso-stat>\n <iso-stat data=\"mug-lid.csv\" column=\"temp_c\" of=\"last\" where=\"t_min<=$t\" label=\"with a lid (°C)\" tone=\"gold\"></iso-stat>\n <iso-stat data=\"mug-open.csv\" column=\"temp_c\" of=\"last\" label=\"open after the hour (°C)\"></iso-stat>\n <iso-stat data=\"mug-lid.csv\" column=\"temp_c\" of=\"last\" label=\"lid after the hour (°C)\"></iso-stat>\n </iso-stats>\n </iso-panel>\n <iso-panel class=\"wide\">\n <h2>Both, cooling</h2>\n <iso-chart data=\"mug-open.csv mug-lid.csv\" x=\"t_min\" y=\"temp_c\" by=\"file\" at=\"t\" y-scale=\"$scale\" x-label=\"time (min)\" y-label=\"temperature (°C)\" height=\"300\"></iso-chart>\n <p>Both fall fast at first and slower as they near the room's 21 °C; the lid halves the early loss.</p>\n </iso-panel>\n <iso-panel>\n <h2>Open</h2>\n <iso-table data=\"mug-open.csv\" limit=\"12\"></iso-table>\n </iso-panel>\n <iso-panel>\n <h2>With a lid</h2>\n <iso-table data=\"mug-lid.csv\" limit=\"12\"></iso-table>\n </iso-panel>\n </iso-grid>\n</iso-page>\n</body>\n</html>\n"
358
+ },
359
+ "simulation": {
360
+ "about": "a simulation with parameters, play and reset, drawn on a canvas, with a live chart (a script)",
361
+ "html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>A damped spring</title>\n<meta name=\"description\" content=\"A mass on a spring, integrated live: change the stiffness and the damping and see the motion, the energy draining away, and the period against theory's.\">\n<meta name=\"isorite:kit\" content=\"1\">\n<meta name=\"isorite:uses\" content=\"\">\n<style>\n /* Layout only. */\n .readout { margin-top: 12px; }\n</style>\n</head>\n<body>\n<!-- Recipe 3: a simulation with parameters and play. The parts to change are marked CHANGE: the state (what the\n model is), step() (how it moves), draw() (what's on the canvas), and what's measured. The rest stays. -->\n<iso-page>\n <header>\n <h1>A damped spring</h1>\n <p>How do stiffness and damping shape an oscillation?</p>\n </header>\n <iso-grid>\n <iso-panel class=\"wide\">\n <h2>The model</h2>\n <iso-row>\n <iso-slider name=\"k\" label=\"stiffness k (N/m)\" min=\"1\" max=\"40\" step=\"0.5\" value=\"10\" digits=\"1\"></iso-slider>\n <iso-slider name=\"c\" label=\"damping c (kg/s)\" min=\"0\" max=\"2\" step=\"0.05\" value=\"0.3\" digits=\"2\"></iso-slider>\n </iso-row>\n <iso-row>\n <iso-toggle name=\"playing\" label=\"play\" value=\"true\"></iso-toggle>\n <button type=\"button\" class=\"iso-button\" id=\"reset\">reset</button>\n </iso-row>\n <iso-stats class=\"readout\">\n <iso-stat id=\"time\" label=\"time (s)\"></iso-stat>\n <iso-stat id=\"energy\" label=\"energy left\" tone=\"gold\"></iso-stat>\n <iso-stat id=\"period\" label=\"period, theory (s)\" tone=\"blue\"></iso-stat>\n </iso-stats>\n </iso-panel>\n <iso-panel>\n <h2>The mass</h2>\n <iso-canvas id=\"scene\" height=\"300\"></iso-canvas>\n </iso-panel>\n <iso-panel>\n <h2>Energy <span>a share of the start</span></h2>\n <iso-chart id=\"chart\" x=\"t\" y=\"energy\" x-label=\"time (s)\" y-label=\"energy left\" height=\"300\"></iso-chart>\n <p>With no damping the energy stays; damping drains it, faster the larger c is.</p>\n </iso-panel>\n </iso-grid>\n</iso-page>\n<script>\n // CHANGE: the model's state and its start.\n const m = 1;\n let s, history;\n function reset() { s = { t: 0, x: 1, v: 0 }; history = []; }\n reset();\n const energy = () => 0.5 * m * s.v * s.v + 0.5 * iso.state.k * s.x * s.x;\n let e0 = energy();\n\n // CHANGE: one step of the model (here semi-implicit Euler, small steps).\n function step(dt) {\n const a = (-iso.state.k * s.x - iso.state.c * s.v) / m;\n s.v += a * dt;\n s.x += s.v * dt;\n s.t += dt;\n }\n\n // CHANGE: what's drawn. t is the theme: t.line, t.text, t.soft, t.faint, t.lattice, t.growth, t.ui.\n const scene = document.getElementById(\"scene\");\n scene.draw = (ctx, w, h, t) => {\n const cx = w / 2, y0 = 24, rest = h * 0.5, y = rest + s.x * h * 0.3;\n ctx.strokeStyle = t.line; ctx.lineWidth = 1;\n ctx.beginPath(); ctx.moveTo(cx - 60, y0); ctx.lineTo(cx + 60, y0); ctx.stroke();\n ctx.setLineDash([3, 4]); ctx.beginPath(); ctx.moveTo(cx - 80, rest); ctx.lineTo(cx + 80, rest); ctx.stroke(); ctx.setLineDash([]);\n // The spring, as a zigzag from the ceiling to the mass.\n ctx.strokeStyle = t.soft; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(cx, y0);\n for (let i = 1; i < 16; i++) ctx.lineTo(cx + (i % 2 ? 12 : -12), y0 + (y - 20 - y0) * i / 16);\n ctx.lineTo(cx, y - 20); ctx.stroke();\n ctx.fillStyle = t.lattice; ctx.fillRect(cx - 20, y - 20, 40, 40);\n ctx.fillStyle = t.faint; ctx.font = `11px ${t.ui}`; ctx.fillText(\"rest\", cx + 84, rest + 4);\n };\n\n // Measured, and shown: the readouts and the chart, a few times a second.\n const chart = document.getElementById(\"chart\");\n let since = 0;\n function show() {\n document.getElementById(\"time\").value = s.t.toFixed(1);\n document.getElementById(\"energy\").value = iso.format(energy() / e0);\n document.getElementById(\"period\").value = iso.format(2 * Math.PI * Math.sqrt(m / iso.state.k));\n chart.rows = history;\n }\n\n iso.loop((dt) => {\n if (!iso.state.playing) return;\n for (let i = 0; i < 20; i++) step(dt / 20);\n scene.redraw();\n since += dt;\n if (since > 0.1) { since = 0; history.push({ t: +s.t.toFixed(2), energy: energy() / e0 }); if (history.length > 600) history.shift(); show(); }\n });\n iso.watch((name) => { if (name === \"k\") { e0 = energy(); } show(); scene.redraw(); });\n document.getElementById(\"reset\").addEventListener(\"click\", () => { reset(); e0 = energy(); show(); scene.redraw(); });\n show();\n<\/script>\n</body>\n</html>\n"
362
+ },
363
+ "explorable": {
364
+ "about": "an explorable explanation: inputs, numbers derived from them, and linked views (a script)",
365
+ "html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Why a fast wheel turns backwards</title>\n<meta name=\"description\" content=\"Sample a sine too slowly and a slower one appears: choose the signal's frequency and the sampling rate, and see the samples, the wave they seem to come from, and where every frequency lands.\">\n<meta name=\"isorite:kit\" content=\"1\">\n<meta name=\"isorite:uses\" content=\"\">\n</head>\n<body>\n<!-- Recipe 4: an explorable explanation. An input, numbers derived from it, and linked views that all follow the same\n state; captions say what to see. CHANGE: the inputs (sliders), derive() (the numbers), the drawing, and the\n chart's rows. Keep: every view redrawn from iso.state in one place, update(). -->\n<iso-page>\n <header>\n <h1>Why a fast wheel turns backwards</h1>\n <p>Sampled too slowly, a signal is indistinguishable from a slower one. Which one, and when?</p>\n </header>\n <iso-grid>\n <iso-panel class=\"wide\">\n <h2>The signal and the sampling</h2>\n <iso-row>\n <iso-slider name=\"f\" label=\"signal (Hz)\" min=\"0.2\" max=\"12\" step=\"0.1\" value=\"7\" digits=\"1\"></iso-slider>\n <iso-slider name=\"fs\" label=\"samples per second\" min=\"2\" max=\"24\" step=\"1\" value=\"8\" digits=\"0\"></iso-slider>\n </iso-row>\n <iso-stats>\n <iso-stat id=\"nyquist\" label=\"Nyquist limit (Hz)\" tone=\"blue\"></iso-stat>\n <iso-stat id=\"apparent\" label=\"what the samples show (Hz)\" tone=\"gold\"></iso-stat>\n <iso-stat id=\"verdict\" label=\"faithful?\"></iso-stat>\n </iso-stats>\n </iso-panel>\n <iso-panel class=\"wide\">\n <h2>One second <span>the signal, its samples, and the slower wave through them</span></h2>\n <iso-canvas id=\"wave\" height=\"260\"></iso-canvas>\n <p>Gold dots are the samples. Above the Nyquist limit the blue wave, slower, passes through every one of them as\n well as the true signal does: from the samples alone, the two can't be told apart.</p>\n </iso-panel>\n <iso-panel class=\"wide\">\n <h2>Where every frequency lands</h2>\n <iso-chart id=\"fold\" x=\"signal\" y=\"apparent\" at=\"f\" x-label=\"signal (Hz)\" y-label=\"frequency the samples show (Hz)\" height=\"220\"></iso-chart>\n <p>The frequency seen climbs with the signal's up to half the sampling rate, then folds back down:\n $f_\\text{seen} = |f - f_s \\operatorname{round}(f / f_s)|$. See <iso-note href=\"signals/sampling\">sampling</iso-note>.</p>\n </iso-panel>\n </iso-grid>\n</iso-page>\n<script>\n // CHANGE: the numbers derived from the inputs.\n function derive(f, fs) {\n const nyquist = fs / 2;\n const folded = Math.abs(f - fs * Math.round(f / fs));\n return { nyquist, apparent: folded, faithful: f < nyquist };\n }\n\n // CHANGE: the drawing. t is the theme (t.line, t.soft, t.faint, t.lattice, t.growth, t.ui).\n const wave = document.getElementById(\"wave\");\n wave.draw = (ctx, w, h, t) => {\n const { f, fs } = iso.state, d = derive(f, fs), mid = h / 2, amp = h * 0.38;\n const X = (s) => 40 + s * (w - 60), Y = (v) => mid - v * amp;\n ctx.strokeStyle = t.line; ctx.lineWidth = 1;\n ctx.beginPath(); ctx.moveTo(X(0), mid); ctx.lineTo(X(1), mid); ctx.stroke();\n const curve = (freq, colour, width) => {\n ctx.strokeStyle = colour; ctx.lineWidth = width; ctx.beginPath();\n for (let i = 0; i <= 600; i++) { const s = i / 600, v = Math.sin(2 * Math.PI * freq * s); i ? ctx.lineTo(X(s), Y(v)) : ctx.moveTo(X(s), Y(v)); }\n ctx.stroke();\n };\n curve(f, t.soft, 1);\n // The slower wave through the same samples: the signal less the nearest whole number of sampling rates.\n if (!d.faithful) curve(f - fs * Math.round(f / fs), t.lattice, 2);\n ctx.fillStyle = t.growth;\n for (let k = 0; k <= fs; k++) { const s = k / fs; ctx.beginPath(); ctx.arc(X(s), Y(Math.sin(2 * Math.PI * f * s)), 4, 0, 2 * Math.PI); ctx.fill(); }\n ctx.fillStyle = t.faint; ctx.font = `11px ${t.ui}`; ctx.fillText(\"0 s\", X(0) - 8, h - 6); ctx.fillText(\"1 s\", X(1) - 8, h - 6);\n };\n\n // One place redraws every view from the state.\n const fold = document.getElementById(\"fold\");\n function update() {\n const { f, fs } = iso.state, d = derive(f, fs);\n document.getElementById(\"nyquist\").value = iso.format(d.nyquist);\n document.getElementById(\"apparent\").value = iso.format(d.apparent);\n document.getElementById(\"verdict\").value = d.faithful ? \"yes\" : \"no: aliased\";\n // CHANGE: the chart's rows, computed from the state.\n const rows = [];\n for (let s = 0; s <= 12; s += 0.05) rows.push({ signal: +s.toFixed(2), apparent: derive(s, fs).apparent });\n fold.rows = rows;\n wave.redraw();\n }\n iso.watch(update);\n update();\n<\/script>\n</body>\n</html>\n"
366
+ },
367
+ "viewer": {
368
+ "about": "a 3D model's viewer (glTF, GLB, STL, OBJ), with what it is beside it",
369
+ "html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>The corner bracket</title>\n<meta name=\"description\" content=\"The printed corner bracket in 3D, with its dimensions and the notes on how it was designed and tested.\">\n<meta name=\"isorite:kit\" content=\"1\">\n<meta name=\"isorite:uses\" content=\"bracket.stl\">\n</head>\n<body>\n<!-- Recipe 5: a 3D model's viewer, with what it is beside it. iso-model shows glTF, GLB, STL and OBJ; list the file\n in isorite:uses. CHANGE: the model's file, the numbers, the notes it links to. -->\n<iso-page>\n <header>\n <h1>The corner bracket</h1>\n <p>The part as printed: an L of two plates, 6 cm by 5 cm.</p>\n </header>\n <iso-grid style=\"--cols: 3\">\n <iso-panel class=\"wide\">\n <h2>The part <span>bracket.stl</span></h2>\n <iso-model src=\"bracket.stl\" height=\"380\"></iso-model>\n </iso-panel>\n <iso-panel>\n <h2>Size</h2>\n <iso-stats>\n <iso-stat label=\"long arm (mm)\" value=\"60\" tone=\"blue\"></iso-stat>\n <iso-stat label=\"short arm (mm)\" value=\"50\"></iso-stat>\n <iso-stat label=\"thickness (mm)\" value=\"10\"></iso-stat>\n </iso-stats>\n </iso-panel>\n <iso-panel>\n <h2>Printed</h2>\n <iso-stats>\n <iso-stat label=\"infill\" value=\"40%\"></iso-stat>\n <iso-stat label=\"mass (g)\" value=\"31\" tone=\"gold\"></iso-stat>\n </iso-stats>\n </iso-panel>\n <iso-panel>\n <h2>Notes</h2>\n <p><iso-note href=\"design/bracket-loads\">The loads it takes</iso-note>, and\n <iso-note href=\"design/print-settings\">how it was printed</iso-note>.</p>\n </iso-panel>\n </iso-grid>\n</iso-page>\n</body>\n</html>\n"
370
+ }
371
+ };
372
+ }));
373
+ //#endregion
374
+ //#region packages/kit/src/index.ts
375
+ var init_src$2 = __esmMin((() => {
376
+ init_assemble();
377
+ init_rules();
378
+ init_data();
379
+ init_context_gen();
380
+ }));
381
+ //#endregion
46
382
  //#region packages/core/src/text.ts
47
383
  /** Compare by code point, as Python sorts strings. */
48
384
  function cmp(a, b) {
@@ -488,7 +824,7 @@ function pictureSize(bytes, type) {
488
824
  } catch {}
489
825
  return null;
490
826
  }
491
- var TYPES$1, pictureType;
827
+ var TYPES$1, pictureType, OTHER, fileType;
492
828
  var init_picture = __esmMin((() => {
493
829
  TYPES$1 = {
494
830
  png: "image/png",
@@ -503,6 +839,36 @@ var init_picture = __esmMin((() => {
503
839
  const dot = path.lastIndexOf(".");
504
840
  return dot < 0 || path.lastIndexOf("/") > dot ? null : TYPES$1[path.slice(dot + 1).toLowerCase()] ?? null;
505
841
  };
842
+ OTHER = {
843
+ html: "text/html",
844
+ htm: "text/html",
845
+ csv: "text/csv",
846
+ tsv: "text/tab-separated-values",
847
+ json: "application/json",
848
+ jsonl: "application/jsonl",
849
+ ndjson: "application/x-ndjson",
850
+ txt: "text/plain",
851
+ tex: "text/plain",
852
+ pdf: "application/pdf",
853
+ glb: "model/gltf-binary",
854
+ gltf: "model/gltf+json",
855
+ stl: "model/stl",
856
+ obj: "model/obj",
857
+ zip: "application/zip",
858
+ parquet: "application/vnd.apache.parquet",
859
+ arrow: "application/vnd.apache.arrow.file",
860
+ npy: "application/octet-stream",
861
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
862
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
863
+ epub: "application/epub+zip",
864
+ odt: "application/vnd.oasis.opendocument.text"
865
+ };
866
+ fileType = (path) => {
867
+ const dot = path.lastIndexOf(".");
868
+ if (dot < 0 || path.lastIndexOf("/") > dot) return null;
869
+ const ext = path.slice(dot + 1).toLowerCase();
870
+ return ext === "md" ? null : pictureType(path) ?? OTHER[ext] ?? "application/octet-stream";
871
+ };
506
872
  }));
507
873
  //#endregion
508
874
  //#region packages/core/src/kinds.ts
@@ -613,16 +979,24 @@ var init_kinds = __esmMin((() => {
613
979
  function comparePaths(a, b) {
614
980
  return cmpTuple(a.split("/"), b.split("/"));
615
981
  }
616
- var AGENTS_FILE, ARTIFACT, IMAGE, citeKind, isEntry, Concept, Directory, SCHEME, LOG_DATE, Bundle, oneLine, escape;
982
+ var AGENTS_FILE, KINDS$2, citeKind, isEntry, Concept, Directory, SCHEME, LOG_DATE, Bundle, oneLine, escape;
617
983
  var init_bundle = __esmMin((() => {
618
984
  init_frontmatter();
619
985
  init_markdown();
620
986
  init_kinds();
621
987
  init_text();
622
988
  AGENTS_FILE = "AGENTS.md";
623
- ARTIFACT = /\.html?$/i;
624
- IMAGE = /\.(png|jpe?g|gif|svg|webp|avif)$/i;
625
- citeKind = (path) => ARTIFACT.test(path) ? "artifact" : IMAGE.test(path) ? "image" : null;
989
+ KINDS$2 = [
990
+ ["artifact", /\.html?$/i],
991
+ ["image", /\.(png|jpe?g|gif|svg|webp|avif)$/i],
992
+ ["data", /\.(csv|tsv|json|jsonl|ndjson|parquet|arrow|feather|npy|npz|xlsx|xls|h5|hdf5|nc)$/i],
993
+ ["document", /\.(pdf|docx?|odt|rtf|epub|pptx?|odp|txt|tex)$/i]
994
+ ];
995
+ citeKind = (path) => {
996
+ const name = basename$1(path);
997
+ if (/\.md$/i.test(name) || !/\.[^.]+$/.test(name.replace(/^\./, ""))) return null;
998
+ return KINDS$2.find(([, re]) => re.test(name))?.[0] ?? "misc";
999
+ };
626
1000
  isEntry = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
627
1001
  Concept = class {
628
1002
  id;
@@ -1884,10 +2258,54 @@ function mapModel(base) {
1884
2258
  ]);
1885
2259
  }
1886
2260
  const holdsFiles = /* @__PURE__ */ new Set();
1887
- for (const f of base.files) holdsFiles.add(f.includes("/") ? f.slice(0, f.lastIndexOf("/")) : "");
2261
+ const parentOf = (p) => p.includes("/") ? p.slice(0, p.lastIndexOf("/")) : "";
2262
+ for (const f of base.files) holdsFiles.add(parentOf(f));
2263
+ const pictureFolder = (d) => {
2264
+ const dd = base.directories.get(d);
2265
+ const here = [...base.files].filter((f) => parentOf(f) === d);
2266
+ return !!d && (!dd || !dd.concepts.length && !dd.children.length) && here.length > 0 && here.every((f) => citeKind(f) === "image");
2267
+ };
2268
+ const refOf = (f) => citeKind(f) === "image" && pictureFolder(parentOf(f)) ? parentOf(f) + "/" : f;
2269
+ const fileEdges = /* @__PURE__ */ new Map();
2270
+ for (const c of base.concepts.values()) for (const x of c.cites) {
2271
+ if (x.broken || !base.files.has(x.target)) continue;
2272
+ const ref = refOf(x.target);
2273
+ if (!fileEdges.has(c.id)) fileEdges.set(c.id, /* @__PURE__ */ new Set());
2274
+ fileEdges.get(c.id).add(ref);
2275
+ }
2276
+ const rankOf = /* @__PURE__ */ new Map();
2277
+ for (const [from, refs] of fileEdges) for (const ref of refs) {
2278
+ edges.push([
2279
+ from,
2280
+ ref,
2281
+ STRENGTH.uses
2282
+ ]);
2283
+ rankOf.set(ref, Math.max(rankOf.get(ref) ?? 0, order.get(from)?.depth ?? 0));
2284
+ }
2285
+ const filesIn = /* @__PURE__ */ new Map();
2286
+ for (const f of [...base.files].sort()) {
2287
+ const of = citeKind(f), ref = of ? refOf(f) : null;
2288
+ if (!of || !ref) continue;
2289
+ const at = ref.endsWith("/") ? parentOf(ref.slice(0, -1)) : parentOf(f);
2290
+ const list = filesIn.get(at) ?? [];
2291
+ if (!list.some((n) => n.ref === ref)) list.push({
2292
+ kind: "concept",
2293
+ id: "f:" + ref,
2294
+ ref,
2295
+ weight: 1,
2296
+ rank: rankOf.get(ref) ?? 0,
2297
+ of
2298
+ });
2299
+ filesIn.set(at, list);
2300
+ }
1888
2301
  const dir = (id) => {
1889
2302
  const d = base.directories.get(id);
1890
- const children = [...[...d.children].sort().map(dir).filter((c) => c !== null), ...[...d.concepts].sort().map(note)];
2303
+ if (id && pictureFolder(id)) return null;
2304
+ const children = [
2305
+ ...[...d.children].sort().map(dir).filter((c) => c !== null),
2306
+ ...[...d.concepts].sort().map(note),
2307
+ ...filesIn.get(id) ?? []
2308
+ ];
1891
2309
  return id && !children.length && (holdsFiles.has(id) || d.children.length) ? null : {
1892
2310
  kind: "dir",
1893
2311
  id: "d:" + id,
@@ -1901,6 +2319,7 @@ function mapModel(base) {
1901
2319
  };
1902
2320
  }
1903
2321
  var init_map = __esmMin((() => {
2322
+ init_bundle();
1904
2323
  init_graph();
1905
2324
  }));
1906
2325
  //#endregion
@@ -1925,6 +2344,7 @@ var init_src$1 = __esmMin((() => {
1925
2344
  }));
1926
2345
  //#endregion
1927
2346
  //#region packages/core/src/node.ts
2347
+ init_src$2();
1928
2348
  init_src$1();
1929
2349
  init_bundle();
1930
2350
  init_text();
@@ -2111,7 +2531,8 @@ async function completeOnce(http, p, key, call, heard) {
2111
2531
  ...call.tools?.length ? {
2112
2532
  tools: call.tools,
2113
2533
  tool_choice: call.toolChoice ?? "auto"
2114
- } : {}
2534
+ } : {},
2535
+ ...call.effort && p.name === OPENROUTER.name ? { reasoning: { effort: call.effort } } : {}
2115
2536
  });
2116
2537
  let res;
2117
2538
  try {
@@ -2369,13 +2790,14 @@ async function rounds(o) {
2369
2790
  let firstIn = 0;
2370
2791
  for (let round = 0, calls = 1;; round++, calls++) {
2371
2792
  let first = true;
2372
- const ask = (maxTokens) => complete(o.http, o.provider, o.key, {
2793
+ const ask = (maxTokens, effort) => complete(o.http, o.provider, o.key, {
2373
2794
  model,
2374
2795
  messages,
2375
2796
  maxTokens,
2376
2797
  tools: most ? o.lookup.defs() : [],
2377
2798
  toolChoice: round < most ? "auto" : "none",
2378
2799
  signal: o.signal,
2800
+ ...effort ? { effort } : {},
2379
2801
  onText: (piece) => {
2380
2802
  o.onText(piece, first && round > 0);
2381
2803
  first = false;
@@ -2392,10 +2814,14 @@ async function rounds(o) {
2392
2814
  add(r);
2393
2815
  if (round === 0) firstIn = r.usage.tokensIn;
2394
2816
  if (empty(r)) {
2395
- r = await ask(o.maxTokens * 3);
2817
+ r = await ask(o.maxTokens * 3, "low");
2396
2818
  add(r);
2397
2819
  if (empty(r)) throw new ModelError(`${model} used its whole output budget (${Math.max(o.maxTokens * 3, o.provider.maxTokens ?? 0)} tokens) thinking, and wrote nothing. Try a lower tier, or raise max_tokens under [provider] in config.toml.`, 502, { kind: "cut" });
2398
2820
  }
2821
+ if (!r.text.trim() && !r.calls.length && !r.cut) {
2822
+ r = await ask(o.maxTokens);
2823
+ add(r);
2824
+ }
2399
2825
  if (!r.calls.length || round >= most) return {
2400
2826
  text: r.text,
2401
2827
  usage,
@@ -3311,11 +3737,12 @@ function turnMessages(a) {
3311
3737
  PRACTICE,
3312
3738
  LEARNING,
3313
3739
  MODE_RULES[a.mode],
3740
+ ...a.mode === "write" ? [ARTIFACTS] : [],
3314
3741
  ASKING(a.mode === "write"),
3315
3742
  ...checking ? [CHECKING] : [],
3316
3743
  ...understood.length ? [UNDERSTANDING] : [],
3317
3744
  FORMAT_RULES,
3318
- LOOKING(ROUNDS[a.tier], a.meaning, a.lookup)
3745
+ LOOKING(a.mode === "write" ? Math.max(ROUNDS[a.tier], 6) : ROUNDS[a.tier], a.meaning, a.lookup)
3319
3746
  ].join("\n\n");
3320
3747
  const briefed = a.briefings?.length ? `# Earlier in this conversation, summed up\n\n${a.briefings.join("\n\n---\n\n")}` : "";
3321
3748
  const talk = a.earlier.map((t) => turnExchange(t).text).join("\n\n---\n\n");
@@ -3394,7 +3821,7 @@ function turnMessages(a) {
3394
3821
  ]
3395
3822
  };
3396
3823
  }
3397
- var MODES, ITEMS, CONVERSATIONS_DIR, conversationPath, WHO, ISORITE, PRACTICE, LEARNING, ASK, WRITE, UNDERSTANDING, ASKING, CHECKING, MODE_RULES, LOOKING, COMPACT$1, str, fn, POINT_TO_NOTE, EDIT_NOTE$1, WRITE_NOTE$1, MOVE_NOTE, DELETE_NOTE, WRITE_UNDERSTANDING, ASK_QUESTION, OFFER_TEXT, MODE_TOOLS;
3824
+ var MODES, ITEMS, CONVERSATIONS_DIR, conversationPath, WHO, ISORITE, PRACTICE, LEARNING, ASK, WRITE, UNDERSTANDING, ARTIFACTS, ASKING, CHECKING, MODE_RULES, LOOKING, COMPACT$1, str, fn, POINT_TO_NOTE, EDIT_NOTE$1, WRITE_NOTE$1, MOVE_NOTE, DELETE_NOTE, WRITE_UNDERSTANDING, ASK_QUESTION, READ_DATA, ARTIFACT_RECIPE, WRITE_ARTIFACT, READ_ARTIFACT, OFFER_TEXT, MODE_TOOLS;
3398
3825
  var init_turn = __esmMin((() => {
3399
3826
  init_iso();
3400
3827
  init_loop();
@@ -3432,6 +3859,11 @@ what you change is shown to them there as you do it.`;
3432
3859
  problem note is practice that \`uses\` notes. Theirs alone to mark:
3433
3860
  \`resolved\` or \`solved\`, with the day. \`[[!name]]\` alone on a line
3434
3861
  embeds one in a note.
3862
+ - A problem of several steps is several problem notes, each linked to the one
3863
+ before it as "requires", in a folder of their own when they belong
3864
+ together. A problem has no steps inside it.
3865
+ - Files beside the notes that are not notes (data, documents, pictures,
3866
+ artifacts) are linked as any note is: [runs](/simulation/runs.csv).
3435
3867
  - Every change you make is marked as yours, in the note's history, and they
3436
3868
  can undo it.
3437
3869
 
@@ -3517,6 +3949,22 @@ far, from its Understanding note.
3517
3949
  linked: what they wrote in a note, a question of theirs open or resolved,
3518
3950
  a problem solved or not. Say nothing the evidence does not show.
3519
3951
  - Never invent a note, a link or a quotation.`;
3952
+ ARTIFACTS = `# Artifacts
3953
+
3954
+ An artifact is an HTML page beside their notes that isorite shows working: a
3955
+ dashboard of their data, a comparison, a simulation, an explorable
3956
+ explanation, a 3D model's viewer. Make one when they ask for something to see
3957
+ or try; not for what an answer or a note says as well.
3958
+
3959
+ 1. artifact_recipe with the recipe nearest what they asked: dashboard,
3960
+ compare, simulation, explorable or viewer. It gives a complete working
3961
+ artifact and the kit's reference. Start from the recipe and change what it
3962
+ marks; keep the rest.
3963
+ 2. read_data for every data file it will read. Use the columns as given.
3964
+ 3. write_artifact with the whole file. It is checked and run. If it reports a
3965
+ problem, fix that and write the whole file again.
3966
+ 4. Say in a sentence what it shows, linked: [what it shows](/folder/name.html).
3967
+ Link it from the note it belongs to if they want it there.`;
3520
3968
  ASKING = (write) => `# Asking back, and their own answers
3521
3969
 
3522
3970
  - Where a question to them would do more than an answer (what is unclear in
@@ -3664,6 +4112,50 @@ was not said.`;
3664
4112
  "text",
3665
4113
  "about"
3666
4114
  ]);
4115
+ READ_DATA = fn("read_data", "A data file's shape: its columns with their types and ranges, the row count and the first rows. Read it before writing anything that uses the file, so the column names are real.", { path: {
4116
+ ...str,
4117
+ description: "The file's path in the base: folder/name.csv."
4118
+ } }, ["path"]);
4119
+ ARTIFACT_RECIPE = fn("artifact_recipe", "A recipe for an artifact: a complete working one to start from, the kit's reference, and the rules write_artifact checks. dashboard: numbers, charts and a table of one data file. compare: two data files side by side. simulation: a model with parameters and play. explorable: inputs, derived numbers and linked views. viewer: a 3D model.", { name: {
4120
+ type: "string",
4121
+ enum: [
4122
+ "dashboard",
4123
+ "compare",
4124
+ "simulation",
4125
+ "explorable",
4126
+ "viewer"
4127
+ ]
4128
+ } }, ["name"]);
4129
+ WRITE_ARTIFACT = fn("write_artifact", "Write an artifact: a whole HTML file on the kit. The title, description and uses given here are written into it. It is checked, and run where it can be; the result says what to fix. Nothing is written while a rule is broken.", {
4130
+ path: {
4131
+ ...str,
4132
+ description: "Where: folder/name.html, beside the notes it belongs to."
4133
+ },
4134
+ title: str,
4135
+ description: {
4136
+ ...str,
4137
+ description: "One sentence: what it shows, and what can be done with it."
4138
+ },
4139
+ uses: {
4140
+ type: "array",
4141
+ items: str,
4142
+ description: "The data or model files it reads, relative to it (runs.csv). Those its markup and script name are added."
4143
+ },
4144
+ html: {
4145
+ ...str,
4146
+ description: "The whole file."
4147
+ }
4148
+ }, [
4149
+ "path",
4150
+ "title",
4151
+ "description",
4152
+ "uses",
4153
+ "html"
4154
+ ]);
4155
+ READ_ARTIFACT = fn("read_artifact", "An artifact's whole file, to change it and write it again.", { path: {
4156
+ ...str,
4157
+ description: "folder/name.html"
4158
+ } }, ["path"]);
3667
4159
  OFFER_TEXT = fn("offer_text", "Offer them their own answers, lightly joined, to insert at the end of a note as their writing. Only their words.", {
3668
4160
  id: {
3669
4161
  ...str,
@@ -3678,7 +4170,8 @@ was not said.`;
3678
4170
  ask: [
3679
4171
  POINT_TO_NOTE,
3680
4172
  ASK_QUESTION,
3681
- OFFER_TEXT
4173
+ OFFER_TEXT,
4174
+ READ_DATA
3682
4175
  ],
3683
4176
  write: [
3684
4177
  EDIT_NOTE$1,
@@ -3687,7 +4180,11 @@ was not said.`;
3687
4180
  DELETE_NOTE,
3688
4181
  POINT_TO_NOTE,
3689
4182
  ASK_QUESTION,
3690
- OFFER_TEXT
4183
+ OFFER_TEXT,
4184
+ READ_DATA,
4185
+ ARTIFACT_RECIPE,
4186
+ WRITE_ARTIFACT,
4187
+ READ_ARTIFACT
3691
4188
  ]
3692
4189
  };
3693
4190
  }));
@@ -3892,6 +4389,11 @@ function checkNoteId(id) {
3892
4389
  if (!id || id.length > 400 || parts.some((p) => !p || BAD_PART.test(p))) throw new EngineError(`not a note's id: ${JSON.stringify(id)}`);
3893
4390
  return id;
3894
4391
  }
4392
+ /** A file's path in the base as Iso gives one: no leading slash, no "..", nothing hidden. */
4393
+ function filePathOf(raw) {
4394
+ const p = String(raw ?? "").trim().replace(/^\/+/, "");
4395
+ return p && !p.split("/").some((x) => !x || x === "." || x === ".." || x.startsWith(".")) ? p : null;
4396
+ }
3895
4397
  async function walk(files, root) {
3896
4398
  const out = [];
3897
4399
  const at = (path) => root && path ? `${root}/${path}` : root || path;
@@ -3929,7 +4431,7 @@ function keepDone(before, next) {
3929
4431
  return next;
3930
4432
  }
3931
4433
  }
3932
- var SIGN_IN_MS, fromBase, REPLY_TOKENS, NOTE_TOKENS, DRAFT_PREFIX, DRAFT_DIR, isDraftId, bare, inBase, BAD_PART, TURN_ID, escapeRe, MAX_SENT, BRIEF, NOTE_ARG, MARKED, EDIT_NOTE, WRITE_NOTE, Engine;
4434
+ var SIGN_IN_MS, fromBase, REPLY_TOKENS, NOTE_TOKENS, ARTIFACT_TOKENS, DRAFT_PREFIX, DRAFT_DIR, isDraftId, bare, inBase, BAD_PART, TURN_ID, escapeRe, MAX_SENT, BRIEF, NOTE_ARG, MARKED, EDIT_NOTE, WRITE_NOTE, Engine;
3933
4435
  var init_engine = __esmMin((() => {
3934
4436
  init_src$1();
3935
4437
  init_api();
@@ -3945,6 +4447,7 @@ var init_engine = __esmMin((() => {
3945
4447
  fromBase = (base, text) => `(From the base ${base}: name its notes ${base}:<id>, or give base "${base}".)\n${text}`;
3946
4448
  REPLY_TOKENS = 2500;
3947
4449
  NOTE_TOKENS = 6e3;
4450
+ ARTIFACT_TOKENS = 32e3;
3948
4451
  DRAFT_PREFIX = "@draft/";
3949
4452
  DRAFT_DIR = "drafts";
3950
4453
  isDraftId = (id) => id.startsWith(DRAFT_PREFIX);
@@ -4149,9 +4652,54 @@ var init_engine = __esmMin((() => {
4149
4652
  ...pictures.length ? { pictures } : {}
4150
4653
  };
4151
4654
  }
4655
+ const model = mapModel(this.bundle), files = {};
4656
+ const items = [];
4657
+ const walk = (d) => {
4658
+ for (const c of d.children) if (c.kind === "dir") walk(c);
4659
+ else if (c.id.startsWith("f:")) items.push(c.ref);
4660
+ };
4661
+ walk(model.root);
4662
+ const all = new Set(items);
4663
+ for (const ref of items) {
4664
+ if (ref.endsWith("/")) {
4665
+ const count = [...this.bundle.files].filter((f) => f.startsWith(ref) && !f.slice(ref.length).includes("/")).length;
4666
+ files[ref] = {
4667
+ title: `${ref.slice(0, -1).split("/").at(-1)} · ${count} ${count === 1 ? "picture" : "pictures"}`,
4668
+ kind: "image",
4669
+ count
4670
+ };
4671
+ continue;
4672
+ }
4673
+ const kind = citeKind(ref), name = ref.split("/").at(-1);
4674
+ if (kind !== "artifact") {
4675
+ files[ref] = {
4676
+ title: name,
4677
+ kind
4678
+ };
4679
+ continue;
4680
+ }
4681
+ const html = await this.opts.notes.read(ref).catch(() => null), m = html ? manifestOf(html) : null;
4682
+ files[ref] = {
4683
+ title: m?.title || name,
4684
+ kind
4685
+ };
4686
+ const dir = ref.split("/").slice(0, -1);
4687
+ for (const u of m?.uses ?? []) {
4688
+ const parts = [...dir];
4689
+ for (const p of u.split("/")) if (p === "..") parts.pop();
4690
+ else if (p && p !== ".") parts.push(p);
4691
+ const to = parts.join("/");
4692
+ if (all.has(to) && !model.edges.some(([a, b]) => a === ref && b === to)) model.edges.push([
4693
+ ref,
4694
+ to,
4695
+ STRENGTH.uses
4696
+ ]);
4697
+ }
4698
+ }
4152
4699
  return {
4153
- model: mapModel(this.bundle),
4154
- notes
4700
+ model,
4701
+ notes,
4702
+ ...items.length ? { files } : {}
4155
4703
  };
4156
4704
  }
4157
4705
  /** The base's settings, from `.isorite/settings.toml`. */
@@ -4505,7 +5053,7 @@ var init_engine = __esmMin((() => {
4505
5053
  }
4506
5054
  async deleteNote(id, base, who = "you") {
4507
5055
  if (!inBase(id)) throw new EngineError("only a note of the base is deleted here");
4508
- const notes = this.opts.notes, path = `${checkNoteId(id)}.md`;
5056
+ const { files: notes, path } = this.where(id);
4509
5057
  await this.one(async () => {
4510
5058
  const now = await notes.read(path);
4511
5059
  if (now === null) throw new Conflict(`${id} is gone`, null);
@@ -4543,10 +5091,10 @@ var init_engine = __esmMin((() => {
4543
5091
  this.queue = run.catch(() => void 0);
4544
5092
  return run;
4545
5093
  }
4546
- /** A picture (M22), in the notes or in the base's repository. The path must be a picture's, within its folder,
4547
- * and (in the notes) never in a hidden folder such as `.isorite/`: nothing but a picture is given this way. */
5094
+ /** A file in the notes that isn't a note (M23: any kind; M22 gave pictures only), or a picture in the base's
5095
+ * repository. Within its folder, and (in the notes) never in a hidden folder such as `.isorite/`. */
4548
5096
  async file(path, from = "notes") {
4549
- const type = pictureType(path);
5097
+ const type = from === "code" ? pictureType(path) : fileType(path);
4550
5098
  if (!type) return null;
4551
5099
  let files, at;
4552
5100
  if (from === "code") {
@@ -4646,6 +5194,10 @@ var init_engine = __esmMin((() => {
4646
5194
  }
4647
5195
  /** Where a note's file is: drafts and Understanding notes are among isorite's own files. */
4648
5196
  where(id) {
5197
+ if (inBase(id) && citeKind(id) === "artifact") return {
5198
+ files: this.opts.notes,
5199
+ path: checkNoteId(id)
5200
+ };
4649
5201
  if (!inBase(id)) return {
4650
5202
  files: this.opts.own,
4651
5203
  path: `${isDraftId(id) ? DRAFT_DIR : UNDERSTANDING_DIR}/${checkNoteId(bare(id))}.md`
@@ -6008,9 +6560,9 @@ var init_engine = __esmMin((() => {
6008
6560
  tier: req.tier,
6009
6561
  messages,
6010
6562
  lookup: tools,
6011
- maxTokens: REPLY_TOKENS * 2,
6012
6563
  signal: abort.signal,
6013
- most: ROUNDS[req.tier],
6564
+ maxTokens: mode === "write" ? ARTIFACT_TOKENS : REPLY_TOKENS * 2,
6565
+ most: mode === "write" ? Math.max(ROUNDS[req.tier], 6) : ROUNDS[req.tier],
6014
6566
  onText: (delta, again) => this.publish({
6015
6567
  type: "iso-text",
6016
6568
  request: turn,
@@ -6321,8 +6873,101 @@ var init_engine = __esmMin((() => {
6321
6873
  });
6322
6874
  return `Your reading of ${title(id)} is written.`;
6323
6875
  }
6876
+ if (name === "read_data") {
6877
+ const path = filePathOf(args.path);
6878
+ if (!path) return `${String(args.path ?? "")} is not a file's path in their base.`;
6879
+ const got = await this.file(path).catch(() => null);
6880
+ if (!got) return `There is no file ${path}.${this.nearFiles(path)}`;
6881
+ try {
6882
+ return describeShape(shapeOf(path, parseData(path, new TextDecoder().decode(got.bytes))));
6883
+ } catch (e) {
6884
+ return `${path} could not be read as data: ${e.message}`;
6885
+ }
6886
+ }
6887
+ if (name === "artifact_recipe") {
6888
+ const r = RECIPES[String(args.name ?? "")];
6889
+ if (!r) return `There's no recipe ${String(args.name ?? "")}: there are ${Object.keys(RECIPES).join(", ")}.`;
6890
+ return `${REFERENCE}\n\n# The recipe: ${String(args.name)}\n\n${r.about}. A complete working artifact; change what it marks, keep the rest.\n\n\`\`\`html\n${r.html}\`\`\``;
6891
+ }
6892
+ if (name === "read_artifact") {
6893
+ const path = filePathOf(args.path);
6894
+ if (!path || citeKind(path) !== "artifact") return `${String(args.path ?? "")} is not an artifact's path (folder/name.html).`;
6895
+ const got = await this.opts.notes.read(path);
6896
+ return got === null ? `There is no artifact ${path}.` : got;
6897
+ }
6898
+ if (name === "write_artifact") return this.writeArtifact(args, t);
6324
6899
  return `${name} cannot be used yet.`;
6325
6900
  }
6901
+ /** Files in the same folder as a path, to say which there are when one isn't. */
6902
+ nearFiles(path) {
6903
+ const dir = path.split("/").slice(0, -1).join("/"), there = [...this.files.keys()].filter((f) => !f.endsWith(".md") && f.split("/").slice(0, -1).join("/") === dir);
6904
+ return there.length ? ` In ${dir || "the top folder"} there are: ${there.map((f) => f.split("/").at(-1)).join(", ")}.` : "";
6905
+ }
6906
+ /** write_artifact (M23, plan-m23 §3.5, §3.7): the manifest written as given, the rules checked before anything is
6907
+ * written, then written and journalled as a note is, and run where it can be, with all it reports said back. */
6908
+ async writeArtifact(args, t) {
6909
+ const path = filePathOf(args.path);
6910
+ if (!path || citeKind(path) !== "artifact" || !inBase(path)) return `${String(args.path ?? "")} is not an artifact's path: give folder/name.html.`;
6911
+ const raw = String(args.html ?? "");
6912
+ if (!raw.trim()) return "Give the whole file, in html.";
6913
+ const given = (Array.isArray(args.uses) ? args.uses : typeof args.uses === "string" ? args.uses.split(/[\s,]+/) : []).map((u) => String(u));
6914
+ const uses = [...new Set([
6915
+ ...given,
6916
+ ...manifestOf(raw).uses,
6917
+ ...readsOf(raw)
6918
+ ].map((u) => u.trim().replace(/^\.\//, "")).filter(Boolean))];
6919
+ const title = String(args.title ?? "").trim() || manifestOf(raw).title || path.split("/").at(-1).replace(/\.html?$/, "").replace(/-/g, " ");
6920
+ const html = withManifest(raw, {
6921
+ title,
6922
+ description: String(args.description ?? "").trim() || manifestOf(raw).description,
6923
+ author: "Iso",
6924
+ date: new Date(this.opts.clock.now()).toISOString().slice(0, 10),
6925
+ kit: "1",
6926
+ uses
6927
+ });
6928
+ const dir = path.split("/").slice(0, -1);
6929
+ const at = (rel) => {
6930
+ const parts = [...dir];
6931
+ for (const p of rel.split("/")) if (p === "..") parts.pop();
6932
+ else if (p && p !== ".") parts.push(p);
6933
+ return parts.join("/");
6934
+ };
6935
+ const files = [], columns = {};
6936
+ for (const u of uses) {
6937
+ const got = await this.file(at(u)).catch(() => null);
6938
+ if (!got) continue;
6939
+ files.push(u);
6940
+ if (citeKind(u) === "data") try {
6941
+ columns[u] = columnsOf(parseData(u, new TextDecoder().decode(got.bytes)));
6942
+ } catch {}
6943
+ }
6944
+ const broken = checkArtifact(html, {
6945
+ files,
6946
+ columns
6947
+ }), errors = broken.filter((b) => b.level === "error");
6948
+ const list = (bs) => bs.map((b) => `- ${b.message}`).join("\n");
6949
+ if (errors.length) return `Not written: ${errors.length === 1 ? "a rule is" : "rules are"} broken.\n${list(errors)}\nFix ${errors.length === 1 ? "it" : "them"} and call write_artifact again with the whole file.`;
6950
+ const was = await this.opts.notes.read(path);
6951
+ const saved = await this.one(() => this.put(path, html, was === null ? null : hashText(was), "edit", { asked: t.asked }, t.who));
6952
+ if (saved.entry) await t.item({
6953
+ kind: "change",
6954
+ note: path,
6955
+ entry: saved.entry,
6956
+ why: was === null ? "create" : "edit",
6957
+ title,
6958
+ added: html.split("\n").length,
6959
+ removed: was?.split("\n").length ?? 0
6960
+ });
6961
+ const warnings = broken.filter((b) => b.level === "warning");
6962
+ const run = await this.opts.renderArtifact?.(path, html).catch(() => null) ?? null;
6963
+ const problems = [
6964
+ ...run && !run.rendered ? ["it didn't start: the kit never said it was ready"] : [],
6965
+ ...(run?.errors ?? []).map((e) => `it reported: ${e}`),
6966
+ ...run && run.rendered && run.drawn === 0 ? ["it drew nothing: no chart, canvas or 3D view has anything in it"] : [],
6967
+ ...warnings.map((w) => w.message)
6968
+ ];
6969
+ return `${was === null ? "Written" : "Written anew"}: ${path}. ${run ? "It was run, with every control touched" : "It couldn't be run here (no browser), so only its rules were checked"}${problems.length ? `, and:\n${problems.map((p) => `- ${p}`).join("\n")}\nFix these and call write_artifact again with the whole file.` : run ? ", with no error." : "."}`;
6970
+ }
6326
6971
  /** A note's Understanding note (M10): Iso's reading and your scratchpad. */
6327
6972
  async understanding(note) {
6328
6973
  if (!inBase(checkNoteId(note))) throw new EngineError("only a note of the base has an Understanding note");
@@ -8476,7 +9121,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal
8476
9121
  if (e instanceof EngineError) return refuse(c, e.status, e.message);
8477
9122
  throw e;
8478
9123
  }
8479
- if (!found) return refuse(c, 404, `no picture at ${path}`);
9124
+ if (!found) return refuse(c, 404, `no file at ${path}`);
8480
9125
  const etag = `"${found.version}"`;
8481
9126
  const headers = {
8482
9127
  "Content-Type": found.type,
@@ -9134,7 +9779,7 @@ var Setup = class {
9134
9779
  };
9135
9780
  //#endregion
9136
9781
  //#region packages/server/src/version.ts
9137
- const VERSION = "0.10.0";
9782
+ const VERSION = "0.11.0";
9138
9783
  //#endregion
9139
9784
  //#region packages/server/src/git.ts
9140
9785
  /** The top of the repository `folder` is in, or null: the nearest folder holding `.git`, a directory or, in a
@@ -9188,9 +9833,155 @@ function openStudyBase(path) {
9188
9833
  };
9189
9834
  }
9190
9835
  //#endregion
9836
+ //#region packages/server/src/render.ts
9837
+ /** A Chromium on this machine: ISORITE_BROWSER, Playwright's own, or a browser installed in the usual places. */
9838
+ function findBrowser(env = process.env) {
9839
+ if (env.ISORITE_BROWSER && existsSync(env.ISORITE_BROWSER)) return env.ISORITE_BROWSER;
9840
+ const cache = env.PLAYWRIGHT_BROWSERS_PATH ?? (process.platform === "darwin" ? join(homedir(), "Library", "Caches", "ms-playwright") : process.platform === "win32" ? join(env.LOCALAPPDATA ?? "", "ms-playwright") : join(homedir(), ".cache", "ms-playwright"));
9841
+ const found = [];
9842
+ try {
9843
+ for (const d of readdirSync(cache).filter((d) => /^chromium(_headless_shell)?-\d+$/.test(d)).sort().reverse()) for (const p of [
9844
+ "chrome-linux/headless_shell",
9845
+ "chrome-headless-shell-linux64/chrome-headless-shell",
9846
+ "chrome-linux/chrome",
9847
+ "chrome-linux64/chrome",
9848
+ "chrome-mac/headless_shell",
9849
+ "chrome-headless-shell-mac-arm64/chrome-headless-shell",
9850
+ "chrome-mac/Chromium.app/Contents/MacOS/Chromium",
9851
+ "chrome-win/headless_shell.exe",
9852
+ "chrome-headless-shell-win64/chrome-headless-shell.exe",
9853
+ "chrome-win/chrome.exe"
9854
+ ]) found.push(join(cache, d, p));
9855
+ } catch {}
9856
+ found.push("/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/snap/bin/chromium", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Chromium.app/Contents/MacOS/Chromium", join(env["PROGRAMFILES"] ?? "C:\\Program Files", "Google", "Chrome", "Application", "chrome.exe"), join(env["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)", "Microsoft", "Edge", "Application", "msedge.exe"));
9857
+ return found.find((p) => existsSync(p)) ?? null;
9858
+ }
9859
+ /** The host: a sandboxed frame given the document, and the bridge, answering files through window.isoRead. */
9860
+ const HOST = `(doc, ground) => {
9861
+ const f = document.getElementById("f");
9862
+ window.said = []; window.ready = false;
9863
+ const tell = (m, t = []) => f.contentWindow.postMessage({ isorite: 0, ...m }, "*", t);
9864
+ addEventListener("message", async (e) => {
9865
+ const m = e.data;
9866
+ if (e.source !== f.contentWindow || !m || m.isorite !== 0) return;
9867
+ if (m.type === "ready") { window.ready = true; tell({ type: "ground", ground }); tell({ type: "look", look: "isorite" }); }
9868
+ else if (m.type === "file" || m.type === "bytes") {
9869
+ const got = await window.isoRead(m.path);
9870
+ if (got.error) tell({ type: "reply", id: m.id, error: got.error });
9871
+ else if (m.type === "bytes") { const buf = got.b64 !== undefined ? Uint8Array.from(atob(got.b64), (c) => c.charCodeAt(0)).buffer : new TextEncoder().encode(got.text).buffer; tell({ type: "reply", id: m.id, bytes: buf }, [buf]); }
9872
+ else tell({ type: "reply", id: m.id, text: got.text ?? atob(got.b64) });
9873
+ }
9874
+ else if (m.type === "height") f.style.height = Math.min(8000, Math.ceil(m.height) + 4) + "px";
9875
+ else if (m.type === "error") window.said.push(String(m.message));
9876
+ });
9877
+ f.srcdoc = doc;
9878
+ }`;
9879
+ /** What it drew: SVG marks, a table's cells, and 100 for each canvas with anything on it (a WebGL one, if it has a size). */
9880
+ const DRAWN = `(() => {
9881
+ let n = document.querySelectorAll("svg path, svg line, svg circle, svg rect, svg polyline, svg polygon, svg text, iso-table td").length;
9882
+ for (const c of document.querySelectorAll("canvas")) {
9883
+ try {
9884
+ const g = c.getContext("2d");
9885
+ if (g) { const d = g.getImageData(0, 0, c.width, c.height).data; if (d.some((v, i) => v !== d[i % 4])) n += 100; }
9886
+ else n += 100;
9887
+ } catch { n += 100; }
9888
+ }
9889
+ return n;
9890
+ })()`;
9891
+ /** Every control touched: sliders to three quarters, choices to their second, buttons pressed, a drag on a canvas. */
9892
+ const TOUCH = `(async () => {
9893
+ for (const r of document.querySelectorAll("input[type=range]")) { r.value = String(+r.min + (+r.max - +r.min) * 0.75); r.dispatchEvent(new Event("input", { bubbles: true })); r.dispatchEvent(new Event("change", { bubbles: true })); }
9894
+ for (const s of document.querySelectorAll("select")) if (s.options.length > 1) { s.selectedIndex = 1; s.dispatchEvent(new Event("change", { bubbles: true })); }
9895
+ for (const b of document.querySelectorAll("button, input[type=checkbox]")) if (!b.closest("iso-note")) b.click();
9896
+ await new Promise((r) => setTimeout(r, 400));
9897
+ const c = document.querySelector("canvas");
9898
+ if (c) {
9899
+ const r = c.getBoundingClientRect(), at = (t, x) => c.dispatchEvent(new PointerEvent(t, { clientX: r.left + x, clientY: r.top + r.height / 2, bubbles: true, pointerId: 1 }));
9900
+ at("pointerdown", r.width / 2); at("pointermove", r.width / 2 + 60); at("pointerup", r.width / 2 + 60);
9901
+ c.dispatchEvent(new MouseEvent("click", { clientX: r.left + r.width / 3, clientY: r.top + r.height / 3, bubbles: true }));
9902
+ }
9903
+ })()`;
9904
+ let launched = null;
9905
+ async function browser(path) {
9906
+ launched ??= (async () => {
9907
+ const { chromium } = await import("playwright-core");
9908
+ return chromium.launch({
9909
+ executablePath: path,
9910
+ args: [
9911
+ "--use-gl=angle",
9912
+ "--use-angle=swiftshader",
9913
+ "--enable-unsafe-swiftshader"
9914
+ ]
9915
+ });
9916
+ })().catch((e) => {
9917
+ launched = null;
9918
+ throw e;
9919
+ });
9920
+ return launched;
9921
+ }
9922
+ /** Render an assembled artifact (packages/kit's assemble) unseen, answer its bridge from `read` for the files in
9923
+ * `uses`, touch its controls, and say what happened. */
9924
+ async function renderArtifact(doc, uses, read, opts) {
9925
+ const b = await browser(opts.executable), width = opts.width ?? 1100;
9926
+ const page = await b.newPage({ viewport: {
9927
+ width,
9928
+ height: 900
9929
+ } });
9930
+ const errors = [];
9931
+ page.on("console", (m) => {
9932
+ if (m.type?.() === "error") errors.push(m.text());
9933
+ });
9934
+ page.on("pageerror", (e) => errors.push(e.message ?? String(e)));
9935
+ const work = (async () => {
9936
+ await page.exposeFunction("isoRead", async (rel) => {
9937
+ if (!uses.includes(rel)) return { error: `${rel} is not in the artifact's manifest (isorite:uses)` };
9938
+ const got = await read(rel);
9939
+ if (got === null) return { error: `${rel}: no such file beside the artifact` };
9940
+ return typeof got === "string" ? { text: got } : { b64: Buffer.from(got).toString("base64") };
9941
+ });
9942
+ await page.setContent(`<!doctype html><html><body style="margin:0;background:${opts.ground === "quartz" ? "#F2F0EA" : "#07101C"}"><iframe id="f" sandbox="allow-scripts" style="display:block;width:${width}px;height:900px;border:0"></iframe></body></html>`);
9943
+ await page.evaluate(`(${HOST})(${JSON.stringify(doc)}, ${JSON.stringify(opts.ground ?? "lapis")})`);
9944
+ await page.waitForTimeout(2e3);
9945
+ const ready = await page.evaluate("window.ready");
9946
+ const frame = page.frames().find((fr) => fr !== page.mainFrame());
9947
+ const height = await page.evaluate("document.getElementById(\"f\").offsetHeight");
9948
+ const drawn = frame ? await frame.evaluate(DRAWN) : 0;
9949
+ let screenshot;
9950
+ if (opts.screenshot) {
9951
+ await page.setViewportSize({
9952
+ width,
9953
+ height: Math.max(400, Math.min(height, 4e3))
9954
+ });
9955
+ screenshot = await page.screenshot({ fullPage: true });
9956
+ }
9957
+ if (frame) await frame.evaluate(TOUCH);
9958
+ await page.waitForTimeout(600);
9959
+ const said = await page.evaluate("window.said");
9960
+ return {
9961
+ rendered: ready,
9962
+ errors: [.../* @__PURE__ */ new Set([...said, ...errors])].slice(0, 12),
9963
+ drawn,
9964
+ height,
9965
+ ...screenshot ? { screenshot } : {}
9966
+ };
9967
+ })();
9968
+ const timeout = new Promise((resolve) => setTimeout(() => resolve({
9969
+ rendered: false,
9970
+ errors: ["it didn't finish starting within the time allowed: a loop that never ends?"],
9971
+ drawn: 0,
9972
+ height: 0
9973
+ }), opts.timeout ?? 2e4));
9974
+ try {
9975
+ return await Promise.race([work, timeout]);
9976
+ } finally {
9977
+ await page.close().catch(() => void 0);
9978
+ }
9979
+ }
9980
+ //#endregion
9191
9981
  //#region packages/server/src/serve.ts
9192
9982
  init_src$1();
9193
9983
  init_src();
9984
+ init_src$2();
9194
9985
  /** The built app: in a checkout, where npm run build puts it; installed from npm, in the package's web/. */
9195
9986
  const WEB_DIR = [join(dirname(fileURLToPath(import.meta.url)), "..", "web"), join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "app", "build")].find((dir) => existsSync(join(dir, "index.html"))) ?? join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "app", "build");
9196
9987
  const moveIt = (study) => `${study} is a study base, which this isorite no longer reads (format 1). Move it into its notes with: isorite migrate ${study}`;
@@ -9248,9 +10039,28 @@ async function openEngine({ notes }, device = deviceId(), config = configPath(),
9248
10039
  name: `vectors-${createHash("sha256").update(notes).digest("hex").slice(0, 16)}.json`
9249
10040
  }
9250
10041
  } : {},
9251
- ...extras.elsewhere ? { elsewhere: extras.elsewhere } : {}
10042
+ ...extras.elsewhere ? { elsewhere: extras.elsewhere } : {},
10043
+ renderArtifact: (path, html) => runArtifact(notes, path, html)
9252
10044
  });
9253
10045
  }
10046
+ /** Run an artifact unseen: null when there's no browser, or no built kit, to run it with. */
10047
+ async function runArtifact(notes, path, html) {
10048
+ const executable = findBrowser(), kitDir = join(WEB_DIR, "kit");
10049
+ if (!executable || !existsSync(join(kitDir, "kit.js"))) return null;
10050
+ const kit = Object.fromEntries(KIT_FILES.map((f) => [f, readFileSync(join(kitDir, f), "utf8")]));
10051
+ const dir = dirname(resolve(notes, path)), uses = manifestOf(html).uses;
10052
+ const read = async (rel) => {
10053
+ const at = resolve(dir, rel);
10054
+ if (!at.startsWith(resolve(notes) + sep) || !existsSync(at)) return null;
10055
+ return /\.(csv|tsv|json|jsonl|ndjson|txt|gltf|obj)$/i.test(at) ? readFileSync(at, "utf8") : new Uint8Array(readFileSync(at));
10056
+ };
10057
+ const got = await renderArtifact(assemble(html, kit), uses, read, { executable });
10058
+ return {
10059
+ rendered: got.rendered,
10060
+ errors: got.errors,
10061
+ drawn: got.drawn
10062
+ };
10063
+ }
9254
10064
  /** The guide's finder, as a provider: no key, no sign-in, and a name that says what it is. */
9255
10065
  const { auth: _auth, ...openrouter } = OPENROUTER;
9256
10066
  const FINDER = {
@@ -9981,6 +10791,8 @@ async function doctor(folder, o = {}) {
9981
10791
  say("browser pages", asking.length ? "fail" : "ok", asking.length ? `${asking.join(" and ")} asked for a password (401, Basic): that is not isorite's server; stop it, or give isorite another port (--port <n>)` : `isorite's pages at ${url}/ ask for no password. ${hint}`);
9982
10792
  }
9983
10793
  if (env.SSH_CONNECTION) say("browser", "ok", `over SSH: on your own machine, ssh -L ${port}:localhost:${port} <this host>, then http://localhost:${port}/`);
10794
+ const chrome = findBrowser(env);
10795
+ say("artifacts", chrome ? "ok" : "warn", chrome ? `run unseen when Iso writes one, in ${chrome}` : "no Chromium found to run them unseen, so only their rules are checked when Iso writes one; ISORITE_BROWSER names one (Chrome, Chromium, Edge)");
9984
10796
  let model = null;
9985
10797
  if (env.ISORITE_EMBED === "off") say("search by meaning", "skip", "switched off (ISORITE_EMBED=off)");
9986
10798
  else {
@@ -11377,14 +12189,14 @@ const finderHttp = (pace = 0) => scriptedHttp(finderAnswer, pace);
11377
12189
  //#region packages/guide/src/node.ts
11378
12190
  /** In a checkout, the package's notes/; in the npm package, where this is bundled into dist/, the guide/ beside it. */
11379
12191
  const GUIDE_DIR = [join(import.meta.dirname, "..", "guide"), join(import.meta.dirname, "..", "notes")].find((d) => existsSync(join(d, "start-here.md"))) ?? join(import.meta.dirname, "..", "notes");
11380
- /** The guide's pictures (M22), by path, as bytes. */
12192
+ /** The guide's files that aren't notes, by path, as bytes: its pictures (M22), and its artifacts and their data (M23). */
11381
12193
  function loadGuidePictures(dir = GUIDE_DIR) {
11382
12194
  const out = {};
11383
12195
  const visit = (at) => {
11384
12196
  for (const name of readdirSync(at)) {
11385
12197
  const path = join(at, name);
11386
12198
  if (statSync(path).isDirectory()) visit(path);
11387
- else if (/\.(png|jpe?g|gif|svg|webp|avif)$/i.test(name)) out[relative(dir, path).split(sep).join("/")] = new Uint8Array(readFileSync(path));
12199
+ else if (/\.(png|jpe?g|gif|svg|webp|avif|html|csv|tsv|json|jsonl|stl|glb|gltf|obj|pdf)$/i.test(name)) out[relative(dir, path).split(sep).join("/")] = new Uint8Array(readFileSync(path));
11388
12200
  }
11389
12201
  };
11390
12202
  visit(dir);
@@ -11606,6 +12418,7 @@ function bases(args, { config = configPath(), out = console.log } = {}) {
11606
12418
  }
11607
12419
  //#endregion
11608
12420
  //#region packages/server/src/main.ts
12421
+ init_src$2();
11609
12422
  init_src$1();
11610
12423
  init_model();
11611
12424
  const USAGE = `isorite: an agentic harness for understanding
@@ -11658,8 +12471,29 @@ function check(notes, out = console.log, unused = false) {
11658
12471
  const bundle = loadBundle(root), issues = bundle.lint(), cycles = bundle.requiresCycles();
11659
12472
  for (const i of issues) out(`${i.level.padEnd(7)} ${i.path}: ${i.message}`);
11660
12473
  for (const c of cycles) out(`warning requires cycle: ${c.join(" -> ")}`);
11661
- const errors = issues.filter((i) => i.level === "error").length;
11662
- out(`${bundle.concepts.size} notes in ${bundle.directories.size} folders; ${errors} errors, ${issues.length - errors} warnings, ${cycles.length} requires cycles`);
12474
+ let artifactErrors = 0, artifactWarnings = 0;
12475
+ for (const f of [...bundle.files].filter((x) => citeKind(x) === "artifact").sort()) {
12476
+ const html = readFileSync(join(root, f), "utf8");
12477
+ if (manifestOf(html).kit === null) continue;
12478
+ const dir = dirname(join(root, f)), files = manifestOf(html).uses.filter((u) => existsSync(join(dir, u)));
12479
+ const columns = Object.fromEntries(files.filter((u) => citeKind(u) === "data").flatMap((u) => {
12480
+ try {
12481
+ return [[u, columnsOf(parseData(u, readFileSync(join(dir, u), "utf8")))]];
12482
+ } catch {
12483
+ return [];
12484
+ }
12485
+ }));
12486
+ for (const b of checkArtifact(html, {
12487
+ files,
12488
+ columns
12489
+ })) {
12490
+ out(`${b.level.padEnd(7)} ${f}: ${b.message}`);
12491
+ if (b.level === "error") artifactErrors++;
12492
+ else artifactWarnings++;
12493
+ }
12494
+ }
12495
+ const errors = issues.filter((i) => i.level === "error").length + artifactErrors;
12496
+ out(`${bundle.concepts.size} notes in ${bundle.directories.size} folders; ${errors} errors, ${issues.length - issues.filter((i) => i.level === "error").length + artifactWarnings} warnings, ${cycles.length} requires cycles`);
11663
12497
  if (unused) {
11664
12498
  const cited = new Set([...bundle.concepts.values()].flatMap((c) => c.cites.map((x) => x.target)));
11665
12499
  const idle = [...bundle.files].filter((f) => citeKind(f) === "image" && !cited.has(f)).sort();