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,280 @@
1
+ /**
2
+ * Change follow-up (Spec §5.5, CKC-11 AC-14–AC-19; owner D48). After something upstream moved, the next round of
3
+ * daily upkeep walks its downstream and says, for each one, how it stands. This is a judgement, not a re-check: the
4
+ * Keeper does not re-run verification, does not re-do work an agent already did, and does not grade anyone.
5
+ *
6
+ * The work is organized by object, not by change. On a project that has been worked on for a while, one object sits
7
+ * downstream of many changes (on the trial, 4871 pending entries were 227 objects, a median of 18 changes each), so a
8
+ * batch takes a few objects of one area and shows each with every change that reached it: the object is read once and
9
+ * judged against all of them.
10
+ *
11
+ * Since D56 the asset is per object and per round as well: one object gets one state covering every item that
12
+ * reached it, written with pk_judge_object. Objects that are never judged — point-in-time records and decisions
13
+ * (§2.10) — never enter a batch and never count as pending, and an item settled in an earlier round comes back only
14
+ * when the object itself has moved.
15
+ */
16
+ import type { ChangeRecord, GraphNode, PropagationEntry } from '../../model/types.ts';
17
+ import type { ProjectStore } from '../../store/project-store.ts';
18
+ import { itemKey, itemStandings, itemsForObject, itemsOf, notJudgedReason, objectUpdatedAt, openRound, type ItemStanding } from '../adjustment.ts';
19
+
20
+ export interface PendingFollowUp {
21
+ readonly changeId: string;
22
+ readonly nodeId: string;
23
+ /** Which items of that record reached this object. Absent on an entry seeded before it was recorded, which is
24
+ * read as the whole record. */
25
+ readonly itemIds?: readonly string[];
26
+ }
27
+ export interface FollowUpBatch {
28
+ /** The area these downstream objects belong to; null for objects that hang from nothing. */
29
+ readonly areaId: string | null;
30
+ readonly areaName: string;
31
+ /** The objects of this batch, each with the pending entries (one per change) to judge. */
32
+ readonly objects: readonly { readonly nodeId: string; readonly entries: readonly PendingFollowUp[] }[];
33
+ readonly entries: readonly PendingFollowUp[];
34
+ readonly changeIds: readonly string[];
35
+ }
36
+
37
+ /** The key of one entry: a change and one of its downstream objects. */
38
+ export const entryKey = (e: PendingFollowUp) => `${e.changeId}>${e.nodeId}`;
39
+
40
+ export interface BatchLimits {
41
+ /** At most this many objects in one batch. */
42
+ readonly objects: number;
43
+ /** A batch stops taking objects once it holds this many entries; an object with more still goes whole, alone. */
44
+ readonly entries: number;
45
+ }
46
+ export const DEFAULT_LIMITS: BatchLimits = { objects: 8, entries: 80 };
47
+
48
+ /** Whether an object still has something for this round: at least one item of a change that reached it has no
49
+ * conclusion yet. An object whose outstanding items were all settled in an earlier round, and which has not moved
50
+ * since, is not judged again (§5.5). */
51
+ export function objectPending(store: ProjectStore, nodeId: string): boolean {
52
+ if (notJudgedReason(store, nodeId) !== null) return false;
53
+ const roundId = openRound(store)?.id ?? 'round_9999';
54
+ return itemsForObject(store, nodeId, roundId).covers.length > 0;
55
+ }
56
+
57
+ /**
58
+ * Entries still to judge, grouped by object and the objects by area; an area's objects with the most waiting go
59
+ * first. Entries already handed to a running job are left out one by one, so a change arriving mid-round never hands
60
+ * the same pair out twice.
61
+ */
62
+ export function pendingByArea(store: ProjectStore, limits: BatchLimits = DEFAULT_LIMITS, inFlight: ReadonlySet<string> = new Set()): FollowUpBatch[] {
63
+ const nodes = new Map(store.nodes.all().map((n) => [n.id, n]));
64
+ const areaName = (id: string | null) => (id ? store.reference.get(id)?.name ?? id : 'No area');
65
+ const areaOfNode = (n: GraphNode | undefined): string | null => (n ? (n.category === 'Area' ? n.id : n.areaId ?? null) : null);
66
+ const byObject = new Map<string, PendingFollowUp[]>();
67
+ const judged = new Map<string, boolean>();
68
+ const takes = (id: string) => { if (!judged.has(id)) judged.set(id, objectPending(store, id)); return judged.get(id)!; };
69
+ const roundId = openRound(store)?.id ?? null;
70
+ // One object, one judgement, one round: an object this round has already judged is not handed out again, however
71
+ // its individual entries read.
72
+ const done = (nodeId: string) => roundId !== null && store.propagation.get(`${roundId}:${nodeId}`) !== undefined;
73
+ // An entry a model has already answered — including "Not yet checked" with the reason it could not check it —
74
+ // is not handed back, or the round would ask the same question forever. It comes back when the object itself
75
+ // moves after that answer: §5.5 re-opens an object as soon as it changes. The answer is the round's judgement, not
76
+ // the entry's own time: other writers touch an entry too — the seeding merges into it an item that reached the object
77
+ // later, and refreshes its time — and an item that arrived after the judgement has not been asked about at all.
78
+ const standings = new Map<string, Map<string, ItemStanding>>();
79
+ const standingsOf = (nodeId: string) => {
80
+ if (!standings.has(nodeId)) standings.set(nodeId, itemStandings(store, nodeId, roundId ?? 'round_9999'));
81
+ return standings.get(nodeId)!;
82
+ };
83
+ const answered = (c: ChangeRecord, p: PropagationEntry) => {
84
+ if (roundId !== null && p.roundId === roundId) return true;
85
+ if (!p.sourceOrReason) return false;
86
+ const judgement = p.roundId ? store.propagation.get(`${p.roundId}:${p.nodeId}`) : undefined;
87
+ if (!judgement) return objectUpdatedAt(store, p.nodeId) <= p.updatedAt;
88
+ const arrived = p.itemIds ?? itemsOf(c).map((i) => i.id);
89
+ if (arrived.some((itemId) => !standingsOf(p.nodeId).has(itemKey({ changeId: c.id, itemId })))) return false;
90
+ return objectUpdatedAt(store, p.nodeId) <= judgement.at;
91
+ };
92
+ for (const c of store.changes.all().sort((a, b) => b.at.localeCompare(a.at))) {
93
+ for (const p of c.propagation) {
94
+ if (!nodes.has(p.nodeId) && !store.reference.has(p.nodeId) && !store.threads.has(p.nodeId)) continue;
95
+ if (inFlight.has(`${c.id}>${p.nodeId}`)) continue;
96
+ if (done(p.nodeId) || answered(c, p)) continue;
97
+ // §2.10: point-in-time records and decisions never enter a batch, and an item settled in an earlier round
98
+ // comes back only after the object itself has moved. The entry's own state is not the gate — scanning only
99
+ // `Not yet checked` meant a changed object was pending by rule (§5.5 re-opens it as soon as it changes) and
100
+ // yet no batch ever carried it: `objectPending` said true while `pendingByArea` came back empty.
101
+ if (!takes(p.nodeId)) continue;
102
+ if (!byObject.has(p.nodeId)) byObject.set(p.nodeId, []);
103
+ byObject.get(p.nodeId)!.push({ changeId: c.id, nodeId: p.nodeId, itemIds: p.itemIds });
104
+ }
105
+ }
106
+ const byArea = new Map<string, { nodeId: string; entries: PendingFollowUp[] }[]>();
107
+ for (const [nodeId, entries] of byObject) {
108
+ const key = areaOfNode(nodes.get(nodeId)) ?? '';
109
+ if (!byArea.has(key)) byArea.set(key, []);
110
+ byArea.get(key)!.push({ nodeId, entries });
111
+ }
112
+ const out: FollowUpBatch[] = [];
113
+ for (const [key, objects] of byArea) {
114
+ objects.sort((a, b) => b.entries.length - a.entries.length || a.nodeId.localeCompare(b.nodeId));
115
+ let current: { nodeId: string; entries: PendingFollowUp[] }[] = [];
116
+ let count = 0;
117
+ const flush = () => {
118
+ if (!current.length) return;
119
+ const entries = current.flatMap((o) => o.entries);
120
+ out.push({ areaId: key || null, areaName: areaName(key || null), objects: current, entries, changeIds: [...new Set(entries.map((e) => e.changeId))] });
121
+ current = []; count = 0;
122
+ };
123
+ for (const o of objects) {
124
+ if (current.length && (current.length >= limits.objects || count + o.entries.length > limits.entries)) flush();
125
+ current.push(o); count += o.entries.length;
126
+ }
127
+ flush();
128
+ }
129
+ // The batches with the most waiting first: the area the owner is most likely to be looking at has the most to say.
130
+ return out.sort((a, b) => b.entries.length - a.entries.length);
131
+ }
132
+
133
+ /**
134
+ * What is still to judge. Objects §2.10 never judges are not counted — the old count called 5,237 pairs pending on
135
+ * the 2026-09-18 assets, and about six in ten of what came back was a closed record judged behind.
136
+ */
137
+ export function pendingCount(store: ProjectStore): { entries: number; changes: number; objects: number; notJudged: number } {
138
+ let entries = 0;
139
+ const changes = new Set<string>();
140
+ const objects = new Set<string>();
141
+ const judged = new Map<string, boolean>();
142
+ const takes = (id: string) => { if (!judged.has(id)) judged.set(id, objectPending(store, id)); return judged.get(id)!; };
143
+ for (const c of store.changes.all()) for (const p of c.propagation) {
144
+ if (p.state !== 'Not yet checked' || !takes(p.nodeId)) continue;
145
+ entries++; changes.add(c.id); objects.add(p.nodeId);
146
+ }
147
+ const notJudged = new Set(store.changes.all().flatMap((c) => (c.notJudged ?? []).map((e) => e.nodeId)));
148
+ return { entries, changes: changes.size, objects: objects.size, notJudged: notJudged.size };
149
+ }
150
+
151
+ export interface FollowUpItemForPrompt {
152
+ readonly id: string;
153
+ readonly effect: string;
154
+ readonly title: string;
155
+ readonly summary: string;
156
+ readonly before: string | null;
157
+ readonly after: string | null;
158
+ }
159
+ export interface FollowUpChangeForPrompt {
160
+ readonly id: string;
161
+ readonly at: string;
162
+ readonly effect: string;
163
+ readonly title: string;
164
+ readonly summary: string;
165
+ readonly before: string | null;
166
+ readonly after: string | null;
167
+ readonly sourceIds: readonly string[];
168
+ /** The piece of work's own net changes; the judgement names the ones the object followed and the ones it lacks. */
169
+ readonly items?: readonly FollowUpItemForPrompt[];
170
+ /** How this piece of work was cut out: which session or execution, or that it was grouped by time. */
171
+ readonly work?: string | null;
172
+ }
173
+ export interface FollowUpObjectForPrompt {
174
+ readonly id: string;
175
+ readonly label: string;
176
+ readonly category: string;
177
+ readonly progress: string | null;
178
+ readonly validity: string;
179
+ readonly marks: readonly string[];
180
+ readonly text: string;
181
+ readonly factRecordIds: readonly string[];
182
+ /** The changes to judge this object against, newest first, each saying whether the object moved after it and
183
+ * which of its items reached this object — an object is asked only about the items that came near it. */
184
+ readonly changes: readonly { readonly id: string; readonly updatedAfter: boolean; readonly itemIds?: readonly string[] }[];
185
+ /** What the previous round said this object still lacks and nothing has closed since. It is judged again with
186
+ * the rest: an item ends only when the object follows it, a later change supersedes it, or the owner or holder
187
+ * says it need not be handled (§5.5), so the model has to see it to be able to conclude on it. */
188
+ readonly stillLacks: readonly { readonly changeId: string; readonly itemId: string; readonly what: string }[];
189
+ }
190
+
191
+ /** What one follow-up job is given: the batch's changes, described once, and its objects, each with its changes. */
192
+ export function packFollowUp(store: ProjectStore, batch: FollowUpBatch): { changes: FollowUpChangeForPrompt[]; objects: FollowUpObjectForPrompt[] } {
193
+ const nodes = new Map(store.nodes.all().map((n) => [n.id, n]));
194
+ const marksOn = new Map<string, string[]>();
195
+ for (const m of store.marks.filter((x) => !x.closed)) { if (!marksOn.has(m.targetId)) marksOn.set(m.targetId, []); marksOn.get(m.targetId)!.push(m.kind); }
196
+ const changes = batch.changeIds.flatMap((id) => store.changes.get(id) ?? []).sort((a, b) => b.at.localeCompare(a.at));
197
+ const at = new Map(changes.map((c) => [c.id, c.at]));
198
+ return {
199
+ changes: changes.map((c: ChangeRecord) => ({
200
+ id: c.id, at: c.at, effect: c.effect, title: c.title, summary: c.summary, before: c.before, after: c.after, sourceIds: c.sourceIds,
201
+ work: c.work ? `${c.work.kind}: ${c.work.label}${c.work.openEnded ? ' (up to its last pause)' : ''}` : null,
202
+ items: itemsOf(c).map((i) => ({ id: i.id, effect: i.effect, title: i.title, summary: i.summary, before: i.before, after: i.after })),
203
+ })),
204
+ objects: batch.objects.map((o) => {
205
+ const n = nodes.get(o.nodeId);
206
+ const thread = store.threads.get(o.nodeId);
207
+ const ref = store.reference.get(o.nodeId);
208
+ const updatedAt = thread?.updatedAt ?? ref?.updatedAt ?? '';
209
+ return {
210
+ id: o.nodeId,
211
+ label: n?.label ?? thread?.title ?? ref?.name ?? o.nodeId,
212
+ category: n?.category ?? (thread ? 'Work item' : ref?.category ?? 'unknown'),
213
+ progress: thread?.progress ?? null,
214
+ validity: thread?.validity ?? ref?.validity ?? 'Current',
215
+ marks: marksOn.get(o.nodeId) ?? [],
216
+ text: (thread ? `${thread.doing} ${thread.results}` : ref?.text ?? '').slice(0, 400),
217
+ factRecordIds: thread?.factRecordIds.slice(0, 6) ?? [],
218
+ changes: o.entries.map((e) => ({ id: e.changeId, updatedAfter: updatedAt > (at.get(e.changeId) ?? ''), itemIds: e.itemIds }))
219
+ .sort((a, b) => (at.get(b.id) ?? '').localeCompare(at.get(a.id) ?? '')),
220
+ stillLacks: itemsForObject(store, o.nodeId, openRound(store)?.id ?? 'round_9999').carried,
221
+ };
222
+ }),
223
+ };
224
+ }
225
+
226
+ const clip = (s: string | null, n: number) => (s && s.length > n ? `${s.slice(0, n)}…` : s ?? '');
227
+
228
+ /** The changes of a batch, each described once, then its objects, each with the changes to judge it against. */
229
+ function renderPacked(packed: { changes: readonly FollowUpChangeForPrompt[]; objects: readonly FollowUpObjectForPrompt[] }): string[] {
230
+ const parts: string[] = ['=== Changes (one record per piece of work; its items are its net changes)'];
231
+ for (const c of packed.changes) {
232
+ parts.push(`- ${c.id} · ${c.at} · ${c.title}${c.work ? ` · ${c.work}` : ''}`);
233
+ if (c.summary) parts.push(` ${clip(c.summary, 400)}`);
234
+ const items = c.items ?? [];
235
+ if (items.length > 1 || (items[0]?.id ?? '')) {
236
+ for (const i of items) {
237
+ parts.push(` · item ${i.id || '(the record itself)'} · ${i.effect} · ${i.title}`);
238
+ if (i.summary) parts.push(` ${clip(i.summary, 300)}`);
239
+ if (i.before || i.after) parts.push(` before: ${clip(i.before, 200) || '—'} | after: ${clip(i.after, 200) || '—'}`);
240
+ }
241
+ } else if (c.before || c.after) {
242
+ parts.push(` before: ${clip(c.before, 200) || '—'} | after: ${clip(c.after, 200) || '—'}`);
243
+ }
244
+ parts.push(` sources: ${c.sourceIds.join(', ')}`);
245
+ }
246
+ parts.push('', '=== Objects to judge');
247
+ for (const o of packed.objects) {
248
+ parts.push(`- ${o.id} · ${o.category} · ${o.label}${o.progress ? ` · ${o.progress}` : ''} · ${o.validity}${o.marks.length ? ` · marks: ${o.marks.join(', ')}` : ''}`);
249
+ if (o.text) parts.push(` ${o.text}`);
250
+ if (o.factRecordIds.length) parts.push(` fact records: ${o.factRecordIds.join(', ')}`);
251
+ // Only the items that reached this object are named, so the object is not asked about the other items of the
252
+ // same piece of work — those went somewhere else and judging against them invents findings.
253
+ parts.push(` judge against: ${o.changes.map((c) => {
254
+ const items = c.itemIds && c.itemIds.length ? ` items ${c.itemIds.map((i) => i || '(the record itself)').join(', ')}` : '';
255
+ return `${c.id}${items}${c.updatedAfter ? ' (object updated after it)' : ' (not updated since)'}`;
256
+ }).join(', ')}`);
257
+ for (const l of o.stillLacks) parts.push(` still lacking from an earlier round: ${l.changeId}${l.itemId ? ` item ${l.itemId}` : ''} — ${clip(l.what, 200)}. Conclude on it too: say it is followed now, or closed, or that it is still lacked.`);
258
+ }
259
+ return parts;
260
+ }
261
+
262
+ /** A round is not cut into batches: its one job holds the whole of it, so the per-batch caps do not apply. */
263
+ export const ROUND_LIMITS: BatchLimits = { objects: 100_000, entries: 1_000_000 };
264
+
265
+ /**
266
+ * What the open round still has to judge: every object waiting on an item, area by area, with the items that reached
267
+ * it and what it still lacks. Objects this round already judged are left out. A round's main job asks for it
268
+ * (pk_round_pending) after recording the changes of its own material, so the objects those changes reached are judged
269
+ * in the same round (§3.8).
270
+ */
271
+ export function pendingForRound(store: ProjectStore): { objects: number; text: string } {
272
+ const parts: string[] = [];
273
+ let objects = 0;
274
+ for (const batch of pendingByArea(store, ROUND_LIMITS)) {
275
+ const packed = packFollowUp(store, batch);
276
+ objects += packed.objects.length;
277
+ parts.push(`=== Area: ${batch.areaName} (${packed.objects.length} object${packed.objects.length === 1 ? '' : 's'})`, ...renderPacked(packed), '');
278
+ }
279
+ return { objects, text: parts.join('\n').trim() };
280
+ }