isorite 0.10.0 → 0.12.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 (75) hide show
  1. package/README.md +2 -5
  2. package/dist/core.js +184 -13
  3. package/dist/main.js +1018 -200
  4. package/docs/setup-for-agents.md +11 -36
  5. package/docs/setup.md +20 -61
  6. package/docs/terminal.md +28 -80
  7. package/package.json +2 -1
  8. package/web/_app/immutable/assets/0.BXPxx6Q0.css +1 -0
  9. package/web/_app/immutable/assets/5.rBvyRuST.css +1 -0
  10. package/web/_app/immutable/assets/{7.ljiu9bn5.css → 7.C00vyVPo.css} +1 -1
  11. package/web/_app/immutable/assets/Drawing.Q7J9MyIo.css +1 -0
  12. package/web/_app/immutable/assets/NoteEditor.S3q5lmKq.css +1 -0
  13. package/web/_app/immutable/assets/{NoteList.F579Zvj_.css → NoteList.E6jC-AtZ.css} +1 -1
  14. package/web/_app/immutable/assets/Timeline.LsRXYIBv.css +1 -0
  15. package/web/_app/immutable/assets/ground.hxi5N5fC.css +1 -0
  16. package/web/_app/immutable/chunks/4kgFUGOH.js +1 -0
  17. package/web/_app/immutable/chunks/8SS1q0nk.js +7 -0
  18. package/web/_app/immutable/chunks/{C5MPRibb.js → BK6pLgdi.js} +1 -1
  19. package/web/_app/immutable/chunks/{CiU6tYJv.js → Bk_JinGf.js} +1 -1
  20. package/web/_app/immutable/chunks/CFEI5lV9.js +1 -0
  21. package/web/_app/immutable/chunks/{6RXH8WsD.js → CiMp12cC.js} +1 -1
  22. package/web/_app/immutable/chunks/{BcgO-fBX.js → D4YEq54N.js} +1 -1
  23. package/web/_app/immutable/chunks/{CsRPJjQS.js → DDCg82xL.js} +1 -1
  24. package/web/_app/immutable/chunks/DThNJvmp.js +1 -0
  25. package/web/_app/immutable/chunks/{DQjIi5Jv.js → D_jbSWJl.js} +1 -1
  26. package/web/_app/immutable/chunks/DbeMVOH4.js +1 -0
  27. package/web/_app/immutable/chunks/DsQj0LyB.js +523 -0
  28. package/web/_app/immutable/chunks/{CBkjDkEN.js → Uo6irDQW.js} +80 -79
  29. package/web/_app/immutable/chunks/{DCD8_jrW.js → jTXVas70.js} +1 -1
  30. package/web/_app/immutable/entry/app.CvK8zypr.js +2 -0
  31. package/web/_app/immutable/entry/start.Dcgm6sQd.js +1 -0
  32. package/web/_app/immutable/nodes/0.0TdRBEoR.js +26 -0
  33. package/web/_app/immutable/nodes/{1.rzuq9VRh.js → 1.Dchqgt4j.js} +1 -1
  34. package/web/_app/immutable/nodes/2.C0CMUdSu.js +1 -0
  35. package/web/_app/immutable/nodes/3.MjtWDMWd.js +1 -0
  36. package/web/_app/immutable/nodes/4.hhS0SQta.js +2 -0
  37. package/web/_app/immutable/nodes/5.C33_cw2J.js +9 -0
  38. package/web/_app/immutable/nodes/6.owNfwwRl.js +1 -0
  39. package/web/_app/immutable/nodes/7.BpxHVHoY.js +1 -0
  40. package/web/_app/version.json +1 -1
  41. package/web/index.html +20 -18
  42. package/web/kit/chart.js +55 -0
  43. package/web/kit/isorite.css +32 -0
  44. package/web/kit/kit.css +80 -0
  45. package/web/kit/kit.js +543 -0
  46. package/web/kit/licenses/d3.txt +13 -0
  47. package/web/kit/licenses/interval-tree-1d.txt +22 -0
  48. package/web/kit/licenses/isoformat.txt +13 -0
  49. package/web/kit/licenses/katex.txt +21 -0
  50. package/web/kit/licenses/observablehq-plot.txt +13 -0
  51. package/web/kit/licenses/three.txt +21 -0
  52. package/web/kit/math.css +1 -0
  53. package/web/kit/math.js +257 -0
  54. package/web/kit/model.js +4236 -0
  55. package/web/kit/plain.css +16 -0
  56. package/web/_app/immutable/assets/0.CPW9fjPu.css +0 -1
  57. package/web/_app/immutable/assets/5.TsqJFEFF.css +0 -1
  58. package/web/_app/immutable/assets/NoteEditor.Wt5vU9Tb.css +0 -1
  59. package/web/_app/immutable/assets/Timeline.KTc3LKuK.css +0 -1
  60. package/web/_app/immutable/chunks/B0A4Snxv.js +0 -6
  61. package/web/_app/immutable/chunks/B8Qvvm53.js +0 -1
  62. package/web/_app/immutable/chunks/BJo70SOc.js +0 -1
  63. package/web/_app/immutable/chunks/BYSDTLqw.js +0 -1
  64. package/web/_app/immutable/chunks/BimZp6R0.js +0 -523
  65. package/web/_app/immutable/chunks/DlKQFacy.js +0 -1
  66. package/web/_app/immutable/chunks/c7PFFMl4.js +0 -1
  67. package/web/_app/immutable/entry/app.iw6ZIOh2.js +0 -2
  68. package/web/_app/immutable/entry/start.CFnKMSWs.js +0 -1
  69. package/web/_app/immutable/nodes/0.BzEgwgsa.js +0 -26
  70. package/web/_app/immutable/nodes/2.D_jWsQLW.js +0 -1
  71. package/web/_app/immutable/nodes/3.BzRx3SiD.js +0 -1
  72. package/web/_app/immutable/nodes/4.DP2MGav1.js +0 -2
  73. package/web/_app/immutable/nodes/5.BPS-CCQT.js +0 -5
  74. package/web/_app/immutable/nodes/6.DQMCyNJ3.js +0 -1
  75. package/web/_app/immutable/nodes/7.MWEn-qPC.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: the look (every colour, font and corner), the elements below, and `window.iso`. Write what it shows and does, never how 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 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: `data=\"euler.csv leapfrog.csv\"` adds a column `file` (`euler`, `leapfrog`) to compare them with `by=\"file\"`. `where=\"integrator=leapfrog\"` keeps some rows; `where=\"t<=$t\"` compares with a control's value. `abs(col)` is a column'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 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`, `mean`, `sum`, `count`, `first`, `last`. Tones: `gold`, `blue`, `green`, `rose`. Without `data`, set it from a 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 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 a canvas sized to its panel; call `.redraw()` after a change. `t` is the theme: `t.text`, `t.soft`, `t.faint`, `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 `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)`, `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 what 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;
@@ -1196,6 +1570,79 @@ var init_source = __esmMin((() => {
1196
1570
  };
1197
1571
  }));
1198
1572
  //#endregion
1573
+ //#region packages/core/src/unwrap.ts
1574
+ function unwrap(text) {
1575
+ const lines = text.split("\n"), out = [];
1576
+ let i = 0, fence = "", maths = false;
1577
+ /** What the last line written may be continued by: prose, a quoted line, or nothing. */
1578
+ let open = "";
1579
+ if (lines[0] === "---") {
1580
+ const end = lines.indexOf("---", 1);
1581
+ if (end > 0) {
1582
+ out.push(...lines.slice(0, end + 1));
1583
+ i = end + 1;
1584
+ }
1585
+ }
1586
+ for (; i < lines.length; i++) {
1587
+ const l = lines[i], t = l.trim();
1588
+ if (fence) {
1589
+ out.push(l);
1590
+ if (t.startsWith(fence)) fence = "";
1591
+ continue;
1592
+ }
1593
+ if (maths) {
1594
+ out.push(l);
1595
+ if (t.endsWith("$$")) maths = false;
1596
+ continue;
1597
+ }
1598
+ if (t.startsWith("```") || t.startsWith("~~~")) {
1599
+ fence = t.slice(0, 3);
1600
+ out.push(l);
1601
+ open = "";
1602
+ continue;
1603
+ }
1604
+ if (t.startsWith("$$") && !t.slice(2).includes("$$")) {
1605
+ maths = true;
1606
+ out.push(l);
1607
+ open = "";
1608
+ continue;
1609
+ }
1610
+ if (!t) {
1611
+ out.push(l);
1612
+ open = "";
1613
+ continue;
1614
+ }
1615
+ const hard = /( {2}|\\)$/.test(out.at(-1) ?? "");
1616
+ if (APART.test(l)) {
1617
+ if (open === "quote" && QUOTED.test(l) && !/^\s*>\s*([-*+]|\d+[.)]|#)/.test(l) && !hard) {
1618
+ out[out.length - 1] += " " + l.replace(/^\s*>\s?/, "").trim();
1619
+ continue;
1620
+ }
1621
+ out.push(l);
1622
+ open = QUOTED.test(l) ? "quote" : "";
1623
+ continue;
1624
+ }
1625
+ if (ITEM.test(l)) {
1626
+ out.push(l);
1627
+ open = "prose";
1628
+ continue;
1629
+ }
1630
+ if (open === "prose" && !hard) {
1631
+ out[out.length - 1] += " " + t;
1632
+ continue;
1633
+ }
1634
+ out.push(l);
1635
+ open = open === "" && /^( {4}|\t)/.test(l) ? "" : "prose";
1636
+ }
1637
+ return out.join("\n");
1638
+ }
1639
+ var ITEM, APART, QUOTED;
1640
+ var init_unwrap = __esmMin((() => {
1641
+ ITEM = /^\s*([-*+]|\d+[.)])\s+/;
1642
+ APART = /^\s*(#{1,6}\s|\||<|```|~~~|\$\$|>|---\s*$|\*\*\*\s*$|___\s*$|\[[^\]]+\]:\s)/;
1643
+ QUOTED = /^\s*>\s?\S/;
1644
+ }));
1645
+ //#endregion
1199
1646
  //#region packages/core/src/search.ts
