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
@@ -0,0 +1,405 @@
1
+ /**
2
+ * What the workbench receives for increment K (src/model/views-k.ts), assembled from the assets and — when the build
3
+ * carries them — the ledger (src/ledger) and the process engines (src/process). Pure reads, apart from the owner's
4
+ * `No action needed` on a breakpoint or a send-back.
5
+ *
6
+ * Every part that needs the ledger or the engines says so by what it leaves out rather than by inventing it: a work
7
+ * item's steps stay empty until the engines can derive them, a document's versions until the ledger has read them.
8
+ */
9
+ import { cliCommandFor, copyForAgentText } from '../keeper/sendback-text.ts';
10
+ import type { ProjectStore } from '../store/project-store.ts';
11
+ import type { Authorization, KeeperJob, Project, Usage } from '../model/types.ts';
12
+ import type { Breakpoint, Occurred, SemanticPatch, SendBack, SixThing, StepTiming } from '../model/k-types.ts';
13
+ import type {
14
+ BreakpointView, CodeView, DocCurrentView, FourThingsView, GenerationBandView, GenerationPlanTextView, LineageView, PatchView, PlanShapeView,
15
+ ProcessStepView, ProcessView, RoundDocView, RoundStepView, RoundView, ScopeKView, SendBackView, TerritoryDetailView, VersionsView,
16
+ WorkProcessView, WorkTerritoryView,
17
+ } from '../model/views-k.ts';
18
+ import { codeMapEngines, type CodeMapEngines } from '../codemap/engines.ts';
19
+ import { MAX_DOC_BYTES } from '../ledger/docs.ts';
20
+ import { MAX_LOOSE_BYTES } from '../ledger/rebuild.ts';
21
+ import { patchBrief } from '../context/k-briefs.ts';
22
+ import { generationPlanDocs, generationPlanIds } from '../process/generations.ts';
23
+ import { isCandidate } from '../process/breakpoints.ts';
24
+ import { anchorLabel } from '../sources/anchor.ts';
25
+ import type { App } from './app.ts';
26
+ import { draftRefsOfJob } from './drafts-api.ts';
27
+ import { isMainAgentRound, mainAgentParts, mainDocsOf } from './k-rounds-d99.ts';
28
+ import { looksAtStandingNotes, standingCounts } from '../keeper/organize/standing-notes.ts';
29
+ import { HttpError, optionalString, requireString, type HttpApp } from './http.ts';
30
+ import { roundDepth } from '../keeper/organize/takeover-state.ts';
31
+ import { carriedOn } from '../keeper/organize/carried-on.ts';
32
+
33
+ /** The ledger and the process engines as the views use them; absent until a build carries them. */
34
+ export interface KEngines {
35
+ readonly ledger?: {
36
+ coverage(project: Project): ScopeKView['ledger'];
37
+ versions(store: ProjectStore, project: Project, objectId: string): VersionsView | null;
38
+ lineage(store: ProjectStore, project: Project, objectId: string): LineageView | null;
39
+ docCurrent(store: ProjectStore, project: Project, objectId: string): DocCurrentView | null;
40
+ code(store: ProjectStore, project: Project): CodeView | null;
41
+ territory(store: ProjectStore, project: Project, territoryId: string): TerritoryDetailView | null;
42
+ } | null;
43
+ readonly process?: {
44
+ work(store: ProjectStore, project: Project, workId: string): { readonly steps: readonly ProcessStepView[]; readonly four: FourThingsView } | null;
45
+ plan(store: ProjectStore, project: Project, planId: string): PlanShapeView | null;
46
+ /** The work item a work item is a step of (a fix or a check of it), or null. */
47
+ stepOf?(store: ProjectStore, project: Project, workId: string): string | null;
48
+ /** An earlier generation's plan document as it stood (§2.12, D82): its `index` in `Generation.planRefs`, a page of it. */
49
+ generationPlan?(store: ProjectStore, project: Project, generationId: string, index: number, fromLine?: number | null): GenerationPlanTextView | null;
50
+ } | null;
51
+ /**
52
+ * The code map beside the ledger's own views (src/codemap/engines.ts): the territories each work changed, a territory's
53
+ * paths gone from the current version, a file of the current version. `registerKRoutes` supplies it from the app's
54
+ * ledger when the app's engines do not carry it.
55
+ */
56
+ readonly codemap?: CodeMapEngines | null;
57
+ /** The text `Copy for agent` puts on the clipboard, and the CLI command in it (§1.18). */
58
+ readonly copyForAgent?: (projectId: string, sendBack: SendBack, store: ProjectStore) => { readonly text: string; readonly command: string };
59
+ }
60
+
61
+ type FolderAuthorization = Authorization & { readonly projectFolder?: { readonly path: string; readonly commits: boolean; readonly lastWrite: { readonly at: string; readonly commit: string | null } | null } | null };
62
+
63
+ const noActionOf = (r: { readonly reason: string; readonly at: string } | null): { reason: string; at: string } | null => (r ? { reason: r.reason, at: r.at } : null);
64
+
65
+ export function breakpointView(b: Breakpoint): BreakpointView {
66
+ return {
67
+ id: b.id, kind: b.kind, targetId: b.targetId, why: b.why, since: b.since, evidence: b.evidence, basis: b.basis === 'Explicit' ? 'Explicit' : 'Inferred',
68
+ lit: b.lit && !b.ownerResponse, ownerResponse: noActionOf(b.ownerResponse), sixThing: b.sixThing, sendBackId: b.sendBackId,
69
+ state: b.lit && !b.ownerResponse ? 'Lit' : isCandidate(b) ? 'Candidate' : 'Out',
70
+ looked: b.looked ? { where: b.looked.where, at: b.looked.at } : null, checked: b.checked ? { at: b.checked.at } : null,
71
+ };
72
+ }
73
+
74
+ export { cliCommandFor } from '../keeper/sendback-text.ts';
75
+
76
+ /** An object's number as the process view shows it: the project's own first, else the one the Keeper gave (§1.19). */
77
+ function objectNumber(store: ProjectStore, id: string, projectIds: readonly string[]): { value: string; byKeeper: boolean } | null {
78
+ if (projectIds.length) return { value: projectIds[0]!, byKeeper: false };
79
+ const k = store.numbers.find((n) => n.objectId === id);
80
+ return k ? { value: k.projectNumber ?? k.number, byKeeper: k.projectNumber === null } : null;
81
+ }
82
+
83
+ /**
84
+ * The copy is the send-back text module's (Spec §1.18, §7.10): where, the evidence word for word, where to, and the
85
+ * command that shows the same send-back, with credentials redacted, the same text `pk get` prints for it.
86
+ */
87
+ function defaultCopy(store: ProjectStore, projectId: string, sb: SendBack): { text: string; command: string } {
88
+ const thread = store.threads.get(sb.targetId);
89
+ const ref = thread ? undefined : store.reference.get(sb.targetId);
90
+ const label = thread?.title ?? ref?.name ?? null;
91
+ const number = label ? objectNumber(store, sb.targetId, thread?.ids ?? ref?.ids ?? [])?.value ?? null : null;
92
+ return { command: cliCommandFor(projectId, sb.id), text: copyForAgentText(projectId, sb, sb.evidence, { target: label ? { label, number } : null }) };
93
+ }
94
+
95
+ export function sendBackView(store: ProjectStore, projectId: string, sb: SendBack, engines: KEngines = {}): SendBackView {
96
+ const copy = engines.copyForAgent ? engines.copyForAgent(projectId, sb, store) : defaultCopy(store, projectId, sb);
97
+ const at = (o: Occurred) => o;
98
+ return {
99
+ id: sb.id, to: sb.to, stage: sb.stage, targetId: sb.targetId, what: sb.what, suggestion: sb.suggestion, evidence: sb.evidence, occurred: sb.occurred,
100
+ returned: sb.returned ? { label: sb.returned.by.label, occurred: at(sb.returned.by.occurred ?? { at: sb.returned.at, basis: 'First observed', anchor: null }) } : null,
101
+ closed: sb.closed ? { label: sb.closed.by.label, occurred: at(sb.closed.by.occurred ?? { at: sb.closed.at, basis: 'First observed', anchor: null }) } : null,
102
+ ownerResponse: noActionOf(sb.ownerResponse), sixThing: sb.sixThing, copyForAgent: copy.text, cliCommand: copy.command,
103
+ lit: sb.stage !== 'Closed' && !sb.ownerResponse,
104
+ from: { kind: sb.from.kind, id: sb.from.id }, onTerritory: store.territories.has(sb.targetId),
105
+ };
106
+ }
107
+
108
+ // ───────────────────────── the process view ─────────────────────────
109
+
110
+
111
+ export function processView(store: ProjectStore, project: Project, engines: KEngines = {}): ProcessView {
112
+ const breakpoints = store.breakpoints.all().map(breakpointView);
113
+ const sendBacks = store.sendbacks.all().sort((a, b) => a.occurred.at.localeCompare(b.occurred.at)).map((sb) => sendBackView(store, project.id, sb, engines));
114
+ const litOn = new Map<string, string[]>();
115
+ for (const b of breakpoints) if (b.lit) litOn.set(b.targetId, [...(litOn.get(b.targetId) ?? []), b.id]);
116
+ const openOn = new Map<string, string[]>();
117
+ for (const s of sendBacks) if (s.lit) openOn.set(s.targetId, [...(openOn.get(s.targetId) ?? []), s.id]);
118
+ const sixOn = new Map<string, Set<SixThing>>();
119
+ const tagSix = (id: string, t: SixThing | null | undefined) => { if (!t) return; if (!sixOn.has(id)) sixOn.set(id, new Set()); sixOn.get(id)!.add(t); };
120
+ for (const m of store.marks.filter((x) => !x.closed)) tagSix(m.targetId, m.sixThing);
121
+ for (const n of store.notes.filter((x) => x.status === 'Current')) for (const id of n.mount.ids) tagSix(id, n.sixThing);
122
+ for (const b of breakpoints) if (b.lit) tagSix(b.targetId, b.sixThing);
123
+ for (const s of sendBacks) if (s.lit) tagSix(s.targetId, s.sixThing);
124
+ // A code anomaly counts where it is tagged; the two kinds §2.13 row 6 names are 6 even before anyone tags them.
125
+ for (const t of store.territories.all()) for (const a of t.anomalies) tagSix(t.id, a.sixThing ?? (a.kind === 'Unreferenced' || a.kind === 'Looks residual, is live' ? 6 : null));
126
+ // CZ: a work item carried on under the same number is current work: it belongs to no earlier generation here, though
127
+ // its generation's band still lists it (with where it went).
128
+ const carried = carriedOn(store);
129
+ const generationOf = new Map<string, string>();
130
+ for (const g of store.generations.all()) for (const w of g.workIds) if (!carried.has(w)) generationOf.set(w, g.id);
131
+ const numberOf = (id: string, projectIds: readonly string[]): WorkProcessView['number'] => objectNumber(store, id, projectIds);
132
+ // The territories each work changed, once for the whole view (CKC-25 AC-7): absent without a code map.
133
+ const changedCode: Readonly<Record<string, readonly WorkTerritoryView[]>> | null = engines.codemap?.workTerritories(store, project) ?? null;
134
+
135
+ const works: Record<string, WorkProcessView> = {};
136
+ for (const t of store.threads.all()) {
137
+ const derived = engines.process?.work(store, project, t.id) ?? null;
138
+ const breakpointIds = litOn.get(t.id) ?? [];
139
+ const sendBackIds = openOn.get(t.id) ?? [];
140
+ const four: FourThingsView = derived?.four ?? {
141
+ // Without the engine the ledger says nothing about execution, so none is shown rather than one guessed from progress.
142
+ execution: null, progress: t.progress, check: null,
143
+ open: { findings: 0, sendBacks: sendBackIds.length, breakpoints: breakpointIds.length },
144
+ };
145
+ const steps = derived?.steps ?? [];
146
+ const sent = steps.filter((s) => s.sendBackId).length;
147
+ works[t.id] = {
148
+ workId: t.id, number: numberOf(t.id, t.ids), steps, four,
149
+ folded: `${steps.length} step${steps.length === 1 ? '' : 's'}${sent ? ` · ${sent} send-back${sent === 1 ? '' : 's'}` : ''}${sendBackIds.length || breakpointIds.length || four.open.findings ? ' · open items' : steps.length ? ' · all closed' : ''}`,
150
+ stepOf: engines.process?.stepOf?.(store, project, t.id) ?? null,
151
+ generationId: generationOf.get(t.id) ?? null,
152
+ struck: t.validity === 'Replaced' || t.validity === 'Abandoned' || t.validity === 'Deferred' ? { validity: t.validity, why: t.replacedBy ? `Replaced by ${t.replacedBy}` : t.unresolved || t.validity, byId: t.replacedBy } : null,
153
+ breakpointIds, sendBackIds, sixThings: [...(sixOn.get(t.id) ?? [])],
154
+ ...(changedCode ? { territories: changedCode[t.id] ?? [] } : {}),
155
+ };
156
+ }
157
+ const plans: Record<string, PlanShapeView> = {};
158
+ const docs: Record<string, DocCurrentView> = {};
159
+ for (const r of store.reference.all()) {
160
+ if (r.category === 'Plan') { const shape = engines.process?.plan(store, project, r.id); if (shape) plans[r.id] = shape; }
161
+ if (['Product', 'Requirement', 'Design', 'Decision', 'Plan'].includes(r.category)) { const cur = engines.ledger?.docCurrent(store, project, r.id); if (cur) docs[r.id] = cur; }
162
+ }
163
+ // A plan with an execution shape is a unit of the List and the Graph too (its batches fold under it, CKC-24 AC-2): it
164
+ // has no steps of its own, and its four things are what the plan item and what hangs on it say. Without this entry the
165
+ // live List drew "No data" where the plan's execution order belongs (found while fixing QC AY B11).
166
+ // An earlier generation's plans, by generation (its band draws them; the plan's unit names its generation).
167
+ const bandPlans = new Map(store.generations.all().map((g) => [g.id, generationPlanIds(store, g)]));
168
+ const planGeneration = new Map([...bandPlans].flatMap(([gid, ids]) => ids.map((id) => [id, gid] as const)));
169
+ for (const [id, shape] of Object.entries(plans)) {
170
+ if (works[id]) continue;
171
+ const r = store.reference.get(id)!;
172
+ const breakpointIds = litOn.get(id) ?? [];
173
+ const sendBackIds = openOn.get(id) ?? [];
174
+ works[id] = {
175
+ workId: id, number: numberOf(id, r.ids), steps: [],
176
+ four: { execution: null, progress: r.progress ?? '', check: null, open: { findings: 0, sendBacks: sendBackIds.length, breakpoints: breakpointIds.length } },
177
+ folded: `${shape.batches.length} batch${shape.batches.length === 1 ? '' : 'es'}${shape.differs ? ' · execution differed from the plan' : ''}${sendBackIds.length || breakpointIds.length ? ' · open items' : ''}`,
178
+ stepOf: null, generationId: planGeneration.get(id) ?? null,
179
+ struck: r.validity === 'Replaced' || r.validity === 'Abandoned' || r.validity === 'Deferred' ? { validity: r.validity, why: r.replacedBy ? `Replaced by ${r.replacedBy}` : r.validity, byId: r.replacedBy } : null,
180
+ breakpointIds, sendBackIds, sixThings: [...(sixOn.get(id) ?? [])],
181
+ };
182
+ }
183
+ // An earlier generation's band holds its plans as well as its work (CKC-24 AC-18): the plan objects read from its plan
184
+ // documents or planned into by its work, and the documents themselves, read as they stood when the band is unrolled.
185
+ const generations: GenerationBandView[] = store.generations.all().sort((a, b) => a.ended.at.localeCompare(b.ended.at)).map((g) => {
186
+ const items = g.workIds.flatMap((id) => store.threads.get(id) ?? []);
187
+ const planIds = bandPlans.get(g.id) ?? [];
188
+ return {
189
+ id: g.id, name: g.name, started: g.started, ended: g.ended, endedBy: g.endedBy, workIds: g.workIds, carriedIds: g.workIds.filter((id) => carried.has(id)),
190
+ planIds, plans: planIds.flatMap((id) => { const r = store.reference.get(id); return r ? [{ id, name: r.name, validity: r.validity }] : []; }),
191
+ planDocs: generationPlanDocs(store, g),
192
+ planned: items.length, done: items.filter((t) => t.progress === 'Done').length, struck: items.filter((t) => t.validity === 'Replaced' || t.validity === 'Abandoned' || t.validity === 'Deferred').length,
193
+ };
194
+ });
195
+ const byKind: Record<string, number> = {};
196
+ for (const b of breakpoints) if (b.lit) byKind[b.kind] = (byKind[b.kind] ?? 0) + 1;
197
+ const ledgerAsOf = store.clerkRounds.all().filter((r) => r.ledger).map((r) => r.startedAt).sort().pop() ?? null;
198
+ return {
199
+ projectId: project.id, asOf: new Date().toISOString(), ledgerAsOf, works, plans, docs, generations, breakpoints, sendBacks,
200
+ sixThings: ([1, 2, 3, 4, 5, 6] as SixThing[]).map((thing) => ({ thing, objectIds: [...sixOn].filter(([, s]) => s.has(thing)).map(([id]) => id) })),
201
+ // Only lit breakpoints are findings (D99): candidates are counted apart and never shown red.
202
+ counts: { breakpointsLit: breakpoints.filter((b) => b.lit).length, byKind, sendBacksOpen: sendBacks.filter((s) => s.lit).length, breakpointCandidates: breakpoints.filter((b) => b.state === 'Candidate').length },
203
+ };
204
+ }
205
+
206
+ // ───────────────────────── a round in the Keeper view ─────────────────────────
207
+
208
+ const zero: Usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
209
+ const addUsage = (a: Usage, b: Usage): Usage => ({ input: a.input + b.input, output: a.output + b.output, cacheRead: a.cacheRead + b.cacheRead, cacheWrite: a.cacheWrite + b.cacheWrite, cost: a.cost === null || b.cost === null ? (a.cost ?? b.cost) : a.cost + b.cost });
210
+ const addTiming = (a: StepTiming, b: StepTiming): StepTiming => ({ wallMs: a.wallMs + b.wallMs, generationMs: a.generationMs + b.generationMs, toolMs: a.toolMs + b.toolMs, queueMs: a.queueMs + b.queueMs, parseRetryMs: a.parseRetryMs + b.parseRetryMs, otherMs: a.otherMs + b.otherMs });
211
+
212
+ export function roundsView(store: ProjectStore): RoundView[] {
213
+ const docsOf = (roundId: string, jobId: string | null) => store.roundDocs.filter((d) => d.roundId === roundId && (jobId === null || d.jobId === jobId)).map((d) => ({ id: d.id, kind: d.kind, title: d.title, path: d.path, at: d.at }));
214
+ // A job's children are the jobs it sent that are not steps of the round themselves (an investigation, say): a lane the
215
+ // main agent sent is a step of the round and stands under the main agent by `RoundView.lanes`.
216
+ const stepView = (j: KeeperJob): RoundStepView => ({
217
+ jobId: j.id, kind: j.step!.kind, label: j.scope.label, path: j.step!.path, status: j.status, model: j.model, startedAt: j.startedAt, endedAt: j.endedAt,
218
+ timing: j.timing ?? null, usage: j.agent === 'program' ? null : j.usage, docs: docsOf(j.step!.roundId, j.id),
219
+ children: store.jobs.filter((c) => c.parentJobId === j.id && c.step?.roundId !== j.step!.roundId).map((c) => ({ jobId: c.id, kind: j.step!.kind, label: c.scope.label, path: null, status: c.status, model: c.model, startedAt: c.startedAt, endedAt: c.endedAt, timing: c.timing ?? null, usage: c.usage, docs: [], children: [] })),
220
+ // The session drafts the step wrote, each openable where the round is drilled into (CKC-23 AC-18).
221
+ ...(j.step!.kind === 'session-drafts' ? { drafts: draftRefsOfJob(store, j.id) } : {}),
222
+ });
223
+ return store.clerkRounds.all().sort((a, b) => b.startedAt.localeCompare(a.startedAt)).map((r) => {
224
+ const steps = store.jobs.filter((j) => j.step?.roundId === r.id).sort((a, b) => a.queuedAt.localeCompare(b.queuedAt));
225
+ const timings = steps.flatMap((j) => (j.timing ? [j.timing] : []));
226
+ // What the round spent includes the jobs its steps sent (an investigation, say); their time is already inside the
227
+ // step that waited for them, so they add to the usage, not to the timing (QC AY). A lane is both a step and a job the
228
+ // main agent sent: counted once.
229
+ const descendants = (ids: readonly string[]): KeeperJob[] => { const kids = store.jobs.filter((c) => c.parentJobId !== null && ids.includes(c.parentJobId)); return kids.length ? [...kids, ...descendants(kids.map((k) => k.id))] : []; };
230
+ const usageOf = (jobs: readonly KeeperJob[]): Usage => [...new Map(jobs.map((j) => [j.id, j])).values()].filter((j) => j.agent !== 'program').reduce((u, j) => addUsage(u, j.usage), zero);
231
+ const usage = usageOf([...steps, ...descendants(steps.map((j) => j.id))]);
232
+ const wallMs = r.endedAt ? Date.parse(r.endedAt) - Date.parse(r.startedAt) : Date.now() - Date.parse(r.startedAt);
233
+ // With several paths at once a round lasts as long as its longest path (§3.10): the steps in sequence, each as long
234
+ // as its slowest job. The main agent waits while its lanes run, so its own time already holds the longest lane.
235
+ const byStep = new Map<string, number>();
236
+ for (const j of steps) { if (j.step!.kind === 'lane') continue; const w = j.timing ? j.timing.wallMs + j.timing.queueMs : 0; byStep.set(j.step!.kind, Math.max(byStep.get(j.step!.kind) ?? 0, w)); }
237
+ // A round the main agent runs (D99): its stages, its lanes, the coverage check and the missing steps looked for.
238
+ const d99 = isMainAgentRound(r) ? mainAgentParts(store, r, steps, usageOf) : null;
239
+ const views = steps.map(stepView).map((s) => (d99 && s.kind === 'main' ? { ...s, docs: mainDocsOf(s.docs, d99.lanes ?? []) } : s));
240
+ // The sweeps the program added for the kinds of question orientation left out (CKC-23 AC-4), each with why and the
241
+ // brief it composed: a document of this round no step's job wrote, so `docsOf` gives it to no step.
242
+ const composed = (path: string) => store.roundDocs.find((d) => d.roundId === r.id && d.kind === 'Brief' && d.jobId === null && (d.path ?? d.title) === path);
243
+ const sweepsAdded = (r.sweepsAdded ?? []).map((s) => { const d = composed(s.path); return { path: s.path, why: s.why, doc: d ? { id: d.id, kind: d.kind, title: d.title, path: d.path, at: d.at } : null }; });
244
+ return {
245
+ id: r.id, kind: r.kind, number: r.number, rootJobId: r.rootJobId, depth: roundDepth(r, store.jobs.get(r.rootJobId)), startedBy: r.startedBy ?? null, startedAt: r.startedAt, endedAt: r.endedAt, status: r.status, wallMs,
246
+ longestPathMs: [...byStep.values()].reduce((n, x) => n + x, 0),
247
+ timing: timings.length ? timings.reduce(addTiming) : null, usage,
248
+ steps: views, outputs: r.outputs, groundwork: r.groundwork, unplaced: r.unplaced, spotCheck: r.spotCheck, ledger: r.ledger, folder: r.folder ?? null,
249
+ // D103: what the round did with the notes standing from earlier rounds — as counted at its close, live while it runs.
250
+ ...(looksAtStandingNotes(r) ? { standingNotes: r.standingNotes ?? (r.status === 'Running' ? standingCounts(store, r) : null) } : {}),
251
+ ...(sweepsAdded.length ? { sweepsAdded } : {}),
252
+ ...(d99 ?? {}),
253
+ };
254
+ });
255
+ }
256
+
257
+ export function roundDocView(store: ProjectStore, roundId: string, docId: string): RoundDocView | null {
258
+ const d = store.roundDocs.get(docId);
259
+ return d && d.roundId === roundId ? { id: d.id, roundId: d.roundId, kind: d.kind, title: d.title, path: d.path, markdown: d.markdown, at: d.at } : null;
260
+ }
261
+
262
+ // ───────────────────────── `Code` (§6.17) ─────────────────────────
263
+
264
+ /**
265
+ * The `Code` view as the workbench receives it: the ledger's view of the territories, with what the code map and the assets
266
+ * add — the paths a territory names that the current version no longer has (CKC-25 AC-10), the notes behind each anomaly's
267
+ * `noteIds` by title (the jump to them, AC-7), and the send-backs its anomalies carry, so `Code` shows them without the
268
+ * process view loaded first (QC AY B6). Null when the build has no ledger.
269
+ */
270
+ export function codeViewOf(store: ProjectStore, project: Project, engines: KEngines = {}): CodeView | null {
271
+ const base = engines.ledger?.code(store, project) ?? null;
272
+ if (!base) return null;
273
+ const gone = engines.codemap?.gonePaths(store, project) ?? {};
274
+ const noteOf = (id: string) => {
275
+ const n = store.notes.get(id);
276
+ const v = n?.versions[n.versions.length - 1];
277
+ return n && v ? [{ id: n.id, title: v.title, ask: v.ask, status: n.status }] : [];
278
+ };
279
+ const sendBacks: Record<string, SendBackView> = {};
280
+ const territories = base.territories.map((t) => ({
281
+ ...t,
282
+ ...(gone[t.id]?.length ? { gonePaths: gone[t.id] } : {}),
283
+ anomalies: t.anomalies.map((a) => {
284
+ const sb = a.sendBackId ? store.sendbacks.get(a.sendBackId) : undefined;
285
+ if (sb) sendBacks[sb.id] = sendBackView(store, project.id, sb, engines);
286
+ return { ...a, notes: a.noteIds.flatMap(noteOf) };
287
+ }),
288
+ }));
289
+ return { ...base, territories, sendBacks };
290
+ }
291
+
292
+ // ───────────────────────── semantic patches (§1.17; CKC-26 AC-4) ─────────────────────────
293
+
294
+ /** An object's name as the workbench shows it, whatever kind of asset it is. */
295
+ function objectLabel(store: ProjectStore, id: string): string {
296
+ const s = store.sources.get(id);
297
+ return store.nodes.get(id)?.label ?? store.reference.get(id)?.name ?? store.threads.get(id)?.title ?? store.territories.get(id)?.name
298
+ ?? store.generations.get(id)?.name ?? store.layers.get(id)?.path ?? store.patches.get(id)?.title ?? (s ? anchorLabel(s.anchor) : id);
299
+ }
300
+
301
+ /** An object's name as the agent entry names it in a brief (server/k-agent-api.ts): the same words the CLI prints. */
302
+ const briefName = (store: ProjectStore, id: string): string => store.threads.get(id)?.title ?? store.reference.get(id)?.name ?? store.nodes.get(id)?.label
303
+ ?? store.territories.get(id)?.name ?? store.patches.get(id)?.title ?? id;
304
+
305
+ /** A semantic patch as `Change log` and its details show it; `withText` adds the text `pk get SP-n` prints, word for word. */
306
+ export function patchView(store: ProjectStore, p: SemanticPatch, withText = false): PatchView {
307
+ return {
308
+ id: p.id, number: p.number, title: p.title, status: p.status, partial: p.partial, occurred: p.occurred,
309
+ invalidated: p.invalidated, replacedBy: p.replacedBy, affects: p.affects.map((id) => ({ id, label: objectLabel(store, id) })), affectsText: p.affectsText,
310
+ mustNotPassAsCurrent: p.mustNotPassAsCurrent, oldAnchor: p.oldAnchor, newAnchor: p.newAnchor, decision: p.decision, candidate: p.candidate,
311
+ writtenToFolder: p.writtenToFolder,
312
+ ...(withText ? { text: patchBrief(p, (id) => briefName(store, id)) } : {}),
313
+ };
314
+ }
315
+
316
+ /** Every semantic patch, oldest first by when the supersession happened (§2.11); `Change log` shows the confirmed ones. */
317
+ export function patchesView(store: ProjectStore): PatchView[] {
318
+ const ms = (o: Occurred) => Date.parse(/^\d{4}-\d\d-\d\d$/.test(o.at) ? `${o.at}T00:00:00Z` : o.at) || 0;
319
+ return store.patches.all().sort((a, b) => ms(a.occurred) - ms(b.occurred) || a.number.localeCompare(b.number, undefined, { numeric: true })).map((p) => patchView(store, p));
320
+ }
321
+
322
+ // ───────────────────────── Project scope additions ─────────────────────────
323
+
324
+ export function scopeKView(store: ProjectStore, project: Project, engines: KEngines = {}): ScopeKView {
325
+ const firstRound = store.clerkRounds.filter((r) => r.kind === 'First usable').sort((a, b) => b.startedAt.localeCompare(a.startedAt))[0];
326
+ const questions = firstRound ? store.roundDocs.find((d) => d.roundId === firstRound.id && d.kind === 'Questions') : undefined;
327
+ // The `Project folder` authorization (§1.14): the one standing authorization with a folder, not revoked.
328
+ const auth = store.authorizations.find((a) => !a.revokedAt && (a as FolderAuthorization).projectFolder != null) as FolderAuthorization | undefined;
329
+ // What the ledger keeps without reading it says the sizes it reads to, so Project scope can name them (QC AY; Spec §6.7).
330
+ const ledger = engines.ledger?.coverage(project) ?? null;
331
+ return {
332
+ ledger: ledger?.notRead ? { ...ledger, notRead: { ...ledger.notRead, documentLimitBytes: MAX_DOC_BYTES, looseLimitBytes: MAX_LOOSE_BYTES } } : ledger,
333
+ layers: store.layers.all().sort((a, b) => a.path.localeCompare(b.path)).map((l) => ({ path: l.path, layer: l.layer, note: l.note, current: l.current })),
334
+ generations: store.generations.all().sort((a, b) => a.ended.at.localeCompare(b.ended.at)).map((g) => ({ id: g.id, name: g.name, ended: g.ended, endedBy: g.endedBy })),
335
+ questions: questions && firstRound ? { roundId: firstRound.id, docId: questions.id, paths: store.roundDocs.filter((d) => d.roundId === firstRound.id && d.kind === 'Brief').map((d) => d.path ?? d.title) } : null,
336
+ projectFolder: auth?.projectFolder ? { granted: true, path: auth.projectFolder.path, commits: auth.projectFolder.commits, lastWrite: auth.projectFolder.lastWrite, authorizationId: auth.id } : { granted: false, path: null, commits: true, lastWrite: null, authorizationId: null },
337
+ };
338
+ }
339
+
340
+ // ───────────────────────── the owner's `No action needed` ─────────────────────────
341
+
342
+ export function respondToBreakpoint(store: ProjectStore, id: string, reason: string): BreakpointView {
343
+ const b = store.breakpoints.get(id);
344
+ if (!b) throw new HttpError(404, 'Unknown breakpoint');
345
+ const now = new Date().toISOString();
346
+ const next: Breakpoint = { ...b, ownerResponse: { text: 'No action needed', reason, at: now, sourceId: null }, updatedAt: now };
347
+ store.breakpoints.put(next, { jobId: null, summary: `The owner: no action needed on ${b.kind} (${b.targetId})` });
348
+ return breakpointView(next);
349
+ }
350
+
351
+ export function respondToSendBack(store: ProjectStore, projectId: string, id: string, reason: string, engines: KEngines = {}): SendBackView {
352
+ const sb = store.sendbacks.get(id);
353
+ if (!sb) throw new HttpError(404, 'Unknown send-back');
354
+ const now = new Date().toISOString();
355
+ const next: SendBack = { ...sb, ownerResponse: { text: 'No action needed', reason, at: now, sourceId: null }, updatedAt: now };
356
+ store.sendbacks.put(next, { jobId: null, summary: `The owner: no action needed on send-back ${sb.id}` });
357
+ return sendBackView(store, projectId, next, engines);
358
+ }
359
+
360
+ // ───────────────────────── routes ─────────────────────────
361
+
362
+ export function registerKRoutes(http: HttpApp, app: App): void {
363
+ // The code map rides on the app's ledger when the app's engines do not carry it already (one per registration).
364
+ let codemap: CodeMapEngines | null = null;
365
+ const engines = (): KEngines => ({ ...app.kEngines, codemap: app.kEngines.codemap ?? (codemap ??= codeMapEngines(app.ledger)) });
366
+ const notYet = (what: string) => new HttpError(404, `${what} needs the ledger, which this build does not carry yet`);
367
+ http.route('GET', '/api/projects/:id/process', ({ params }) => processView(app.store(params.id!), app.project(params.id!), engines()));
368
+ http.route('GET', '/api/projects/:id/objects/:oid/lineage', ({ params }) => engines().ledger?.lineage(app.store(params.id!), app.project(params.id!), params.oid!) ?? (() => { throw notYet('How it got here'); })());
369
+ http.route('GET', '/api/projects/:id/objects/:oid/versions', ({ params }) => engines().ledger?.versions(app.store(params.id!), app.project(params.id!), params.oid!) ?? (() => { throw notYet('The versions'); })());
370
+ http.route('GET', '/api/projects/:id/code', ({ params }) => codeViewOf(app.store(params.id!), app.project(params.id!), engines()) ?? (() => { throw notYet('Code'); })());
371
+ // A file of the current version, found by the ledger's fileRefs (§6.17 "点文件打开原文阅读"; QC AY B13).
372
+ http.route('GET', '/api/projects/:id/code/file', ({ params, query }) => {
373
+ const project = app.project(params.id!);
374
+ const path = requireString(query.get('path'), 'path');
375
+ const from = Number(query.get('from') ?? '1');
376
+ const page = engines().codemap?.fileText(project, { path, repo: optionalString(query.get('repo')), fromLine: Number.isFinite(from) && from > 0 ? Math.floor(from) : 1 }) ?? null;
377
+ if (page === null) throw notYet('A file of the current version');
378
+ if (typeof page === 'string') throw new HttpError(404, page);
379
+ return page;
380
+ });
381
+ // An earlier generation's plan document as it stood — before its deletion, or when the generation ended (D82).
382
+ http.route('GET', '/api/projects/:id/generations/:gid/plans/:n', ({ params, query }) => {
383
+ const store = app.store(params.id!);
384
+ const g = store.generations.get(params.gid!);
385
+ if (!g) throw new HttpError(404, 'Unknown generation');
386
+ const index = Number(params.n);
387
+ if (!Number.isInteger(index) || index < 0 || index >= g.planRefs.length) throw new HttpError(404, `${g.name} has no plan document ${params.n}`);
388
+ const from = Number(query.get('from') ?? '1');
389
+ return engines().process?.generationPlan?.(store, app.project(params.id!), g.id, index, Number.isFinite(from) && from > 0 ? Math.floor(from) : 1) ?? (() => { throw notYet('An earlier plan document'); })();
390
+ });
391
+ // Semantic patches: every one for `Change log` (which shows the confirmed), and one with its details (CKC-26 AC-4).
392
+ http.route('GET', '/api/projects/:id/patches', ({ params }) => ({ patches: patchesView(app.store(params.id!)) }));
393
+ http.route('GET', '/api/projects/:id/patches/:pid', ({ params }) => {
394
+ const store = app.store(params.id!);
395
+ const p = store.patches.get(params.pid!) ?? store.patches.find((x) => x.number === params.pid);
396
+ if (!p) throw new HttpError(404, 'Unknown semantic patch');
397
+ return patchView(store, p, true);
398
+ });
399
+ http.route('GET', '/api/projects/:id/code/territories/:tid', ({ params }) => engines().ledger?.territory(app.store(params.id!), app.project(params.id!), params.tid!) ?? (() => { throw notYet('The territory'); })());
400
+ http.route('GET', '/api/projects/:id/k-rounds', ({ params }) => roundsView(app.store(params.id!)));
401
+ http.route('GET', '/api/projects/:id/k-rounds/:rid/docs/:docId', ({ params }) => roundDocView(app.store(params.id!), params.rid!, params.docId!) ?? (() => { throw new HttpError(404, 'Unknown round document'); })());
402
+ http.route('GET', '/api/projects/:id/scope-k', ({ params }) => scopeKView(app.store(params.id!), app.project(params.id!), engines()));
403
+ http.route('POST', '/api/projects/:id/breakpoints/:bid/response', ({ params, body }) => respondToBreakpoint(app.store(params.id!), params.bid!, requireString((body as { reason?: unknown })?.reason, 'reason')));
404
+ http.route('POST', '/api/projects/:id/sendbacks/:sid/response', ({ params, body }) => respondToSendBack(app.store(params.id!), params.id!, params.sid!, requireString((body as { reason?: unknown })?.reason, 'reason'), engines()));
405
+ }