isorite 0.2.0 → 0.3.1
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/README.md +6 -3
- package/dist/core.js +207 -426
- package/dist/embed-worker.js +9 -4
- package/dist/main.js +7917 -6864
- package/docs/setup-for-agents.md +166 -0
- package/docs/setup.md +133 -0
- package/docs/terminal.md +119 -0
- package/package.json +2 -1
- package/plugin/isorite.ts +3 -3
- package/plugin/notepage.tsx +1 -9
- package/plugin/tui.tsx +9 -10
- package/web/_app/immutable/assets/0.IOi79DXH.css +1 -0
- package/web/_app/immutable/assets/{4.PVZR2_N5.css → 4.CUdclsPi.css} +1 -1
- package/web/_app/immutable/assets/5.TsqJFEFF.css +1 -0
- package/web/_app/immutable/assets/{NoteEditor.ChlpmyHO.css → NoteEditor.BCjOff8L.css} +1 -1
- package/web/_app/immutable/chunks/{B_nIecMW.js → 8IM6BNk6.js} +36 -37
- package/web/_app/immutable/chunks/{BEVz4Dgg.js → BHPhFydX.js} +1 -1
- package/web/_app/immutable/chunks/B_AYGp9o.js +1 -0
- package/web/_app/immutable/chunks/Bze-M_kr.js +3 -0
- package/web/_app/immutable/chunks/CFA70_0c.js +20 -0
- package/web/_app/immutable/chunks/CGgq5NWd.js +1 -0
- package/web/_app/immutable/chunks/CHlFgDNB.js +1 -0
- package/web/_app/immutable/chunks/CMp5T_-1.js +1 -0
- package/web/_app/immutable/chunks/D89YN-Dr.js +1 -0
- package/web/_app/immutable/chunks/DF3lgcLW.js +1 -0
- package/web/_app/immutable/chunks/DGVcYHaJ.js +43 -0
- package/web/_app/immutable/chunks/DHMLhlug.js +1 -0
- package/web/_app/immutable/chunks/DmgpLvxp.js +1 -0
- package/web/_app/immutable/chunks/DrsVvzvg.js +1 -0
- package/web/_app/immutable/entry/app.hlk6OrE8.js +2 -0
- package/web/_app/immutable/entry/start.BE1F0HZV.js +1 -0
- package/web/_app/immutable/nodes/0.DMGdYhkH.js +15 -0
- package/web/_app/immutable/nodes/1.Dn_0fgt7.js +1 -0
- package/web/_app/immutable/nodes/2.Y1Dd5PBI.js +1 -0
- package/web/_app/immutable/nodes/3.BlxuRbLf.js +1 -0
- package/web/_app/immutable/nodes/4.Bt_H0MbN.js +2 -0
- package/web/_app/immutable/nodes/5.BJ-EVWbz.js +5 -0
- package/web/_app/immutable/nodes/6.DDIiJnwi.js +1 -0
- package/web/_app/immutable/nodes/7.DmPPLDvb.js +1 -0
- package/web/_app/immutable/nodes/{9.NF5w8YC2.js → 8.CwMTBog4.js} +2 -2
- package/web/_app/immutable/workers/worker-CWkpeSVG.js +1 -0
- package/web/_app/version.json +1 -1
- package/web/index.html +25 -19
- package/web/_app/immutable/assets/0.s5xqevrq.css +0 -1
- package/web/_app/immutable/assets/5.DCxNIgyW.css +0 -1
- package/web/_app/immutable/assets/8.C0NYkyNY.css +0 -1
- package/web/_app/immutable/chunks/B0NIb88Q.js +0 -1
- package/web/_app/immutable/chunks/B7ZjnqOt.js +0 -20
- package/web/_app/immutable/chunks/BEk5idXo.js +0 -1
- package/web/_app/immutable/chunks/C8UdwlAg.js +0 -3
- package/web/_app/immutable/chunks/CcQ3P3p1.js +0 -13
- package/web/_app/immutable/chunks/CcRs965g.js +0 -1
- package/web/_app/immutable/chunks/Cw2GjAJR.js +0 -1
- package/web/_app/immutable/chunks/DjBHskLm.js +0 -1
- package/web/_app/immutable/chunks/Dm229u8R.js +0 -1
- package/web/_app/immutable/chunks/UM_L7-f7.js +0 -1
- package/web/_app/immutable/chunks/b-AOhhy1.js +0 -1
- package/web/_app/immutable/chunks/k55859zq.js +0 -1
- package/web/_app/immutable/entry/app.B3QXxjjH.js +0 -2
- package/web/_app/immutable/entry/start.BauJm-6k.js +0 -1
- package/web/_app/immutable/nodes/0.DP4c84Wn.js +0 -14
- package/web/_app/immutable/nodes/1.BcwcVDJZ.js +0 -1
- package/web/_app/immutable/nodes/2.DgGnQZ7P.js +0 -1
- package/web/_app/immutable/nodes/3.BudzO7We.js +0 -1
- package/web/_app/immutable/nodes/4.DmswyrSt.js +0 -2
- package/web/_app/immutable/nodes/5.DjXsfjL5.js +0 -5
- package/web/_app/immutable/nodes/6.uBh-sdQx.js +0 -1
- package/web/_app/immutable/nodes/7.BYBNXM1A.js +0 -1
- package/web/_app/immutable/nodes/8.Tbz9Rh77.js +0 -1
- package/web/_app/immutable/workers/worker-rKDNrEKQ.js +0 -1
- /package/web/_app/immutable/assets/{9.CFA9_nYZ.css → 8.CFA9_nYZ.css} +0 -0
package/dist/core.js
CHANGED
|
@@ -344,6 +344,27 @@ function linkRefs(body) {
|
|
|
344
344
|
for (const [href, title] of defs) if (!used.has(href)) refs.push([href, rating(title)]);
|
|
345
345
|
return refs;
|
|
346
346
|
}
|
|
347
|
+
/** Every link in a body, in order, with its text as written (marks dropped). */
|
|
348
|
+
function linkTexts(body) {
|
|
349
|
+
const { tokens } = parse$1(body);
|
|
350
|
+
const out = [];
|
|
351
|
+
for (const t of tokens) {
|
|
352
|
+
if (t.type !== "inline" || !t.children) continue;
|
|
353
|
+
let open = null;
|
|
354
|
+
for (const c of t.children) if (c.type === "link_open") open = {
|
|
355
|
+
href: c.attrGet("href") ?? "",
|
|
356
|
+
text: ""
|
|
357
|
+
};
|
|
358
|
+
else if (c.type === "link_close" && open) {
|
|
359
|
+
out.push({
|
|
360
|
+
href: open.href,
|
|
361
|
+
text: open.text.trim()
|
|
362
|
+
});
|
|
363
|
+
open = null;
|
|
364
|
+
} else if (open && (c.type === "text" || c.type === "code_inline")) open.text += c.content;
|
|
365
|
+
}
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
347
368
|
/** Every link and image in a body, in order: where it points, its title, and whether it is an image
|
|
348
369
|
* (``, which for an artifact or a picture means "shown here", an embed). */
|
|
349
370
|
function citeRefs(body) {
|
|
@@ -403,6 +424,92 @@ function section(body, heading) {
|
|
|
403
424
|
return null;
|
|
404
425
|
}
|
|
405
426
|
//#endregion
|
|
427
|
+
//#region packages/core/src/kinds.ts
|
|
428
|
+
function kindOf(type) {
|
|
429
|
+
const t = type.trim().toLowerCase();
|
|
430
|
+
return t === "question" ? "question" : t === "problem" ? "problem" : "note";
|
|
431
|
+
}
|
|
432
|
+
/** The field naming what a kind is about, and the one saying it is done. */
|
|
433
|
+
const ABOUT = {
|
|
434
|
+
question: "about",
|
|
435
|
+
problem: "uses"
|
|
436
|
+
};
|
|
437
|
+
const DONE = {
|
|
438
|
+
question: "resolved",
|
|
439
|
+
problem: "solved"
|
|
440
|
+
};
|
|
441
|
+
/** Note ids from frontmatter: a list, or one; `/a/b.md` and `a/b` alike. */
|
|
442
|
+
function idsOf(raw) {
|
|
443
|
+
return (typeof raw === "string" ? [raw] : Array.isArray(raw) ? raw : []).map((t) => text(t).trim().replace(/^\/+/, "").replace(/\.md$/, "")).filter(Boolean);
|
|
444
|
+
}
|
|
445
|
+
/** When it was resolved or solved, as written (a date), or null while it is open. */
|
|
446
|
+
function doneOf(meta, kind) {
|
|
447
|
+
if (kind === "note") return null;
|
|
448
|
+
const v = meta[DONE[kind]];
|
|
449
|
+
if (v === null || v === void 0 || v === false || v === "") return null;
|
|
450
|
+
return v instanceof Date ? v.toISOString().slice(0, 10) : text(v);
|
|
451
|
+
}
|
|
452
|
+
/** An embed: `[[!name]]` alone on a line, outside code. */
|
|
453
|
+
const EMBED = /^[ \t]*\[\[!([^\]\n]+)\]\][ \t]*$/;
|
|
454
|
+
/** The embeds in a note's body, in order, as written (a name, or a path). */
|
|
455
|
+
function embedRefs(body) {
|
|
456
|
+
const out = [];
|
|
457
|
+
let fenced = false;
|
|
458
|
+
for (const line of body.split("\n")) {
|
|
459
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
460
|
+
fenced = !fenced;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (fenced) continue;
|
|
464
|
+
const m = EMBED.exec(line);
|
|
465
|
+
if (m) out.push(m[1].trim());
|
|
466
|
+
}
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
469
|
+
/** The embed that names a note, in its shortest form: its file name, or its path where the name is shared. */
|
|
470
|
+
function embedFor(id, ids) {
|
|
471
|
+
const name = id.split("/").at(-1);
|
|
472
|
+
let shared = 0;
|
|
473
|
+
for (const other of ids) if (other.split("/").at(-1) === name) shared++;
|
|
474
|
+
return `[[!${shared > 1 ? id : name}]]`;
|
|
475
|
+
}
|
|
476
|
+
/** A code link's file and lines, or null when it is not one, or names a path outside the repository. */
|
|
477
|
+
function codeTarget(href) {
|
|
478
|
+
const m = /^code:([^#?]+)(?:#L(\d+)(?:-L?(\d+))?)?$/.exec(href.trim());
|
|
479
|
+
if (!m) return null;
|
|
480
|
+
const path = decodeURIComponent(m[1]).replace(/^\.?\/+/, "");
|
|
481
|
+
if (!path || path.split("/").some((p) => p === ".." || !p)) return null;
|
|
482
|
+
return {
|
|
483
|
+
path,
|
|
484
|
+
...m[2] ? { line: Number(m[2]) } : {},
|
|
485
|
+
...m[3] ? { end: Number(m[3]) } : {}
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
/** A note's links out of the base, in order, each once. */
|
|
489
|
+
function outwardLinks(body) {
|
|
490
|
+
const out = [], seen = /* @__PURE__ */ new Set();
|
|
491
|
+
for (const { href, text: words } of linkTexts(body)) {
|
|
492
|
+
if (seen.has(href)) continue;
|
|
493
|
+
if (/^https?:\/\//i.test(href)) out.push({
|
|
494
|
+
kind: "web",
|
|
495
|
+
href,
|
|
496
|
+
text: words || href.replace(/^https?:\/\/(www\.)?/i, "")
|
|
497
|
+
});
|
|
498
|
+
else {
|
|
499
|
+
const c = codeTarget(href);
|
|
500
|
+
if (!c) continue;
|
|
501
|
+
out.push({
|
|
502
|
+
kind: "code",
|
|
503
|
+
href,
|
|
504
|
+
text: words || c.path.split("/").at(-1),
|
|
505
|
+
...c
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
seen.add(href);
|
|
509
|
+
}
|
|
510
|
+
return out;
|
|
511
|
+
}
|
|
512
|
+
//#endregion
|
|
406
513
|
//#region packages/core/src/bundle.ts
|
|
407
514
|
const OKF_VERSION = "0.2";
|
|
408
515
|
const ARTIFACT = /\.html?$/i;
|
|
@@ -418,6 +525,13 @@ var Concept = class {
|
|
|
418
525
|
mtime;
|
|
419
526
|
links = [];
|
|
420
527
|
cites = [];
|
|
528
|
+
/** The notes it embeds (`[[!name]]`, M12), resolved to ids, in order. */
|
|
529
|
+
embeds = [];
|
|
530
|
+
/** Its links out of the base: to the web, and to code (M12). */
|
|
531
|
+
outward = [];
|
|
532
|
+
/** For a question or a problem: the notes it is about (or uses), from its frontmatter and from the notes that
|
|
533
|
+
* embed it. */
|
|
534
|
+
attached = [];
|
|
421
535
|
constructor(id, path, meta, body, mtime = 0) {
|
|
422
536
|
this.id = id;
|
|
423
537
|
this.path = path;
|
|
@@ -442,6 +556,14 @@ var Concept = class {
|
|
|
442
556
|
get description() {
|
|
443
557
|
return text(this.meta.description || "");
|
|
444
558
|
}
|
|
559
|
+
/** A note, an open question, or a problem (M12). */
|
|
560
|
+
get kind() {
|
|
561
|
+
return kindOf(this.type);
|
|
562
|
+
}
|
|
563
|
+
/** When a question was resolved or a problem solved, as written; null while open, and for a note. */
|
|
564
|
+
get done() {
|
|
565
|
+
return doneOf(this.meta, this.kind);
|
|
566
|
+
}
|
|
445
567
|
get tags() {
|
|
446
568
|
let tags = this.meta.tags || [];
|
|
447
569
|
if (typeof tags === "string") tags = [tags];
|
|
@@ -571,6 +693,7 @@ var Bundle = class Bundle {
|
|
|
571
693
|
}
|
|
572
694
|
}
|
|
573
695
|
concept.links = links;
|
|
696
|
+
concept.outward = outwardLinks(concept.body);
|
|
574
697
|
for (const link of links) if (link.broken) this.issue(concept.path, "warning", "broken-link", `broken link to ${link.target}`);
|
|
575
698
|
for (const { href, title, embed } of citeRefs(concept.body)) {
|
|
576
699
|
const path = this.filePath(concept, href), kind = path === null ? null : citeKind(path);
|
|
@@ -588,6 +711,25 @@ var Bundle = class Bundle {
|
|
|
588
711
|
if (cite.broken) this.issue(concept.path, "warning", "broken-link", `broken link to ${path}`);
|
|
589
712
|
}
|
|
590
713
|
}
|
|
714
|
+
const byName = /* @__PURE__ */ new Map();
|
|
715
|
+
for (const id of this.concepts.keys()) {
|
|
716
|
+
const n = basename(id);
|
|
717
|
+
byName.set(n, [...byName.get(n) ?? [], id]);
|
|
718
|
+
}
|
|
719
|
+
for (const concept of this.concepts.values()) for (const ref of embedRefs(concept.body)) {
|
|
720
|
+
const path = ref.replace(/^\/+/, "").replace(/\.md$/, "");
|
|
721
|
+
const found = path.includes("/") ? this.concepts.has(path) ? [path] : [] : byName.get(path) ?? [];
|
|
722
|
+
if (found.length === 1) concept.embeds.push(found[0]);
|
|
723
|
+
else this.issue(concept.path, "warning", "embed-broken", found.length ? `two notes are named ${path}: embed one by its path` : `no note to embed: ${ref}`);
|
|
724
|
+
}
|
|
725
|
+
for (const concept of this.concepts.values()) {
|
|
726
|
+
if (concept.kind === "note") continue;
|
|
727
|
+
concept.attached = idsOf(concept.meta[ABOUT[concept.kind]]).filter((id) => this.concepts.has(id) && id !== concept.id);
|
|
728
|
+
}
|
|
729
|
+
for (const concept of this.concepts.values()) for (const id of concept.embeds) {
|
|
730
|
+
const embedded = this.concepts.get(id);
|
|
731
|
+
if (embedded.kind !== "note" && id !== concept.id && !embedded.attached.includes(concept.id)) embedded.attached.push(concept.id);
|
|
732
|
+
}
|
|
591
733
|
for (const group of this.requiresCycles()) this.issue(this.concepts.get(group[0]).path, "warning", "requires-cycle", `requires cycle: ${group.join(", ")} require each other`);
|
|
592
734
|
}
|
|
593
735
|
issue(path, level, code, message) {
|
|
@@ -834,7 +976,7 @@ var Bundle = class Bundle {
|
|
|
834
976
|
const lines = [`# ${typeName}`, ""];
|
|
835
977
|
for (const c of items) {
|
|
836
978
|
const desc = c.description ? ` - ${oneLine(c.description)}` : "";
|
|
837
|
-
lines.push(`* [${escape
|
|
979
|
+
lines.push(`* [${escape(c.title)}](${basename(c.path)})${desc}`);
|
|
838
980
|
}
|
|
839
981
|
parts.push(lines.join("\n") + "\n");
|
|
840
982
|
}
|
|
@@ -853,7 +995,7 @@ var Bundle = class Bundle {
|
|
|
853
995
|
}
|
|
854
996
|
};
|
|
855
997
|
const oneLine = (s) => strip(s).split(/\s+/u).filter(Boolean).join(" ");
|
|
856
|
-
const escape
|
|
998
|
+
const escape = (s) => s.replaceAll("[", "\\[").replaceAll("]", "\\]");
|
|
857
999
|
//#endregion
|
|
858
1000
|
//#region packages/core/src/source.ts
|
|
859
1001
|
const BOM = "";
|
|
@@ -1381,6 +1523,14 @@ function applyHunks(a, hunks) {
|
|
|
1381
1523
|
}
|
|
1382
1524
|
//#endregion
|
|
1383
1525
|
//#region packages/core/src/journal.ts
|
|
1526
|
+
/** The kinds an entry may be. Format 1 also wrote `marks` (Iso's marking of an exercise): a journal moved in by
|
|
1527
|
+
* `isorite migrate` may hold them, and they are passed over. */
|
|
1528
|
+
const KINDS$1 = [
|
|
1529
|
+
"change",
|
|
1530
|
+
"proposal",
|
|
1531
|
+
"revise",
|
|
1532
|
+
"discard"
|
|
1533
|
+
];
|
|
1384
1534
|
const SNAPSHOT_EVERY = 40;
|
|
1385
1535
|
const SESSION_GAP_MS = 18e5;
|
|
1386
1536
|
const hashText = (text) => sha256(text).slice(0, 16);
|
|
@@ -1409,7 +1559,7 @@ function readJournal(text) {
|
|
|
1409
1559
|
}
|
|
1410
1560
|
if (typeof e !== "object" || e === null) continue;
|
|
1411
1561
|
const entry = e;
|
|
1412
|
-
if (typeof entry.id !== "string" ||
|
|
1562
|
+
if (typeof entry.id !== "string" || !KINDS$1.includes(entry.kind) || byId.has(entry.id)) continue;
|
|
1413
1563
|
byId.set(entry.id, entry);
|
|
1414
1564
|
}
|
|
1415
1565
|
return [...byId.keys()].sort().map((k) => byId.get(k));
|
|
@@ -1755,12 +1905,7 @@ function moments(entries) {
|
|
|
1755
1905
|
offered.set(e.id, m);
|
|
1756
1906
|
asked.set(e.id, e.asked);
|
|
1757
1907
|
out.push(m);
|
|
1758
|
-
} else if (e.kind === "
|
|
1759
|
-
...base,
|
|
1760
|
-
what: "marked",
|
|
1761
|
-
result: e.result
|
|
1762
|
-
});
|
|
1763
|
-
else if (e.kind === "discard") {
|
|
1908
|
+
} else if (e.kind === "discard") {
|
|
1764
1909
|
const m = offered.get(e.proposal);
|
|
1765
1910
|
if (m) {
|
|
1766
1911
|
m.what = "discarded";
|
|
@@ -1855,11 +2000,39 @@ function sessionMoments(journals) {
|
|
|
1855
2000
|
return out.sort((a, b) => a.ids[0] < b.ids[0] ? -1 : 1);
|
|
1856
2001
|
}
|
|
1857
2002
|
//#endregion
|
|
2003
|
+
//#region packages/core/src/own.ts
|
|
2004
|
+
const OWN_DIR = ".isorite";
|
|
2005
|
+
const OWN_FORMAT = 2;
|
|
2006
|
+
const GITATTRIBUTES = "*.jsonl merge=union\n";
|
|
2007
|
+
const SETTINGS_FILE = "settings.toml";
|
|
2008
|
+
function parseSettings(text) {
|
|
2009
|
+
if (!text) return {};
|
|
2010
|
+
let raw;
|
|
2011
|
+
try {
|
|
2012
|
+
raw = parse(text);
|
|
2013
|
+
} catch {
|
|
2014
|
+
return {};
|
|
2015
|
+
}
|
|
2016
|
+
return typeof raw.repository === "string" && raw.repository.trim() ? { repository: raw.repository.trim() } : {};
|
|
2017
|
+
}
|
|
2018
|
+
function settingsText(s) {
|
|
2019
|
+
return `# isorite's settings for this base (format 2)\n${stringify$1(s)}\n`;
|
|
2020
|
+
}
|
|
2021
|
+
const SAFE = /^[^\0]+$/;
|
|
2022
|
+
function checkId(id) {
|
|
2023
|
+
if (!SAFE.test(id) || id.startsWith("/") || id.split("/").some((part) => part === ".." || part === "." || !part)) throw new Error(`not a note's id: ${JSON.stringify(id)}`);
|
|
2024
|
+
return id;
|
|
2025
|
+
}
|
|
2026
|
+
/** Where a note's journal is. Drafts and Understanding notes are journalled under their `@draft/` and
|
|
2027
|
+
* `@understanding/` ids. */
|
|
2028
|
+
const journalPath = (noteId) => `journal/${checkId(noteId)}.jsonl`;
|
|
2029
|
+
/** This device's record of usage: what was cleared from Iso's context, and what each turn cost (record.ts). */
|
|
2030
|
+
const recordPath = (device) => `record/${checkId(device)}.jsonl`;
|
|
2031
|
+
//#endregion
|
|
1858
2032
|
//#region packages/core/src/study.ts
|
|
1859
2033
|
const STUDY_FORMAT = 1;
|
|
1860
2034
|
const CONFIG_FILE = "isorite.toml";
|
|
1861
2035
|
const STUDY_DIR = "study";
|
|
1862
|
-
const GITATTRIBUTES = "*.jsonl merge=union\n";
|
|
1863
2036
|
var StudyError = class extends Error {};
|
|
1864
2037
|
function parseStudyConfig(source) {
|
|
1865
2038
|
let raw;
|
|
@@ -1881,38 +2054,23 @@ function parseStudyConfig(source) {
|
|
|
1881
2054
|
}
|
|
1882
2055
|
};
|
|
1883
2056
|
}
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
function
|
|
1889
|
-
if (!SAFE.test(id) || id.startsWith("/") || id.split("/").some((part) => part === ".." || part === "." || !part)) throw new StudyError(`not a note's id: ${JSON.stringify(id)}`);
|
|
1890
|
-
return id;
|
|
1891
|
-
}
|
|
1892
|
-
/** Where a note's journal is, in the study base. Notes of the study base itself are journalled under their `@study/` id. */
|
|
1893
|
-
const journalPath = (noteId) => `journal/${checkId(noteId)}.jsonl`;
|
|
1894
|
-
const recordPath = (device) => `record/${checkId(device)}.jsonl`;
|
|
1895
|
-
/** The prefix of a study note's or study folder's id wherever the two bases are spoken of together. */
|
|
1896
|
-
const STUDY_PREFIX = "@study/";
|
|
1897
|
-
const isStudyId = (id) => id.startsWith("@study/") || id === "@study";
|
|
1898
|
-
const studyId = (idInStudyBundle) => STUDY_PREFIX + idInStudyBundle;
|
|
1899
|
-
/** The notes of the knowledge base a study note tests: its `tests` frontmatter, as note ids. */
|
|
1900
|
-
function testsOf(meta) {
|
|
1901
|
-
const raw = meta.tests;
|
|
1902
|
-
return (typeof raw === "string" ? [raw] : Array.isArray(raw) ? raw : []).map((t) => text(t).replace(/^\//, "").replace(/\.md$/, "")).filter(Boolean);
|
|
1903
|
-
}
|
|
1904
|
-
/** The Atlas's model of a knowledge base and, with it, its study base: every study folder is an ordinary folder,
|
|
1905
|
-
* flagged, standing beside the folder it studies (the root's stands inside the root), and each study note is
|
|
1906
|
-
* joined to the notes it tests. `study` is the bundle read from the study base's study/ folder. */
|
|
1907
|
-
function mapModel(base, study = null) {
|
|
2057
|
+
//#endregion
|
|
2058
|
+
//#region packages/core/src/map.ts
|
|
2059
|
+
/** The Atlas's model of a knowledge base: its folders and notes, and the links between notes, each with its strength.
|
|
2060
|
+
* A question or a problem is joined to each note it is about, or uses, as a "uses" link from it (M12). */
|
|
2061
|
+
function mapModel(base) {
|
|
1908
2062
|
const order = base.prerequisiteOrder();
|
|
1909
|
-
const note = (id) =>
|
|
1910
|
-
kind
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
2063
|
+
const note = (id) => {
|
|
2064
|
+
const k = base.concepts.get(id).kind;
|
|
2065
|
+
return {
|
|
2066
|
+
kind: "concept",
|
|
2067
|
+
id: "c:" + id,
|
|
2068
|
+
ref: id,
|
|
2069
|
+
weight: 1,
|
|
2070
|
+
rank: order.get(id)?.depth ?? 0,
|
|
2071
|
+
...k === "note" ? {} : { of: k }
|
|
2072
|
+
};
|
|
2073
|
+
};
|
|
1916
2074
|
const edges = [];
|
|
1917
2075
|
for (const c of base.concepts.values()) {
|
|
1918
2076
|
const strongest = /* @__PURE__ */ new Map();
|
|
@@ -1920,52 +2078,20 @@ function mapModel(base, study = null) {
|
|
|
1920
2078
|
if (l.broken || l.kind !== "concept" || l.target === c.id || !base.concepts.has(l.target)) continue;
|
|
1921
2079
|
strongest.set(l.target, Math.max(strongest.get(l.target) ?? 0, l.rel && STRENGTH[l.rel] || STRENGTH.uses));
|
|
1922
2080
|
}
|
|
2081
|
+
for (const to of c.attached) if (!strongest.has(to)) strongest.set(to, STRENGTH.uses);
|
|
1923
2082
|
for (const [to, s] of strongest) edges.push([
|
|
1924
2083
|
c.id,
|
|
1925
2084
|
to,
|
|
1926
2085
|
s
|
|
1927
2086
|
]);
|
|
1928
2087
|
}
|
|
1929
|
-
const byFolder = /* @__PURE__ */ new Map();
|
|
1930
|
-
if (study) for (const s of [...study.concepts.values()].sort((a, b) => a.id < b.id ? -1 : 1)) {
|
|
1931
|
-
let folder = dirname(s.id);
|
|
1932
|
-
while (folder && !base.directories.has(folder)) folder = dirname(folder);
|
|
1933
|
-
const ref = studyId(s.id);
|
|
1934
|
-
byFolder.set(folder, [...byFolder.get(folder) ?? [], {
|
|
1935
|
-
kind: "concept",
|
|
1936
|
-
id: "s:" + s.id,
|
|
1937
|
-
ref,
|
|
1938
|
-
weight: 1,
|
|
1939
|
-
rank: 0,
|
|
1940
|
-
study: true
|
|
1941
|
-
}]);
|
|
1942
|
-
for (const t of testsOf(s.meta)) if (base.concepts.has(t)) edges.push([
|
|
1943
|
-
ref,
|
|
1944
|
-
t,
|
|
1945
|
-
STRENGTH.uses
|
|
1946
|
-
]);
|
|
1947
|
-
}
|
|
1948
|
-
const studyDir = (folder) => {
|
|
1949
|
-
const notes = byFolder.get(folder);
|
|
1950
|
-
return notes ? [{
|
|
1951
|
-
kind: "dir",
|
|
1952
|
-
id: "sd:" + folder,
|
|
1953
|
-
ref: folder ? studyId(folder) : "@study",
|
|
1954
|
-
children: notes,
|
|
1955
|
-
study: true
|
|
1956
|
-
}] : [];
|
|
1957
|
-
};
|
|
1958
2088
|
const dir = (id) => {
|
|
1959
2089
|
const d = base.directories.get(id);
|
|
1960
|
-
const children = [];
|
|
1961
|
-
for (const child of [...d.children].sort()) children.push(dir(child), ...studyDir(child));
|
|
1962
|
-
children.push(...[...d.concepts].sort().map(note));
|
|
1963
|
-
if (id === "") children.push(...studyDir(""));
|
|
1964
2090
|
return {
|
|
1965
2091
|
kind: "dir",
|
|
1966
2092
|
id: "d:" + id,
|
|
1967
2093
|
ref: id,
|
|
1968
|
-
children
|
|
2094
|
+
children: [...[...d.children].sort().map(dir), ...[...d.concepts].sort().map(note)]
|
|
1969
2095
|
};
|
|
1970
2096
|
};
|
|
1971
2097
|
return {
|
|
@@ -1973,27 +2099,15 @@ function mapModel(base, study = null) {
|
|
|
1973
2099
|
edges
|
|
1974
2100
|
};
|
|
1975
2101
|
}
|
|
1976
|
-
/** The map without the study base: its folders and notes, and the edges to them, left out. The Atlas draws study
|
|
1977
|
-
* folders only in its Understand lens (M10). */
|
|
1978
|
-
function withoutStudy(model) {
|
|
1979
|
-
const strip = (d) => ({
|
|
1980
|
-
...d,
|
|
1981
|
-
children: d.children.filter((c) => !c.study).map((c) => c.kind === "dir" ? strip(c) : c)
|
|
1982
|
-
});
|
|
1983
|
-
return {
|
|
1984
|
-
root: strip(model.root),
|
|
1985
|
-
edges: model.edges.filter(([a, b]) => !a.startsWith("@") && !b.startsWith("@"))
|
|
1986
|
-
};
|
|
1987
|
-
}
|
|
1988
2102
|
/** What lights when a note is chosen on the Atlas (refs, as the map model names notes). Where the base rates its
|
|
1989
|
-
* links, only "requires" is followed back; where it rates none, every drawn link is.
|
|
2103
|
+
* links, only "requires" is followed back; where it rates none, every drawn link is. */
|
|
1990
2104
|
function reach(model, note) {
|
|
1991
|
-
const least = model.edges.some(([
|
|
2105
|
+
const least = model.edges.some(([, , s]) => s >= STRENGTH.requires) ? STRENGTH.requires : STRENGTH.uses;
|
|
1992
2106
|
const needs = /* @__PURE__ */ new Map(), neededBy = /* @__PURE__ */ new Map();
|
|
1993
2107
|
const uses = /* @__PURE__ */ new Set(), also = /* @__PURE__ */ new Set(), weak = [];
|
|
1994
2108
|
for (const [from, to, s] of model.edges) {
|
|
1995
2109
|
if (from === to) continue;
|
|
1996
|
-
if (s < least
|
|
2110
|
+
if (s < least) {
|
|
1997
2111
|
if (from !== note && to !== note) continue;
|
|
1998
2112
|
(s >= STRENGTH.uses ? uses : also).add(from === note ? to : from);
|
|
1999
2113
|
if (s >= STRENGTH.uses) weak.push({
|
|
@@ -2060,337 +2174,6 @@ function reach(model, note) {
|
|
|
2060
2174
|
};
|
|
2061
2175
|
}
|
|
2062
2176
|
//#endregion
|
|
2063
|
-
//#region packages/core/src/growth.ts
|
|
2064
|
-
const DX = [
|
|
2065
|
-
0,
|
|
2066
|
-
1,
|
|
2067
|
-
1,
|
|
2068
|
-
1,
|
|
2069
|
-
0,
|
|
2070
|
-
-1,
|
|
2071
|
-
-1,
|
|
2072
|
-
-1
|
|
2073
|
-
];
|
|
2074
|
-
const DY = [
|
|
2075
|
-
-1,
|
|
2076
|
-
-1,
|
|
2077
|
-
0,
|
|
2078
|
-
1,
|
|
2079
|
-
1,
|
|
2080
|
-
1,
|
|
2081
|
-
0,
|
|
2082
|
-
-1
|
|
2083
|
-
];
|
|
2084
|
-
/** Grow a plant. `depth` is how many times the rule is applied (0 is a bare shoot); `step` the length of a stroke. */
|
|
2085
|
-
function sprout(seed, depth, { step = 1, pad = .5 } = {}) {
|
|
2086
|
-
let s = "X", r = (Math.abs(Math.trunc(seed)) * 7919 + 13) % 2147483647;
|
|
2087
|
-
const rnd = () => {
|
|
2088
|
-
r = r * 16807 % 2147483647;
|
|
2089
|
-
return r / 2147483647;
|
|
2090
|
-
};
|
|
2091
|
-
for (let i = 0; i < Math.max(0, Math.min(depth, 6)); i++) s = s.replace(/[XF]/g, (c) => c === "X" ? "F[+X]F[-X]+X" : "FF");
|
|
2092
|
-
if (s === "X") s = "F";
|
|
2093
|
-
let x = 0, y = 0, d = 0, g = 1, pen = false, x0 = 0, x1 = 0, y0 = 0, y1 = 0;
|
|
2094
|
-
const stack = [], out = [], tips = [], t = step / 2;
|
|
2095
|
-
for (const c of s) if (c === "F") {
|
|
2096
|
-
if (!pen) {
|
|
2097
|
-
out.push(`M${x} ${y}`);
|
|
2098
|
-
pen = true;
|
|
2099
|
-
}
|
|
2100
|
-
x += DX[d] * step;
|
|
2101
|
-
y += DY[d] * step;
|
|
2102
|
-
out.push(`L${x} ${y}`);
|
|
2103
|
-
x0 = Math.min(x0, x);
|
|
2104
|
-
x1 = Math.max(x1, x);
|
|
2105
|
-
y0 = Math.min(y0, y);
|
|
2106
|
-
y1 = Math.max(y1, y);
|
|
2107
|
-
} else if (c === "+") d = (d + g + 8) % 8;
|
|
2108
|
-
else if (c === "-") d = (d - g + 8) % 8;
|
|
2109
|
-
else if (c === "[") {
|
|
2110
|
-
stack.push([
|
|
2111
|
-
x,
|
|
2112
|
-
y,
|
|
2113
|
-
d,
|
|
2114
|
-
g
|
|
2115
|
-
]);
|
|
2116
|
-
if (rnd() < .5) g = -g;
|
|
2117
|
-
} else if (c === "]") {
|
|
2118
|
-
tips.push(`M${x - t / 2} ${y - t / 2}h${t}v${t}h${-t}z`);
|
|
2119
|
-
[x, y, d, g] = stack.pop();
|
|
2120
|
-
pen = false;
|
|
2121
|
-
}
|
|
2122
|
-
const w = x1 - x0 + 2 * pad, h = y1 - y0 + 2 * pad;
|
|
2123
|
-
return {
|
|
2124
|
-
d: out.join(""),
|
|
2125
|
-
tips: tips.join(""),
|
|
2126
|
-
viewBox: `${x0 - pad} ${y0 - pad} ${w} ${h}`,
|
|
2127
|
-
ratio: w / h
|
|
2128
|
-
};
|
|
2129
|
-
}
|
|
2130
|
-
//#endregion
|
|
2131
|
-
//#region packages/core/src/understanding.ts
|
|
2132
|
-
const LEVELS = 6;
|
|
2133
|
-
/** Days that evidence at each level holds, from level 1. */
|
|
2134
|
-
const INTERVALS = [
|
|
2135
|
-
1,
|
|
2136
|
-
3,
|
|
2137
|
-
7,
|
|
2138
|
-
16,
|
|
2139
|
-
35,
|
|
2140
|
-
80
|
|
2141
|
-
];
|
|
2142
|
-
const DAY = 864e5;
|
|
2143
|
-
const clamp = (x) => Math.max(0, Math.min(6, x));
|
|
2144
|
-
const stepOf = (squares) => squares <= 0 ? 0 : squares <= 2 ? 1 : squares <= 4 ? 2 : 3;
|
|
2145
|
-
/** Every note the record bears on, with its understanding as of `now`. Events must be in time order (as readRecord and mergeRecords give them). */
|
|
2146
|
-
function understanding(events, now = Date.now()) {
|
|
2147
|
-
const state = /* @__PURE__ */ new Map();
|
|
2148
|
-
const at = (note) => {
|
|
2149
|
-
let s = state.get(note);
|
|
2150
|
-
if (!s) state.set(note, s = {
|
|
2151
|
-
level: 0,
|
|
2152
|
-
last: null,
|
|
2153
|
-
evidence: []
|
|
2154
|
-
});
|
|
2155
|
-
return s;
|
|
2156
|
-
};
|
|
2157
|
-
for (const e of events) {
|
|
2158
|
-
const when = Date.parse(String(e.at));
|
|
2159
|
-
if (Number.isNaN(when) || when > now) continue;
|
|
2160
|
-
if (e.event === "attempt" && Array.isArray(e.tests)) {
|
|
2161
|
-
const a = e;
|
|
2162
|
-
for (const note of a.tests) {
|
|
2163
|
-
const s = at(note), by = a.moves?.[note]?.by;
|
|
2164
|
-
s.level = clamp(s.level + (typeof by === "number" && Number.isFinite(by) ? Math.max(-1, Math.min(1, by)) : 0));
|
|
2165
|
-
s.last = when;
|
|
2166
|
-
s.evidence.push(e.id);
|
|
2167
|
-
}
|
|
2168
|
-
} else if (e.event === "understood" && typeof e.note === "string") {
|
|
2169
|
-
const s = at(e.note), by = e.by;
|
|
2170
|
-
s.level = clamp(s.level + (typeof by === "number" && Number.isFinite(by) ? Math.max(-1, Math.min(1, by)) : 0));
|
|
2171
|
-
s.last = when;
|
|
2172
|
-
s.evidence.push(e.id);
|
|
2173
|
-
} else if (e.event === "moved" && typeof e.from === "string" && typeof e.to === "string") {
|
|
2174
|
-
const s = state.get(e.from);
|
|
2175
|
-
if (s) {
|
|
2176
|
-
state.delete(e.from);
|
|
2177
|
-
state.set(e.to, s);
|
|
2178
|
-
}
|
|
2179
|
-
} else if (e.event === "wind-back" && typeof e.note === "string") {
|
|
2180
|
-
const s = at(e.note);
|
|
2181
|
-
s.level = clamp(s.level - 1);
|
|
2182
|
-
s.evidence.push(e.id);
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
const out = /* @__PURE__ */ new Map();
|
|
2186
|
-
for (const [note, s] of state) {
|
|
2187
|
-
const whole = Math.floor(s.level), holds = whole ? INTERVALS[whole - 1] * DAY : 0;
|
|
2188
|
-
const age = s.last === null ? 0 : now - s.last;
|
|
2189
|
-
const due = whole > 0 && age > holds, lapsed = whole > 0 && age > 2 * holds;
|
|
2190
|
-
const squares = Math.max(0, whole - (lapsed ? 1 : 0));
|
|
2191
|
-
out.set(note, {
|
|
2192
|
-
level: s.level,
|
|
2193
|
-
squares,
|
|
2194
|
-
step: stepOf(squares),
|
|
2195
|
-
due,
|
|
2196
|
-
lapsed,
|
|
2197
|
-
last: s.last,
|
|
2198
|
-
evidence: s.evidence
|
|
2199
|
-
});
|
|
2200
|
-
}
|
|
2201
|
-
return out;
|
|
2202
|
-
}
|
|
2203
|
-
/** What an exercise shows: the lowest reading among the notes it tests (none tested yet reads as nothing). */
|
|
2204
|
-
function lowest(readings, tests) {
|
|
2205
|
-
let squares = tests.length ? 6 : 0, due = false;
|
|
2206
|
-
for (const t of tests) {
|
|
2207
|
-
const u = readings.get(t);
|
|
2208
|
-
squares = Math.min(squares, u?.squares ?? 0);
|
|
2209
|
-
due ||= !!u?.due;
|
|
2210
|
-
}
|
|
2211
|
-
return {
|
|
2212
|
-
squares,
|
|
2213
|
-
step: stepOf(squares),
|
|
2214
|
-
due
|
|
2215
|
-
};
|
|
2216
|
-
}
|
|
2217
|
-
//#endregion
|
|
2218
|
-
//#region packages/core/src/exercise.ts
|
|
2219
|
-
const WORKING = "## Working";
|
|
2220
|
-
const WORKING_RE = /^##[ \t]+Working[ \t]*$/m;
|
|
2221
|
-
var ExerciseError = class extends Error {};
|
|
2222
|
-
/** Read an exercise's file. A file that is not an exercise still reads: its whole body is the question. */
|
|
2223
|
-
function exercise(source) {
|
|
2224
|
-
let meta = {}, body = source;
|
|
2225
|
-
try {
|
|
2226
|
-
const [m, b] = splitFrontmatter(source);
|
|
2227
|
-
meta = m ?? {};
|
|
2228
|
-
body = b;
|
|
2229
|
-
} catch {}
|
|
2230
|
-
const start = source.length - body.length;
|
|
2231
|
-
const m = WORKING_RE.exec(body);
|
|
2232
|
-
const headEnd = m ? start + m.index + m[0].length : source.length;
|
|
2233
|
-
const from = m ? Math.min(source.length, headEnd + (source[headEnd] === "\n" ? 1 : 0)) : source.length;
|
|
2234
|
-
const next = text(meta.next ?? "").replace(/^\//, "").replace(/\.md$/, "");
|
|
2235
|
-
return {
|
|
2236
|
-
kind: text(meta.kind ?? "") || "written",
|
|
2237
|
-
title: text(meta.title ?? ""),
|
|
2238
|
-
tests: testsOf(meta),
|
|
2239
|
-
next: next || null,
|
|
2240
|
-
question: (m ? body.slice(0, m.index) : body).trim(),
|
|
2241
|
-
from,
|
|
2242
|
-
working: source.slice(from),
|
|
2243
|
-
headed: !!m
|
|
2244
|
-
};
|
|
2245
|
-
}
|
|
2246
|
-
/** The file of a new exercise. */
|
|
2247
|
-
function exerciseText(e) {
|
|
2248
|
-
const meta = {
|
|
2249
|
-
type: "Exercise",
|
|
2250
|
-
kind: e.kind ?? "written",
|
|
2251
|
-
title: e.title,
|
|
2252
|
-
tests: e.tests
|
|
2253
|
-
};
|
|
2254
|
-
if (e.next) meta.next = e.next;
|
|
2255
|
-
meta.generated = {
|
|
2256
|
-
by: e.by,
|
|
2257
|
-
at: e.at
|
|
2258
|
-
};
|
|
2259
|
-
return `---\n${dumpFrontmatter(meta)}---\n\n${e.question.trim()}\n\n${WORKING}\n\n`;
|
|
2260
|
-
}
|
|
2261
|
-
/** The same file with `next` set (or taken out, with null). */
|
|
2262
|
-
function withNext(source, next) {
|
|
2263
|
-
const [meta, body] = splitFrontmatter(source);
|
|
2264
|
-
const m = { ...meta ?? {} };
|
|
2265
|
-
if (next) m.next = next;
|
|
2266
|
-
else delete m.next;
|
|
2267
|
-
return `---\n${dumpFrontmatter(m)}---\n${body.startsWith("\n") ? "" : "\n"}${body}`;
|
|
2268
|
-
}
|
|
2269
|
-
const RESULTS = [
|
|
2270
|
-
"right",
|
|
2271
|
-
"partly",
|
|
2272
|
-
"wrong"
|
|
2273
|
-
];
|
|
2274
|
-
const MOVES = [
|
|
2275
|
-
-1,
|
|
2276
|
-
-.5,
|
|
2277
|
-
0,
|
|
2278
|
-
.5,
|
|
2279
|
-
1
|
|
2280
|
-
];
|
|
2281
|
-
const MAX_WHY = 300;
|
|
2282
|
-
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2283
|
-
/** Where a quote is in `source` between `from` and `to`, ignoring differences in runs of white space. Each later call
|
|
2284
|
-
* with the same quote finds the next place, through `after`. */
|
|
2285
|
-
function findQuote(source, quote, from, to, after) {
|
|
2286
|
-
const words = quote.trim().split(/\s+/).filter(Boolean);
|
|
2287
|
-
if (!words.length) return null;
|
|
2288
|
-
const re = new RegExp(words.map(escape).join("\\s+"), "g");
|
|
2289
|
-
re.lastIndex = Math.max(from, after);
|
|
2290
|
-
const m = re.exec(source);
|
|
2291
|
-
if (!m || m.index + m[0].length > to) return null;
|
|
2292
|
-
return {
|
|
2293
|
-
from: m.index,
|
|
2294
|
-
to: m.index + m[0].length
|
|
2295
|
-
};
|
|
2296
|
-
}
|
|
2297
|
-
/** Read Iso's reply to a marking, against the exercise's file as it was sent. The reply is one JSON object (it may
|
|
2298
|
-
* stand in a code fence, or have words around it):
|
|
2299
|
-
*
|
|
2300
|
-
* {"result": "partly",
|
|
2301
|
-
* "marks": [{"quote": "exact words from the working", "mark": "right"}, …],
|
|
2302
|
-
* "moves": {"<note id>": {"by": 0.5, "why": "one line"}, …}}
|
|
2303
|
-
*
|
|
2304
|
-
* Every note tested must have a move, and no other. A quote not found in the working is dropped and counted. */
|
|
2305
|
-
function markingReply(reply, source, tests) {
|
|
2306
|
-
const open = reply.indexOf("{"), close = reply.lastIndexOf("}");
|
|
2307
|
-
if (open < 0 || close < open) throw new ExerciseError("the reply has no marking in it");
|
|
2308
|
-
let raw;
|
|
2309
|
-
try {
|
|
2310
|
-
raw = JSON.parse(reply.slice(open, close + 1));
|
|
2311
|
-
} catch {
|
|
2312
|
-
throw new ExerciseError("the marking is not valid JSON");
|
|
2313
|
-
}
|
|
2314
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new ExerciseError("the marking is not an object");
|
|
2315
|
-
const r = raw;
|
|
2316
|
-
if (!RESULTS.includes(r.result)) throw new ExerciseError(`the result is one of ${RESULTS.join(", ")}`);
|
|
2317
|
-
const ex = exercise(source);
|
|
2318
|
-
const marks = [], last = /* @__PURE__ */ new Map();
|
|
2319
|
-
let dropped = 0;
|
|
2320
|
-
for (const m of Array.isArray(r.marks) ? r.marks : []) {
|
|
2321
|
-
const q = typeof m === "object" && m ? m : {};
|
|
2322
|
-
if (typeof q.quote !== "string" || q.mark !== "right" && q.mark !== "wrong") throw new ExerciseError("each mark is a quote and right or wrong");
|
|
2323
|
-
const key = q.quote.trim().replace(/\s+/g, " ");
|
|
2324
|
-
const at = findQuote(source, q.quote, ex.from, source.length, last.get(key) ?? 0) ?? findQuote(source, q.quote, ex.from, source.length, 0);
|
|
2325
|
-
if (!at) {
|
|
2326
|
-
dropped++;
|
|
2327
|
-
continue;
|
|
2328
|
-
}
|
|
2329
|
-
last.set(key, at.to);
|
|
2330
|
-
marks.push({
|
|
2331
|
-
passage: passageOf(source, at.from, at.to),
|
|
2332
|
-
mark: q.mark
|
|
2333
|
-
});
|
|
2334
|
-
}
|
|
2335
|
-
const moves = {};
|
|
2336
|
-
const given = typeof r.moves === "object" && r.moves && !Array.isArray(r.moves) ? r.moves : {};
|
|
2337
|
-
for (const note of Object.keys(given)) if (!tests.includes(note)) throw new ExerciseError(`a move for a note the exercise does not test: ${note}`);
|
|
2338
|
-
for (const note of tests) {
|
|
2339
|
-
const g = given[note];
|
|
2340
|
-
const v = typeof g === "object" && g ? g : null;
|
|
2341
|
-
if (!v || typeof v.by !== "number" || !MOVES.includes(v.by)) throw new ExerciseError(`a move for ${note} of −1, −½, 0, +½ or +1`);
|
|
2342
|
-
moves[note] = {
|
|
2343
|
-
by: v.by,
|
|
2344
|
-
why: text(v.why ?? "").trim().slice(0, 300)
|
|
2345
|
-
};
|
|
2346
|
-
}
|
|
2347
|
-
return {
|
|
2348
|
-
result: r.result,
|
|
2349
|
-
marks,
|
|
2350
|
-
dropped,
|
|
2351
|
-
moves
|
|
2352
|
-
};
|
|
2353
|
-
}
|
|
2354
|
-
/** Where each mark falls in the file as it is now. A passage edited away is left out. */
|
|
2355
|
-
function placeMarks(source, marks) {
|
|
2356
|
-
const out = [];
|
|
2357
|
-
for (const m of marks) {
|
|
2358
|
-
const at = findPassage(source, m.passage);
|
|
2359
|
-
if (at) out.push({
|
|
2360
|
-
...at,
|
|
2361
|
-
mark: m.mark
|
|
2362
|
-
});
|
|
2363
|
-
}
|
|
2364
|
-
return out.sort((a, b) => a.from - b.from);
|
|
2365
|
-
}
|
|
2366
|
-
/** Every exercise, in the order the study page lists them: due first, then never tried, then the fewest squares,
|
|
2367
|
-
* then the longest since tried. `exercises` are the study base's exercise ids with what they test; `events` the
|
|
2368
|
-
* record in time order; `readings` what `understanding` made of it. */
|
|
2369
|
-
function studyList(exercises, events, readings) {
|
|
2370
|
-
const lastOf = /* @__PURE__ */ new Map();
|
|
2371
|
-
for (const e of events) {
|
|
2372
|
-
if (e.event !== "attempt" || typeof e.exercise !== "string") continue;
|
|
2373
|
-
const at = Date.parse(String(e.at));
|
|
2374
|
-
if (!Number.isNaN(at)) lastOf.set(e.exercise, {
|
|
2375
|
-
result: e.result,
|
|
2376
|
-
at
|
|
2377
|
-
});
|
|
2378
|
-
}
|
|
2379
|
-
const out = exercises.map(({ id, tests }) => {
|
|
2380
|
-
const l = lastOf.get(id), low = lowest(readings, tests);
|
|
2381
|
-
return {
|
|
2382
|
-
id,
|
|
2383
|
-
tests: [...tests],
|
|
2384
|
-
result: l?.result ?? null,
|
|
2385
|
-
last: l?.at ?? null,
|
|
2386
|
-
squares: low.squares,
|
|
2387
|
-
due: low.due
|
|
2388
|
-
};
|
|
2389
|
-
});
|
|
2390
|
-
const rank = (x) => x.due ? 0 : x.last === null ? 1 : 2;
|
|
2391
|
-
return out.sort((a, b) => rank(a) - rank(b) || a.squares - b.squares || (a.last ?? 0) - (b.last ?? 0) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
2392
|
-
}
|
|
2393
|
-
//#endregion
|
|
2394
2177
|
//#region packages/core/src/agents.ts
|
|
2395
2178
|
const NAMES = {
|
|
2396
2179
|
opencode: "OpenCode",
|
|
@@ -2413,13 +2196,11 @@ const WHAT = {
|
|
|
2413
2196
|
discarded: "removed",
|
|
2414
2197
|
restored: "restored an earlier version",
|
|
2415
2198
|
moved: "moved the note",
|
|
2416
|
-
"changed elsewhere": "changed elsewhere"
|
|
2417
|
-
marked: "marked"
|
|
2199
|
+
"changed elsewhere": "changed elsewhere"
|
|
2418
2200
|
};
|
|
2419
|
-
/** Who did what: "you edited", "Iso · “give the Bethe lattice form” · kept"
|
|
2201
|
+
/** Who did what: "you edited", "Iso · “give the Bethe lattice form” · kept". */
|
|
2420
2202
|
function describe(m) {
|
|
2421
2203
|
const who = m.who === "you" ? "you" : m.who === "external" ? "outside isorite" : m.who.startsWith("agent:") ? agentName(m.who) : "Iso";
|
|
2422
|
-
if (m.what === "marked") return `Iso marked: ${m.result ?? "done"}`;
|
|
2423
2204
|
if (m.asked !== void 0) return `${who} · “${m.asked.length > 80 ? m.asked.slice(0, 80) + "…" : m.asked}” · ${m.what === "offered" ? "not kept yet" : WHAT[m.what]}`;
|
|
2424
2205
|
if (m.who === "external") return m.what === "found" ? "found as it was, outside isorite" : "changed outside isorite";
|
|
2425
2206
|
return `${who} ${WHAT[m.what]}`;
|
|
@@ -2433,4 +2214,4 @@ function ago(at, now = Date.now()) {
|
|
|
2433
2214
|
/** A moment is named by its last entry: the one the note is shown at. */
|
|
2434
2215
|
const momentKey = (m) => m.ids.at(-1);
|
|
2435
2216
|
//#endregion
|
|
2436
|
-
export { Bundle, CONFIG_FILE, Concept, DEVICE_RE, DiffError, Directory,
|
|
2217
|
+
export { ABOUT, Bundle, CONFIG_FILE, Concept, DEVICE_RE, DONE, DiffError, Directory, FrontmatterError, GITATTRIBUTES, ID_RE, KINDS, LearnerError, MAX_EVENT_BYTES, OKF_VERSION, OWN_DIR, OWN_FORMAT, RATINGS, SESSION_GAP_MS, SETTINGS_FILE, SNAPSHOT_EVERY, STRENGTH, STUDY_DIR, STUDY_FORMAT, SearchIndex, StudyError, agentName, ago, anchorOf, applyHunks, authors, blocks, change, citeKind, cleanEvent, cmp, cmpTuple, codeTarget, contentHash, describe, diffLines, doneOf, dumpFrontmatter, editFrontmatter, embedFor, embedRefs, findAnchor, findPassage, floatRepr, frontmatterText, hashText, headings, idsOf, impliedLinks, insertAt, iso, journalLine, journalPath, kindOf, legacyId, linkRefs, linkTexts, locate, mapModel, mergeRecords, momentKey, moments, newId, noteIds, outwardLinks, pagerank, parseSettings, parseStudyConfig, parseYaml, passageOf, pending, pyRepr, rating, reach, readJournal, readRecord, reconcile, recordPath, replay, round3, section, sessionMoments, settingsText, sha256, slugify, snippet, spliceText, splitFrontmatter, splitSource, stamp, strengthEdges, strip, stronglyConnected, text, textAt, toTime, tokenize, touched, wordAuthors, written };
|