1200
1647
  function stem(token) {
1201
1648
  for (const suffix of ["ness", "ing"]) if (token.endsWith(suffix) && token.length - suffix.length >= 4) return token.slice(0, -suffix.length);
@@ -1719,16 +2166,21 @@ function written$1(note, p, region) {
1719
2166
  lines.splice(at, 0, ...blank(at - 1) ? [] : [""], ...ins, ...blank(at) ? [] : [""]);
1720
2167
  return lines.join("\n");
1721
2168
  }
1722
- /** The journal as the timeline shows it. Your own edits in a row within a session are one moment; a proposal is
1723
- * one moment whose `what` says how it ended. Revisions are not moments. */
2169
+ /** The journal as the timeline shows it. Your own edits in a row within a session are one moment, until ten minutes
2170
+ * pass with none (LAYER_IDLE_MS); a proposal is one moment whose `what` says how it ended. Revisions are not
2171
+ * moments. */
1724
2172
  function moments(entries) {
1725
2173
  const out = [], offered = /* @__PURE__ */ new Map(), asked = /* @__PURE__ */ new Map();
2174
+ /** Lines a change put in and took out: a note's first entry, all of its text. */
2175
+ const size = (c) => c.why === "start" || !c.hunks.length && c.before === null ? [(c.text ?? "").split("\n").length, 0] : [c.hunks.reduce((n, h) => n + h.ins.length, 0), c.hunks.reduce((n, h) => n + h.del.length, 0)];
1726
2176
  for (const e of entries) {
1727
2177
  const base = {
1728
2178
  ids: [e.id],
1729
2179
  at: e.at,
1730
2180
  session: e.session,
1731
- who: e.who
2181
+ who: e.who,
2182
+ added: 0,
2183
+ removed: 0
1732
2184
  };
1733
2185
  if (e.kind === "proposal") {
1734
2186
  const m = {
@@ -1747,22 +2199,29 @@ function moments(entries) {
1747
2199
  }
1748
2200
  } else if (e.kind === "change") {
1749
2201
  const m = e.proposal ? offered.get(e.proposal) : void 0;
2202
+ const [added, removed] = size(e);
1750
2203
  if (e.why === "keep" && m) {
1751
2204
  m.what = "kept";
1752
2205
  m.ids.push(e.id);
1753
2206
  m.at = e.at;
2207
+ m.added += added;
2208
+ m.removed += removed;
1754
2209
  continue;
1755
2210
  }
1756
2211
  const last = out[out.length - 1];
1757
- if (e.why === "edit" && last && last.what === "edited" && last.who === e.who && last.session === e.session) {
2212
+ if (e.why === "edit" && last && last.what === "edited" && last.who === e.who && last.session === e.session && Date.parse(e.at) - Date.parse(last.at) <= 6e5) {
1758
2213
  last.ids.push(e.id);
1759
2214
  last.at = e.at;
2215
+ last.added += added;
2216
+ last.removed += removed;
1760
2217
  continue;
1761
2218
  }
1762
2219
  const what = e.why === "start" ? e.who === "external" ? "found" : "wrote" : e.why === "edit" ? "edited" : e.why === "restore" ? "restored" : e.why === "keep" ? "kept" : e.why === "move" ? "moved" : "changed elsewhere";
1763
2220
  out.push({
1764
2221
  ...base,
1765
2222
  what,
2223
+ added,
2224
+ removed,
1766
2225
  ...e.asked ? { asked: e.asked } : {}
1767
2226
  });
1768
2227
  }
@@ -1884,10 +2343,54 @@ function mapModel(base) {
1884
2343
  ]);
1885
2344
  }
1886
2345
  const holdsFiles = /* @__PURE__ */ new Set();
1887
- for (const f of base.files) holdsFiles.add(f.includes("/") ? f.slice(0, f.lastIndexOf("/")) : "");
2346
+ const parentOf = (p) => p.includes("/") ? p.slice(0, p.lastIndexOf("/")) : "";
2347
+ for (const f of base.files) holdsFiles.add(parentOf(f));
2348
+ const pictureFolder = (d) => {
2349
+ const dd = base.directories.get(d);
2350
+ const here = [...base.files].filter((f) => parentOf(f) === d);
2351
+ return !!d && (!dd || !dd.concepts.length && !dd.children.length) && here.length > 0 && here.every((f) => citeKind(f) === "image");
2352
+ };
2353
+ const refOf = (f) => citeKind(f) === "image" && pictureFolder(parentOf(f)) ? parentOf(f) + "/" : f;
2354
+ const fileEdges = /* @__PURE__ */ new Map();
2355
+ for (const c of base.concepts.values()) for (const x of c.cites) {
2356
+ if (x.broken || !base.files.has(x.target)) continue;
2357
+ const ref = refOf(x.target);
2358
+ if (!fileEdges.has(c.id)) fileEdges.set(c.id, /* @__PURE__ */ new Set());
2359
+ fileEdges.get(c.id).add(ref);
2360
+ }
2361
+ const rankOf = /* @__PURE__ */ new Map();
2362
+ for (const [from, refs] of fileEdges) for (const ref of refs) {
2363
+ edges.push([
2364
+ from,
2365
+ ref,
2366
+ STRENGTH.uses
2367
+ ]);
2368
+ rankOf.set(ref, Math.max(rankOf.get(ref) ?? 0, order.get(from)?.depth ?? 0));
2369
+ }
2370
+ const filesIn = /* @__PURE__ */ new Map();
2371
+ for (const f of [...base.files].sort()) {
2372
+ const of = citeKind(f), ref = of ? refOf(f) : null;
2373
+ if (!of || !ref) continue;
2374
+ const at = ref.endsWith("/") ? parentOf(ref.slice(0, -1)) : parentOf(f);
2375
+ const list = filesIn.get(at) ?? [];
2376
+ if (!list.some((n) => n.ref === ref)) list.push({
2377
+ kind: "concept",
2378
+ id: "f:" + ref,
2379
+ ref,
2380
+ weight: 1,
2381
+ rank: rankOf.get(ref) ?? 0,
2382
+ of
2383
+ });
2384
+ filesIn.set(at, list);
2385
+ }
1888
2386
  const dir = (id) => {
1889
2387
  const d = base.directories.get(id);
1890
- const children = [...[...d.children].sort().map(dir).filter((c) => c !== null), ...[...d.concepts].sort().map(note)];
2388
+ if (id && pictureFolder(id)) return null;
2389
+ const children = [
2390
+ ...[...d.children].sort().map(dir).filter((c) => c !== null),
2391
+ ...[...d.concepts].sort().map(note),
2392
+ ...filesIn.get(id) ?? []
2393
+ ];
1891
2394
  return id && !children.length && (holdsFiles.has(id) || d.children.length) ? null : {
1892
2395
  kind: "dir",
1893
2396
  id: "d:" + id,
@@ -1901,6 +2404,7 @@ function mapModel(base) {
1901
2404
  };
1902
2405
  }
1903
2406
  var init_map = __esmMin((() => {
2407
+ init_bundle();
1904
2408
  init_graph();
1905
2409
  }));
1906
2410
  //#endregion
@@ -1911,6 +2415,7 @@ var init_src$1 = __esmMin((() => {
1911
2415
  init_frontmatter();
1912
2416
  init_source();
1913
2417
  init_markdown();
2418
+ init_unwrap();
1914
2419
  init_text();
1915
2420
  init_search();
1916
2421
  init_record();
@@ -1925,6 +2430,7 @@ var init_src$1 = __esmMin((() => {
1925
2430
  }));
1926
2431
  //#endregion
1927
2432
  //#region packages/core/src/node.ts
2433
+ init_src$2();
1928
2434
  init_src$1();
1929
2435
  init_bundle();
1930
2436
  init_text();
@@ -2111,7 +2617,8 @@ async function completeOnce(http, p, key, call, heard) {
2111
2617
  ...call.tools?.length ? {
2112
2618
  tools: call.tools,
2113
2619
  tool_choice: call.toolChoice ?? "auto"
2114
- } : {}
2620
+ } : {},
2621
+ ...call.effort && p.name === OPENROUTER.name ? { reasoning: { effort: call.effort } } : {}
2115
2622
  });
2116
2623
  let res;
2117
2624
  try {
@@ -2369,13 +2876,14 @@ async function rounds(o) {
2369
2876
  let firstIn = 0;
2370
2877
  for (let round = 0, calls = 1;; round++, calls++) {
2371
2878
  let first = true;
2372
- const ask = (maxTokens) => complete(o.http, o.provider, o.key, {
2879
+ const ask = (maxTokens, effort) => complete(o.http, o.provider, o.key, {
2373
2880
  model,
2374
2881
  messages,
2375
2882
  maxTokens,
2376
2883
  tools: most ? o.lookup.defs() : [],
2377
2884
  toolChoice: round < most ? "auto" : "none",
2378
2885
  signal: o.signal,
2886
+ ...effort ? { effort } : {},
2379
2887
  onText: (piece) => {
2380
2888
  o.onText(piece, first && round > 0);
2381
2889
  first = false;
@@ -2392,10 +2900,14 @@ async function rounds(o) {
2392
2900
  add(r);
2393
2901
  if (round === 0) firstIn = r.usage.tokensIn;
2394
2902
  if (empty(r)) {
2395
- r = await ask(o.maxTokens * 3);
2903
+ r = await ask(o.maxTokens * 3, "low");
2396
2904
  add(r);
2397
2905
  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
2906
  }
2907
+ if (!r.text.trim() && !r.calls.length && !r.cut) {
2908
+ r = await ask(o.maxTokens);
2909
+ add(r);
2910
+ }
2399
2911
  if (!r.calls.length || round >= most) return {
2400
2912
  text: r.text,
2401
2913
  usage,
@@ -2627,7 +3139,7 @@ function parseRewrite(reply, count) {
2627
3139
  /** A passage's new text, set between the same neighbours as the old: it keeps the old one's space and line
2628
3140
  * breaks at either end, whatever the model put round what it wrote. An empty one takes the passage out. */
2629
3141
  function fitted(old, text) {
2630
- const core = text.trim();
3142
+ const core = unwrap(text.trim());
2631
3143
  if (!core) return "";
2632
3144
  return /^\s*/.exec(old)[0] + core + /\s*$/.exec(old)[0];
2633
3145
  }
@@ -2686,7 +3198,7 @@ function replyText(reply) {
2686
3198
  let text = reply.replaceAll(HERE, "").replace(/^\s*\n/, "").trimEnd();
2687
3199
  const fenced = /^```(?:markdown|md)?\n([\s\S]*)\n```$/.exec(text.trim());
2688
3200
  if (fenced) text = fenced[1];
2689
- return text.trim();
3201
+ return unwrap(text.trim());
2690
3202
  }
2691
3203
  var HERE, WHO$1, INSERT, OPEN, CLOSE, SAY_OPEN, SAY_CLOSE, REWRITE, NOTE, FORMAT, FORMAT_RULES, LOOKUP, estimate, CONVERSATION_BUDGET, NOTE_BUDGET, OLD_EXCHANGE, NO_LOOKUP, looked, LINKED_BUDGET;
2692
3204
  var init_iso = __esmMin((() => {
@@ -2781,6 +3293,8 @@ something at one place in the note they have open.`;
2781
3293
  - A picture is shown as ![what it shows](its/path.png). You can't see it: what
2782
3294
  you know of it is the words in the brackets. Keep a picture's line as it is
2783
3295
  when you rewrite around it, and never make up a picture's path.
3296
+ - A paragraph or a list item is one line, however long: do not break lines at
3297
+ a width. The editor wraps them to the pane.
2784
3298
  - Ordinary Markdown otherwise. No HTML.`;
2785
3299
  FORMAT_RULES = FORMAT;
2786
3300
  LOOKUP = (rounds, meaning) => `# Looking things up
@@ -3098,38 +3612,6 @@ var init_lookup = __esmMin((() => {
3098
3612
  };
3099
3613
  }));
3100
3614
  //#endregion
3101
- //#region packages/engine/src/understand.ts
3102
- /** The two parts of an Understanding note's file. A file in another shape is read as all scratchpad. */
3103
- function parseUnderstanding(text) {
3104
- if (!text) return {
3105
- reading: "",
3106
- scratchpad: ""
3107
- };
3108
- const body = text.replace(/^---\n[\s\S]*?\n---\n/, "");
3109
- const r = body.indexOf(READING$1), p = body.indexOf(PAD);
3110
- if (r < 0 && p < 0) return {
3111
- reading: "",
3112
- scratchpad: body.trim()
3113
- };
3114
- return {
3115
- reading: r < 0 ? "" : body.slice(r + 16, p > r ? p : void 0).trim(),
3116
- scratchpad: p < 0 ? "" : body.slice(p + 13, r > p ? r : void 0).trim()
3117
- };
3118
- }
3119
- /** An Understanding note's file, from its parts. */
3120
- function understandingText(note, title, parts) {
3121
- return `---\ntype: Understanding\ntitle: ${JSON.stringify(`Understanding · ${title}`)}\nof: ${note}\n---\n\n${READING$1}\n\n${parts.reading.trim()}\n\n${PAD}\n\n${parts.scratchpad.trim()}\n`;
3122
- }
3123
- var UNDERSTANDING_PREFIX, UNDERSTANDING_DIR, isUnderstandingId, understandingId, READING$1, PAD;
3124
- var init_understand = __esmMin((() => {
3125
- UNDERSTANDING_PREFIX = "@understanding/";
3126
- UNDERSTANDING_DIR = "understanding";
3127
- isUnderstandingId = (id) => id.startsWith(UNDERSTANDING_PREFIX);
3128
- understandingId = (note) => UNDERSTANDING_PREFIX + note;
3129
- READING$1 = "## Iso's reading";
3130
- PAD = "## Scratchpad";
3131
- }));
3132
- //#endregion
3133
3615
  //#region packages/engine/src/hand.ts
3134
3616
  /** The lines about the working set in a conversation's file, in order. Turns' own lines are passed over. */
3135
3617
  function readHand(text) {
@@ -3250,6 +3732,7 @@ function readTurns(text) {
3250
3732
  turns.set(t.id, {
3251
3733
  ...t,
3252
3734
  mode: MODES.includes(t.mode) ? t.mode : "ask",
3735
+ given: (t.given ?? []).filter((g) => GIVEN.includes(g.kind)),
3253
3736
  items: [],
3254
3737
  state: "running"
3255
3738
  });
@@ -3258,7 +3741,7 @@ function readTurns(text) {
3258
3741
  const t = turns.get(line.turn);
3259
3742
  if (!t) continue;
3260
3743
  if (line.kind === "item") {
3261
- if (ITEMS.includes(line.item.kind)) t.items.push(line.item);
3744
+ if (ITEMS.includes(line.item.kind) && !(line.item.kind === "change" && line.item.note.startsWith("@understanding/"))) t.items.push(line.item);
3262
3745
  } else if (line.kind === "answer") {
3263
3746
  for (const i of t.items) if (i.kind === "permission" && i.id === line.permission) i.answer = line.decision;
3264
3747
  } else if (line.kind === "taken") {
@@ -3275,9 +3758,9 @@ function readTurns(text) {
3275
3758
  state: "stopped"
3276
3759
  } : t).sort((a, b) => a.id < b.id ? -1 : 1);
3277
3760
  }
3278
- /** A turn's own tools: its mode's, and with an Understanding note given, the one that rewrites Iso's reading. */
3279
- function turnTools(mode, sent) {
3280
- return [...MODE_TOOLS[mode], ...sent.some((s) => s.kind === "understanding") ? [WRITE_UNDERSTANDING] : []];
3761
+ /** A turn's own tools: its mode's. */
3762
+ function turnTools(mode) {
3763
+ return MODE_TOOLS[mode];
3281
3764
  }
3282
3765
  function sentText(s) {
3283
3766
  if (s.kind === "note") {
@@ -3286,15 +3769,14 @@ function sentText(s) {
3286
3769
  }
3287
3770
  if (s.kind === "passage") return `## A passage they chose in ${s.title} (/${s.id}.md)\n\n${s.text.split("\n").map((l) => `> ${l}`).join("\n")}`;
3288
3771
  if (s.kind === "read") return `## ${s.title} (/${s.id}.md)${s.sections === "whole" ? "" : `, ${s.sections.length === 1 ? "the section" : "the sections"} ${s.sections.map((x) => `"${x}"`).join(", ")}`}\n\n${s.text}`;
3289
- if (s.kind === "folder") return `## The folder ${s.id || "at the top of the base"}: its notes\n\n${s.notes.map((n) => `- ${n.title} (/${n.id}.md)${n.description ? `: ${n.description}` : ""}`).join("\n") || "(empty)"}`;
3290
- return `## Their understanding of ${s.title} (/${s.id}.md)\n\nYour reading so far: ${s.reading || "none yet."}`;
3772
+ return `## The folder ${s.id || "at the top of the base"}: its notes\n\n${s.notes.map((n) => `- ${n.title} (/${n.id}.md)${n.description ? `: ${n.description}` : ""}`).join("\n") || "(empty)"}`;
3291
3773
  }
3292
3774
  /** An earlier turn as it is sent: in the shape `conversation()` shortens. */
3293
3775
  function turnExchange(t) {
3294
3776
  const text = t.items.filter((i) => i.kind === "text").map((i) => i.text).join("\n\n").trim();
3295
3777
  const changed = t.items.flatMap((i) => i.kind === "change" ? [`${i.why === "create" ? "made" : i.why === "delete" ? "deleted" : i.why === "move" ? "moved" : "changed"} ${i.title} (/${i.note}.md)`] : []);
3296
3778
  const asked = t.items.flatMap((i) => i.kind === "question" ? [`You asked them: ${i.title}${i.made ? ` (/${i.made}.md)` : ""}.`] : i.kind === "offer" ? [`You offered them a passage for /${i.note}.md, which they ${i.taken ? "inserted" : "have not inserted"}.`] : []);
3297
- const gave = t.given.filter((g) => g.kind !== "understanding").map((g) => g.kind === "passage" ? `a passage of /${g.id}.md` : `/${g.id}${g.kind === "note" ? ".md" : "/"}`);
3779
+ const gave = t.given.map((g) => g.kind === "passage" ? `a passage of /${g.id}.md` : `/${g.id}${g.kind === "note" ? ".md" : "/"}`);
3298
3780
  return {
3299
3781
  id: t.id,
3300
3782
  note: "",
@@ -3303,7 +3785,6 @@ function turnExchange(t) {
3303
3785
  }
3304
3786
  /** The messages for a turn, and what they hold, part by part. */
3305
3787
  function turnMessages(a) {
3306
- const understood = a.sent.filter((s) => s.kind === "understanding");
3307
3788
  const checking = a.sent.some((s) => s.kind === "note" && s.of === "problem");
3308
3789
  const system = [
3309
3790
  WHO,
@@ -3311,22 +3792,21 @@ function turnMessages(a) {
3311
3792
  PRACTICE,
3312
3793
  LEARNING,
3313
3794
  MODE_RULES[a.mode],
3795
+ ...a.mode === "write" ? [ARTIFACTS] : [],
3314
3796
  ASKING(a.mode === "write"),
3315
3797
  ...checking ? [CHECKING] : [],
3316
- ...understood.length ? [UNDERSTANDING] : [],
3317
3798
  FORMAT_RULES,
3318
- LOOKING(ROUNDS[a.tier], a.meaning, a.lookup)
3799
+ LOOKING(a.mode === "write" ? Math.max(ROUNDS[a.tier], 6) : ROUNDS[a.tier], a.meaning, a.lookup)
3319
3800
  ].join("\n\n");
3320
3801
  const briefed = a.briefings?.length ? `# Earlier in this conversation, summed up\n\n${a.briefings.join("\n\n---\n\n")}` : "";
3321
3802
  const talk = a.earlier.map((t) => turnExchange(t).text).join("\n\n---\n\n");
3322
3803
  const earlier = [briefed, talk ? `# Earlier in this conversation\n\n${talk}` : ""].filter(Boolean).join("\n\n");
3323
- const given = a.sent.filter((s) => s.kind !== "understanding" && s.kind !== "read"), reads = a.sent.filter((s) => s.kind === "read");
3804
+ const given = a.sent.filter((s) => s.kind !== "read"), reads = a.sent.filter((s) => s.kind === "read");
3324
3805
  const gave = given.length ? `# What they gave you\n\n${given.map(sentText).join("\n\n")}` : "";
3325
3806
  const held = reads.length ? `# Notes you read earlier in this conversation\n\n${reads.map(sentText).join("\n\n")}` : "";
3326
3807
  const aside = a.aside?.length ? `# Read earlier, set aside\n\n${a.aside.map((n) => `- ${n.title} (/${n.id}.md)`).join("\n")}` : "";
3327
3808
  const agents = a.agents?.trim() ? `# Their instructions for this knowledge base (AGENTS.md)\n\n${a.agents.trim()}` : "";
3328
3809
  const lent = a.lookup && a.lent?.length ? `# Other knowledge bases lent to you\n\nYou may read these with the same look-up tools, giving their base; you change notes only in this one. Look in them where what is asked may be there: how the person works, their tools, what was settled before.\n\n${a.lent.map((l) => `- ${l.name}: ${l.about}`).join("\n")}` : "";
3329
- const known = understood.length ? `# Their understanding, as you have recorded it\n\n${understood.map(sentText).join("\n\n")}` : "";
3330
3810
  const broken = a.broken?.length ? `# Code links that no longer resolve\n\nSince you were last told, these links in their notes point to files no longer in their repository. Mention it only where it bears on what they ask.\n\n${a.broken.map((b) => `- in ${b.title} (/${b.id}.md): ${b.href}`).join("\n")}` : "";
3331
3811
  const request = `# What they ask now\n\n${a.asked}`;
3332
3812
  return {
@@ -3342,7 +3822,6 @@ function turnMessages(a) {
3342
3822
  agents,
3343
3823
  lent,
3344
3824
  earlier,
3345
- known,
3346
3825
  gave,
3347
3826
  held,
3348
3827
  aside,
@@ -3354,7 +3833,7 @@ function turnMessages(a) {
3354
3833
  {
3355
3834
  name: "instructions and tools",
3356
3835
  detail: `${a.mode} mode${checking ? ", checking" : ""}${a.lookup ? "" : ", no look-ups"}`,
3357
- tokens: estimate(system) + estimate(lent) + (a.lookup ? 500 : 0) + turnTools(a.mode, a.sent).length * 120
3836
+ tokens: estimate(system) + estimate(lent) + (a.lookup ? 500 : 0) + turnTools(a.mode).length * 120
3358
3837
  },
3359
3838
  {
3360
3839
  name: "earlier in this conversation",
@@ -3376,11 +3855,6 @@ function turnMessages(a) {
3376
3855
  detail: `${reads.length} in hand${a.aside?.length ? `, ${a.aside.length} set aside` : ""}`,
3377
3856
  tokens: estimate(held) + estimate(aside)
3378
3857
  }] : [],
3379
- ...understood.length ? [{
3380
- name: "your understanding",
3381
- detail: `${understood.length} ${understood.length === 1 ? "note" : "notes"}`,
3382
- tokens: estimate(known)
3383
- }] : [],
3384
3858
  ...broken ? [{
3385
3859
  name: "code links broken",
3386
3860
  detail: `${a.broken.length}`,
@@ -3394,7 +3868,7 @@ function turnMessages(a) {
3394
3868
  ]
3395
3869
  };
3396
3870
  }
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;
3871
+ var MODES, GIVEN, ITEMS, CONVERSATIONS_DIR, conversationPath, WHO, ISORITE, PRACTICE, LEARNING, ASK, WRITE, ARTIFACTS, ASKING, CHECKING, MODE_RULES, LOOKING, COMPACT$1, str, fn, POINT_TO_NOTE, EDIT_NOTE$1, WRITE_NOTE$1, MOVE_NOTE, DELETE_NOTE, ASK_QUESTION, READ_DATA, ARTIFACT_RECIPE, WRITE_ARTIFACT, READ_ARTIFACT, OFFER_TEXT, MODE_TOOLS;
3398
3872
  var init_turn = __esmMin((() => {
3399
3873
  init_iso();
3400
3874
  init_loop();
@@ -3403,6 +3877,11 @@ var init_turn = __esmMin((() => {
3403
3877
  "write",
3404
3878
  "build"
3405
3879
  ];
3880
+ GIVEN = [
3881
+ "note",
3882
+ "folder",
3883
+ "passage"
3884
+ ];
3406
3885
  ITEMS = [
3407
3886
  "text",
3408
3887
  "look",
@@ -3432,6 +3911,11 @@ what you change is shown to them there as you do it.`;
3432
3911
  problem note is practice that \`uses\` notes. Theirs alone to mark:
3433
3912
  \`resolved\` or \`solved\`, with the day. \`[[!name]]\` alone on a line
3434
3913
  embeds one in a note.
3914
+ - A problem of several steps is several problem notes, each linked to the one
3915
+ before it as "requires", in a folder of their own when they belong
3916
+ together. A problem has no steps inside it.
3917
+ - Files beside the notes that are not notes (data, documents, pictures,
3918
+ artifacts) are linked as any note is: [runs](/simulation/runs.csv).
3435
3919
  - Every change you make is marked as yours, in the note's history, and they
3436
3920
  can undo it.
3437
3921
 
@@ -3468,6 +3952,9 @@ It follows from [surprise](/basics/surprise.md "requires"), but how?
3468
3952
  stop. Explain once they have answered. When they ask for a fact, a check or
3469
3953
  a change, just answer.
3470
3954
  - Prefer a question back, or a problem to try, to a summary.
3955
+ - Where they stand shows in their notes: what they wrote, their open questions,
3956
+ the problems they have solved and not. Where you find it stops, offer it to
3957
+ them as a question to keep (ask_question) or a problem to try.
3471
3958
  - Check a solution; never solve it for them.`;
3472
3959
  ASK = `# Ask mode: what you may do
3473
3960
 
@@ -3481,6 +3968,9 @@ note, no folder.
3481
3968
  the notes you used, so they can open them. Where the base does not hold what
3482
3969
  is asked, answer from what you know of the subject, exactly, and say that
3483
3970
  their notes do not cover it.
3971
+ - Cite in the sentence: the phrase that rests on a note is the link to it,
3972
+ with its kind. No numbered references and no list of sources at the end;
3973
+ the notes you read are shown under your reply.
3484
3974
  - When one of their notes would help them and they may not know it, point it
3485
3975
  out with point_to_note. Use it sparingly: one or two notes.
3486
3976
  - Never invent a note, a link or a quotation.`;
@@ -3498,25 +3988,29 @@ change marked as yours, and can undo it.
3498
3988
  - A new note (write_note) is written as one of theirs: frontmatter with type
3499
3989
  and title, their notation, their level, plain words, linked to the notes it
3500
3990
  rests on.
3991
+ - Write each paragraph and each list item on one line. Do not break lines at a
3992
+ width: the editor wraps them to the pane.
3501
3993
  - Moving or deleting a note is asked of them before it happens. Do it only
3502
3994
  when they asked for it.
3503
3995
  - When you are done, say in a sentence or two what you changed and why. Do not
3504
3996
  repeat the text you wrote: they see it in the note.
3505
3997
  - Never invent a note, a link or a quotation.`;
3506
- UNDERSTANDING = `# Their understanding
3998
+ ARTIFACTS = `# Artifacts
3507
3999
 
3508
- Below, for each note in hand, is your reading of their understanding of it so
3509
- far, from its Understanding note.
4000
+ An artifact is an HTML page beside their notes that isorite shows working: a
4001
+ dashboard of their data, a comparison, a simulation, an explorable
4002
+ explanation, a 3D model's viewer. Make one when they ask for something to see
4003
+ or try; not for what an answer or a note says as well.
3510
4004
 
3511
- - Start from where they are: do not explain what they have shown they know; go
3512
- to what they have not yet shown, and ask about that first.
3513
- - At the end of a conversation about a note, or when your reading of them has
3514
- changed, rewrite your reading with write_understanding: a few plain
3515
- sentences on what they can do with the note and what they have not yet
3516
- shown. It is theirs to read. Rest each sentence on evidence they can open,
3517
- linked: what they wrote in a note, a question of theirs open or resolved,
3518
- a problem solved or not. Say nothing the evidence does not show.
3519
- - Never invent a note, a link or a quotation.`;
4005
+ 1. artifact_recipe with the recipe nearest what they asked: dashboard,
4006
+ compare, simulation, explorable or viewer. It gives a complete working
4007
+ artifact and the kit's reference. Start from the recipe and change what it
4008
+ marks; keep the rest.
4009
+ 2. read_data for every data file it will read. Use the columns as given.
4010
+ 3. write_artifact with the whole file. It is checked and run. If it reports a
4011
+ problem, fix that and write the whole file again.
4012
+ 4. Say in a sentence what it shows, linked: [what it shows](/folder/name.html).
4013
+ Link it from the note it belongs to if they want it there.`;
3520
4014
  ASKING = (write) => `# Asking back, and their own answers
3521
4015
 
3522
4016
  - Where a question to them would do more than an answer (what is unclear in
@@ -3638,13 +4132,6 @@ was not said.`;
3638
4132
  }
3639
4133
  }, ["id", "to"]);
3640
4134
  DELETE_NOTE = fn("delete_note", "Delete a note. They are asked first. Its history stays.", { id: str }, ["id"]);
3641
- WRITE_UNDERSTANDING = fn("write_understanding", "Rewrite your reading of their understanding of one note: a few plain sentences on what they can do with it and what they have not yet shown, each resting on evidence linked as [their words](/id.md), [a question](/id.md) or [a problem](/id.md). Replaces the reading you had.", {
3642
- id: {
3643
- ...str,
3644
- description: "The note's id."
3645
- },
3646
- reading: str
3647
- }, ["id", "reading"]);
3648
4135
  ASK_QUESTION = fn("ask_question", "Ask them a question back, kept as an open question in their base, about one or more of their notes.", {
3649
4136
  title: {
3650
4137
  ...str,
@@ -3664,6 +4151,50 @@ was not said.`;
3664
4151
  "text",
3665
4152
  "about"
3666
4153
  ]);
4154
+ 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: {
4155
+ ...str,
4156
+ description: "The file's path in the base: folder/name.csv."
4157
+ } }, ["path"]);
4158
+ 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: {
4159
+ type: "string",
4160
+ enum: [
4161
+ "dashboard",
4162
+ "compare",
4163
+ "simulation",
4164
+ "explorable",
4165
+ "viewer"
4166
+ ]
4167
+ } }, ["name"]);
4168
+ 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.", {
4169
+ path: {
4170
+ ...str,
4171
+ description: "Where: folder/name.html, beside the notes it belongs to."
4172
+ },
4173
+ title: str,
4174
+ description: {
4175
+ ...str,
4176
+ description: "One sentence: what it shows, and what can be done with it."
4177
+ },
4178
+ uses: {
4179
+ type: "array",
4180
+ items: str,
4181
+ description: "The data or model files it reads, relative to it (runs.csv). Those its markup and script name are added."
4182
+ },
4183
+ html: {
4184
+ ...str,
4185
+ description: "The whole file."
4186
+ }
4187
+ }, [
4188
+ "path",
4189
+ "title",
4190
+ "description",
4191
+ "uses",
4192
+ "html"
4193
+ ]);
4194
+ READ_ARTIFACT = fn("read_artifact", "An artifact's whole file, to change it and write it again.", { path: {
4195
+ ...str,
4196
+ description: "folder/name.html"
4197
+ } }, ["path"]);
3667
4198
  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
4199
  id: {
3669
4200
  ...str,
@@ -3678,7 +4209,8 @@ was not said.`;
3678
4209
  ask: [
3679
4210
  POINT_TO_NOTE,
3680
4211
  ASK_QUESTION,
3681
- OFFER_TEXT
4212
+ OFFER_TEXT,
4213
+ READ_DATA
3682
4214
  ],
3683
4215
  write: [
3684
4216
  EDIT_NOTE$1,
@@ -3687,7 +4219,11 @@ was not said.`;
3687
4219
  DELETE_NOTE,
3688
4220
  POINT_TO_NOTE,
3689
4221
  ASK_QUESTION,
3690
- OFFER_TEXT
4222
+ OFFER_TEXT,
4223
+ READ_DATA,
4224
+ ARTIFACT_RECIPE,
4225
+ WRITE_ARTIFACT,
4226
+ READ_ARTIFACT
3691
4227
  ]
3692
4228
  };
3693
4229
  }));
@@ -3892,6 +4428,11 @@ function checkNoteId(id) {
3892
4428
  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
4429
  return id;
3894
4430
  }
4431
+ /** A file's path in the base as Iso gives one: no leading slash, no "..", nothing hidden. */
4432
+ function filePathOf(raw) {
4433
+ const p = String(raw ?? "").trim().replace(/^\/+/, "");
4434
+ return p && !p.split("/").some((x) => !x || x === "." || x === ".." || x.startsWith(".")) ? p : null;
4435
+ }
3895
4436
  async function walk(files, root) {
3896
4437
  const out = [];
3897
4438
  const at = (path) => root && path ? `${root}/${path}` : root || path;
@@ -3929,14 +4470,13 @@ function keepDone(before, next) {
3929
4470
  return next;
3930
4471
  }
3931
4472
  }
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;
4473
+ 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
4474
  var init_engine = __esmMin((() => {
3934
4475
  init_src$1();
3935
4476
  init_api();
3936
4477
  init_iso();
3937
4478
  init_loop();
3938
4479
  init_lookup();
3939
- init_understand();
3940
4480
  init_hand();
3941
4481
  init_turn();
3942
4482
  init_meaning();
@@ -3945,10 +4485,11 @@ var init_engine = __esmMin((() => {
3945
4485
  fromBase = (base, text) => `(From the base ${base}: name its notes ${base}:<id>, or give base "${base}".)\n${text}`;
3946
4486
  REPLY_TOKENS = 2500;
3947
4487
  NOTE_TOKENS = 6e3;
4488
+ ARTIFACT_TOKENS = 32e3;
3948
4489
  DRAFT_PREFIX = "@draft/";
3949
4490
  DRAFT_DIR = "drafts";
3950
4491
  isDraftId = (id) => id.startsWith(DRAFT_PREFIX);
3951
- bare = (id) => isDraftId(id) ? id.slice(7) : isUnderstandingId(id) ? id.slice(UNDERSTANDING_PREFIX.length) : id;
4492
+ bare = (id) => isDraftId(id) ? id.slice(7) : id;
3952
4493
  inBase = (id) => !id.startsWith("@");
3953
4494
  BAD_PART = /^\.|[\0\\:*?"<>|]|^\s|\s$/;
3954
4495
  TURN_ID = /^[0-9A-HJKMNP-TV-Z]{26}$/;
@@ -4149,9 +4690,54 @@ var init_engine = __esmMin((() => {
4149
4690
  ...pictures.length ? { pictures } : {}
4150
4691
  };
4151
4692
  }
4693
+ const model = mapModel(this.bundle), files = {};
4694
+ const items = [];
4695
+ const walk = (d) => {
4696
+ for (const c of d.children) if (c.kind === "dir") walk(c);
4697
+ else if (c.id.startsWith("f:")) items.push(c.ref);
4698
+ };
4699
+ walk(model.root);
4700
+ const all = new Set(items);
4701
+ for (const ref of items) {
4702
+ if (ref.endsWith("/")) {
4703
+ const count = [...this.bundle.files].filter((f) => f.startsWith(ref) && !f.slice(ref.length).includes("/")).length;
4704
+ files[ref] = {
4705
+ title: `${ref.slice(0, -1).split("/").at(-1)} · ${count} ${count === 1 ? "picture" : "pictures"}`,
4706
+ kind: "image",
4707
+ count
4708
+ };
4709
+ continue;
4710
+ }
4711
+ const kind = citeKind(ref), name = ref.split("/").at(-1);
4712
+ if (kind !== "artifact") {
4713
+ files[ref] = {
4714
+ title: name,
4715
+ kind
4716
+ };
4717
+ continue;
4718
+ }
4719
+ const html = await this.opts.notes.read(ref).catch(() => null), m = html ? manifestOf(html) : null;
4720
+ files[ref] = {
4721
+ title: m?.title || name,
4722
+ kind
4723
+ };
4724
+ const dir = ref.split("/").slice(0, -1);
4725
+ for (const u of m?.uses ?? []) {
4726
+ const parts = [...dir];
4727
+ for (const p of u.split("/")) if (p === "..") parts.pop();
4728
+ else if (p && p !== ".") parts.push(p);
4729
+ const to = parts.join("/");
4730
+ if (all.has(to) && !model.edges.some(([a, b]) => a === ref && b === to)) model.edges.push([
4731
+ ref,
4732
+ to,
4733
+ STRENGTH.uses
4734
+ ]);
4735
+ }
4736
+ }
4152
4737
  return {
4153
- model: mapModel(this.bundle),
4154
- notes
4738
+ model,
4739
+ notes,
4740
+ ...items.length ? { files } : {}
4155
4741
  };
4156
4742
  }
4157
4743
  /** The base's settings, from `.isorite/settings.toml`. */
@@ -4505,7 +5091,7 @@ var init_engine = __esmMin((() => {
4505
5091
  }
4506
5092
  async deleteNote(id, base, who = "you") {
4507
5093
  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`;
5094
+ const { files: notes, path } = this.where(id);
4509
5095
  await this.one(async () => {
4510
5096
  const now = await notes.read(path);
4511
5097
  if (now === null) throw new Conflict(`${id} is gone`, null);
@@ -4543,10 +5129,10 @@ var init_engine = __esmMin((() => {
4543
5129
  this.queue = run.catch(() => void 0);
4544
5130
  return run;
4545
5131
  }
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. */
5132
+ /** A file in the notes that isn't a note (M23: any kind; M22 gave pictures only), or a picture in the base's
5133
+ * repository. Within its folder, and (in the notes) never in a hidden folder such as `.isorite/`. */
4548
5134
  async file(path, from = "notes") {
4549
- const type = pictureType(path);
5135
+ const type = from === "code" ? pictureType(path) : fileType(path);
4550
5136
  if (!type) return null;
4551
5137
  let files, at;
4552
5138
  if (from === "code") {
@@ -4644,12 +5230,19 @@ var init_engine = __esmMin((() => {
4644
5230
  };
4645
5231
  });
4646
5232
  }
4647
- /** Where a note's file is: drafts and Understanding notes are among isorite's own files. */
5233
+ /** Where a note's file is: drafts are among isorite's own files. */
4648
5234
  where(id) {
4649
- if (!inBase(id)) return {
4650
- files: this.opts.own,
4651
- path: `${isDraftId(id) ? DRAFT_DIR : UNDERSTANDING_DIR}/${checkNoteId(bare(id))}.md`
5235
+ if (inBase(id) && citeKind(id) === "artifact") return {
5236
+ files: this.opts.notes,
5237
+ path: checkNoteId(id)
4652
5238
  };
5239
+ if (!inBase(id)) {
5240
+ if (!isDraftId(id)) throw new EngineError(`there is no note ${id}`);
5241
+ return {
5242
+ files: this.opts.own,
5243
+ path: `${DRAFT_DIR}/${checkNoteId(bare(id))}.md`
5244
+ };
5245
+ }
4653
5246
  return {
4654
5247
  files: this.opts.notes,
4655
5248
  path: `${checkNoteId(id)}.md`
@@ -5398,6 +5991,32 @@ var init_engine = __esmMin((() => {
5398
5991
  } : t)
5399
5992
  };
5400
5993
  }
5994
+ async conversations() {
5995
+ const study = this.opts.own;
5996
+ const names = await study.list(CONVERSATIONS_DIR).catch(() => []);
5997
+ const all = [];
5998
+ for (const f of names) {
5999
+ const id = f.name.slice(0, -6);
6000
+ if (f.dir || !f.name.endsWith(".jsonl") || !TURN_ID.test(id)) continue;
6001
+ const turns = readTurns(await study.read(conversationPath(id)) ?? "");
6002
+ if (!turns.length) continue;
6003
+ const notes = /* @__PURE__ */ new Set();
6004
+ for (const t of turns) {
6005
+ for (const g of t.given) if (g.kind !== "folder") notes.add(g.id);
6006
+ for (const i of t.items) if (i.kind === "look" && i.step.opened && !i.step.away) notes.add(i.step.opened);
6007
+ else if (i.kind === "point" || i.kind === "change" && i.why !== "delete") notes.add(i.kind === "change" && i.to ? i.to : i.note);
6008
+ }
6009
+ all.push({
6010
+ id,
6011
+ at: turns[0].at,
6012
+ last: turns.at(-1).at,
6013
+ asked: turns[0].asked,
6014
+ turns: turns.length,
6015
+ notes: notes.size
6016
+ });
6017
+ }
6018
+ return all.sort((a, b) => a.last < b.last ? 1 : a.last > b.last ? -1 : 0);
6019
+ }
5401
6020
  /** What was given, checked: notes and passages of the base, and its folders. */
5402
6021
  given(raw) {
5403
6022
  if (!Array.isArray(raw) || raw.length > 40) throw new EngineError("give Iso between none and forty notes, folders and passages");
@@ -5415,10 +6034,6 @@ var init_engine = __esmMin((() => {
5415
6034
  kind: "note",
5416
6035
  id
5417
6036
  };
5418
- if (g?.kind === "understanding") return {
5419
- kind: "understanding",
5420
- id
5421
- };
5422
6037
  if (g?.kind === "passage" && typeof g.text === "string" && g.text.trim() && g.text.length <= 2e4) return {
5423
6038
  kind: "passage",
5424
6039
  id,
@@ -5454,16 +6069,6 @@ var init_engine = __esmMin((() => {
5454
6069
  }
5455
6070
  const c = this.bundle.concepts.get(g.id);
5456
6071
  if (!c) continue;
5457
- if (g.kind === "understanding") {
5458
- const u = await this.understanding(g.id);
5459
- out.push({
5460
- kind: "understanding",
5461
- id: g.id,
5462
- title: c.title,
5463
- reading: u.reading
5464
- });
5465
- continue;
5466
- }
5467
6072
  if (g.kind === "passage") out.push({
5468
6073
  kind: "passage",
5469
6074
  id: g.id,
@@ -5594,7 +6199,7 @@ var init_engine = __esmMin((() => {
5594
6199
  group: "notes",
5595
6200
  kind: g.kind,
5596
6201
  title: g.kind === "folder" ? g.id || "the top of the base" : title(g.id),
5597
- detail: g.kind === "passage" ? "a passage" : g.kind === "folder" ? `${s.kind === "folder" ? s.notes.length : 0} notes listed` : g.kind === "understanding" ? "your understanding" : "",
6202
+ detail: g.kind === "passage" ? "a passage" : g.kind === "folder" ? `${s.kind === "folder" ? s.notes.length : 0} notes listed` : "",
5598
6203
  tokens: scaled(estimate(sentText(s))),
5599
6204
  on: !w.off.has(k),
5600
6205
  by: "you",
@@ -5884,7 +6489,7 @@ var init_engine = __esmMin((() => {
5884
6489
  const big = [...w.built.parts].sort((a, b) => b.tokens - a.tokens)[0], k = (n) => `${Math.round(n / 1e3)}k`;
5885
6490
  throw new EngineError(`This turn would be sent about ${k(size())} tokens, more than the ${k(limit)} the model takes. The largest part is ${big.name} (${k(w.scaled(big.tokens))}): switch parts off in the Context view.`, 413);
5886
6491
  }
5887
- const sent = [...w.sent, ...w.reads], own = turnTools(mode, sent);
6492
+ const sent = [...w.sent, ...w.reads], own = turnTools(mode);
5888
6493
  const inHand = new Map(w.reads.flatMap((r) => r.kind === "read" ? [[r.id, r.sections]] : []));
5889
6494
  for (const g of w.givenOn) if (g.kind === "note") inHand.set(g.id, "whole");
5890
6495
  const { messages } = turnMessages({
@@ -6008,9 +6613,9 @@ var init_engine = __esmMin((() => {
6008
6613
  tier: req.tier,
6009
6614
  messages,
6010
6615
  lookup: tools,
6011
- maxTokens: REPLY_TOKENS * 2,
6012
6616
  signal: abort.signal,
6013
- most: ROUNDS[req.tier],
6617
+ maxTokens: mode === "write" ? ARTIFACT_TOKENS : REPLY_TOKENS * 2,
6618
+ most: mode === "write" ? Math.max(ROUNDS[req.tier], 6) : ROUNDS[req.tier],
6014
6619
  onText: (delta, again) => this.publish({
6015
6620
  type: "iso-text",
6016
6621
  request: turn,
@@ -6172,7 +6777,7 @@ var init_engine = __esmMin((() => {
6172
6777
  if (name === "edit_note") {
6173
6778
  const id = noteId(this.bundle, args.id);
6174
6779
  if (!id) return `There is no note ${String(args.id ?? "")}. Search for it by its words.`;
6175
- const old = String(args.old ?? ""), next = String(args.new ?? "");
6780
+ const old = String(args.old ?? ""), next = unwrap(String(args.new ?? ""));
6176
6781
  if (!old) return "Give the passage as it is now, in old.";
6177
6782
  const now = await this.readNote(id);
6178
6783
  const at = now.text.indexOf(old), again = at < 0 ? -1 : now.text.indexOf(old, at + 1);
@@ -6193,7 +6798,7 @@ var init_engine = __esmMin((() => {
6193
6798
  if (name === "write_note") {
6194
6799
  const id = this.newNoteId(args.id);
6195
6800
  if (!id) return `${String(args.id ?? "")} is not a note's id in their base: give folder/name, lower case with hyphens.`;
6196
- let text = String(args.text ?? "");
6801
+ let text = unwrap(String(args.text ?? ""));
6197
6802
  if (!text.trim()) return "Give the note's whole text.";
6198
6803
  if (!/^---\n/.test(text)) text = `---\ntype: Concept\ntitle: ${JSON.stringify(/^#\s+(.+)$/m.exec(text)?.[1]?.trim() ?? id.split("/").at(-1).replace(/-/g, " "))}\n---\n\n${text.replace(/^#\s+.+\n+/, "")}`;
6199
6804
  const was = this.bundle.concepts.has(id) ? await this.readNote(id) : null;
@@ -6299,52 +6904,100 @@ var init_engine = __esmMin((() => {
6299
6904
  });
6300
6905
  return `It is offered to them for ${agents ? "their AGENTS.md" : title(id)}.`;
6301
6906
  }
6302
- if (name === "write_understanding") {
6303
- const id = noteId(this.bundle, args.id);
6304
- if (!id) return `There is no note ${String(args.id ?? "")}.`;
6305
- const reading = String(args.reading ?? "").trim();
6306
- if (!reading) return "Give your reading.";
6307
- const uid = understandingId(id), was = await this.readNote(uid).catch(() => null);
6308
- const text = understandingText(id, title(id), {
6309
- ...parseUnderstanding(was?.text ?? null),
6310
- reading
6311
- });
6312
- const saved = await this.one(() => this.put(uid, text, was?.version ?? null, "edit", {}, t.who));
6313
- if (saved.entry) await t.item({
6314
- kind: "change",
6315
- note: uid,
6316
- entry: saved.entry,
6317
- why: was ? "edit" : "create",
6318
- title: `Understanding · ${title(id)}`,
6319
- added: 0,
6320
- removed: 0
6321
- });
6322
- return `Your reading of ${title(id)} is written.`;
6907
+ if (name === "read_data") {
6908
+ const path = filePathOf(args.path);
6909
+ if (!path) return `${String(args.path ?? "")} is not a file's path in their base.`;
6910
+ const got = await this.file(path).catch(() => null);
6911
+ if (!got) return `There is no file ${path}.${this.nearFiles(path)}`;
6912
+ try {
6913
+ return describeShape(shapeOf(path, parseData(path, new TextDecoder().decode(got.bytes))));
6914
+ } catch (e) {
6915
+ return `${path} could not be read as data: ${e.message}`;
6916
+ }
6917
+ }
6918
+ if (name === "artifact_recipe") {
6919
+ const r = RECIPES[String(args.name ?? "")];
6920
+ if (!r) return `There's no recipe ${String(args.name ?? "")}: there are ${Object.keys(RECIPES).join(", ")}.`;
6921
+ 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}\`\`\``;
6323
6922
  }
6923
+ if (name === "read_artifact") {
6924
+ const path = filePathOf(args.path);
6925
+ if (!path || citeKind(path) !== "artifact") return `${String(args.path ?? "")} is not an artifact's path (folder/name.html).`;
6926
+ const got = await this.opts.notes.read(path);
6927
+ return got === null ? `There is no artifact ${path}.` : got;
6928
+ }
6929
+ if (name === "write_artifact") return this.writeArtifact(args, t);
6324
6930
  return `${name} cannot be used yet.`;
6325
6931
  }
6326
- /** A note's Understanding note (M10): Iso's reading and your scratchpad. */
6327
- async understanding(note) {
6328
- if (!inBase(checkNoteId(note))) throw new EngineError("only a note of the base has an Understanding note");
6329
- const uid = understandingId(note);
6330
- const source = await this.readNote(uid).catch(() => null);
6331
- return {
6332
- note,
6333
- ...parseUnderstanding(source?.text ?? null),
6334
- version: source?.version ?? null
6932
+ /** Files in the same folder as a path, to say which there are when one isn't. */
6933
+ nearFiles(path) {
6934
+ const dir = path.split("/").slice(0, -1).join("/"), there = [...this.files.keys()].filter((f) => !f.endsWith(".md") && f.split("/").slice(0, -1).join("/") === dir);
6935
+ return there.length ? ` In ${dir || "the top folder"} there are: ${there.map((f) => f.split("/").at(-1)).join(", ")}.` : "";
6936
+ }
6937
+ /** write_artifact (M23, plan-m23 §3.5, §3.7): the manifest written as given, the rules checked before anything is
6938
+ * written, then written and journalled as a note is, and run where it can be, with all it reports said back. */
6939
+ async writeArtifact(args, t) {
6940
+ const path = filePathOf(args.path);
6941
+ if (!path || citeKind(path) !== "artifact" || !inBase(path)) return `${String(args.path ?? "")} is not an artifact's path: give folder/name.html.`;
6942
+ const raw = String(args.html ?? "");
6943
+ if (!raw.trim()) return "Give the whole file, in html.";
6944
+ const given = (Array.isArray(args.uses) ? args.uses : typeof args.uses === "string" ? args.uses.split(/[\s,]+/) : []).map((u) => String(u));
6945
+ const uses = [...new Set([
6946
+ ...given,
6947
+ ...manifestOf(raw).uses,
6948
+ ...readsOf(raw)
6949
+ ].map((u) => u.trim().replace(/^\.\//, "")).filter(Boolean))];
6950
+ const title = String(args.title ?? "").trim() || manifestOf(raw).title || path.split("/").at(-1).replace(/\.html?$/, "").replace(/-/g, " ");
6951
+ const html = withManifest(raw, {
6952
+ title,
6953
+ description: String(args.description ?? "").trim() || manifestOf(raw).description,
6954
+ author: "Iso",
6955
+ date: new Date(this.opts.clock.now()).toISOString().slice(0, 10),
6956
+ kit: "1",
6957
+ uses
6958
+ });
6959
+ const dir = path.split("/").slice(0, -1);
6960
+ const at = (rel) => {
6961
+ const parts = [...dir];
6962
+ for (const p of rel.split("/")) if (p === "..") parts.pop();
6963
+ else if (p && p !== ".") parts.push(p);
6964
+ return parts.join("/");
6335
6965
  };
6336
- }
6337
- /** Your scratchpad in a note's Understanding note: yours alone; Iso does not write it. `base` as for saveNote, or
6338
- * null when there is no Understanding note yet. */
6339
- async saveScratchpad(note, scratchpad, base) {
6340
- if (!inBase(checkNoteId(note))) throw new EngineError("only a note of the base has an Understanding note");
6341
- const uid = understandingId(note), was = await this.readNote(uid).catch(() => null);
6342
- const title = this.bundle.concepts.get(note)?.title ?? note;
6343
- await this.one(() => this.put(uid, understandingText(note, title, {
6344
- ...parseUnderstanding(was?.text ?? null),
6345
- scratchpad
6346
- }), base));
6347
- return this.understanding(note);
6966
+ const files = [], columns = {};
6967
+ for (const u of uses) {
6968
+ const got = await this.file(at(u)).catch(() => null);
6969
+ if (!got) continue;
6970
+ files.push(u);
6971
+ if (citeKind(u) === "data") try {
6972
+ columns[u] = columnsOf(parseData(u, new TextDecoder().decode(got.bytes)));
6973
+ } catch {}
6974
+ }
6975
+ const broken = checkArtifact(html, {
6976
+ files,
6977
+ columns
6978
+ }), errors = broken.filter((b) => b.level === "error");
6979
+ const list = (bs) => bs.map((b) => `- ${b.message}`).join("\n");
6980
+ 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.`;
6981
+ const was = await this.opts.notes.read(path);
6982
+ const saved = await this.one(() => this.put(path, html, was === null ? null : hashText(was), "edit", { asked: t.asked }, t.who));
6983
+ if (saved.entry) await t.item({
6984
+ kind: "change",
6985
+ note: path,
6986
+ entry: saved.entry,
6987
+ why: was === null ? "create" : "edit",
6988
+ title,
6989
+ added: html.split("\n").length,
6990
+ removed: was?.split("\n").length ?? 0
6991
+ });
6992
+ const warnings = broken.filter((b) => b.level === "warning");
6993
+ const run = await this.opts.renderArtifact?.(path, html).catch(() => null) ?? null;
6994
+ const problems = [
6995
+ ...run && !run.rendered ? ["it didn't start: the kit never said it was ready"] : [],
6996
+ ...(run?.errors ?? []).map((e) => `it reported: ${e}`),
6997
+ ...run && run.rendered && run.drawn === 0 ? ["it drew nothing: no chart, canvas or 3D view has anything in it"] : [],
6998
+ ...warnings.map((w) => w.message)
6999
+ ];
7000
+ 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." : "."}`;
6348
7001
  }
6349
7002
  /** A question asked in a turn, made a question note: in the folder of the first note it is about, named by its
6350
7003
  * title. */
@@ -6781,7 +7434,6 @@ var init_src = __esmMin((() => {
6781
7434
  init_iso();
6782
7435
  init_meaning();
6783
7436
  init_agent();
6784
- init_understand();
6785
7437
  init_turn();
6786
7438
  }));
6787
7439
  //#endregion
@@ -7964,7 +8616,7 @@ function toTurns(messages, { live = /* @__PURE__ */ new Map(), ours = /* @__PURE
7964
8616
  /** What is asked from a page, with what was given in the Iso pane's context: notes and folders by name, for the
7965
8617
  * agent to read with isorite's tools, and passages as they are. */
7966
8618
  function given(asked, parts, title) {
7967
- const notes = parts.filter((p) => p.kind === "note" || p.kind === "understanding").map((p) => `- ${title(p.id)} (${p.id})`);
8619
+ const notes = parts.filter((p) => p.kind === "note").map((p) => `- ${title(p.id)} (${p.id})`);
7968
8620
  const folders = parts.filter((p) => p.kind === "folder").map((p) => `- the folder ${p.id}`);
7969
8621
  const passages = parts.flatMap((p) => p.kind === "passage" ? [`From ${title(p.id)} (${p.id}):\n${p.text.split("\n").map((l) => "> " + l).join("\n")}`] : []);
7970
8622
  const out = [asked.trim()];
@@ -8288,10 +8940,6 @@ const Given = z.discriminatedUnion("kind", [
8288
8940
  kind: z.literal("passage"),
8289
8941
  id: z.string().max(400),
8290
8942
  text: z.string().max(2e4)
8291
- }).strict(),
8292
- z.object({
8293
- kind: z.literal("understanding"),
8294
- id: z.string().max(400)
8295
8943
  }).strict()
8296
8944
  ]);
8297
8945
  const Mode = z.enum([
@@ -8476,7 +9124,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal
8476
9124
  if (e instanceof EngineError) return refuse(c, e.status, e.message);
8477
9125
  throw e;
8478
9126
  }
8479
- if (!found) return refuse(c, 404, `no picture at ${path}`);
9127
+ if (!found) return refuse(c, 404, `no file at ${path}`);
8480
9128
  const etag = `"${found.version}"`;
8481
9129
  const headers = {
8482
9130
  "Content-Type": found.type,
@@ -8509,11 +9157,6 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal
8509
9157
  });
8510
9158
  post("/api/iso/ask", Ask, (_c, b) => api.ask(b));
8511
9159
  post("/api/iso/converse", TurnAsk, (_c, b) => api.converse(b));
8512
- app.get("/api/understanding", (c) => answer(c, () => api.understanding(id(c))));
8513
- post("/api/understanding/scratchpad", z.object({
8514
- scratchpad: z.string().max(2e5),
8515
- base: z.string().max(64).nullable()
8516
- }).strict(), (c, b) => api.saveScratchpad(id(c), b.scratchpad, b.base));
8517
9160
  post("/api/iso/take", z.object({
8518
9161
  conversation: z.string().max(64),
8519
9162
  turn: z.string().max(64),
@@ -8544,6 +9187,7 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal
8544
9187
  "reject"
8545
9188
  ])
8546
9189
  }).strict(), (_c, b) => api.answer(b.turn, b.permission, b.decision));
9190
+ app.get("/api/conversations", (c) => answer(c, () => api.conversations()));
8547
9191
  app.get("/api/conversation", (c) => answer(c, async () => await api.conversation(c.req.query("id") || void 0) ?? {
8548
9192
  id: "",
8549
9193
  turns: []
@@ -9134,7 +9778,7 @@ var Setup = class {
9134
9778
  };
9135
9779
  //#endregion
9136
9780
  //#region packages/server/src/version.ts
9137
- const VERSION = "0.10.0";
9781
+ const VERSION = "0.12.0";
9138
9782
  //#endregion
9139
9783
  //#region packages/server/src/git.ts
9140
9784
  /** The top of the repository `folder` is in, or null: the nearest folder holding `.git`, a directory or, in a
@@ -9188,9 +9832,155 @@ function openStudyBase(path) {
9188
9832
  };
9189
9833
  }
9190
9834
  //#endregion
9835
+ //#region packages/server/src/render.ts
9836
+ /** A Chromium on this machine: ISORITE_BROWSER, Playwright's own, or a browser installed in the usual places. */
9837
+ function findBrowser(env = process.env) {
9838
+ if (env.ISORITE_BROWSER && existsSync(env.ISORITE_BROWSER)) return env.ISORITE_BROWSER;
9839
+ 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"));
9840
+ const found = [];
9841
+ try {
9842
+ for (const d of readdirSync(cache).filter((d) => /^chromium(_headless_shell)?-\d+$/.test(d)).sort().reverse()) for (const p of [
9843
+ "chrome-linux/headless_shell",
9844
+ "chrome-headless-shell-linux64/chrome-headless-shell",
9845
+ "chrome-linux/chrome",
9846
+ "chrome-linux64/chrome",
9847
+ "chrome-mac/headless_shell",
9848
+ "chrome-headless-shell-mac-arm64/chrome-headless-shell",
9849
+ "chrome-mac/Chromium.app/Contents/MacOS/Chromium",
9850
+ "chrome-win/headless_shell.exe",
9851
+ "chrome-headless-shell-win64/chrome-headless-shell.exe",
9852
+ "chrome-win/chrome.exe"
9853
+ ]) found.push(join(cache, d, p));
9854
+ } catch {}
9855
+ 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"));
9856
+ return found.find((p) => existsSync(p)) ?? null;
9857
+ }
9858
+ /** The host: a sandboxed frame given the document, and the bridge, answering files through window.isoRead. */
9859
+ const HOST = `(doc, ground) => {
9860
+ const f = document.getElementById("f");
9861
+ window.said = []; window.ready = false;
9862
+ const tell = (m, t = []) => f.contentWindow.postMessage({ isorite: 0, ...m }, "*", t);
9863
+ addEventListener("message", async (e) => {
9864
+ const m = e.data;
9865
+ if (e.source !== f.contentWindow || !m || m.isorite !== 0) return;
9866
+ if (m.type === "ready") { window.ready = true; tell({ type: "ground", ground }); tell({ type: "look", look: "isorite" }); }
9867
+ else if (m.type === "file" || m.type === "bytes") {
9868
+ const got = await window.isoRead(m.path);
9869
+ if (got.error) tell({ type: "reply", id: m.id, error: got.error });
9870
+ 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]); }
9871
+ else tell({ type: "reply", id: m.id, text: got.text ?? atob(got.b64) });
9872
+ }
9873
+ else if (m.type === "height") f.style.height = Math.min(8000, Math.ceil(m.height) + 4) + "px";
9874
+ else if (m.type === "error") window.said.push(String(m.message));
9875
+ });
9876
+ f.srcdoc = doc;
9877
+ }`;
9878
+ /** 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). */
9879
+ const DRAWN = `(() => {
9880
+ let n = document.querySelectorAll("svg path, svg line, svg circle, svg rect, svg polyline, svg polygon, svg text, iso-table td").length;
9881
+ for (const c of document.querySelectorAll("canvas")) {
9882
+ try {
9883
+ const g = c.getContext("2d");
9884
+ if (g) { const d = g.getImageData(0, 0, c.width, c.height).data; if (d.some((v, i) => v !== d[i % 4])) n += 100; }
9885
+ else n += 100;
9886
+ } catch { n += 100; }
9887
+ }
9888
+ return n;
9889
+ })()`;
9890
+ /** Every control touched: sliders to three quarters, choices to their second, buttons pressed, a drag on a canvas. */
9891
+ const TOUCH = `(async () => {
9892
+ 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 })); }
9893
+ for (const s of document.querySelectorAll("select")) if (s.options.length > 1) { s.selectedIndex = 1; s.dispatchEvent(new Event("change", { bubbles: true })); }
9894
+ for (const b of document.querySelectorAll("button, input[type=checkbox]")) if (!b.closest("iso-note")) b.click();
9895
+ await new Promise((r) => setTimeout(r, 400));
9896
+ const c = document.querySelector("canvas");
9897
+ if (c) {
9898
+ 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 }));
9899
+ at("pointerdown", r.width / 2); at("pointermove", r.width / 2 + 60); at("pointerup", r.width / 2 + 60);
9900
+ c.dispatchEvent(new MouseEvent("click", { clientX: r.left + r.width / 3, clientY: r.top + r.height / 3, bubbles: true }));
9901
+ }
9902
+ })()`;
9903
+ let launched = null;
9904
+ async function browser(path) {
9905
+ launched ??= (async () => {
9906
+ const { chromium } = await import("playwright-core");
9907
+ return chromium.launch({
9908
+ executablePath: path,
9909
+ args: [
9910
+ "--use-gl=angle",
9911
+ "--use-angle=swiftshader",
9912
+ "--enable-unsafe-swiftshader"
9913
+ ]
9914
+ });
9915
+ })().catch((e) => {
9916
+ launched = null;
9917
+ throw e;
9918
+ });
9919
+ return launched;
9920
+ }
9921
+ /** Render an assembled artifact (packages/kit's assemble) unseen, answer its bridge from `read` for the files in
9922
+ * `uses`, touch its controls, and say what happened. */
9923
+ async function renderArtifact(doc, uses, read, opts) {
9924
+ const b = await browser(opts.executable), width = opts.width ?? 1100;
9925
+ const page = await b.newPage({ viewport: {
9926
+ width,
9927
+ height: 900
9928
+ } });
9929
+ const errors = [];
9930
+ page.on("console", (m) => {
9931
+ if (m.type?.() === "error") errors.push(m.text());
9932
+ });
9933
+ page.on("pageerror", (e) => errors.push(e.message ?? String(e)));
9934
+ const work = (async () => {
9935
+ await page.exposeFunction("isoRead", async (rel) => {
9936
+ if (!uses.includes(rel)) return { error: `${rel} is not in the artifact's manifest (isorite:uses)` };
9937
+ const got = await read(rel);
9938
+ if (got === null) return { error: `${rel}: no such file beside the artifact` };
9939
+ return typeof got === "string" ? { text: got } : { b64: Buffer.from(got).toString("base64") };
9940
+ });
9941
+ 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>`);
9942
+ await page.evaluate(`(${HOST})(${JSON.stringify(doc)}, ${JSON.stringify(opts.ground ?? "lapis")})`);
9943
+ await page.waitForTimeout(2e3);
9944
+ const ready = await page.evaluate("window.ready");
9945
+ const frame = page.frames().find((fr) => fr !== page.mainFrame());
9946
+ const height = await page.evaluate("document.getElementById(\"f\").offsetHeight");
9947
+ const drawn = frame ? await frame.evaluate(DRAWN) : 0;
9948
+ let screenshot;
9949
+ if (opts.screenshot) {
9950
+ await page.setViewportSize({
9951
+ width,
9952
+ height: Math.max(400, Math.min(height, 4e3))
9953
+ });
9954
+ screenshot = await page.screenshot({ fullPage: true });
9955
+ }
9956
+ if (frame) await frame.evaluate(TOUCH);
9957
+ await page.waitForTimeout(600);
9958
+ const said = await page.evaluate("window.said");
9959
+ return {
9960
+ rendered: ready,
9961
+ errors: [.../* @__PURE__ */ new Set([...said, ...errors])].slice(0, 12),
9962
+ drawn,
9963
+ height,
9964
+ ...screenshot ? { screenshot } : {}
9965
+ };
9966
+ })();
9967
+ const timeout = new Promise((resolve) => setTimeout(() => resolve({
9968
+ rendered: false,
9969
+ errors: ["it didn't finish starting within the time allowed: a loop that never ends?"],
9970
+ drawn: 0,
9971
+ height: 0
9972
+ }), opts.timeout ?? 2e4));
9973
+ try {
9974
+ return await Promise.race([work, timeout]);
9975
+ } finally {
9976
+ await page.close().catch(() => void 0);
9977
+ }
9978
+ }
9979
+ //#endregion
9191
9980
  //#region packages/server/src/serve.ts
9192
9981
  init_src$1();
9193
9982
  init_src();
9983
+ init_src$2();
9194
9984
  /** The built app: in a checkout, where npm run build puts it; installed from npm, in the package's web/. */
9195
9985
  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
9986
  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 +10038,28 @@ async function openEngine({ notes }, device = deviceId(), config = configPath(),
9248
10038
  name: `vectors-${createHash("sha256").update(notes).digest("hex").slice(0, 16)}.json`
9249
10039
  }
9250
10040
  } : {},
9251
- ...extras.elsewhere ? { elsewhere: extras.elsewhere } : {}
10041
+ ...extras.elsewhere ? { elsewhere: extras.elsewhere } : {},
10042
+ renderArtifact: (path, html) => runArtifact(notes, path, html)
9252
10043
  });
9253
10044
  }
10045
+ /** Run an artifact unseen: null when there's no browser, or no built kit, to run it with. */
10046
+ async function runArtifact(notes, path, html) {
10047
+ const executable = findBrowser(), kitDir = join(WEB_DIR, "kit");
10048
+ if (!executable || !existsSync(join(kitDir, "kit.js"))) return null;
10049
+ const kit = Object.fromEntries(KIT_FILES.map((f) => [f, readFileSync(join(kitDir, f), "utf8")]));
10050
+ const dir = dirname(resolve(notes, path)), uses = manifestOf(html).uses;
10051
+ const read = async (rel) => {
10052
+ const at = resolve(dir, rel);
10053
+ if (!at.startsWith(resolve(notes) + sep) || !existsSync(at)) return null;
10054
+ return /\.(csv|tsv|json|jsonl|ndjson|txt|gltf|obj)$/i.test(at) ? readFileSync(at, "utf8") : new Uint8Array(readFileSync(at));
10055
+ };
10056
+ const got = await renderArtifact(assemble(html, kit), uses, read, { executable });
10057
+ return {
10058
+ rendered: got.rendered,
10059
+ errors: got.errors,
10060
+ drawn: got.drawn
10061
+ };
10062
+ }
9254
10063
  /** The guide's finder, as a provider: no key, no sign-in, and a name that says what it is. */
9255
10064
  const { auth: _auth, ...openrouter } = OPENROUTER;
9256
10065
  const FINDER = {
@@ -9981,6 +10790,8 @@ async function doctor(folder, o = {}) {
9981
10790
  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
10791
  }
9983
10792
  if (env.SSH_CONNECTION) say("browser", "ok", `over SSH: on your own machine, ssh -L ${port}:localhost:${port} <this host>, then http://localhost:${port}/`);
10793
+ const chrome = findBrowser(env);
10794
+ 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
10795
  let model = null;
9985
10796
  if (env.ISORITE_EMBED === "off") say("search by meaning", "skip", "switched off (ISORITE_EMBED=off)");
9986
10797
  else {
@@ -11057,7 +11868,7 @@ const ISO_QUESTIONS = {
11057
11868
  about: [TRIANGLE]
11058
11869
  }
11059
11870
  };
11060
- /** Iso's reading of you on Gauss–Bonnet, each sentence resting on what you can open. */
11871
+ /** Iso's answer to where you stand on Gauss–Bonnet, each sentence resting on what you can open. */
11061
11872
  const READING = `You can state it and use it: the [examples](/${BONNET}.md) are yours, and you [solved the torus](/${TORUS}.md). [The proof sketch](/${BONNET}.md) is still mine, untouched: you have not yet said it in your own words. [Where compactness is used](/${COMPACT}.md) is open, and so is [the triangle](/${TRIANGLE}.md), whose solution leans on a step not yet shown.`;
11062
11873
  const anchor = (text) => ({
11063
11874
  text,
@@ -11306,7 +12117,6 @@ async function buildGeometry(sources, code, now = Date.now()) {
11306
12117
  await write(CYLINDER, final(CYLINDER));
11307
12118
  await engine.setDone(CYLINDER, true, (await engine.readNote(CYLINDER)).version);
11308
12119
  });
11309
- on(1, 55, async () => engine.saveScratchpad(BONNET, "Compact: I think it is only that the triangulation is finite. Check against the Möbius band, which is compact and still fails, so orientation must be a separate matter.", null));
11310
12120
  on(0, -40, async () => {
11311
12121
  replies.push({
11312
12122
  calls: [{
@@ -11322,34 +12132,20 @@ async function buildGeometry(sources, code, now = Date.now()) {
11322
12132
  usd: .0071
11323
12133
  }
11324
12134
  }, {
11325
- calls: [{
11326
- name: "write_understanding",
11327
- arguments: {
11328
- id: BONNET,
11329
- reading: READING
11330
- }
11331
- }],
12135
+ text: READING,
11332
12136
  usage: {
11333
12137
  in: 7200,
11334
12138
  out: 150,
11335
12139
  cached: 4200,
11336
12140
  usd: .0103
11337
12141
  }
11338
- }, {
11339
- text: "I have written my reading of you on Gauss–Bonnet.",
11340
- usage: {
11341
- in: 7500,
11342
- out: 20,
11343
- cached: 7e3,
11344
- usd: .0037
11345
- }
11346
12142
  });
11347
12143
  const { turn: id } = await engine.converse({
11348
12144
  mode: "ask",
11349
12145
  asked: "where do I stand on Gauss–Bonnet?",
11350
12146
  tier: "mid",
11351
12147
  given: [{
11352
- kind: "understanding",
12148
+ kind: "note",
11353
12149
  id: BONNET
11354
12150
  }]
11355
12151
  });
@@ -11377,14 +12173,14 @@ const finderHttp = (pace = 0) => scriptedHttp(finderAnswer, pace);
11377
12173
  //#region packages/guide/src/node.ts
11378
12174
  /** In a checkout, the package's notes/; in the npm package, where this is bundled into dist/, the guide/ beside it. */
11379
12175
  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. */
12176
+ /** The guide's files that aren't notes, by path, as bytes: its pictures (M22), and its artifacts and their data (M23). */
11381
12177
  function loadGuidePictures(dir = GUIDE_DIR) {
11382
12178
  const out = {};
11383
12179
  const visit = (at) => {
11384
12180
  for (const name of readdirSync(at)) {
11385
12181
  const path = join(at, name);
11386
12182
  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));
12183
+ 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
12184
  }
11389
12185
  };
11390
12186
  visit(dir);
@@ -11606,6 +12402,7 @@ function bases(args, { config = configPath(), out = console.log } = {}) {
11606
12402
  }
11607
12403
  //#endregion
11608
12404
  //#region packages/server/src/main.ts
12405
+ init_src$2();
11609
12406
  init_src$1();
11610
12407
  init_model();
11611
12408
  const USAGE = `isorite: an agentic harness for understanding
@@ -11658,8 +12455,29 @@ function check(notes, out = console.log, unused = false) {
11658
12455
  const bundle = loadBundle(root), issues = bundle.lint(), cycles = bundle.requiresCycles();
11659
12456
  for (const i of issues) out(`${i.level.padEnd(7)} ${i.path}: ${i.message}`);
11660
12457
  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`);
12458
+ let artifactErrors = 0, artifactWarnings = 0;
12459
+ for (const f of [...bundle.files].filter((x) => citeKind(x) === "artifact").sort()) {
12460
+ const html = readFileSync(join(root, f), "utf8");
12461
+ if (manifestOf(html).kit === null) continue;
12462
+ const dir = dirname(join(root, f)), files = manifestOf(html).uses.filter((u) => existsSync(join(dir, u)));
12463
+ const columns = Object.fromEntries(files.filter((u) => citeKind(u) === "data").flatMap((u) => {
12464
+ try {
12465
+ return [[u, columnsOf(parseData(u, readFileSync(join(dir, u), "utf8")))]];
12466
+ } catch {
12467
+ return [];
12468
+ }
12469
+ }));
12470
+ for (const b of checkArtifact(html, {
12471
+ files,
12472
+ columns
12473
+ })) {
12474
+ out(`${b.level.padEnd(7)} ${f}: ${b.message}`);
12475
+ if (b.level === "error") artifactErrors++;
12476
+ else artifactWarnings++;
12477
+ }
12478
+ }
12479
+ const errors = issues.filter((i) => i.level === "error").length + artifactErrors;
12480
+ 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
12481
  if (unused) {
11664
12482
  const cited = new Set([...bundle.concepts.values()].flatMap((c) => c.cites.map((x) => x.target)));
11665
12483
  const idle = [...bundle.files].filter((f) => citeKind(f) === "image" && !cited.has(f)).sort();