projectkeeper 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/LICENSE +21 -0
  3. package/README.md +283 -2
  4. package/README.zh-CN.md +285 -0
  5. package/THIRD_PARTY_NOTICES.md +53 -0
  6. package/bin/pk.mjs +6 -0
  7. package/bin/postinstall.mjs +5 -0
  8. package/bin/strip-types.mjs +28 -0
  9. package/package.json +68 -4
  10. package/patches/README.md +111 -0
  11. package/patches/pi-ai-0.85.0.ts +122 -0
  12. package/patches/pi-ai-0.87.1.ts +122 -0
  13. package/scripts/apply-pi-streaming-patch.ts +86 -0
  14. package/skills/clerk/coverage/SKILL.md +35 -0
  15. package/skills/clerk/cross-check/SKILL.md +118 -0
  16. package/skills/clerk/deepen-dispatch/SKILL.md +69 -0
  17. package/skills/clerk/lane-follow-up/SKILL.md +94 -0
  18. package/skills/clerk/lane-plan/SKILL.md +107 -0
  19. package/skills/clerk/lane-slot/SKILL.md +113 -0
  20. package/skills/clerk/lane-topic/SKILL.md +116 -0
  21. package/skills/clerk/orientation/SKILL.md +160 -0
  22. package/skills/clerk/reconcile/SKILL.md +77 -0
  23. package/skills/clerk/skeleton-dispatch/SKILL.md +43 -0
  24. package/skills/clerk/spot-check/SKILL.md +52 -0
  25. package/skills/clerk/synthesis/SKILL.md +65 -0
  26. package/skills/table-follow-up/SKILL.md +42 -0
  27. package/src/cli.ts +319 -0
  28. package/src/codemap/README.md +13 -0
  29. package/src/codemap/engines.ts +102 -0
  30. package/src/codemap/facts.ts +504 -0
  31. package/src/context/agent-entry.ts +232 -0
  32. package/src/context/ask.ts +97 -0
  33. package/src/context/assemble.ts +1058 -0
  34. package/src/context/code-source.ts +103 -0
  35. package/src/context/k-briefs.ts +114 -0
  36. package/src/context/lineage.ts +100 -0
  37. package/src/context/object-brief.ts +300 -0
  38. package/src/context/owner-words.ts +191 -0
  39. package/src/context/pack-fixture.ts +342 -0
  40. package/src/context/project-rules.ts +160 -0
  41. package/src/intake/intake.ts +392 -0
  42. package/src/intake/material-rules.ts +166 -0
  43. package/src/intake/removal.ts +316 -0
  44. package/src/intake/skipped.ts +72 -0
  45. package/src/keeper/adjustment.ts +565 -0
  46. package/src/keeper/bounds/boundary.ts +509 -0
  47. package/src/keeper/bounds/command.ts +1665 -0
  48. package/src/keeper/bounds/paths.ts +164 -0
  49. package/src/keeper/bounds/reads.ts +718 -0
  50. package/src/keeper/bounds/shell-write-guard.ts +521 -0
  51. package/src/keeper/capabilities.ts +136 -0
  52. package/src/keeper/clerk-steps.ts +151 -0
  53. package/src/keeper/clerk-tools.ts +1424 -0
  54. package/src/keeper/conversation.ts +253 -0
  55. package/src/keeper/evidence.ts +662 -0
  56. package/src/keeper/fake-provider.ts +132 -0
  57. package/src/keeper/held.ts +15 -0
  58. package/src/keeper/history-tools.ts +163 -0
  59. package/src/keeper/jobs/answering.ts +53 -0
  60. package/src/keeper/key-check.ts +105 -0
  61. package/src/keeper/ledger-tools.ts +295 -0
  62. package/src/keeper/link-check.ts +185 -0
  63. package/src/keeper/merge.ts +260 -0
  64. package/src/keeper/note-origin.ts +56 -0
  65. package/src/keeper/numbers-check.ts +102 -0
  66. package/src/keeper/open-in-pi.ts +57 -0
  67. package/src/keeper/organize/absence.ts +266 -0
  68. package/src/keeper/organize/carried-on.ts +214 -0
  69. package/src/keeper/organize/clerk-coverage.ts +293 -0
  70. package/src/keeper/organize/clerk-prompts.ts +132 -0
  71. package/src/keeper/organize/clerk.ts +1395 -0
  72. package/src/keeper/organize/coverage-gate.ts +19 -0
  73. package/src/keeper/organize/coverage-tools.ts +765 -0
  74. package/src/keeper/organize/entries.ts +153 -0
  75. package/src/keeper/organize/entry-gate.ts +289 -0
  76. package/src/keeper/organize/fill-tools.ts +718 -0
  77. package/src/keeper/organize/follow-up.ts +280 -0
  78. package/src/keeper/organize/generation-check.ts +504 -0
  79. package/src/keeper/organize/graph.ts +230 -0
  80. package/src/keeper/organize/lane-tools.ts +289 -0
  81. package/src/keeper/organize/looked-tools.ts +54 -0
  82. package/src/keeper/organize/materials.ts +243 -0
  83. package/src/keeper/organize/owner-lines.ts +493 -0
  84. package/src/keeper/organize/place-tools.ts +216 -0
  85. package/src/keeper/organize/placement-inference.ts +1045 -0
  86. package/src/keeper/organize/placement-time.ts +320 -0
  87. package/src/keeper/organize/placing.ts +821 -0
  88. package/src/keeper/organize/positioning.ts +33 -0
  89. package/src/keeper/organize/readiness.ts +47 -0
  90. package/src/keeper/organize/reading.ts +401 -0
  91. package/src/keeper/organize/round-blocks.ts +65 -0
  92. package/src/keeper/organize/round-news.ts +164 -0
  93. package/src/keeper/organize/round-open.ts +586 -0
  94. package/src/keeper/organize/schedule-state.ts +39 -0
  95. package/src/keeper/organize/schedule.ts +137 -0
  96. package/src/keeper/organize/service.ts +409 -0
  97. package/src/keeper/organize/skills.ts +116 -0
  98. package/src/keeper/organize/slots.ts +85 -0
  99. package/src/keeper/organize/stage-gate.ts +175 -0
  100. package/src/keeper/organize/stage-tools.ts +471 -0
  101. package/src/keeper/organize/standing-notes.ts +141 -0
  102. package/src/keeper/organize/table-follow-up.ts +330 -0
  103. package/src/keeper/organize/takeover-clerk.ts +832 -0
  104. package/src/keeper/organize/takeover-state.ts +103 -0
  105. package/src/keeper/organize/takeover.ts +216 -0
  106. package/src/keeper/organize/update-pending.ts +535 -0
  107. package/src/keeper/organize/workbench-placement.ts +92 -0
  108. package/src/keeper/own-keys.ts +114 -0
  109. package/src/keeper/owner-text.ts +165 -0
  110. package/src/keeper/project-folder.ts +334 -0
  111. package/src/keeper/prompts.ts +101 -0
  112. package/src/keeper/references.ts +177 -0
  113. package/src/keeper/relook/compose.ts +120 -0
  114. package/src/keeper/relook/prompts.ts +36 -0
  115. package/src/keeper/roles.ts +259 -0
  116. package/src/keeper/round-tools.ts +227 -0
  117. package/src/keeper/route.ts +148 -0
  118. package/src/keeper/rules.ts +97 -0
  119. package/src/keeper/runtime.ts +1858 -0
  120. package/src/keeper/scope-tools.ts +194 -0
  121. package/src/keeper/sendback-text.ts +83 -0
  122. package/src/keeper/step-timing.ts +207 -0
  123. package/src/keeper/testing/clerk-script.ts +212 -0
  124. package/src/keeper/testing/streaming-arguments.ts +57 -0
  125. package/src/keeper/tools.ts +2340 -0
  126. package/src/keeper/utterance-tools.ts +64 -0
  127. package/src/keeper/verbatim.ts +397 -0
  128. package/src/ledger/adapters.ts +186 -0
  129. package/src/ledger/arrangements.ts +257 -0
  130. package/src/ledger/blame.ts +58 -0
  131. package/src/ledger/code-engine-child.ts +69 -0
  132. package/src/ledger/code-engine.ts +548 -0
  133. package/src/ledger/code.ts +607 -0
  134. package/src/ledger/docs.ts +294 -0
  135. package/src/ledger/git-read.ts +321 -0
  136. package/src/ledger/index.ts +1850 -0
  137. package/src/ledger/item-range.ts +16 -0
  138. package/src/ledger/lines.ts +328 -0
  139. package/src/ledger/numbering.ts +269 -0
  140. package/src/ledger/ranges.ts +74 -0
  141. package/src/ledger/rebuild-worker.ts +15 -0
  142. package/src/ledger/rebuild.ts +387 -0
  143. package/src/ledger/repo-scan.ts +181 -0
  144. package/src/ledger/schema.ts +450 -0
  145. package/src/ledger/sessions.ts +134 -0
  146. package/src/ledger/text-scan.ts +466 -0
  147. package/src/ledger/time.ts +185 -0
  148. package/src/ledger/ts-symbols.ts +180 -0
  149. package/src/ledger/views.ts +377 -0
  150. package/src/mcp/server.ts +127 -0
  151. package/src/mcp/tools.ts +187 -0
  152. package/src/model/ids.ts +23 -0
  153. package/src/model/k-types.ts +912 -0
  154. package/src/model/time.ts +85 -0
  155. package/src/model/types.ts +1382 -0
  156. package/src/model/views-k.ts +707 -0
  157. package/src/model/vocab.ts +269 -0
  158. package/src/process/analysis.ts +79 -0
  159. package/src/process/breakpoint-candidates.ts +383 -0
  160. package/src/process/breakpoints.ts +746 -0
  161. package/src/process/claim-check.ts +295 -0
  162. package/src/process/delivery-links.ts +52 -0
  163. package/src/process/facts.ts +487 -0
  164. package/src/process/generations.ts +138 -0
  165. package/src/process/index.ts +243 -0
  166. package/src/process/integration.ts +128 -0
  167. package/src/process/placement.ts +107 -0
  168. package/src/process/plan.ts +276 -0
  169. package/src/process/sendbacks.ts +244 -0
  170. package/src/process/text.ts +185 -0
  171. package/src/process/units.ts +543 -0
  172. package/src/process/unnumbered.ts +431 -0
  173. package/src/process/work.ts +884 -0
  174. package/src/scope/candidates.ts +86 -0
  175. package/src/scope/decisions.ts +229 -0
  176. package/src/scope/discover.ts +741 -0
  177. package/src/scope/ignore.ts +126 -0
  178. package/src/scope/ignored-place.ts +93 -0
  179. package/src/scope/skip.ts +161 -0
  180. package/src/scope/toolchain.ts +180 -0
  181. package/src/scope/worktree.ts +187 -0
  182. package/src/server/api.ts +549 -0
  183. package/src/server/app.ts +595 -0
  184. package/src/server/drafts-api.ts +170 -0
  185. package/src/server/folders-api.ts +243 -0
  186. package/src/server/graph-view.ts +639 -0
  187. package/src/server/http.ts +224 -0
  188. package/src/server/k-agent-api.ts +56 -0
  189. package/src/server/k-rounds-d99.ts +91 -0
  190. package/src/server/k-views.ts +405 -0
  191. package/src/server/keeper-page.ts +446 -0
  192. package/src/server/keys-api.ts +239 -0
  193. package/src/server/project-folder-api.ts +104 -0
  194. package/src/server/vendor.ts +14 -0
  195. package/src/server/workbench-content.ts +561 -0
  196. package/src/sources/anchor.ts +129 -0
  197. package/src/sources/files.ts +267 -0
  198. package/src/sources/gitobs.ts +41 -0
  199. package/src/sources/history.ts +231 -0
  200. package/src/sources/scan-worker.ts +10 -0
  201. package/src/sources/sessions/locate.ts +292 -0
  202. package/src/sources/sessions/read.ts +346 -0
  203. package/src/sources/sessions/scope.ts +42 -0
  204. package/src/sources/sessions/utterances.ts +223 -0
  205. package/src/sources/watch.ts +215 -0
  206. package/src/store/json-file.ts +50 -0
  207. package/src/store/paths.ts +39 -0
  208. package/src/store/project-store.ts +295 -0
  209. package/src/store/versions.ts +172 -0
  210. package/src/store/workspace.ts +216 -0
  211. package/src/table/check.ts +333 -0
  212. package/src/table/follow-up-tools.ts +30 -0
  213. package/src/table/follow-up.ts +344 -0
  214. package/src/table/import.ts +610 -0
  215. package/src/table/project-home.ts +61 -0
  216. package/src/table/sessions.ts +168 -0
  217. package/src/table/takeover.ts +483 -0
  218. package/src/util/git-check.ts +29 -0
  219. package/src/util/git.ts +411 -0
  220. package/src/util/own-ts.ts +15 -0
  221. package/src/util/paths.ts +225 -0
  222. package/template/FILLING-IN.md +148 -0
  223. package/template/examples/papertrail.json +747 -0
  224. package/template/projectkeeper.schema.json +1001 -0
  225. package/ui/app.js +1135 -0
  226. package/ui/failures.js +82 -0
  227. package/ui/flyout.js +120 -0
  228. package/ui/folder-chooser.js +264 -0
  229. package/ui/glossary.js +156 -0
  230. package/ui/graph-fit.js +157 -0
  231. package/ui/graph-material.js +61 -0
  232. package/ui/graph-palette.js +109 -0
  233. package/ui/graph-tools.js +62 -0
  234. package/ui/graph.js +1712 -0
  235. package/ui/index.html +31 -0
  236. package/ui/k/code-view.js +230 -0
  237. package/ui/k/draft-view.js +93 -0
  238. package/ui/k/k-views.css +224 -0
  239. package/ui/k/round-tree.js +450 -0
  240. package/ui/k/scope-k.js +159 -0
  241. package/ui/k-data.js +118 -0
  242. package/ui/k-fold.js +106 -0
  243. package/ui/k-process.css +302 -0
  244. package/ui/k-process.js +934 -0
  245. package/ui/keeper-activity.js +175 -0
  246. package/ui/keeper-view.js +801 -0
  247. package/ui/keeper.css +101 -0
  248. package/ui/key-order.js +62 -0
  249. package/ui/list-fold.js +67 -0
  250. package/ui/markdown.d.ts +21 -0
  251. package/ui/markdown.js +472 -0
  252. package/ui/placement.d.ts +92 -0
  253. package/ui/placement.js +445 -0
  254. package/ui/popover-place.d.ts +16 -0
  255. package/ui/popover-place.js +89 -0
  256. package/ui/popover.js +198 -0
  257. package/ui/project-folder.js +37 -0
  258. package/ui/shell-state.js +40 -0
  259. package/ui/strip-rows.js +171 -0
  260. package/ui/styles.css +492 -0
  261. package/ui/theme-pick.js +36 -0
  262. package/ui/theme.js +74 -0
  263. package/ui/themes/LICENSE.md +13 -0
  264. package/ui/themes/README.md +44 -0
  265. package/ui/themes/TOKENS.md +291 -0
  266. package/ui/themes/a1/board.css +1557 -0
  267. package/ui/themes/a1/pk.css +392 -0
  268. package/ui/themes/b1/board.css +1163 -0
  269. package/ui/themes/b1/pk.css +218 -0
  270. package/ui/themes/d2/board.css +518 -0
  271. package/ui/themes/d2/pk.css +243 -0
  272. package/ui/themes/guard.css +73 -0
  273. package/ui/themes/i1/board.css +776 -0
  274. package/ui/themes/i1/pk.css +223 -0
  275. package/ui/themes/manifest.json +13 -0
  276. package/ui/views.js +2796 -0
