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