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,295 @@
1
+ /**
2
+ * What a claim that something was not handled is checked against (Spec v3.0 §2.13 rows 3 and 5 — dropped along the way,
3
+ * let pass; CKC-27): for the items a "let pass" or "dropped" candidate names from a report or QC — a QC's item with its
4
+ * task number ("AY B13"), a task number, an id the ledger defines in one document only — every later commit that names
5
+ * it, with the merges that brought the commit in, and every later line of a document that names it. The model reads
6
+ * them before it writes that nothing handled the item: an item a later commit names is reported with that commit, as
7
+ * handled or partly handled, never as unhandled.
8
+ *
9
+ * Found in test-C-1: a sweep wrote that "AY B13 … at least six low-severity items" had no handling at all; the
10
+ * cross-check adopted it, the synthesis wrote it as a note, and the spot-check confirmed it. Two of the six had been
11
+ * handled before the trial started, by commits whose messages name the item — `c96d40e` "… (QC AY B3, B13)" and
12
+ * `85a2476`/`d377df0` "… (QC AY B4 B6 B7 B11 B13)" — and the ledger indexes both.
13
+ *
14
+ * Where a candidate is: a line or a section of a step's text that says something was not handled, let pass, dropped, or
15
+ * has no follow-up (the claim words below), and the program's own breakpoints that are material for "let pass" or
16
+ * "dropped along the way". The program finds the ids; whether the item was handled is the model's judgement.
17
+ */
18
+ import type { Ledger } from '../ledger/index.ts';
19
+ import { isReportLike } from '../ledger/lines.ts';
20
+ import type { Breakpoint } from '../model/k-types.ts';
21
+ import type { ProjectStore } from '../store/project-store.ts';
22
+ import { Facts, type CommitFact, type NumFact } from './facts.ts';
23
+ import { clip } from './text.ts';
24
+ import { buildUnits, type UnitIndex } from './units.ts';
25
+ import { taskNumbers } from './unnumbered.ts';
26
+ import { expandRanges } from '../ledger/ranges.ts';
27
+
28
+ // ───────────────────────── where a candidate is ─────────────────────────
29
+
30
+ /** Words that say something was not handled, let pass, dropped, or has no follow-up. */
31
+ export const CLAIM_WORDS = new RegExp([
32
+ '[无没未][^。,,;;\\n]{0,4}(?:处置|处理|跟进|下文|后续|修复?|接手?|回应|落实)',
33
+ '无人|没人|没有人|无任何|放过|放过去|丢了|丢失|漏掉|漏了|半路|中途丢|被放下|搁置|悬着|烂尾|不了了之',
34
+ '\\blet pass\\b|\\bnobody\\b|\\bno one\\b|\\bnot (?:handled|fixed|addressed|followed up|re-?checked|picked up)\\b|\\bunhandled\\b',
35
+ '\\b(?:no|without) (?:follow-?up|disposition|handling|fix|action|trace|successor|record of)\\b|\\bnever (?:handled|fixed|followed|picked up|addressed)\\b',
36
+ '\\bdropped\\b|\\bleft open\\b|\\bstill open\\b|\\bopen items?\\b|\\bfindings? open\\b',
37
+ ].join('|'), 'i');
38
+
39
+ export interface ClaimText {
40
+ /** Where it is, as the step can open it: a round document's id, a note's id, a breakpoint's id. */
41
+ readonly from: string;
42
+ readonly text: string;
43
+ /** Every line of it is a candidate (a program breakpoint of "let pass" or "dropped"), not only those with the words. */
44
+ readonly whole?: boolean;
45
+ }
46
+
47
+ /**
48
+ * The lines of a text that make a claim: those with the claim words, a heading among them (`(c)-2 · AY 的 B13 低危项:
49
+ * 部分无逐项处置记录`). Not every line under such a heading: a sweep's "no follow-up" section cites its evidence around the
50
+ * claim — decisions, commits, other tasks — and those are not what it says was let pass (test-C-1's reports named some
51
+ * four hundred numbers under such headings).
52
+ */
53
+ export function claimLines(text: string, whole = false): string[] {
54
+ return text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && (whole || CLAIM_WORDS.test(l)));
55
+ }
56
+
57
+ // ───────────────────────── the items a claim names ─────────────────────────
58
+
59
+ /** An item a candidate names: a QC's item with its task number, a task number, or an id one document defines. */
60
+ export interface ClaimItem {
61
+ /** As the step reads it: "AY B13", "AW", "E127". */
62
+ readonly label: string;
63
+ /** The task whose report or QC it is an item of; null for a task number alone or an id on its own. */
64
+ readonly task: string | null;
65
+ /** The item's own number (the task number itself, for a task). */
66
+ readonly num: string;
67
+ /** Where the candidates that name it are. */
68
+ readonly from: readonly string[];
69
+ }
70
+
71
+ const ID = String.raw`[A-Z]{1,6}-[A-Z]?\d{1,4}[a-z]?|[A-Z]\d{1,4}[a-z]?|[A-Z]{2,3}`;
72
+ const TOKEN = new RegExp(String.raw`(?<![A-Za-z0-9_/.#-])(${ID})(?![A-Za-z0-9_-])`, 'g');
73
+ /** A number's family, as far as telling a task's items from other tasks goes: `XX` for two capitals, else its letters (`CKC-`, `AC-`, `B`). */
74
+ const familyOf = (id: string): string => (/^[A-Z]{2,3}$/.test(id) ? 'XX' : /^[A-Z]{2}-\d+[a-z]?$/.test(id) ? 'XX-n' : /^[A-Z]+-?/.exec(id)?.[0] ?? id);
75
+ /** What may stand between a task number and the items of its report: `AY B13`, `AY 的 B13`, `AY's B3, B13`, `QC AY:B4、B6`. */
76
+ const JOIN = /^\s*(?:(?:的|之|'s|’s|·|:|:|#|-|—|\/)\s*)?$/;
77
+ const LIST = /^\s*(?:,|,|、|;|;|\/|&|\+|和|与|及|and|or|或)?\s*$/;
78
+
79
+ /**
80
+ * The items the claim lines name (see `ClaimItem`): `known` says whether the ledger has a number at all (a task's items
81
+ * are read with it); `alone` whether a number not beside a task names one thing by itself (one document defines it).
82
+ */
83
+ export function claimItems(texts: readonly ClaimText[], tasks: ReadonlySet<string>, known: (num: string) => boolean, alone: (num: string) => boolean = known): ClaimItem[] {
84
+ const items = new Map<string, { label: string; task: string | null; num: string; from: Set<string> }>();
85
+ const add = (task: string | null, num: string, from: string) => {
86
+ const label = task ? `${task} ${num}` : num;
87
+ const e = items.get(label) ?? { label, task, num, from: new Set<string>() };
88
+ e.from.add(from);
89
+ items.set(label, e);
90
+ };
91
+ for (const t of texts) {
92
+ // CM: a range in a claim (`AC-25~AC-31 无人接`) names each of its members.
93
+ for (const line of claimLines(t.text, t.whole).map((l) => expandRanges(l, CLAIM_RANGE))) {
94
+ const tokens = [...line.matchAll(TOKEN)].map((m) => ({ id: m[1]!, start: m.index!, end: m.index! + m[1]!.length }))
95
+ .filter((x) => tasks.has(x.id) || (!/^[A-Z]{2,3}$/.test(x.id) && known(x.id)));
96
+ const paired = new Set<number>();
97
+ for (let i = 0; i < tokens.length; i++) {
98
+ const task = tokens[i]!;
99
+ if (!tasks.has(task.id) || paired.has(i)) continue;
100
+ // The items that follow a task number in a list are that task's (`QC AY B3, B13`), as long as they are numbers of
101
+ // another family: `AA、AB、AC` are three tasks, and B3 after AY is AY's item even where B3 also names a batch.
102
+ let prev = task;
103
+ for (let j = i + 1; j < tokens.length; j++) {
104
+ const next = tokens[j]!;
105
+ const between = line.slice(prev.end, next.start);
106
+ if (!(prev === task ? JOIN.test(between) || LIST.test(between) : LIST.test(between)) || familyOf(next.id) === familyOf(task.id)) break;
107
+ add(task.id, next.id, t.from);
108
+ paired.add(i); paired.add(j);
109
+ prev = next;
110
+ }
111
+ }
112
+ tokens.forEach((x, i) => { if (!paired.has(i) && (tasks.has(x.id) || alone(x.id))) add(null, x.id, t.from); });
113
+ }
114
+ }
115
+ // A number a task's item already stands for — as the task or as the item — is not listed again on its own.
116
+ const inPairs = new Set([...items.values()].filter((e) => e.task).flatMap((e) => [e.task!, e.num]));
117
+ return [...items.values()].filter((e) => e.task || !inPairs.has(e.num)).map((e) => ({ label: e.label, task: e.task, num: e.num, from: [...e.from] }));
118
+ }
119
+
120
+ // ───────────────────────── what later names them ─────────────────────────
121
+
122
+ export interface LaterCommit {
123
+ readonly commit: CommitFact;
124
+ /** The lines of its message that name the item (after the subject). */
125
+ readonly lines: readonly { readonly line: number; readonly text: string }[];
126
+ /** The branch and merge that integrated it, and the trunk's merge that brought it in. */
127
+ readonly merged: { readonly branch: string | null; readonly merge: CommitFact } | null;
128
+ readonly trunk: CommitFact | null;
129
+ }
130
+
131
+ export interface LaterDocLine {
132
+ readonly id: string;
133
+ readonly path: string;
134
+ readonly line: number | null;
135
+ readonly text: string;
136
+ readonly at: string;
137
+ readonly current: boolean;
138
+ }
139
+
140
+ export interface ItemCheck {
141
+ readonly item: ClaimItem;
142
+ /** Where the item is first stated: the report's line that defines it, else its first appearance. */
143
+ readonly first: NumFact | null;
144
+ readonly commits: readonly LaterCommit[];
145
+ readonly docs: readonly LaterDocLine[];
146
+ }
147
+
148
+ /** For each item, what later names it (see the module comment); `isReport` says which documents are reports. */
149
+ export function checkItems(facts: Facts, index: UnitIndex, items: readonly ClaimItem[], isReport: (path: string) => boolean = () => false): ItemCheck[] {
150
+ // CM (E151): a commit or a line that writes a range names each of its members (`3431e93` 「(CKC-03 AC-25-AC-31)」).
151
+ const nums = facts.numsWithRanges(items.flatMap((i) => (i.task ? [i.task, i.num] : [i.num])));
152
+ const integrations = index.integrations;
153
+ return items.map((item) => {
154
+ const own = nums.get(item.num) ?? [];
155
+ const of = item.task ? nums.get(item.task) ?? [] : [];
156
+ const sameCommit = new Set(of.filter((r) => r.kind === 'commit').map((r) => r.commit));
157
+ const sameLine = new Set(of.filter((r) => r.kind === 'doc' || r.kind === 'loose').map((r) => `${r.path}\x1f${r.line}`));
158
+ // For a task's item: the rows that name the task too (the same commit message, the same line of a document).
159
+ const rows = item.task ? own.filter((r) => (r.kind === 'commit' ? sameCommit.has(r.commit) : sameLine.has(`${r.path}\x1f${r.line}`))) : own;
160
+ const defs = rows.filter((r) => r.place === 'definition').sort((a, b) => a.ms - b.ms);
161
+ // A task's item is first stated where the task's own document defines it: one whose path carries the task's number
162
+ // (`runs/AY-1/result.md`, `contracts/CKC-11-….md`), else a report that names the task, else any document that does
163
+ // (B3 is also a batch of the first execution plan; AY's B3 is the line of AY's report).
164
+ const taskDocs = new Set(of.filter((r) => r.kind === 'doc' || r.kind === 'loose').map((r) => r.path));
165
+ const docDefs = own.filter((r) => r.place === 'definition' && (r.kind === 'doc' || r.kind === 'loose')).sort((a, b) => a.ms - b.ms);
166
+ const carries = item.task ? new RegExp(`(?:^|[\\\\/_.-])${item.task.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=[-_.\\\\/]|$)`) : null;
167
+ const rank = (r: NumFact) => (carries && r.path && carries.test(r.path) ? 0 : r.path && isReport(r.path) && taskDocs.has(r.path) ? 1 : taskDocs.has(r.path) ? 2 : 3);
168
+ const definedIn = item.task ? docDefs.filter((r) => rank(r) < 3).sort((a, b) => rank(a) - rank(b) || a.ms - b.ms) : defs;
169
+ const first = definedIn[0] ?? defs[0] ?? [...rows].sort((a, b) => a.ms - b.ms)[0] ?? null;
170
+ const since = first?.ms ?? 0;
171
+ const home = first?.path ?? null;
172
+ const commits = new Map<string, { commit: CommitFact; lines: { line: number; text: string }[] }>();
173
+ const docs: LaterDocLine[] = [];
174
+ for (const r of rows) {
175
+ if (r === first) continue;
176
+ if (r.kind === 'commit' && r.commit) {
177
+ const c = facts.commits.get(r.commit);
178
+ if (!c || c.ms < since) continue;
179
+ const e = commits.get(c.hash) ?? { commit: c, lines: [] };
180
+ if ((r.line ?? 1) > 1 && !e.lines.some((l) => l.line === r.line)) e.lines.push({ line: r.line!, text: r.context });
181
+ commits.set(c.hash, e);
182
+ } else if ((r.kind === 'doc' || r.kind === 'loose') && r.path && r.path !== home && r.ms >= since) {
183
+ docs.push({ id: r.id, path: r.path, line: r.line, text: r.context, at: r.occurred.at, current: r.current });
184
+ }
185
+ }
186
+ const later = [...commits.values()].sort((a, b) => a.commit.ms - b.commit.ms).map((e) => {
187
+ const m = integrations.innermost.get(e.commit.hash);
188
+ const trunk = integrations.trunkMergeOf(e.commit.hash);
189
+ return { commit: e.commit, lines: e.lines.sort((a, b) => a.line - b.line), merged: m ? { branch: m.source, merge: m.merge } : null, trunk: trunk && trunk.hash !== e.commit.hash && trunk.hash !== m?.merge.hash ? trunk : null };
190
+ });
191
+ return { item, first, commits: later, docs: docs.sort((a, b) => a.at.localeCompare(b.at) || a.path.localeCompare(b.path)) };
192
+ });
193
+ }
194
+
195
+ // ───────────────────────── the block a step is given ─────────────────────────
196
+
197
+ /** The widest range a claim line is read as a list of items; a wider one names a whole set (its ends are still read). */
198
+ const CLAIM_RANGE = 8;
199
+ /** How many later commits, and how many later documents, the block shows per item; the counts are always whole. */
200
+ const MAX_COMMITS = 8;
201
+ const MAX_DOCS = 6;
202
+
203
+ const short = (h: string): string => h.slice(0, 7);
204
+ const when = (at: string): string => at.replace('T', ' ').slice(0, 16);
205
+
206
+ /**
207
+ * The candidates the program itself computed: the breakpoints material for "let pass" or "dropped along the way", lit or
208
+ * still a breakpoint candidate (D99: a candidate is exactly a "nothing handled it" to check against the later commits
209
+ * before anyone writes it).
210
+ */
211
+ export function breakpointClaims(store: ProjectStore): ClaimText[] {
212
+ const LET_PASS_OR_DROPPED: readonly Breakpoint['kind'][] = ['Findings open', 'Passed with open items', 'Fix not re-checked', 'Not planned'];
213
+ return store.breakpoints.filter((b) => !b.out && !b.ownerResponse && LET_PASS_OR_DROPPED.includes(b.kind)).map((b) => {
214
+ const t = store.threads.get(b.targetId);
215
+ return { from: b.id, whole: true, text: [`${b.kind}${b.lit ? '' : ' (candidate)'} on ${t ? `${t.ids.join(' ')} ${t.title}` : b.targetId}: ${b.why}`, ...b.evidence.map((e) => e.line ?? e.label)].join('\n') };
216
+ });
217
+ }
218
+
219
+ export interface ClaimCheckOptions {
220
+ /** What the step is told the candidates are ("this round's sweep reports", "what this round wrote"). */
221
+ readonly what: string;
222
+ /** The ledger's facts and the units, when the caller already read them for another block of the same step. */
223
+ readonly analysis?: (() => { readonly facts: Facts; readonly index: UnitIndex } | null) | null;
224
+ }
225
+
226
+ /**
227
+ * The block: for each item the candidates name, where it was first stated and what later names it. An item nothing later
228
+ * names says so — the model still checks the code before it writes that nothing handled it.
229
+ */
230
+ export function claimCheckBlock(store: ProjectStore, ledger: Ledger | null, texts: readonly ClaimText[], opts: ClaimCheckOptions): string {
231
+ const head = `=== Before you write that something was not handled: what later names the items (the program's list from the ledger, for the "let pass" and "dropped" candidates in ${opts.what})`;
232
+ if (!ledger) return `${head}\nThe ledger is not available, so nothing was listed: ask the ledger's numbers yourself (pk_ledger_numbers, pk_ledger_commits num) before any such claim.`;
233
+ const withClaims = texts.filter((t) => claimLines(t.text, t.whole).length > 0);
234
+ if (!withClaims.length) return `${head}\nNo candidate there says that something was not handled, let pass or dropped.`;
235
+ let facts: Facts;
236
+ let index: UnitIndex;
237
+ try {
238
+ const shared = opts.analysis?.() ?? null;
239
+ facts = shared?.facts ?? new Facts(ledger);
240
+ index = shared?.index ?? buildUnits(store, facts);
241
+ } catch (e) { return `${head}\nThe ledger could not be read for it: ${(e as Error).message}`; }
242
+ const tasks = taskNumbers(store, facts, index);
243
+ const known = (num: string) => (facts.numsWithRanges([num]).get(num)?.length ?? 0) > 0;
244
+ // On its own, away from a task number, an id names a report's or QC's item when one document alone defines it and
245
+ // that document is a report (its path or its title says so) or a receipt. A decision or a requirement cited around a
246
+ // claim is its evidence, not what it says was let pass.
247
+ const reportPaths = new Set(facts.arrangements.filter((a) => a.kind === 'receipt').map((a) => a.path));
248
+ const isReport = (path: string) => {
249
+ if (reportPaths.has(path)) return true;
250
+ const versions = facts.docsByPath.get(path) ?? [];
251
+ const last = versions[versions.length - 1];
252
+ return isReportLike(path, last ? facts.textOf(last.id) ?? '' : '');
253
+ };
254
+ const finding = (num: string) => {
255
+ const rows = facts.numsOf([num]).get(num) ?? [];
256
+ const homes = [...new Set(rows.filter((r) => r.place === 'definition' && (r.kind === 'doc' || r.kind === 'loose') && r.path).map((r) => r.path!))];
257
+ return homes.length === 1 && isReport(homes[0]!);
258
+ };
259
+ const items = claimItems(withClaims, tasks, known, finding);
260
+ if (!items.length) return `${head}\nThe candidates (${withClaims.map((t) => t.from).join(', ')}) name no task number, QC item or finding the ledger knows: check each claim against the ledger's numbers and words yourself.`;
261
+ const checks = checkItems(facts, index, items, isReport);
262
+ const lines: string[] = [head, `Candidates read: ${withClaims.map((t) => t.from).join(', ')}. Items they name: ${items.length}.`];
263
+ for (const c of checks) {
264
+ const later = c.commits.length + c.docs.length;
265
+ lines.push(`- ${c.item.label}${c.item.task ? ` (an item of ${c.item.task}'s report or QC)` : ''} — named in ${c.item.from.join(', ')}`);
266
+ if (c.first) lines.push(` first stated: ${c.first.path ? `${c.first.path}${c.first.line ? `:${c.first.line}` : ''}` : `commit ${short(c.first.commit ?? '')}`} (${when(c.first.occurred.at)}; ${c.first.id}): “${clip(c.first.context, 400)}”`);
267
+ if (!later) { lines.push(' nothing later names it: no commit message, no later line of a document.'); continue; }
268
+ if (c.commits.length) {
269
+ // CM: the first few say it was taken up, and by what; the rest are one query away (the block is read, not pasted whole).
270
+ lines.push(` later commits naming it (${c.commits.length}${c.commits.length > MAX_COMMITS ? `; the first ${MAX_COMMITS}, the rest with pk_ledger_commits num ${c.item.num}` : ''}):`);
271
+ for (const x of c.commits.slice(0, MAX_COMMITS)) {
272
+ const merged = x.merged ? ` — ${x.merged.branch ? `branch ${x.merged.branch}, ` : ''}merged ${short(x.merged.merge.hash)} ${when(x.merged.merge.occurred.at)}${x.trunk ? `, into the trunk with ${short(x.trunk.hash)}` : ''}` : x.commit.fpTrunk ? ' — on the trunk' : x.commit.onTrunk ? '' : ' — not on the trunk';
273
+ lines.push(` · ${short(x.commit.hash)} ${when(x.commit.occurred.at)} “${clip(x.commit.subject, 160)}”${merged}`);
274
+ for (const l of x.lines) lines.push(` line ${l.line}: “${clip(l.text, 200)}”`);
275
+ }
276
+ }
277
+ if (c.docs.length) {
278
+ const byPath = new Map<string, LaterDocLine[]>();
279
+ for (const d of c.docs) byPath.set(d.path, [...(byPath.get(d.path) ?? []), d]);
280
+ if (c.item.task) {
281
+ // A task's item: one line per document, the first later line naming it and how many more that document has.
282
+ lines.push(` later lines of documents naming it (${c.docs.length} in ${byPath.size} document${byPath.size === 1 ? '' : 's'}; the first in each${byPath.size > MAX_DOCS ? `, ${MAX_DOCS} documents shown — pk_ledger_numbers num ${c.item.num} for all` : ''}):`);
283
+ for (const [path, ds] of [...byPath].slice(0, MAX_DOCS)) {
284
+ const d = ds[0]!;
285
+ lines.push(` · ${path}${d.line ? `:${d.line}` : ''} (${when(d.at)}${d.current ? '' : ', no longer in the current version'}; ${d.id})${ds.length > 1 ? ` +${ds.length - 1} more line${ds.length === 2 ? '' : 's'}` : ''}: “${clip(d.text, 200)}”`);
286
+ }
287
+ } else {
288
+ // A number on its own is cited all over the project's documents: how often, and where to read them.
289
+ const current = new Set(c.docs.filter((d) => d.current).map((d) => d.path)).size;
290
+ lines.push(` later documents naming it: ${c.docs.length} lines in ${byPath.size} document${byPath.size === 1 ? '' : 's'}, ${current} of them in the current version (pk_ledger_numbers num ${c.item.num} for the lines)`);
291
+ }
292
+ }
293
+ }
294
+ return lines.join('\n');
295
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * What a round found when it put out a breakpoint about delivery (Spec §2.12; §1.4: links tie what the program could not
3
+ * tie by ids). A cross-check that puts out `No trace of done` or `Not started` cites what shows the step did happen; the
4
+ * commits it cites are the work's delivery, which the program could not tie by the work's number. They become confirmed
5
+ * `Delivered` links — the links pk_link_process writes, under the same ids — so the process view and the work's execution
6
+ * read them (work.ts `executionOf`). Seen on the resident run: the cross-check put out 16 `No trace of done`, each citing
7
+ * the work's own commits, and the List still said `Not started` beside every one.
8
+ */
9
+ import type { Breakpoint, EvidenceRef, ProcessLink } from '../model/k-types.ts';
10
+ import type { ProjectStore } from '../store/project-store.ts';
11
+ import { stableId } from '../model/ids.ts';
12
+ import { pathKey } from '../util/paths.ts';
13
+
14
+ const ABOUT_DELIVERY: ReadonlySet<string> = new Set(['No trace of done', 'Not started']);
15
+
16
+ /** The id pk_link_process gives a work item, fact and step: linking the same again updates it. */
17
+ export function linkId(projectId: string, workId: string, ledgerRef: string, stepKind: string, fact: EvidenceRef): string {
18
+ return stableId('link', projectId, workId, ledgerRef, stepKind, fact.line ?? '', ...(fact.repo ? [pathKey(fact.repo)] : []));
19
+ }
20
+
21
+ /** The commits a put-out breakpoint about delivery cites, as confirmed `Delivered` links of its work item. Returns how many were written. */
22
+ export function deliveryLinksFrom(
23
+ store: ProjectStore, bp: Breakpoint, evidence: readonly EvidenceRef[], why: string,
24
+ meta: { readonly roundId: string | null; readonly jobId: string | null; readonly at: string },
25
+ ): number {
26
+ if (!ABOUT_DELIVERY.has(bp.kind) || !store.threads.get(bp.targetId)) return 0;
27
+ let written = 0;
28
+ for (const fact of evidence) {
29
+ if (fact.kind !== 'commit') continue;
30
+ const ledgerRef = `commit:${fact.id}`;
31
+ const id = linkId(bp.projectId, bp.targetId, ledgerRef, 'Delivered', fact);
32
+ if (store.links.get(id)?.confirmed) continue;
33
+ const link: ProcessLink = {
34
+ id, projectId: bp.projectId, workId: bp.targetId, ledgerRef, evidence: fact, stepKind: 'Delivered', why, basis: 'Inferred', confirmed: true,
35
+ roundId: meta.roundId, jobId: meta.jobId, at: meta.at,
36
+ };
37
+ store.links.put(link, { jobId: meta.jobId, basisSourceIds: [], summary: `Link Delivered: ${fact.label} → ${bp.targetId} (the round put out ${bp.kind} citing it)` });
38
+ written++;
39
+ }
40
+ return written;
41
+ }
42
+
43
+ /** At start: breakpoints a round put out before this existed tie the commits they cite too. Returns how many links were written. */
44
+ export function linkPutOutDeliveries(store: ProjectStore): number {
45
+ let written = 0;
46
+ for (const bp of store.breakpoints.filter((b) => b.out?.by === 'model' && ABOUT_DELIVERY.has(b.kind))) {
47
+ written += deliveryLinksFrom(store, bp, bp.out!.evidence ?? [], `The round put out ${bp.kind} citing this commit as the work's delivery`, {
48
+ roundId: bp.out!.decision?.roundId ?? null, jobId: null, at: bp.out!.at,
49
+ });
50
+ }
51
+ return written;
52
+ }