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.
Files changed (71) hide show
  1. package/README.md +6 -3
  2. package/dist/core.js +207 -426
  3. package/dist/embed-worker.js +9 -4
  4. package/dist/main.js +7917 -6864
  5. package/docs/setup-for-agents.md +166 -0
  6. package/docs/setup.md +133 -0
  7. package/docs/terminal.md +119 -0
  8. package/package.json +2 -1
  9. package/plugin/isorite.ts +3 -3
  10. package/plugin/notepage.tsx +1 -9
  11. package/plugin/tui.tsx +9 -10
  12. package/web/_app/immutable/assets/0.IOi79DXH.css +1 -0
  13. package/web/_app/immutable/assets/{4.PVZR2_N5.css → 4.CUdclsPi.css} +1 -1
  14. package/web/_app/immutable/assets/5.TsqJFEFF.css +1 -0
  15. package/web/_app/immutable/assets/{NoteEditor.ChlpmyHO.css → NoteEditor.BCjOff8L.css} +1 -1
  16. package/web/_app/immutable/chunks/{B_nIecMW.js → 8IM6BNk6.js} +36 -37
  17. package/web/_app/immutable/chunks/{BEVz4Dgg.js → BHPhFydX.js} +1 -1
  18. package/web/_app/immutable/chunks/B_AYGp9o.js +1 -0
  19. package/web/_app/immutable/chunks/Bze-M_kr.js +3 -0
  20. package/web/_app/immutable/chunks/CFA70_0c.js +20 -0
  21. package/web/_app/immutable/chunks/CGgq5NWd.js +1 -0
  22. package/web/_app/immutable/chunks/CHlFgDNB.js +1 -0
  23. package/web/_app/immutable/chunks/CMp5T_-1.js +1 -0
  24. package/web/_app/immutable/chunks/D89YN-Dr.js +1 -0
  25. package/web/_app/immutable/chunks/DF3lgcLW.js +1 -0
  26. package/web/_app/immutable/chunks/DGVcYHaJ.js +43 -0
  27. package/web/_app/immutable/chunks/DHMLhlug.js +1 -0
  28. package/web/_app/immutable/chunks/DmgpLvxp.js +1 -0
  29. package/web/_app/immutable/chunks/DrsVvzvg.js +1 -0
  30. package/web/_app/immutable/entry/app.hlk6OrE8.js +2 -0
  31. package/web/_app/immutable/entry/start.BE1F0HZV.js +1 -0
  32. package/web/_app/immutable/nodes/0.DMGdYhkH.js +15 -0
  33. package/web/_app/immutable/nodes/1.Dn_0fgt7.js +1 -0
  34. package/web/_app/immutable/nodes/2.Y1Dd5PBI.js +1 -0
  35. package/web/_app/immutable/nodes/3.BlxuRbLf.js +1 -0
  36. package/web/_app/immutable/nodes/4.Bt_H0MbN.js +2 -0
  37. package/web/_app/immutable/nodes/5.BJ-EVWbz.js +5 -0
  38. package/web/_app/immutable/nodes/6.DDIiJnwi.js +1 -0
  39. package/web/_app/immutable/nodes/7.DmPPLDvb.js +1 -0
  40. package/web/_app/immutable/nodes/{9.NF5w8YC2.js → 8.CwMTBog4.js} +2 -2
  41. package/web/_app/immutable/workers/worker-CWkpeSVG.js +1 -0
  42. package/web/_app/version.json +1 -1
  43. package/web/index.html +25 -19
  44. package/web/_app/immutable/assets/0.s5xqevrq.css +0 -1
  45. package/web/_app/immutable/assets/5.DCxNIgyW.css +0 -1
  46. package/web/_app/immutable/assets/8.C0NYkyNY.css +0 -1
  47. package/web/_app/immutable/chunks/B0NIb88Q.js +0 -1
  48. package/web/_app/immutable/chunks/B7ZjnqOt.js +0 -20
  49. package/web/_app/immutable/chunks/BEk5idXo.js +0 -1
  50. package/web/_app/immutable/chunks/C8UdwlAg.js +0 -3
  51. package/web/_app/immutable/chunks/CcQ3P3p1.js +0 -13
  52. package/web/_app/immutable/chunks/CcRs965g.js +0 -1
  53. package/web/_app/immutable/chunks/Cw2GjAJR.js +0 -1
  54. package/web/_app/immutable/chunks/DjBHskLm.js +0 -1
  55. package/web/_app/immutable/chunks/Dm229u8R.js +0 -1
  56. package/web/_app/immutable/chunks/UM_L7-f7.js +0 -1
  57. package/web/_app/immutable/chunks/b-AOhhy1.js +0 -1
  58. package/web/_app/immutable/chunks/k55859zq.js +0 -1
  59. package/web/_app/immutable/entry/app.B3QXxjjH.js +0 -2
  60. package/web/_app/immutable/entry/start.BauJm-6k.js +0 -1
  61. package/web/_app/immutable/nodes/0.DP4c84Wn.js +0 -14
  62. package/web/_app/immutable/nodes/1.BcwcVDJZ.js +0 -1
  63. package/web/_app/immutable/nodes/2.DgGnQZ7P.js +0 -1
  64. package/web/_app/immutable/nodes/3.BudzO7We.js +0 -1
  65. package/web/_app/immutable/nodes/4.DmswyrSt.js +0 -2
  66. package/web/_app/immutable/nodes/5.DjXsfjL5.js +0 -5
  67. package/web/_app/immutable/nodes/6.uBh-sdQx.js +0 -1
  68. package/web/_app/immutable/nodes/7.BYBNXM1A.js +0 -1
  69. package/web/_app/immutable/nodes/8.Tbz9Rh77.js +0 -1
  70. package/web/_app/immutable/workers/worker-rKDNrEKQ.js +0 -1
  71. /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$1(c.title)}](${basename(c.path)})${desc}`);
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$1 = (s) => s.replaceAll("[", "\\[").replaceAll("]", "\\]");
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" || typeof entry.kind !== "string" || byId.has(entry.id)) continue;
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 === "marks") out.push({
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
- function dumpStudyConfig(config) {
1885
- return "# The knowledge base this study base studies. Written by `isorite study init`.\n" + stringify$1(config) + "\n";
1886
- }
1887
- const SAFE = /^[^\0]+$/;
1888
- function checkId(id) {
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: "concept",
1911
- id: "c:" + id,
1912
- ref: id,
1913
- weight: 1,
1914
- rank: order.get(id)?.depth ?? 0
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. A study note requires what it tests. */
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(([from, , s]) => s >= STRENGTH.requires && !isStudyId(from)) ? STRENGTH.requires : STRENGTH.uses;
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 && !isStudyId(from)) {
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", "Iso marked: partly". */
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, ExerciseError, FrontmatterError, GITATTRIBUTES, ID_RE, INTERVALS, KINDS, LEVELS, LearnerError, MAX_EVENT_BYTES, MAX_WHY, OKF_VERSION, RATINGS, SESSION_GAP_MS, SNAPSHOT_EVERY, STRENGTH, STUDY_DIR, STUDY_FORMAT, STUDY_PREFIX, SearchIndex, StudyError, WORKING, agentName, ago, anchorOf, applyHunks, authors, blocks, change, citeKind, cleanEvent, cmp, cmpTuple, contentHash, describe, diffLines, dumpFrontmatter, dumpStudyConfig, editFrontmatter, exercise, exerciseText, findAnchor, findPassage, floatRepr, frontmatterText, hashText, headings, impliedLinks, insertAt, isStudyId, iso, journalLine, journalPath, legacyId, linkRefs, locate, lowest, mapModel, markingReply, mergeRecords, momentKey, moments, newId, noteIds, pagerank, parseStudyConfig, parseYaml, passageOf, pending, placeMarks, pyRepr, rating, reach, readJournal, readRecord, reconcile, recordPath, replay, round3, section, sessionMoments, sha256, slugify, snippet, spliceText, splitFrontmatter, splitSource, sprout, stamp, stepOf, strengthEdges, strip, stronglyConnected, studyId, studyList, testsOf, text, textAt, toTime, tokenize, touched, understanding, withNext, withoutStudy, wordAuthors, written };
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 };