package/ui/markdown.js ADDED
@@ -0,0 +1,472 @@
1
+ // Markdown in the Keeper's answers (Spec §6.8; CKC-10 AC-21). Bold, italics, strike-through, inline code, code blocks,
2
+ // headings, lists (bulleted, numbered, nested), quotes, rules, tables and links are rendered, so `**` and backticks do
3
+ // not show; what is not recognised is shown as written, never swallowed. `[src_…]` citations become chips.
4
+ //
5
+ // Two layers: parseMarkdown / parseInline turn text into blocks and inline tokens (pure, tested in
6
+ // src/ui/markdown.test.ts); fillMarkdown builds DOM from them. The parser follows WorkflowKeeper's renderer/markdown.js
7
+ // (the same owner's code), with this product's differences: links open in the browser, citations are tokens, and the
8
+ // input is bounded against hostile text.
9
+ //
10
+ // An answer is untrusted text — a model wrote it, possibly from any file it read:
11
+ // - HTML is never parsed: <b>, <img>, <script> are shown as the characters they are. Only createElement,
12
+ // createTextNode and textContent are used; there is no injection surface.
13
+ // - A link is a link only when its address is plain http(s) (safeHref). It opens in a new window with
14
+ // rel="noopener noreferrer". Any other scheme, and anything relative, is shown as its words and goes nowhere.
15
+ // - Images are not loaded: fetching one would send a request on the answer's behalf, and the address can carry
16
+ // whatever the answer read. `![words](address)` is shown as a link to the address.
17
+ // - Nesting depth and the distance searched for a closing mark are capped, so no input overflows the stack or
18
+ // takes quadratic time; past a cap the text is shown as written.
19
+ // While an answer streams it is not finished: an open code fence runs to the end, and an open `**` is shown as it is
20
+ // until its other half arrives.
21
+
22
+ const FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
23
+ const HR = /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/;
24
+ const QUOTE = /^ {0,3}>[ \t]?(.*)$/;
25
+ const ITEM = /^( *)([-*+]|\d{1,9}[.)])([ \t]+)(.*)$/;
26
+ const ESCAPABLE = '\\`*_{}[]()#+-.!|~<>';
27
+ // Where a bare address ends: white space, quotes, angle brackets, a backtick, CJK text and full-width punctuation
28
+ // (in a Chinese sentence the address is often followed directly by words).
29
+ const BARE_URL = /^https?:\/\/[^\s<>"'`⺀-鿿 -〿＀-￯]+/i;
30
+ const URL_TAIL = /[.,;:!?\]}'"]+$/;
31
+ // A citation of a source. Generated ids are `src_` and 16 hex digits and are taken bare or bracketed; in brackets any
32
+ // source id is taken (a project's assets may hold sources with other ids), and `[words][src_…]` cites the words before it.
33
+ const CITE = /^\[?(src_[0-9a-f]{16})(?![0-9A-Za-z_])\]?/;
34
+ const CITE_BRACKETED = /^\[(src_[A-Za-z0-9_-]{1,64})\]/;
35
+ const CITE_LABELLED = /^\[([^[\]\n]{1,300})\]\[(src_[A-Za-z0-9_-]{1,64})\]/;
36
+ const WORD = /[\p{L}\p{N}_]/u;
37
+ const MAX_DEPTH = 16; // blocks inside blocks (quotes, lists), and marks inside marks
38
+ const MAX_SPAN = 5000; // how far a closing mark is looked for
39
+
40
+ const detab = (line) => line.replace(/^[ \t]+/, (ws) => ws.replace(/\t/g, ' '));
41
+ const indentOf = (line) => /^ */.exec(line)[0].length;
42
+ const stripIndent = (line, n) => { let k = 0; while (k < n && line[k] === ' ') k++; return line.slice(k); };
43
+ const isOrdered = (marker) => /\d/.test(marker);
44
+ const runAt = (s, i) => { let k = i; while (s[k] === s[i]) k++; return s.slice(i, k); };
45
+
46
+ /** The address a link may go to: plain http(s), nothing else. Returns the normalised address, or null. */
47
+ export function safeHref(raw) {
48
+ const s = String(raw ?? '');
49
+ if (!/^https?:\/\/[^/]/i.test(s)) return null;
50
+ // No control characters, white space, quotes, angle brackets or backslashes: an address is one unbroken token.
51
+ if (/[\x00-\x20\x7f-\x9f"'<>`\\]/.test(s)) return null;
52
+ try {
53
+ const u = new URL(s);
54
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') return null;
55
+ if (!u.hostname || u.username || u.password) return null; // `https://bank.example@evil.example` reads as the wrong place
56
+ return u.href;
57
+ } catch { return null; }
58
+ }
59
+
60
+ /** @returns {Array<object>} blocks: p / h / code / hr / quote / list / table */
61
+ export function parseMarkdown(text) {
62
+ const lines = String(text ?? '').replace(/\r\n?/g, '\n').split('\n').map(detab);
63
+ return parseBlocks(lines, 0);
64
+ }
65
+
66
+ /** An opening code fence: three or more backticks or tildes, then an optional language. */
67
+ function fenceAt(line) {
68
+ const m = /^( {0,3})(`{3,}|~{3,})(.*)$/.exec(line);
69
+ if (!m) return null;
70
+ const info = m[3].trim();
71
+ if (m[2][0] === '`' && info.includes('`')) return null; // that is inline code, not a fence
72
+ return { indent: m[1].length, fence: m[2], lang: info.split(/\s/)[0] ?? '' };
73
+ }
74
+
75
+ /** A heading's text without its closing run of hashes (`## Title ##`). By hand: a pattern for it backtracks over a long gap. */
76
+ function withoutClosingHashes(text) {
77
+ let end = text.length;
78
+ while (end > 0 && text[end - 1] === '#') end--;
79
+ if (end === text.length) return text;
80
+ if (end === 0) return '';
81
+ if (text[end - 1] !== ' ' && text[end - 1] !== '\t') return text; // `C#` ends in a hash that closes nothing
82
+ while (end > 0 && (text[end - 1] === ' ' || text[end - 1] === '\t')) end--;
83
+ return text.slice(0, end);
84
+ }
85
+
86
+ function headingAt(line) {
87
+ const m = /^ {0,3}(#{1,6})(?:[ \t]+(.*))?$/.exec(line);
88
+ if (!m) return null;
89
+ return { level: m[1].length, text: withoutClosingHashes((m[2] ?? '').trim()) };
90
+ }
91
+
92
+ function splitRow(line) {
93
+ let s = line.trim();
94
+ if (s.startsWith('|')) s = s.slice(1);
95
+ if (s.endsWith('|') && !s.endsWith('\\|')) s = s.slice(0, -1);
96
+ const cells = [];
97
+ let cur = '';
98
+ for (let i = 0; i < s.length; i++) {
99
+ if (s[i] === '\\' && s[i + 1] === '|') { cur += '|'; i++; continue; }
100
+ if (s[i] === '|') { cells.push(cur.trim()); cur = ''; continue; }
101
+ cur += s[i];
102
+ }
103
+ cells.push(cur.trim());
104
+ return cells;
105
+ }
106
+
107
+ /** A table starts here when this line has a bar, the next is a separator row, and both have the same number of cells. */
108
+ function tableAt(lines, i) {
109
+ if (i + 1 >= lines.length || !lines[i].includes('|')) return null;
110
+ const sepLine = lines[i + 1].trim();
111
+ if (!sepLine.includes('-') || /[^\s|:-]/.test(sepLine)) return null;
112
+ const sep = splitRow(sepLine);
113
+ if (!sep.every((c) => /^:?-+:?$/.test(c))) return null;
114
+ const head = splitRow(lines[i]);
115
+ return head.length === sep.length ? { head, sep } : null;
116
+ }
117
+
118
+ function startsBlock(lines, i) {
119
+ const l = lines[i];
120
+ return Boolean(fenceAt(l)) || Boolean(headingAt(l)) || HR.test(l) || QUOTE.test(l) || ITEM.test(l) || Boolean(tableAt(lines, i));
121
+ }
122
+
123
+ function parseBlocks(lines, depth) {
124
+ const out = [];
125
+ // Too deep to be anything a person wrote: the rest is shown as written.
126
+ if (depth > MAX_DEPTH) { const rest = lines.join('\n').trim(); return rest ? [{ t: 'p', kids: plain(rest) }] : out; }
127
+ let i = 0;
128
+ while (i < lines.length) {
129
+ const line = lines[i];
130
+ if (!line.trim()) { i++; continue; }
131
+ const fence = fenceAt(line);
132
+ if (fence) {
133
+ const body = [];
134
+ i++;
135
+ while (i < lines.length) {
136
+ const c = FENCE_CLOSE.exec(lines[i]);
137
+ if (c && c[1][0] === fence.fence[0] && c[1].length >= fence.fence.length) { i++; break; }
138
+ body.push(stripIndent(lines[i], fence.indent));
139
+ i++;
140
+ }
141
+ out.push({ t: 'code', lang: fence.lang, text: body.join('\n') });
142
+ continue;
143
+ }
144
+ const heading = headingAt(line);
145
+ if (heading) { out.push({ t: 'h', level: heading.level, kids: parseInline(heading.text) }); i++; continue; }
146
+ if (HR.test(line)) { out.push({ t: 'hr' }); i++; continue; }
147
+ const tb = tableAt(lines, i);
148
+ if (tb) {
149
+ const align = tb.sep.map((c) => (c.startsWith(':') && c.endsWith(':') ? 'center' : c.endsWith(':') ? 'right' : c.startsWith(':') ? 'left' : ''));
150
+ const fit = (cells) => tb.head.map((_, k) => parseInline(cells[k] ?? ''));
151
+ const rows = [];
152
+ i += 2;
153
+ while (i < lines.length && lines[i].trim() && lines[i].includes('|')) { rows.push(fit(splitRow(lines[i]))); i++; }
154
+ out.push({ t: 'table', align, head: fit(tb.head), rows });
155
+ continue;
156
+ }
157
+ let m;
158
+ if (QUOTE.test(line)) {
159
+ const inner = [];
160
+ while (i < lines.length && (m = QUOTE.exec(lines[i]))) { inner.push(m[1]); i++; }
161
+ out.push({ t: 'quote', kids: parseBlocks(inner, depth + 1) });
162
+ continue;
163
+ }
164
+ if (ITEM.test(line)) { i = parseList(lines, i, out, depth); continue; }
165
+ // A paragraph: consecutive lines up to a blank line or the start of another block. Line breaks are kept — in a
166
+ // conversation a new line is a new line.
167
+ const para = [line.trim()];
168
+ i++;
169
+ while (i < lines.length && lines[i].trim() && !startsBlock(lines, i)) { para.push(lines[i].trim()); i++; }
170
+ out.push({ t: 'p', kids: parseInline(para.join('\n')) });
171
+ }
172
+ return out;
173
+ }
174
+
175
+ /** One list: items at the same indent with the same kind of marker; deeper lines belong to the item above them. */
176
+ function parseList(lines, i, out, depth) {
177
+ const first = ITEM.exec(lines[i]);
178
+ const base = first[1].length;
179
+ const ordered = isOrdered(first[2]);
180
+ const list = { t: 'list', ordered, start: ordered ? parseInt(first[2], 10) : null, items: [] };
181
+ while (i < lines.length) {
182
+ const m = ITEM.exec(lines[i]);
183
+ if (!m || m[1].length !== base || isOrdered(m[2]) !== ordered || HR.test(lines[i])) break;
184
+ const content = m[1].length + m[2].length + m[3].replace(/\t/g, ' ').length;
185
+ const body = [m[4]];
186
+ i++;
187
+ while (i < lines.length) {
188
+ const l = lines[i];
189
+ if (!l.trim()) {
190
+ let j = i + 1;
191
+ while (j < lines.length && !lines[j].trim()) j++;
192
+ if (j < lines.length && indentOf(lines[j]) > base) { while (i < j) { body.push(''); i++; } continue; }
193
+ break;
194
+ }
195
+ const ind = indentOf(l);
196
+ if (ind > base) { body.push(stripIndent(l, Math.min(ind, content))); i++; continue; }
197
+ if (startsBlock(lines, i)) break;
198
+ body.push(l.trim()); // written straight after, not indented: still this item
199
+ i++;
200
+ }
201
+ list.items.push(parseBlocks(body, depth + 1));
202
+ // A blank line between items: the next one is still the same list.
203
+ let j = i;
204
+ while (j < lines.length && !lines[j].trim()) j++;
205
+ if (j > i && j < lines.length) {
206
+ const n = ITEM.exec(lines[j]);
207
+ if (n && n[1].length === base && isOrdered(n[2]) === ordered && !HR.test(lines[j])) i = j;
208
+ }
209
+ }
210
+ out.push(list);
211
+ return i;
212
+ }
213
+
214
+ /** From `from`, a run of exactly `len` backticks (the end of inline code); -1 when there is none. */
215
+ function codeClose(s, from, len) {
216
+ for (let k = s.indexOf('`', from); k >= 0; k = s.indexOf('`', k)) {
217
+ const run = runAt(s, k);
218
+ if (run.length === len) return k;
219
+ k += run.length;
220
+ }
221
+ return -1;
222
+ }
223
+
224
+ /** Past a piece of inline code starting at i: the position after it; i unchanged when no code starts there. */
225
+ function skipCode(s, i) {
226
+ if (s[i] !== '`') return i;
227
+ const run = runAt(s, i);
228
+ const e = codeClose(s, i + run.length, run.length);
229
+ return e >= 0 ? e + run.length : i + run.length;
230
+ }
231
+
232
+ /**
233
+ * `[words](address)` with s[i] === '['. Brackets nest; brackets inside inline code do not count. `last` says where the
234
+ * text's last `](` and last `)` are: past them no link can be, so a long run of brackets is not searched again and
235
+ * again for an ending that is not there.
236
+ */
237
+ function linkAt(s, i, last) {
238
+ if (i > last.mid) return null;
239
+ const limit = Math.min(s.length, i + MAX_SPAN);
240
+ let depth = 0;
241
+ let k = i;
242
+ for (; k < limit; k++) {
243
+ if (s[k] === '\\') { k++; continue; }
244
+ if (s[k] === '`') { k = skipCode(s, k) - 1; continue; }
245
+ if (s[k] === '[') depth++;
246
+ else if (s[k] === ']' && --depth === 0) break;
247
+ }
248
+ if (k >= limit || s[k] !== ']' || s[k + 1] !== '(' || k + 2 > last.close) return null;
249
+ const end = Math.min(s.length, k + 2 + MAX_SPAN);
250
+ let p = k + 2;
251
+ let par = 1;
252
+ for (; p < end; p++) {
253
+ if (s[p] === '\\') { p++; continue; }
254
+ if (s[p] === '\n') return null;
255
+ if (s[p] === '(') par++;
256
+ else if (s[p] === ')' && --par === 0) break;
257
+ }
258
+ if (p >= end || s[p] !== ')') return null;
259
+ const inside = s.slice(k + 2, p).trim();
260
+ const m = /^(<[^<>\n]*>|\S+)(?:[ \t]+(?:"[^"]*"|'[^']*'))?$/.exec(inside);
261
+ if (!m) return null;
262
+ return { label: s.slice(i + 1, k), href: m[1].replace(/^<|>$/g, ''), end: p + 1 };
263
+ }
264
+
265
+ /**
266
+ * Bold, italics or strike-through starting at i: the inner side of the mark is not white space, and it closes.
267
+ * `tried` remembers, for each kind of mark, how far an earlier one looked for its closing mark and found none. What
268
+ * closes a mark does not depend on where it opened, so a later one looks on from there: a long run of marks that
269
+ * never close is read once, not once per mark.
270
+ */
271
+ function emphasisAt(s, i, tried) {
272
+ const c = s[i];
273
+ let d;
274
+ if (c === '~') { if (s[i + 1] !== '~') return null; d = '~~'; }
275
+ else d = s[i + 1] === c ? c + c : c;
276
+ const open = i + d.length;
277
+ if (!s[open] || /\s/.test(s[open])) return null;
278
+ if (c === '_' && WORD.test(s[i - 1] ?? '')) return null; // the underscore in snake_case is not a mark
279
+ const limit = Math.min(s.length - d.length, open + MAX_SPAN);
280
+ let j = Math.max(open + 1, tried[d] ?? 0);
281
+ for (; j <= limit; j++) {
282
+ if (s[j] === '`') { j = skipCode(s, j) - 1; continue; }
283
+ if (s[j] === '\\') { j++; continue; }
284
+ if (s[j] !== c) continue;
285
+ const run = runAt(s, j);
286
+ if (d.length === 1 && run.length !== 1) { j += run.length - 1; continue; } // a single mark does not close on half of a double one
287
+ if (run.length < d.length) continue;
288
+ if (/\s/.test(s[j - 1])) { j += run.length - 1; continue; }
289
+ const at = j + run.length - d.length; // `***x***`: the last two close the bold, what is left inside is italics
290
+ if (c === '_' && WORD.test(s[at + d.length] ?? '')) { j += run.length - 1; continue; }
291
+ return { t: d === '~~' ? 's' : d.length === 2 ? 'b' : 'i', inner: s.slice(open, at), end: at + d.length };
292
+ }
293
+ tried[d] = Math.max(tried[d] ?? 0, j);
294
+ return null;
295
+ }
296
+
297
+ function pushText(out, str) {
298
+ str.split('\n').forEach((part, k) => {
299
+ if (k) out.push({ t: 'br' });
300
+ if (part) out.push({ t: 'text', v: part });
301
+ });
302
+ }
303
+ const plain = (str) => { const out = []; pushText(out, str); return out; };
304
+
305
+ /**
306
+ * @param {string} src
307
+ * @param {{ depth?: number, cites?: boolean }} [o] `cites: false` inside a link's words, where a chip cannot go
308
+ * @returns {Array<object>} inline tokens: text / br / code / b / i / s / a / cite
309
+ */
310
+ export function parseInline(src, { depth = 0, cites = true } = {}) {
311
+ const s = String(src ?? '');
312
+ if (depth > MAX_DEPTH) return plain(s);
313
+ const out = [];
314
+ let buf = '';
315
+ const flush = () => { if (buf) { pushText(out, buf); buf = ''; } };
316
+ const last = { mid: s.lastIndexOf(']('), close: s.lastIndexOf(')') };
317
+ const tried = {};
318
+ let i = 0;
319
+ while (i < s.length) {
320
+ const c = s[i];
321
+ if (c === '\\' && s[i + 1] && ESCAPABLE.includes(s[i + 1])) { buf += s[i + 1]; i += 2; continue; }
322
+ if (c === '`') {
323
+ const run = runAt(s, i);
324
+ const end = codeClose(s, i + run.length, run.length);
325
+ if (end < 0) { buf += run; i += run.length; continue; }
326
+ let v = s.slice(i + run.length, end).replace(/\n/g, ' ');
327
+ if (v.length > 2 && v.startsWith(' ') && v.endsWith(' ') && v.trim()) v = v.slice(1, -1);
328
+ flush();
329
+ out.push({ t: 'code', v });
330
+ i = end + run.length;
331
+ continue;
332
+ }
333
+ // A citation of a source, with or without its brackets (after code, so one inside code is left alone).
334
+ if (cites && (c === '[' || (c === 's' && !WORD.test(s[i - 1] ?? '')))) {
335
+ const near = s.slice(i, i + 400);
336
+ const m = CITE.exec(near) ?? (c === '[' ? CITE_BRACKETED.exec(near) : null);
337
+ if (m) { flush(); out.push({ t: 'cite', id: m[1] }); i += m[0].length; continue; }
338
+ const labelled = c === '[' ? CITE_LABELLED.exec(near) : null;
339
+ if (labelled) { flush(); out.push(...parseInline(labelled[1], { depth: depth + 1, cites: false }), { t: 'cite', id: labelled[2] }); i += labelled[0].length; continue; }
340
+ }
341
+ if (c === '[' || (c === '!' && s[i + 1] === '[')) {
342
+ const img = c === '!';
343
+ const link = linkAt(s, img ? i + 1 : i, last);
344
+ if (link) {
345
+ flush();
346
+ const kids = img ? plain(link.label || link.href) : parseInline(link.label, { depth: depth + 1, cites: false });
347
+ out.push({ t: 'a', href: safeHref(link.href), raw: link.href, ...(img ? { img: true } : {}), kids });
348
+ i = link.end;
349
+ continue;
350
+ }
351
+ }
352
+ if (c === '<') {
353
+ const m = /^<(https?:\/\/[^\s<>]{1,2000})>/i.exec(s.slice(i, i + 2010));
354
+ if (m && safeHref(m[1])) { flush(); out.push({ t: 'a', href: safeHref(m[1]), raw: m[1], kids: [{ t: 'text', v: m[1] }] }); i += m[0].length; continue; }
355
+ }
356
+ if ((c === 'h' || c === 'H') && !/[A-Za-z0-9]/.test(s[i - 1] ?? '')) {
357
+ const m = BARE_URL.exec(s.slice(i, i + 2000));
358
+ if (m) {
359
+ let url = m[0];
360
+ for (let prev = ''; prev !== url;) {
361
+ prev = url;
362
+ url = url.replace(URL_TAIL, '');
363
+ // A closing bracket left over at the end is not part of the address (paired ones inside it are).
364
+ if (url.endsWith(')') && (url.match(/\(/g) ?? []).length < (url.match(/\)/g) ?? []).length) url = url.slice(0, -1);
365
+ }
366
+ if (safeHref(url)) { flush(); out.push({ t: 'a', href: safeHref(url), raw: url, kids: [{ t: 'text', v: url }] }); i += url.length; continue; }
367
+ }
368
+ }
369
+ if (c === '*' || c === '_' || c === '~') {
370
+ const em = emphasisAt(s, i, tried);
371
+ if (em) { flush(); out.push({ t: em.t, kids: parseInline(em.inner, { depth: depth + 1, cites }) }); i = em.end; continue; }
372
+ const run = runAt(s, i); // a run of marks that is not recognised is shown as written
373
+ buf += run;
374
+ i += run.length;
375
+ continue;
376
+ }
377
+ buf += c;
378
+ i++;
379
+ }
380
+ flush();
381
+ return out;
382
+ }
383
+
384
+ /**
385
+ * Draw `text` as Markdown into `target`, replacing what it held.
386
+ * @param {HTMLElement} target
387
+ * @param {string} text
388
+ * @param {{ onCite?: (id: string) => void }} [o] called when a source citation is pressed
389
+ */
390
+ export function fillMarkdown(target, text, { onCite = null } = {}) {
391
+ target.replaceChildren(...blocksDom(target.ownerDocument, parseMarkdown(text), onCite));
392
+ return target;
393
+ }
394
+
395
+ function blocksDom(doc, blocks, onCite) {
396
+ const mk = (tag, cls) => { const n = doc.createElement(tag); if (cls) n.className = cls; return n; };
397
+ const inl = (n, kids) => { n.append(...inlineDom(doc, kids, onCite)); return n; };
398
+ return blocks.map((b) => {
399
+ if (b.t === 'p') return inl(mk('p'), b.kids);
400
+ if (b.t === 'h') return inl(mk(`h${Math.min(6, Math.max(1, b.level))}`), b.kids);
401
+ if (b.t === 'hr') return mk('hr');
402
+ if (b.t === 'code') {
403
+ const pre = mk('pre');
404
+ const code = mk('code');
405
+ code.textContent = b.text;
406
+ if (b.lang) pre.title = b.lang;
407
+ pre.append(code);
408
+ return pre;
409
+ }
410
+ if (b.t === 'quote') { const n = mk('blockquote'); n.append(...blocksDom(doc, b.kids, onCite)); return n; }
411
+ if (b.t === 'list') {
412
+ const n = mk(b.ordered ? 'ol' : 'ul');
413
+ if (b.ordered && b.start != null && b.start !== 1) n.start = b.start;
414
+ for (const it of b.items) { const li = mk('li'); li.append(...blocksDom(doc, it, onCite)); n.append(li); }
415
+ return n;
416
+ }
417
+ // A table too wide for a narrow panel scrolls sideways inside its own box; its cells are not squeezed to one letter a line.
418
+ const wrap = mk('div', 'md-table');
419
+ const t = mk('table');
420
+ const row = (cells, tag) => {
421
+ const tr = mk('tr');
422
+ cells.forEach((cell, k) => { const td = inl(mk(tag), cell); if (b.align[k]) td.style.textAlign = b.align[k]; tr.append(td); });
423
+ return tr;
424
+ };
425
+ const thead = mk('thead');
426
+ thead.append(row(b.head, 'th'));
427
+ const tbody = mk('tbody');
428
+ for (const r of b.rows) tbody.append(row(r, 'td'));
429
+ t.append(thead, tbody);
430
+ wrap.append(t);
431
+ return wrap;
432
+ });
433
+ }
434
+
435
+ function inlineDom(doc, toks, onCite) {
436
+ const out = [];
437
+ for (const k of toks) {
438
+ if (k.t === 'text') out.push(doc.createTextNode(k.v));
439
+ else if (k.t === 'br') out.push(doc.createElement('br'));
440
+ else if (k.t === 'code') { const n = doc.createElement('code'); n.textContent = k.v; out.push(n); }
441
+ else if (k.t === 'cite') {
442
+ const n = doc.createElement('button');
443
+ n.type = 'button';
444
+ n.className = 'cite';
445
+ n.title = 'Open this source';
446
+ n.textContent = k.id.slice(0, 10);
447
+ if (onCite) n.addEventListener('click', (e) => { e.preventDefault(); onCite(k.id); });
448
+ out.push(n);
449
+ } else if (k.t === 'a') {
450
+ // Checked again here, so a token from anywhere else cannot carry an address the parser would have refused.
451
+ const href = safeHref(k.href);
452
+ const n = doc.createElement(href ? 'a' : 'span');
453
+ if (href) {
454
+ n.setAttribute('href', href);
455
+ n.setAttribute('target', '_blank');
456
+ n.setAttribute('rel', 'noopener noreferrer');
457
+ n.className = k.img ? 'md-link md-img' : 'md-link';
458
+ n.title = k.img ? `${href} — image, not loaded` : href;
459
+ } else {
460
+ n.className = 'md-nolink';
461
+ n.title = `Not a web link, so it is not opened: ${String(k.raw ?? '').slice(0, 200)}`;
462
+ }
463
+ n.append(...inlineDom(doc, k.kids, onCite));
464
+ out.push(n);
465
+ } else {
466
+ const n = doc.createElement(k.t === 'b' ? 'strong' : k.t === 'i' ? 'em' : 'del');
467
+ n.append(...inlineDom(doc, k.kids, onCite));
468
+ out.push(n);
469
+ }
470
+ }
471
+ return out;
472
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Types for placement.js, for what the program reads of it under src/ (DB: the program's counts of what is not placed
3
+ * are the workbench's own — src/keeper/organize/workbench-placement.ts). The browser never loads this file.
4
+ */
5
+ export interface PlacementNode {
6
+ readonly id: string;
7
+ readonly category: string;
8
+ readonly label?: string;
9
+ readonly validity?: string;
10
+ readonly progress?: string | null;
11
+ readonly areaId?: string | null;
12
+ readonly parentId?: string | null;
13
+ readonly group?: string | null;
14
+ readonly replacedBy?: string | null;
15
+ readonly recentChange?: boolean;
16
+ readonly servesOrder?: readonly string[];
17
+ readonly foundation?: true;
18
+ readonly wholeProductWhy?: string;
19
+ readonly wholePlanWhy?: string;
20
+ readonly noPlanWhy?: string;
21
+ readonly noAreaWhy?: string;
22
+ }
23
+ export interface PlacementRelation { readonly type: string; readonly from: string; readonly to: string }
24
+ export interface PlacementGeneration {
25
+ readonly id: string;
26
+ readonly name: string;
27
+ readonly started?: unknown;
28
+ readonly ended?: { readonly at?: string } | null;
29
+ readonly endedBy?: unknown;
30
+ readonly workIds?: readonly string[];
31
+ readonly carriedIds?: readonly string[];
32
+ readonly planIds?: readonly string[];
33
+ }
34
+ export interface PlacementData {
35
+ readonly nodes: readonly PlacementNode[];
36
+ readonly relations: readonly PlacementRelation[];
37
+ readonly generations?: readonly PlacementGeneration[];
38
+ }
39
+ /** Where one object sits (see placementOf in placement.js for each zone). */
40
+ export interface Place {
41
+ readonly zone: 'words' | 'product' | 'goal' | 'area' | 'gen-plan' | 'head' | 'gen' | 'cell' | 'exec' | 'intent' | 'ring' | 'result';
42
+ readonly band?: string;
43
+ readonly area?: string | null;
44
+ readonly areas?: readonly string[];
45
+ readonly by?: 'contract' | 'named' | 'first' | null;
46
+ readonly gen?: string | null;
47
+ readonly plan?: string | null;
48
+ readonly ring?: 'multi' | 'whole' | 'product' | 'none';
49
+ readonly why?: string;
50
+ readonly whole?: string;
51
+ readonly noPlanWhy?: string;
52
+ readonly noAreaWhy?: string;
53
+ /** A work item in `Not in a plan`: the plans it serves that have no band (not current, or an earlier generation's). */
54
+ readonly plansNotDrawn?: readonly string[];
55
+ readonly work?: string | null;
56
+ }
57
+ export interface UnplacedCounts {
58
+ readonly workNoPlan: number;
59
+ readonly workNoArea: number;
60
+ readonly workWholePlan: number;
61
+ readonly workWrittenNoPlan: number;
62
+ readonly workWrittenNoArea: number;
63
+ readonly workNeither: number;
64
+ readonly intentProductOnly: Readonly<Record<string, number>>;
65
+ readonly intentNowhere: Readonly<Record<string, number>>;
66
+ readonly intent: number;
67
+ }
68
+ export interface UnplacedLists {
69
+ readonly workNoPlan: readonly string[];
70
+ readonly workNoArea: readonly string[];
71
+ readonly workWholePlan: readonly string[];
72
+ readonly workWrittenNoPlan: readonly string[];
73
+ readonly workWrittenNoArea: readonly string[];
74
+ readonly workNeither: readonly string[];
75
+ readonly intentProductOnly: readonly string[];
76
+ readonly intentNowhere: readonly string[];
77
+ }
78
+ export interface Placement {
79
+ readonly areas: readonly PlacementNode[];
80
+ /** The current plans: the ones the workbench draws a band for. */
81
+ readonly plans: readonly PlacementNode[];
82
+ readonly generations: readonly { readonly id: string; readonly name: string; readonly planIds: readonly string[]; readonly workIds: readonly string[] }[];
83
+ readonly place: ReadonlyMap<string, Place>;
84
+ readonly ring: { readonly multi: readonly string[]; readonly whole: readonly string[]; readonly product: readonly string[]; readonly none: readonly string[] };
85
+ readonly unplaced: UnplacedCounts;
86
+ readonly byId: ReadonlyMap<string, PlacementNode>;
87
+ }
88
+ export const NO_PLAN: string;
89
+ export const HIDDEN_VALIDITY: ReadonlySet<string>;
90
+ export function placementOf(data: PlacementData, options?: { showReplaced?: boolean }): Placement;
91
+ export function unplacedOf(M: Pick<Placement, 'place' | 'ring'>): UnplacedLists;
92
+ export function isUnplacedWork(p: Place | undefined | null): boolean;