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/src/cli.ts ADDED
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pk — ProjectKeeper command line. The same command is also installed under its long name, `projectkeeper`.
4
+ *
5
+ * pk serve [--port N] start the workbench at http://127.0.0.1:N
6
+ * pk add <name> <location> [...] add a project (nothing runs until Start on its Takeover page)
7
+ * pk scope <projectId> print the scope list
8
+ * pk projects list projects
9
+ * pk check [table] [--project <dir>] [--sessions <file>]
10
+ * check a filled ProjectKeeper table (default projectkeeper/project.json here)
11
+ * against template/projectkeeper.schema.json and the project's own files; prints
12
+ * every problem and exits 1 when there is any. Needs no workbench.
13
+ * pk import [table] [--home <dir>] [--name <name>] [--project <dir>] [--sessions <file>]
14
+ * check the table the same way and, when it holds, take it into the home: the
15
+ * project (the folder holding projectkeeper/, or --project) is found or added and
16
+ * read, and the Keeper's picture of it is replaced by the table's. A table with any
17
+ * problem is refused and nothing is written. Prints what it wrote.
18
+ * pk sessions [--project <dir>] [--home <dir>] [--name <name>] [--out <dir>]
19
+ * write the session material a table quotes the owner from: every owner message of
20
+ * the project's agent sessions (default: the current directory), each after the
21
+ * agent's answer it replies to, as session-material.md and .jsonl. They go into
22
+ * ProjectKeeper's own folder for the project (<home>/projects/<id>/table), never
23
+ * into the project; an --out inside it is refused (D107).
24
+ * pk check --owner-part <file> --sessions <file>
25
+ * check the owner part a takeover's first step writes ({ ownerWants, notes })
26
+ * against the schema and the session material it quotes.
27
+ * pk takeover [--project <dir>] [--home <dir>] [--name <name>] [--table-in home|project] --prepare
28
+ * lay out a takeover by the table in three isolated folders under
29
+ * <home>/projects/<id>/table/takeover-<time>/: owner/ (the session material),
30
+ * project-reader/ (a copy of the project), and the main agent's instructions at
31
+ * the top; prints each step's folder and instructions and the pk import command.
32
+ * Any agent can run the steps; Claude Code users run them in their own Claude Code.
33
+ * pk takeover ... --run codex [--model <m>] [--effort <e>]
34
+ * also run the three steps headless with Codex CLI (routes 1 and 2 at once, then
35
+ * the main agent; Codex's default model unless --model, effort high unless
36
+ * --effort), check the merged table and, when it holds, put it at its destination
37
+ * and take it in. --table-in home (default, safe for a public project) keeps the
38
+ * table at <home>/projects/<id>/table/project.json; a private project may use
39
+ * --table-in project (projectkeeper/project.json in the project). There is no
40
+ * --run claude: running a Claude subscription from another tool is against
41
+ * Anthropic's terms.
42
+ *
43
+ * An execution agent's own entry (Spec §7.10), all talking to the running workbench:
44
+ * pk context [--work <id|name>] [--purpose Start|Work] [--kind K] [--recipient R] [--since T]
45
+ * pk get <id> whatever an id in a pack names: a pack, an original (a code file: where it is
46
+ * and the version read), a note, a project rule, or the object itself in full
47
+ * with its neighbours named by id
48
+ * pk options areas, work items, lit breakpoints, open send-backs, six things and context choices
49
+ * pk ask "<question>" ask the Keeper
50
+ * pk help how to use these
51
+ * The project is --project <id>, or the one containing --cwd <dir> (default: the current directory).
52
+ *
53
+ * pk mcp [--port N] [--project <id>] [--cwd <dir>]
54
+ * the same reading commands and the table check as a local MCP server on stdin and
55
+ * stdout, for clients that take their tools over MCP (D108; src/mcp/server.ts).
56
+ * The flags are the tools' defaults; a tool call can name its own.
57
+ */
58
+ import { fileURLToPath } from 'node:url';
59
+ import { dirname, join, resolve } from 'node:path';
60
+ import process from 'node:process';
61
+ import { App } from './server/app.ts';
62
+ import { HttpApp } from './server/http.ts';
63
+ import { registerRoutes } from './server/api.ts';
64
+ import { vendorDir } from './server/vendor.ts';
65
+ import { WorkspaceFileError } from './store/workspace.ts';
66
+ import { gitNotice, gitVersionOutput } from './util/git-check.ts';
67
+ import { checkOwnerPartFile, checkTableFile, readSessionMaterial, tableCheckReport, type TableProblem } from './table/check.ts';
68
+ import { importTable } from './table/import.ts';
69
+ import { exportProjectSessions } from './table/sessions.ts';
70
+ import { prepareTakeover, runTakeover, type PreparedTakeover, type TableIn } from './table/takeover.ts';
71
+ import { agentEntry, askEntry, Exit, type EntryOutput, type EntryTarget } from './context/agent-entry.ts';
72
+
73
+ const here = dirname(fileURLToPath(import.meta.url));
74
+ const appRoot = resolve(here, '..');
75
+
76
+ function arg(flag: string, fallback: string | null = null): string | null {
77
+ const i = process.argv.indexOf(flag);
78
+ return i >= 0 && process.argv[i + 1] ? process.argv[i + 1]! : fallback;
79
+ }
80
+
81
+ /** The table file a table command names (default projectkeeper/project.json here), past the flags that take a value. */
82
+ function tableArg(): string {
83
+ const given = process.argv.slice(3).find((a, i, all) => !a.startsWith('--') && !(i > 0 && ['--project', '--sessions', '--home', '--name'].includes(all[i - 1]!)));
84
+ return resolve(given ?? join('projectkeeper', 'project.json'));
85
+ }
86
+
87
+ function printProblems(tableFile: string, problems: readonly TableProblem[]): void {
88
+ for (const line of tableCheckReport(tableFile, problems)) console.log(line);
89
+ }
90
+
91
+ /** `pk check`: the table, its project folder and the session material it cites; no workbench, no store. */
92
+ function checkCommand(): number {
93
+ const projectDir = arg('--project');
94
+ const sessionsFile = arg('--sessions');
95
+ // The owner part a takeover's first step writes, held to the session material it is read from (D107).
96
+ const ownerPart = arg('--owner-part');
97
+ if (ownerPart) {
98
+ if (!sessionsFile) { console.error('usage: pk check --owner-part <file> --sessions <session-material.jsonl>'); return 2; }
99
+ const file = resolve(ownerPart);
100
+ const problems = checkOwnerPartFile(file, readSessionMaterial(resolve(sessionsFile)));
101
+ if (!problems.length) { console.log(`${file}: no problems.`); return 0; }
102
+ console.log(`${file}: ${problems.length} problem${problems.length === 1 ? '' : 's'}.`);
103
+ for (const p of problems) console.log(` ${p.at} ${p.problem}`);
104
+ return 1;
105
+ }
106
+ const tableFile = tableArg();
107
+ const problems = checkTableFile(tableFile, {
108
+ projectDir: projectDir ? resolve(projectDir) : undefined,
109
+ sessions: sessionsFile ? readSessionMaterial(resolve(sessionsFile)) : null,
110
+ });
111
+ printProblems(tableFile, problems);
112
+ return problems.length ? 1 : 0;
113
+ }
114
+
115
+ /** `pk import`: the same check, then the table taken into the home (src/table/import.ts). */
116
+ async function importCommand(): Promise<number> {
117
+ const tableFile = tableArg();
118
+ const projectDir = arg('--project');
119
+ const sessionsFile = arg('--sessions');
120
+ const r = await importTable({
121
+ tableFile, projectDir: projectDir ? resolve(projectDir) : undefined, home: arg('--home') ?? undefined, name: arg('--name') ?? undefined,
122
+ sessions: sessionsFile ? readSessionMaterial(resolve(sessionsFile)) : null,
123
+ });
124
+ if (r.problems.length) { printProblems(tableFile, r.problems); return 1; }
125
+ const c = r.counts!;
126
+ const list = (counts: Readonly<Record<string, number>>) => Object.entries(counts).map(([k, n]) => `${n} ${k}`).join(', ') || 'nothing';
127
+ console.log(`${tableFile}: taken into ${r.added ? 'the new project' : 'project'} ${r.projectName} (${r.projectId}) in ${r.home}.`);
128
+ console.log(` Replaced the earlier picture: ${list(c.removed)}`);
129
+ console.log(` Sources cited: ${c.sources.cited} (${c.sources.reused} as intake read them, ${c.sources.written} written from the table)`);
130
+ console.log(` Reference items: ${list(c.reference)}`);
131
+ console.log(` Work items: ${c.threads} (with ${c.facts} fact records), relations written: ${c.relations}, earlier generations: ${c.generations}, process links: ${c.links}`);
132
+ console.log(` Code territories: ${c.territories}, notes: ${c.notes}`);
133
+ console.log(` Graph: ${c.graph.nodes} nodes, ${c.graph.relations} relations`);
134
+ for (const w of r.warnings ?? []) console.warn(` ${w}`);
135
+ return 0;
136
+ }
137
+
138
+ /** `pk sessions`: the session material of a project, written outside it (src/table/sessions.ts, D107). */
139
+ async function sessionsCommand(): Promise<number> {
140
+ const out = arg('--out');
141
+ let r: Awaited<ReturnType<typeof exportProjectSessions>>;
142
+ try {
143
+ r = await exportProjectSessions({ projectDir: resolve(arg('--project') ?? process.cwd()), home: arg('--home') ?? undefined, name: arg('--name') ?? undefined, out: out ?? undefined });
144
+ } catch (e) { console.error((e as Error).message); return 2; }
145
+ console.log(`Session material of ${r.added ? 'the new project' : 'project'} ${r.projectName} (${r.projectId}) in ${r.home}:`);
146
+ console.log(` ${r.ownerMessages} owner message${r.ownerMessages === 1 ? '' : 's'} from ${r.sessions} session${r.sessions === 1 ? '' : 's'}, ${r.records} records with the agent answers before them`);
147
+ console.log(` ${r.markdown}`);
148
+ console.log(` ${r.jsonl} (pk check / pk import --sessions)`);
149
+ for (const w of r.warnings) console.warn(` ${w}`);
150
+ return 0;
151
+ }
152
+
153
+ const TAKEOVER_USAGE = 'usage: pk takeover [--project <dir>] [--home <dir>] [--name <name>] [--table-in home|project] (--prepare | --run codex [--model <m>] [--effort <e>])';
154
+
155
+ function printSteps(t: PreparedTakeover): void {
156
+ console.log(`Takeover of ${t.added ? 'the new project' : 'project'} ${t.projectName} (${t.projectId}), laid out in ${t.workDir}`);
157
+ console.log(` Session material: ${t.ownerMessages} owner message${t.ownerMessages === 1 ? '' : 's'} from ${t.sessions} session${t.sessions === 1 ? '' : 's'}`);
158
+ console.log(` Project copy: ${t.repos.map((r) => `${r.path} at ${r.commit}`).join(', ')}${t.skipped.length ? `; ${t.skipped.length} entr${t.skipped.length === 1 ? 'y' : 'ies'} left out (ignored by git, node_modules, links)` : ''}`);
159
+ for (const w of t.warnings) console.warn(` ${w}`);
160
+ const { owner, reader, main } = t.steps;
161
+ console.log('\nStep 1 — the owner\'s words (run at the same time as step 2; each agent sees only its own folder):');
162
+ console.log(` folder: ${owner.folder}\n instructions: ${owner.instructions}\n writes: ${owner.output}`);
163
+ console.log('Step 2 — the project:');
164
+ console.log(` folder: ${reader.folder}\n instructions: ${reader.instructions}\n writes: ${reader.output}`);
165
+ console.log('Step 3 — the main agent, after both:');
166
+ console.log(` folder: ${main.folder}\n instructions: ${main.instructions}\n writes: ${main.output}`);
167
+ console.log(`Then put ${main.output} at ${t.destination} and take it in:\n ${t.importCommand}`);
168
+ }
169
+
170
+ /**
171
+ * `pk takeover`: the three-step takeover by the table (src/table/takeover.ts, D107). `--prepare` lays out the folders
172
+ * for any agent; `--run codex` also runs the steps headless with Codex CLI and takes the table in. PK_TAKEOVER_AGENT
173
+ * names a program to run instead of `codex` (a .mjs, .js or .ts file runs under this node), for tests and stand-ins.
174
+ */
175
+ async function takeoverCommand(): Promise<number> {
176
+ const run = arg('--run');
177
+ const prepare = process.argv.includes('--prepare');
178
+ const tableIn = arg('--table-in', 'home');
179
+ if (run === 'claude') {
180
+ console.error('pk takeover does not run Claude Code: running a Claude subscription from another tool is against Anthropic\'s terms. Run pk takeover --prepare and give the three steps to your own Claude Code, each in its folder.');
181
+ return 2;
182
+ }
183
+ if ((run !== null && run !== 'codex') || (run === null && !prepare) || (tableIn !== 'home' && tableIn !== 'project')) { console.error(TAKEOVER_USAGE); return 2; }
184
+ const options = { projectDir: resolve(arg('--project') ?? process.cwd()), home: arg('--home') ?? undefined, name: arg('--name') ?? undefined, tableIn: tableIn as TableIn };
185
+ if (!run) {
186
+ let t: PreparedTakeover;
187
+ try { t = await prepareTakeover(options); } catch (e) { console.error((e as Error).message); return 2; }
188
+ printSteps(t);
189
+ return 0;
190
+ }
191
+ const standIn = process.env.PK_TAKEOVER_AGENT?.trim();
192
+ const agentCommand = standIn ? (/\.(m?js|ts)$/i.test(standIn) ? [process.execPath, resolve(standIn)] : [standIn]) : undefined;
193
+ let r: Awaited<ReturnType<typeof runTakeover>>;
194
+ try {
195
+ r = await runTakeover({ ...options, model: arg('--model') ?? undefined, effort: arg('--effort') ?? undefined, agentCommand, log: (line) => console.log(line) });
196
+ } catch (e) { console.error((e as Error).message); return 2; }
197
+ const t = r.prepared;
198
+ console.log(`\nWork folder: ${t.workDir} (each step's events, stderr, times and last message under runs/)`);
199
+ for (const s of r.runs) if (s.lastMessage) console.log(`\n${s.step}, last message:\n${s.lastMessage.trim()}`);
200
+ if (r.stopped) {
201
+ console.log(`\nStopped after ${r.stopped.after}: ${r.stopped.why}. Nothing was taken in.`);
202
+ if (r.problems.length) printProblems(t.steps.main.output, r.problems);
203
+ console.log(`Once ${t.steps.main.output} checks clean, put it at ${t.destination} and take it in:\n ${t.importCommand}`);
204
+ return 1;
205
+ }
206
+ console.log(`\nTaken in from ${t.destination}${r.previousTable ? ` (the table that stood there is kept as ${r.previousTable})` : ''}: ${r.imported!.counts!.graph.nodes} nodes in the graph.`);
207
+ for (const w of r.imported!.warnings ?? []) console.warn(` ${w}`);
208
+ return 0;
209
+ }
210
+
211
+ /**
212
+ * `pk mcp` (D108): an MCP server on stdin and stdout until the client closes stdin. Stdout carries the protocol and
213
+ * nothing else, so anything that would be printed there while it runs goes to stderr instead.
214
+ */
215
+ async function mcpCommand(): Promise<number> {
216
+ const toStderr = (...parts: unknown[]) => console.error(...parts);
217
+ console.log = toStderr; console.info = toStderr; console.debug = toStderr;
218
+ const { serveMcp } = await import('./mcp/server.ts');
219
+ await serveMcp(process.stdin, process.stdout, { port: arg('--port'), project: arg('--project'), cwd: resolve(arg('--cwd') ?? process.cwd()), home: arg('--home') ?? undefined });
220
+ return 0;
221
+ }
222
+
223
+ async function main(): Promise<number> {
224
+ const command = process.argv[2] ?? 'serve';
225
+ if (command === 'mcp') return mcpCommand();
226
+ if (command === 'check') return checkCommand();
227
+ if (command === 'takeover') return takeoverCommand();
228
+ // The import opens its home itself, and only for a table that holds.
229
+ if (command === 'import') return importCommand();
230
+ if (command === 'sessions') return sessionsCommand();
231
+ const app = new App(arg('--home') ?? undefined);
232
+ const VALUE_FLAGS = ['--home', '--port', '--project', '--cwd', '--thread', '--fetch', '--work', '--purpose', '--kind', '--recipient', '--since'];
233
+ const positionals = () => process.argv.slice(3).filter((a, i, all) => !a.startsWith('--') && !(i > 0 && VALUE_FLAGS.includes(all[i - 1]!)));
234
+ if (command === 'serve') {
235
+ const http = new HttpApp();
236
+ registerRoutes(http, app, join(appRoot, 'ui'), vendorDir(appRoot));
237
+ http.route('GET', '/api/events', ({ res }) => { http.sse.attach(res); });
238
+ app.on('event', (event) => http.sse.broadcast('app', event));
239
+ const port = Number(arg('--port', String(app.workspace.settings.port)));
240
+ app.servedPort = port;
241
+ await app.initKeeper({ fakeProvider: process.argv.includes('--fake-provider') });
242
+ if (app.workspace.settings.watchProjects === false) console.log('Not watching the projects (settings.watchProjects is false): this home works through what it already holds.');
243
+ // A project the owner has not started is not watched: before `Start` nothing counts as waiting (D105).
244
+ else for (const p of app.workspace.list()) { try { if (p.lastScopedAt) app.startWatching(p.id); } catch (e) { console.warn(`[watch] ${p.id}: ${(e as Error).message}`); } }
245
+ const server = await http.listen(port);
246
+ console.log(`ProjectKeeper workbench: http://127.0.0.1:${server.port}/`);
247
+ // Without git a repository is read as a folder of files and nothing says why (a new Mac has no git until the
248
+ // command line tools are installed): said here, once.
249
+ const aboutGit = gitNotice(gitVersionOutput());
250
+ if (aboutGit) console.warn(`\n${aboutGit}\n`);
251
+ const stop = async () => { app.stopAll(); await app.flushAll(); await server.close(); process.exit(0); };
252
+ process.on('SIGINT', () => { void stop(); });
253
+ process.on('SIGTERM', () => { void stop(); });
254
+ // Closing the terminal window sends this one: stopped the same way, so what was being written is written.
255
+ process.on('SIGHUP', () => { void stop(); });
256
+ return new Promise(() => { /* keep running */ });
257
+ }
258
+ if (command === 'add') {
259
+ const [name, ...locations] = positionals();
260
+ if (!name || locations.length === 0) { console.error('usage: pk add <name> <location> [...]'); return 2; }
261
+ const project = app.addProject(name, locations);
262
+ await app.flushAll();
263
+ console.log(JSON.stringify({ id: project.id, name: project.name, state: 'Not organized yet', next: 'Open the workbench: Keeper → Takeover → choose a depth → Start' }, null, 2));
264
+ return 0;
265
+ }
266
+ if (command === 'intake') {
267
+ const id = positionals()[0];
268
+ if (!id) { console.error('usage: pk intake <projectId>'); return 2; }
269
+ const result = await app.intakeProject(id);
270
+ await app.flushAll();
271
+ console.log(JSON.stringify(result, null, 2));
272
+ app.stopAll();
273
+ return 0;
274
+ }
275
+ if (command === 'scope') {
276
+ const id = positionals()[0];
277
+ if (!id) { console.error('usage: pk scope <projectId>'); return 2; }
278
+ const project = app.project(id);
279
+ for (const item of project.scope) {
280
+ console.log(`${item.category.padEnd(14)} ${item.relation.padEnd(24)} ${item.path}\n${' '.repeat(39)}${item.reason}`);
281
+ }
282
+ if (project.scopeQuestions.length) console.log('\nQuestions:\n' + project.scopeQuestions.map((q) => `- ${q.question}${q.answer ? ` → ${q.answer.text}` : ''}`).join('\n'));
283
+ return 0;
284
+ }
285
+ if (command === 'ask') {
286
+ // Query entry without any host integration (§7.6): talks to the running workbench (src/context/agent-entry.ts).
287
+ const portFlag = arg('--port') ? ` --port ${arg('--port')}` : '';
288
+ const positional = process.argv.slice(3).filter((a, i, all) => !a.startsWith('--') && !(i > 0 && all[i - 1]!.startsWith('--') && ['--project', '--cwd', '--thread', '--port', '--home', '--fetch'].includes(all[i - 1]!)));
289
+ return askEntry(entryTarget(app), console_, {
290
+ question: positional.join(' '), fetch: arg('--fetch'), thread: arg('--thread'), wait: process.argv.includes('--wait'),
291
+ fetchLater: (projectId, jobId) => `pk ask --project ${projectId}${portFlag} --fetch ${jobId}`,
292
+ askAgain: (projectId, thread) => `pk ask --project ${projectId}${portFlag} --thread ${thread} "<question>"`,
293
+ });
294
+ }
295
+ if (['context', 'get', 'options', 'help'].includes(command)) {
296
+ return agentEntry(command, entryTarget(app), console_, positionals(), { work: arg('--work'), purpose: arg('--purpose'), kind: arg('--kind'), recipient: arg('--recipient'), since: arg('--since') });
297
+ }
298
+ if (command === 'projects') {
299
+ for (const p of app.workspace.list()) console.log(`${p.id}\t${p.name}\t${p.locations.join(', ')}`);
300
+ return 0;
301
+ }
302
+ console.error(`unknown command: ${command}`);
303
+ return 2;
304
+ }
305
+
306
+ /** The workbench and project the agent entry reads: `--port` (default from the settings), `--project` or `--cwd`. */
307
+ function entryTarget(app: App): EntryTarget {
308
+ return { base: `http://127.0.0.1:${arg('--port', String(app.workspace.settings.port))}`, project: arg('--project'), cwd: arg('--cwd') ?? process.cwd() };
309
+ }
310
+
311
+ /** The agent entry's text on the terminal: what was asked for on stdout, the rest on stderr. */
312
+ const console_: EntryOutput = { out: (text) => console.log(text), err: (text) => console.error(text) };
313
+
314
+ main().then((code) => { process.exitCode = code; }, (error) => {
315
+ if (error instanceof Exit) { process.exitCode = error.code; return; }
316
+ // A settings file that cannot be read is the owner's to correct: the message names it and says how; no stack.
317
+ console.error(error instanceof WorkspaceFileError ? error.message : error);
318
+ process.exitCode = 1;
319
+ });
@@ -0,0 +1,13 @@
1
+ # Code-map counting rules
2
+
3
+ `CodeMapIndex` takes the existing ledger and the project's stored work items and territories. It does not invoke a model.
4
+
5
+ - **What counts as code:** the ledger decides, not a list of languages. A file is code when a reader reads its references (`code_files.reader`: the TypeScript compiler for TypeScript and JavaScript, the code engine for every other language), or when it is in a language the ledger knows is code though nothing reads it (anything but the prose and data of `ledger/code.ts` `NOT_CODE`: a shell script, a C header with nothing in it to read, any code when the engine cannot run). Generated, classified (third-party) and test files are no territory's code. `Ledger.referenceReach` weighs a residual judgement by the same rule. A ledger built before the `reader` column existed has none; TypeScript and JavaScript then count as read, as the ledger itself falls back.
6
+ - **Built by:** only commits on the trunk's first-parent line count. A merge's `commit_files` already means its diff against its first parent; each commit's changed path counts once. A retained fast-forward branch can name the contiguous first-parent run back to the preceding merge or another known work item. Commit message and branch identifiers match `WorkThread.ids` and `KeeperNumber` aliases. A confirmed `ProcessLink` to `commit:<hash>` overrides a textual match. Rows without a unique work match remain `Unclaimed`. A deleted branch with no identifying commit text or confirmed link cannot be recovered from the current refs.
7
+ - **Generation:** the rebuild runs read-only `git blame --incremental` on the current HEAD's unclassified, non-generated files whose references a reader reads (`ledger/blame.ts`); code nothing reads is not blamed and adds no lines to a generation. The ledger stores ranges of line numbers and commit hashes, keyed by each file's git blob id. Unchanged blobs reuse the cache. A date-only `Generation.ended` includes that local author day; a timestamp uses its exact instant. Lines after all ended generations belong to the current generation (`generationId: null`). `generationOrigins()` gives the line count, share and work breakdown; `TerritoryView.generations` carries each share and the work items its lines came from.
8
+ - **Dependency count:** one edge per distinct source-file/target-file pair crossing two territories. Every non-test kind `code_deps` records counts — the compiler's `import` and `reference`, the engine's `import`, `calls` and the other references it resolved — and duplicate kinds for the same pair count once. External packages and test-file edges do not count.
9
+ - **Current use:** every code file takes part. Tests and generated/classified files cannot keep a source file live. Conventional `main`, `app`, `index`, `cli`, `bootstrap`, `application`, `route` and `router` files and files changed by a work item in the current generation anchor a non-test reference walk; a reference from code a reader reads can reach code nothing reads. Code outside that closure is unreferenced, including chains and isolated cycles. Who names such code comes from the ledger (`code_files.named_by`, `ledger/code.ts` `NamingKind`), read from every tracked text file: a build or manifest file naming it (a C++ source its `CMakeLists.txt` compiles, the activity `AndroidManifest.xml` launches by `.MainActivity`) makes it a named entry point; code the walk reaches naming it, a reference its reader did not resolve (a Dart `part`); a document, data or a test's fixture only mentions it. A named entry point, a file live code names, and code only they reach are not shown to be unreferenced. A territory none of whose code is reached is `Unreferenced` when some of it is read and none of it is such code, and `Not known` otherwise, as is a territory with no code. A territory whose live incoming references all come from files with at least 95% older-generation lines is `Previous generation only`; otherwise it is `In current plan` when current work changed it or its area has requirements, designs, decisions, plans or work items in force, else `In use, not in current plan`. These are program candidates from file references, not proof of runtime use or a plan link. Dynamic registration, routing, reflection and manually invoked scripts need model review. An unreferenced candidate says what is uncertain about it — nothing reads it; it is a named entry point, "named in AndroidManifest.xml"; code names it; only such files reach it — and its judgement is then `Inferred` (`Ledger.referenceReach`); a mention is said too and leaves it computed.
10
+ - **Residual-name candidates:** a path component or token `legacy`, `old`, `deprecated`, `backup` or `v1` with a non-test inbound reference. Generic words only: whether a project's own name looks residual (a folder still called after the product's first idea) is the cross-check's judgement from that project's history. This is a naming heuristic, never an anomaly judgement.
11
+ - **How deep each language is read:** `codeLevels()` gives `CodeView.levels` from the ledger's measured coverage (`Ledger.coverage().languages`): every language a reader reads and every language the ledger knows is code — who reads it, how many of its files, and for the engine the imports it resolved to a repository file and those it left unresolved naming one, the files code names that no counted reference reaches, and parse errors. A language is `complete` when every file is read and nothing was measured unresolved; elsewhere residual calls are `Inferred`. Named entry points say where a language's code is entered, not how deep it is read: the ledger's coverage counts them apart (`namedEntryPoints`), and a residual call on one is `Inferred` by `Ledger.referenceReach`. `Project scope` says the same per language from the same data (`ScopeKView` `ledger.languages[].code`, `ledger/views.ts` `coverageView`).
12
+
13
+ The model-owned `CodeTerritory.anomalies` is unchanged. `codeAnomalyCandidates(ledger, store, project)` supplies cross-check input with ledger file and commit references. With no territories yet, `codeView()` displays measured disjoint directories under their real paths and the message `No territories drawn yet`.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * `KEngines.codemap` (server/k-views.ts): what the process view and `Code` need from the code map besides the ledger's
3
+ * own views (Spec v3.0 §6.17, §1.19; CKC-25 AC-6, AC-7, AC-10):
4
+ * - which territories each work item changed — the jump from a work to its territories, counted as `Built by` counts;
5
+ * - which of a territory's paths the current version no longer has;
6
+ * - a file of the current version, found through the ledger's `fileRefs` and read from the tree the ledger read.
7
+ *
8
+ * Program facts only, read from the ledger's tables and the assets; nothing is judged. One `CodeMapIndex` per repository
9
+ * is kept while neither the ledger nor the assets it reads have changed.
10
+ */
11
+ import type { Ledger } from '../ledger/index.ts';
12
+ import type { LedgerService } from '../ledger/adapters.ts';
13
+ import type { Project } from '../model/types.ts';
14
+ import type { CodeFileTextView, WorkTerritoryView } from '../model/views-k.ts';
15
+ import type { ProjectStore } from '../store/project-store.ts';
16
+ import { endsWithPath, pathKey } from '../util/paths.ts';
17
+ import { CodeMapIndex } from './facts.ts';
18
+
19
+ export interface CodeMapEngines {
20
+ /** The territories each work item changed, by work id; null without a ledger. */
21
+ workTerritories(store: ProjectStore, project: Project): Readonly<Record<string, readonly WorkTerritoryView[]>> | null;
22
+ /** The paths of each territory the current version no longer has, by territory id (only territories with some); null without a ledger. */
23
+ gonePaths(store: ProjectStore, project: Project): Readonly<Record<string, readonly string[]>> | null;
24
+ /** A file of the current version (§6.17 "点文件打开原文阅读"), a page of it from `fromLine`; a string says why not; null without a ledger. */
25
+ fileText(project: Project, input: { readonly path: string; readonly repo?: string | null; readonly fromLine?: number | null }): CodeFileTextView | string | null;
26
+ }
27
+
28
+ /** What the index reads of the assets: when any of it changes, the index is built again. */
29
+ function storeMark(store: ProjectStore): string {
30
+ const part = (xs: readonly { readonly id: string; readonly updatedAt?: string }[]) => `${xs.length}:${xs.reduce((m, x) => ((x.updatedAt ?? '') > m ? x.updatedAt ?? '' : m), '')}`;
31
+ return [
32
+ part(store.threads.all()), part(store.territories.all()), part(store.generations.all()), part(store.nodes.all()),
33
+ part(store.numbers.all().map((n) => ({ id: n.id, updatedAt: n.at }))), part(store.links.all().map((l) => ({ id: `${l.id}${l.confirmed ? '+' : l.check?.passed ? '~' : ''}`, updatedAt: l.at }))),
34
+ ].join('|');
35
+ }
36
+
37
+ function ledgerMark(ledger: Ledger): string {
38
+ const r = ledger.db.prepare('SELECT max(id) id, max(ended_at) at FROM rebuilds').get() as { id: number | null; at: string | null } | undefined;
39
+ return `${r?.id ?? 0}:${r?.at ?? ''}`;
40
+ }
41
+
42
+ /** The territories of one repository of the ledger, matched as `Code` matches them (ledger/views.ts `repoOf`): by id, by
43
+ * path, by the path's last part; one that matches none is the main repository's. */
44
+ function territoriesOf(store: ProjectStore, project: Project, ledger: Ledger, repoId: string) {
45
+ const repos = ledger.repos();
46
+ const repoOf = (t: { repo: string }) => (repos.find((r) => r.id === t.repo || pathKey(r.path) === pathKey(t.repo) || endsWithPath(r.path, t.repo)) ?? repos[0])?.id;
47
+ return store.territories.filter((t) => t.projectId === project.id && repoOf(t) === repoId);
48
+ }
49
+
50
+ export function codeMapEngines(ledgerService: LedgerService): CodeMapEngines {
51
+ const cache = new Map<string, { ledger: Ledger; store: ProjectStore; mark: string; index: CodeMapIndex }>();
52
+ const indexOf = (store: ProjectStore, project: Project, ledger: Ledger, repoId: string): CodeMapIndex => {
53
+ const key = `${project.id}\x1f${repoId}`;
54
+ const mark = `${ledgerMark(ledger)}#${storeMark(store)}`;
55
+ const hit = cache.get(key);
56
+ if (hit && hit.ledger === ledger && hit.store === store && hit.mark === mark) return hit.index;
57
+ const index = new CodeMapIndex(ledger, store, repoId);
58
+ cache.set(key, { ledger, store, mark, index });
59
+ return index;
60
+ };
61
+ const withLedger = <T>(project: Project, fn: (l: Ledger) => T): T | null => {
62
+ const l = ledgerService.ledger(project.id);
63
+ if (!l) return null;
64
+ try { return fn(l); } catch { return null; }
65
+ };
66
+ return {
67
+ workTerritories: (store, project) => withLedger(project, (l) => {
68
+ const out: Record<string, WorkTerritoryView[]> = {};
69
+ for (const r of l.repos()) {
70
+ const territories = territoriesOf(store, project, l, r.id);
71
+ if (!territories.length) continue;
72
+ for (const [workId, rows] of indexOf(store, project, l, r.id).territoriesByWork(territories)) out[workId] = [...(out[workId] ?? []), ...rows];
73
+ }
74
+ for (const rows of Object.values(out)) rows.sort((a, b) => b.files - a.files || a.name.localeCompare(b.name));
75
+ return out;
76
+ }),
77
+ gonePaths: (store, project) => withLedger(project, (l) => {
78
+ const out: Record<string, string[]> = {};
79
+ for (const r of l.repos()) {
80
+ const territories = territoriesOf(store, project, l, r.id);
81
+ if (!territories.length) continue;
82
+ const index = indexOf(store, project, l, r.id);
83
+ for (const t of territories) { const gone = index.gonePaths(t.paths); if (gone.length) out[t.id] = gone; }
84
+ }
85
+ return out;
86
+ }),
87
+ fileText: (project, input) => withLedger(project, (l) => {
88
+ const repoId = l.repoId(input.repo ?? null);
89
+ if (input.repo && !repoId) return `${input.repo} is not a repository of this project's ledger.`;
90
+ const refs = l.fileRefs(input.path, repoId ? { repo: repoId } : {});
91
+ if (typeof refs === 'string') return refs;
92
+ const repo = l.repos().find((r) => r.id === refs.repo);
93
+ if (!repo?.head) return `The ledger has no current version of ${refs.path}.`;
94
+ const page = l.docText({ path: refs.path, commit: repo.head, repo: refs.repo }, { from: input.fromLine ?? 1 });
95
+ if (typeof page === 'string') return page;
96
+ return {
97
+ repo: repo.path, path: refs.path, lang: refs.lang, commit: page.commit, occurred: page.occurred, lines: page.lines,
98
+ fromLine: page.fromLine, toLine: page.toLine, nextFromLine: page.nextFromLine ?? null, text: page.text, generated: refs.generated,
99
+ };
100
+ }),
101
+ };
102
+ }