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,586 @@
1
+ /**
2
+ * What a round has left open on the workbench (D99; Spec §2.12, §3.3; CKC-23 AC-21, AC-22), so the main agent can check
3
+ * it has done the steps its skills give it rather than remember them:
4
+ * - breakpoint candidates with no result yet: not put out, not lit, no lane recorded where it looked
5
+ * (`pk_record_looked`), and no write of this round answers it (a link of the step it misses, a decision recorded as
6
+ * carried out, a superseded downstream item marked so) — each with the briefs of this round that name it;
7
+ * - work items in no plan, and work items in no module (Area);
8
+ * - decisions placed on nothing, and decisions placed only on the Product (or a Goal);
9
+ * - earlier generations with no planned item recorded;
10
+ * - (CJ, E150) what the program can count on the first D100 run's shortfalls: the numbers a current decision record,
11
+ * plan, task index or execution arrangement defines that no item carries and no account names (`entries`,
12
+ * entry-gate.ts); items named by their number alone where the defining line has a title; tickets with no ids, or
13
+ * with another object's number as their id; this round's links not confirmed yet; contracts still Planned although
14
+ * tickets implementing them are Done, with no reason stated; tickets that record no contract; decisions and
15
+ * boundaries placed only on the Product whose own entry names a contract, a Spec section, a module or an R-item
16
+ * ("traceable, placed on product"). Boundaries are counted like decisions.
17
+ * - (CM, E151) what the program can read off the project's own writing: the suspect links (their evidence failed the
18
+ * program's check as they were written; a lane-checked link counts like a confirmed one); the items of an earlier
19
+ * generation with no destination, and the candidate generations with no verdict (generation-check.ts); designs that
20
+ * refine three Areas or more, or sit nowhere; items that name an Area but sit elsewhere; and, for each decision placed
21
+ * only on the Product, where its own entry and the documents that cite it lead (placing.ts). `pk_round_state` gives a
22
+ * count per list and one list in full by its key (`OPEN_KEYS`).
23
+ * - (CZ) the numbers two items of one category carry (`duplicates`); and the entries count also the numbers of a family
24
+ * that one current document alone defines (entry-gate.ts).
25
+ * - (DB) what is not placed is the workbench's own count: the lists of work in no plan or module, of decisions and
26
+ * boundaries on nothing or only on the Product, and of requirements and designs likewise, are read from the story
27
+ * map's placement model (workbench-placement.ts; `ui/placement.js`), so the program and the workbench cannot disagree.
28
+ * A work item whose only plan has no band there (not current: Proposed, Replaced, an earlier generation's) is in no
29
+ * plan, with that plan named. On the DeepSeek run the program counted 0 while the workbench showed 2 + 8.
30
+ * Seen on the first D99 deepening of ContextKeeper (2026-09-30): 55 candidates stayed with no result and no lane called
31
+ * pk_record_looked; 44 work items had no plan and 43 no module; 192 decisions were placed on no module; the two earlier
32
+ * generations had no items. `pk_round_state` lists these; `pk_stage` into the synthesis refuses a `Full` deepening while
33
+ * candidates have no result (unless the main agent says why), and notes the unplaced counts without refusing on them —
34
+ * some work really has no plan.
35
+ */
36
+ import { linkHolds, type Breakpoint, type ClerkRound } from '../../model/k-types.ts';
37
+ import type { GraphRelation, ReferenceItem, WorkThread } from '../../model/types.ts';
38
+ import type { ProjectStore } from '../../store/project-store.ts';
39
+ import { isCandidate } from '../../process/breakpoints.ts';
40
+ import { CHECK_WORDS } from '../../process/units.ts';
41
+ import { Ledger } from '../../ledger/index.ts';
42
+ import { definitionsInText } from '../../ledger/numbering.ts';
43
+ import { isNumberShaped, numberOnlyRefusal, otherObjectOf, undefinedNumbers } from '../numbers-check.ts';
44
+ import { entryGaps, requiredEntries, type EntryGaps } from './entry-gate.ts';
45
+ import { isNumberOnly } from './entries.ts';
46
+ import { primaryIdentifier } from '../tools.ts';
47
+ import { entryNames, lineIndex, misplacedItems, reachOf, traceIndex, tracePlacement, wholePlanSuggestion, WHOLE_PLAN_MODULES } from './placing.ts';
48
+ import { candidateWorkLines, itemsWithoutDestination, openCandidates } from './generation-check.ts';
49
+ import { standingItem, standingNotes } from './standing-notes.ts';
50
+ import { planNotDrawnLine, unplacedIntent, workbenchUnplaced } from './workbench-placement.ts';
51
+ import { NO_AREAS, NO_CONTRACT_LAYER, NO_PLAN_LAYER, areasPresent, contractLayerPresent, inferThread, inferenceContext, inferredPlacementRows, planLayerPresent, programPlacementCounts, type InferredPlacementRow, type ProgramPlacementCounts } from './placement-inference.ts';
52
+
53
+ /** How many entries each list gives at most; its count is always the whole. */
54
+ export const OPEN_LIST_LIMIT = 200;
55
+
56
+ export interface OpenCandidate {
57
+ readonly id: string;
58
+ readonly kind: Breakpoint['kind'];
59
+ /** The object it hangs on, by its number and name. */
60
+ readonly target: string;
61
+ readonly why: string;
62
+ /** The briefs of this round that name it (by lane name): the lanes it was given to. Empty when no brief names it. */
63
+ readonly briefs: readonly string[];
64
+ }
65
+
66
+ export interface OpenItem {
67
+ readonly id: string;
68
+ readonly name: string;
69
+ }
70
+
71
+ export interface OpenList<T> {
72
+ readonly count: number;
73
+ readonly items: readonly T[];
74
+ /** CQ (D104): the defined zero when the project lacks the layer the list counts against — the sentence to print instead of a list. */
75
+ readonly notApplicable?: string;
76
+ }
77
+
78
+ export interface RoundOpen {
79
+ /** Candidates with no result: not answered by a write of this round, not looked for, not put out, not lit. */
80
+ readonly candidates: OpenList<OpenCandidate>;
81
+ readonly workItems: {
82
+ /**
83
+ * In force and in no plan, as the workbench places it (DB): it serves no plan that has a band there, and stands in no
84
+ * earlier generation's band. `notCurrent` names the plans it does serve that have no band — not current (Proposed,
85
+ * Deferred, Replaced, Abandoned), or held by an earlier generation. CN: `suggest` is the plan the decisions it carries
86
+ * out, cites or is cited by lead to, with those decisions.
87
+ */
88
+ readonly noPlan: OpenList<OpenItem & { readonly suggest?: readonly string[]; readonly notCurrent?: readonly string[] }>;
89
+ /**
90
+ * In force and in no module: nothing it serves leads to an Area. CN: work recorded as serving its whole plan
91
+ * (`wholePlanWhy`, in a plan) is placed and not listed; `suggest` says when a range the ticket writes spans three
92
+ * modules or more — work for the whole plan.
93
+ */
94
+ readonly noModule: OpenList<OpenItem & { readonly suggest?: string }>;
95
+ /** CN: work recorded as serving its whole plan and no single module, each with the Keeper's written reason. */
96
+ readonly wholePlan: OpenList<OpenItem & { readonly why: string }>;
97
+ /** CQ (D104): work written as in no plan, each with the recorded reason the program accepted (`noPlanWhy`); not counted in `noPlan`. */
98
+ readonly noPlanWritten: OpenList<OpenItem & { readonly why: string }>;
99
+ /** CQ (D104): work written as in no module, each with its recorded reason (`noAreaWhy`); not counted in `noModule`. */
100
+ readonly noAreaWritten: OpenList<OpenItem & { readonly why: string }>;
101
+ };
102
+ /**
103
+ * CQ (D104), CS: the placements the program wrote from the records, basis Inferred, that still stand and have no result
104
+ * yet — of every round of the project, each with its chain and the round and stage that placed it. They stand as placed
105
+ * (U86); the list is what the main agent can still check.
106
+ */
107
+ readonly inferred: OpenList<InferredPlacementRow>;
108
+ /** CS: how many placements the program wrote in all, in every round, and how many were confirmed, moved, or are still unreviewed. */
109
+ readonly programPlacements: ProgramPlacementCounts;
110
+ readonly decisions: {
111
+ /** What it refines leads to no Area, Plan, Goal or Product. */
112
+ readonly onNothing: OpenList<OpenItem>;
113
+ /** What it refines leads to the Product or a Goal only, to no Area and no Plan. */
114
+ readonly productOnly: OpenList<OpenItem>;
115
+ /**
116
+ * Of those, the traceable ones (CJ; CM): their own entry names a contract, a Spec section, a module, an R-item, a plan
117
+ * or an Area, or current documents cite them from a placed requirement, design or contract — with the Areas and Plans
118
+ * those lead to, most cited first.
119
+ */
120
+ readonly traceable: OpenList<OpenItem & { readonly names: readonly string[]; readonly cited: readonly string[]; readonly suggest: readonly string[] }>;
121
+ };
122
+ /**
123
+ * CM (CL's findings §2): designs that refine three Areas or more (a chapter cut too wide), and designs placed nowhere.
124
+ * DB: `unplaced` is the workbench's — on nothing, or only on the Product (or a Goal) with no written reason (`on`).
125
+ */
126
+ readonly designs: {
127
+ readonly wide: OpenList<OpenItem & { readonly areas: readonly string[] }>;
128
+ readonly unplaced: OpenList<OpenItem & { readonly on: string }>;
129
+ };
130
+ /** DB: requirements the workbench does not place — on nothing, or only on the Product (or a Goal) with no written reason (`on`). */
131
+ readonly requirements: { readonly unplaced: OpenList<OpenItem & { readonly on: string }> };
132
+ /** CM (D101): items whose contract, group or own entry names an Area in the project's writing but which sit elsewhere. */
133
+ readonly misplaced: OpenList<OpenItem & { readonly area: string; readonly how: string; readonly sits: string }>;
134
+ readonly generations: {
135
+ /** Earlier generations with no work item recorded as planned in them. */
136
+ readonly withoutItems: OpenList<OpenItem>;
137
+ /**
138
+ * CM: items of a recorded generation with no destination — not replaced by anything (`replacedBy`), no current item
139
+ * depending on them or carrying them on, not moved into a current plan, not deferred or abandoned.
140
+ */
141
+ readonly withoutDestination: OpenList<OpenItem & { readonly generation: string; readonly validity: string }>;
142
+ /** CM: the candidate generations the program lists that have no verdict yet and that no recorded generation holds. */
143
+ readonly candidates: OpenList<OpenItem & { readonly why: string; readonly documents: number; readonly numbers: number; readonly carriedOn: number; readonly work: readonly string[] }>;
144
+ };
145
+ /**
146
+ * CZ: numbers carried by two items of one category — two work items, two Decisions. A number is written once: on the
147
+ * flash run a lane wrote `E14` a second time under an id of its own, and nothing counted it.
148
+ */
149
+ readonly duplicates: OpenList<OpenItem & { readonly number: string; readonly category: string; readonly items: readonly OpenItem[] }>;
150
+ /** CJ: numbers of the current decision records, plans, task indexes and execution arrangements neither carried nor accounted for, by file. */
151
+ readonly entries: EntryGaps;
152
+ /** CJ: items named by their number alone although the line that defines the number gives a title. */
153
+ readonly names: { readonly numberOnly: OpenList<OpenItem & { readonly suggested: string }> };
154
+ readonly tickets: {
155
+ /** Work items with no ids although a number the project defines for tickets is theirs (their internal id, or in their title). */
156
+ readonly noIds: OpenList<OpenItem & { readonly number: string }>;
157
+ /** Work items whose ids are another object's number (a decision's: D99, D100), not their own. */
158
+ readonly otherNumbers: OpenList<OpenItem & { readonly ids: readonly string[] }>;
159
+ /** Tickets (numbers a task index or execution arrangement defines) that record no contract they implement; checks aside. */
160
+ readonly noContract: OpenList<OpenItem>;
161
+ };
162
+ /**
163
+ * The process links not confirmed yet (CJ), and of them the suspect ones (CM): the program checked each as it was written,
164
+ * and a lane-checked link counts like a confirmed one; the cross-check rejects the suspect ones, or confirms them after
165
+ * reading the original.
166
+ */
167
+ readonly links: { readonly unconfirmed: OpenList<OpenItem & { readonly stepKind: string; readonly evidence: string }>; readonly suspect: OpenList<OpenItem & { readonly stepKind: string; readonly evidence: string; readonly why: string }> };
168
+ /** CJ: contracts still Planned although tickets implementing them are Done, with no reason stated (progressWhy). */
169
+ readonly contracts: { readonly stuckPlanned: OpenList<OpenItem & { readonly done: readonly string[] }> };
170
+ /**
171
+ * E153: the notes still current from earlier rounds that this round — a deepening or a Follow up — gave no result yet:
172
+ * not confirmed as still holding, not updated, not withdrawn (standing-notes.ts). The synthesis' to settle.
173
+ */
174
+ readonly notes: { readonly standing: OpenList<OpenItem & { readonly ask: string; readonly since: string; readonly on: string; readonly claims: string; readonly sources: readonly string[] }> };
175
+ }
176
+
177
+ let listLimit = OPEN_LIST_LIMIT;
178
+ const list = <T>(all: readonly T[]): OpenList<T> => ({ count: all.length, items: all.slice(0, listLimit) });
179
+ const clip = (s: string, n: number): string => (s.length > n ? `${s.slice(0, n - 1)}…` : s);
180
+ const GONE = new Set(['Replaced', 'Removed']);
181
+
182
+ function threadName(t: WorkThread): string {
183
+ return t.ids[0] && !t.title.includes(t.ids[0]) ? `${t.ids[0]} ${t.title}` : t.title;
184
+ }
185
+ function nameOf(store: ProjectStore, id: string): string {
186
+ const t = store.threads.get(id);
187
+ if (t) return threadName(t);
188
+ const r = store.reference.get(id);
189
+ return r ? `${r.category} ${r.ids[0] && !r.name.startsWith(r.ids[0]) ? `${r.ids[0]} ` : ''}${r.name}` : id;
190
+ }
191
+
192
+ /**
193
+ * The steps of the process whose link answers a candidate of this kind (Spec §2.12): the delivery a `No trace of done`
194
+ * misses, the check a `Not checked` misses, and so on.
195
+ */
196
+ const ANSWERING_STEPS: Readonly<Partial<Record<Breakpoint['kind'], readonly string[]>>> = {
197
+ 'No trace of done': ['Delivered', 'Merged', 'Fix'],
198
+ 'Not started': ['Dispatched', 'Delivered', 'Merged', 'Fix'],
199
+ 'Not merged': ['Merged'],
200
+ 'Not checked': ['QC', 'Review', 'Walkthrough'],
201
+ 'Fix not re-checked': ['QC', 'Review', 'Walkthrough'],
202
+ 'Findings open': ['Fix', 'Handed to'],
203
+ 'Passed with open items': ['Fix', 'Handed to'],
204
+ 'No plan': ['Planned'],
205
+ };
206
+
207
+ /**
208
+ * What answers a candidate that has not gone out yet, or null (a lane's "found it, and linked it"): a link of the step it
209
+ * misses — written in this round, or confirmed — a decision recorded as carried out, or a downstream item recorded as no
210
+ * longer current. The program puts the candidate out when it next recomputes the candidates, on confirmed links (a lane's
211
+ * link is confirmed in the cross-check); until then the write itself is the result.
212
+ */
213
+ export function answeredBy(store: ProjectStore, roundId: string, b: Breakpoint): string | null {
214
+ const steps = ANSWERING_STEPS[b.kind];
215
+ if (steps) {
216
+ const link = store.links.find((l) => l.workId === b.targetId && steps.includes(l.stepKind) && (l.roundId === roundId || linkHolds(l)));
217
+ if (link) return `link ${link.stepKind}: ${link.evidence?.label ?? link.ledgerRef}`;
218
+ }
219
+ if (b.kind === 'Not carried out' && store.reference.get(b.targetId)?.carryOut?.status === 'Carried out') return 'recorded as carried out';
220
+ if (b.kind === 'Downstream behind') {
221
+ const validity = store.reference.get(b.targetId)?.validity ?? store.threads.get(b.targetId)?.validity;
222
+ if (validity && validity !== 'Current') return `the downstream item is ${validity}`;
223
+ }
224
+ return null;
225
+ }
226
+
227
+ /** A candidate with no result yet: still a candidate (not out, not lit, no owner answer), no lane looked for it, and no write of this round answers it. */
228
+ export function candidatesWithoutResult(store: ProjectStore, roundId: string): Breakpoint[] {
229
+ return store.breakpoints.filter((b) => isCandidate(b) && !b.looked && !answeredBy(store, roundId, b)).sort((a, b) => a.kind.localeCompare(b.kind) || a.targetId.localeCompare(b.targetId));
230
+ }
231
+
232
+ /** Candidates a write answers that have not gone out yet: they go out once the link is confirmed and the candidates recomputed. */
233
+ export function candidatesAnswered(store: ProjectStore, roundId: string): Breakpoint[] {
234
+ return store.breakpoints.filter((b) => isCandidate(b) && !b.looked && answeredBy(store, roundId, b) !== null);
235
+ }
236
+
237
+ /** What a round has left open on the workbench, as it stands now. */
238
+ export function roundOpen(store: ProjectStore, round: Pick<ClerkRound, 'id' | 'lanes'>, opts: { readonly ledger?: Ledger | null; readonly limit?: number; readonly suggest?: boolean } = {}): RoundOpen {
239
+ const before = listLimit;
240
+ listLimit = opts.limit ?? OPEN_LIST_LIMIT;
241
+ try { return roundOpenNow(store, round, opts); } finally { listLimit = before; }
242
+ }
243
+
244
+ function roundOpenNow(store: ProjectStore, round: Pick<ClerkRound, 'id' | 'lanes'>, opts: { readonly ledger?: Ledger | null; readonly suggest?: boolean }): RoundOpen {
245
+ // The briefs of this round, by lane: a candidate is given to a lane when its brief names the candidate's id.
246
+ const briefs = (round.lanes ?? []).flatMap((l) => { const d = store.roundDocs.get(l.briefDocId); return d ? [{ lane: l.name, text: `${d.title}\n${d.markdown}` }] : []; });
247
+ const candidates = candidatesWithoutResult(store, round.id).map((b): OpenCandidate => ({
248
+ id: b.id, kind: b.kind, target: `${nameOf(store, b.targetId)} (${b.targetId})`, why: clip(b.why, 160),
249
+ briefs: briefs.filter((x) => x.text.includes(b.id)).map((x) => x.lane),
250
+ }));
251
+
252
+ const relations = store.relations.all();
253
+ // DB: what is not placed is read from the workbench's own placement (ui/placement.js through workbench-placement.ts):
254
+ // one definition for the story map and for these counts. CZ: an item carried on under the same number is current
255
+ // work there — it needs its current plan and its module like any other; an earlier generation's items are on its band.
256
+ const bench = workbenchUnplaced(store);
257
+ const placeOf = (id: string) => bench.placement.place.get(id);
258
+ const threadsOf = (ids: readonly string[]): WorkThread[] => ids.flatMap((id) => store.threads.get(id) ?? []);
259
+
260
+ // CQ (D104): the defined zeros. A project with no plan layer has nothing in no plan; one with no Areas nothing in no
261
+ // module; a work item with a recorded reason the program accepted is written as such, not counted as unplaced.
262
+ const planLayer = planLayerPresent(store);
263
+ const hasAreas = areasPresent(store);
264
+ const noPlanThreads = planLayer ? threadsOf(bench.lists.workNoPlan) : [];
265
+ const noModuleThreads = hasAreas ? threadsOf(bench.lists.workNoArea) : [];
266
+ // CN (E152): work for its whole plan, with the Keeper's reason, is placed — in its plan's cross-cutting cell.
267
+ const wholePlan = threadsOf(bench.lists.workWholePlan).map((t) => ({ id: t.id, name: threadName(t), why: clip(placeOf(t.id)?.whole ?? '', 160) }));
268
+ const noPlanWritten = planLayer ? threadsOf(bench.lists.workWrittenNoPlan).map((t) => ({ id: t.id, name: threadName(t), why: clip(placeOf(t.id)?.noPlanWhy ?? '', 160) })) : [];
269
+ const noAreaWritten = hasAreas ? threadsOf(bench.lists.workWrittenNoArea).map((t) => ({ id: t.id, name: threadName(t), why: clip(placeOf(t.id)?.noAreaWhy ?? '', 160) })) : [];
270
+
271
+ const onNothing: OpenItem[] = [];
272
+ const productOnly: OpenItem[] = [];
273
+ const productOnlyItems: ReferenceItem[] = [];
274
+ const designsUnplaced: (OpenItem & { on: string })[] = [];
275
+ const requirementsUnplaced: (OpenItem & { on: string })[] = [];
276
+ const numbered = (d: ReferenceItem): string => `${d.ids[0] && !d.name.startsWith(d.ids[0]) ? `${d.ids[0]} ` : ''}${clip(d.name, 100)}`;
277
+ // Boundaries are counted like decisions (CJ: 13 Boundaries were placed only on the Product and never questioned). CM: one
278
+ // placed on the whole product with its written reason (the cross-cutting ring shows it) is placed, and not listed here.
279
+ for (const { item: d, on } of unplacedIntent(store, bench.lists)) {
280
+ if (d.category === 'Decision' || d.category === 'Boundary') {
281
+ const item = { id: d.id, name: `${d.category === 'Boundary' ? 'Boundary ' : ''}${numbered(d)}` };
282
+ (on === 'nothing' ? onNothing : productOnly).push(item);
283
+ if (on !== 'nothing') productOnlyItems.push(d);
284
+ } else if (d.category === 'Design') designsUnplaced.push({ id: d.id, name: clip(d.name, 100), on });
285
+ else if (d.category === 'Requirement') requirementsUnplaced.push({ id: d.id, name: numbered(d), on });
286
+ }
287
+
288
+ // CM (CL §2): a design that refines three Areas or more was cut by chapter, not by what it serves; one that reaches no
289
+ // Area and no Plan is placed nowhere (Spec §8 had no placement while the unplaced count read 0).
290
+ const reach = reachOf(store, relations);
291
+ const areaShort = (id: string) => store.reference.get(id)?.name.split(/\s*[·::]\s*/)[0] ?? id;
292
+ const wide: (OpenItem & { areas: string[] })[] = [];
293
+ for (const d of store.reference.filter((r) => r.category === 'Design' && !GONE.has(r.validity))) {
294
+ const r = reach.ofReference(d.id);
295
+ if (r.areas.length >= WIDE_DESIGN) wide.push({ id: d.id, name: clip(d.name, 100), areas: r.areas.map(areaShort) });
296
+ }
297
+ const misplaced = misplacedItems(store).map((m) => ({ id: m.id, name: clip(m.name, 100), area: m.area, how: m.how, sits: m.sits }));
298
+
299
+ const withoutItems = store.generations.filter((g) => g.workIds.length === 0).map((g) => ({ id: g.id, name: g.name }));
300
+
301
+ // ── CJ: what the program counts against the ledger ──
302
+ let ledger: Ledger | null = opts.ledger ?? null;
303
+ const opened = opts.ledger === undefined;
304
+ if (opened) { try { ledger = Ledger.openDir(store.dir); } catch { ledger = null; } }
305
+ try {
306
+ // CZ: an item listed because current documents define its number too went on under that number (generation-check.ts).
307
+ const withoutDestination = itemsWithoutDestination(store, ledger).map((x) => ({ id: x.id, name: `${x.number ? `${x.number} ` : ''}${clip(x.name, 80)}`, generation: x.generation, validity: x.validity }));
308
+ const counted = countedOpen(store, round, ledger, relations);
309
+ // Where each Product-only decision traces to: its own entry, and the current documents citing it (CM; placing.ts).
310
+ const index = traceIndex(store, reach);
311
+ const lines = lineIndex(store);
312
+ const traceable: (OpenItem & { names: readonly string[]; cited: readonly string[]; suggest: readonly string[] })[] = [];
313
+ for (const d of productOnlyItems) {
314
+ const t = tracePlacement(store, ledger, d, { reach, index, lines });
315
+ if (!t.names.length && !t.citations) continue;
316
+ traceable.push({ id: d.id, name: `${d.category === 'Boundary' ? 'Boundary ' : ''}${d.ids[0] && !d.name.startsWith(d.ids[0]) ? `${d.ids[0]} ` : ''}${clip(d.name, 100)}`, names: t.names, cited: t.cited, suggest: t.suggest });
317
+ }
318
+ // CN (E152): where the project's own writing leads for the work items not placed — a plan through the decisions a
319
+ // work item carries out, cites or is cited by; the whole plan when a range its ticket writes spans three modules.
320
+ // Read for the lists themselves; a caller that only counts leaves them out (`suggest: false`).
321
+ const suggest = opts.suggest !== false;
322
+ // CQ (D104): the suggestion is the whole inference — rows, front matter, contract, decisions, links — not the decision
323
+ // chain alone; what it leads to clearly the program has placed already, so what is listed here is what it could not.
324
+ const ctx = suggest ? inferenceContext(store, ledger, { reach, index, lines }) : null;
325
+ const noPlan = noPlanThreads.map((t) => {
326
+ // DB: the plans it serves that have no band are named; the records' leads to those same plans are not a suggestion.
327
+ const notDrawn = placeOf(t.id)?.plansNotDrawn ?? [];
328
+ const via = !ctx ? [] : inferThread(ctx, t).plans.filter((l) => bench.drawnPlans.has(l.id)).slice(0, 3).map((l) => `${l.name} (plan) — ${l.through.join('; ')}`);
329
+ return { id: t.id, name: threadName(t), ...(notDrawn.length ? { notCurrent: notDrawn.map((id) => planNotDrawnLine(store, bench.placement, id)) } : {}), ...(via.length ? { suggest: via } : {}) };
330
+ });
331
+ const noModule = noModuleThreads.map((t) => {
332
+ const w = suggest ? wholePlanSuggestion(store, ledger, t, { reach, index }) : null;
333
+ return { id: t.id, name: threadName(t), ...(w ? { suggest: `the whole plan: ${w.from} names ${w.range}, items of ${w.modules.length} modules (${w.modules.join(', ')}) — if it serves the plan as a whole and no single module, pk_write_thread({ id, wholePlanWhy })` } : {}) };
334
+ });
335
+ const zero = <T>(sentence: string): OpenList<T> => ({ count: 0, items: [], notApplicable: sentence });
336
+ return {
337
+ candidates: list(candidates),
338
+ workItems: {
339
+ noPlan: planLayer ? list(noPlan) : zero(NO_PLAN_LAYER), noModule: hasAreas ? list(noModule) : zero(NO_AREAS), wholePlan: list(wholePlan),
340
+ noPlanWritten: list(noPlanWritten), noAreaWritten: list(noAreaWritten),
341
+ },
342
+ inferred: list(inferredPlacementRows(store)),
343
+ programPlacements: programPlacementCounts(store),
344
+ decisions: { onNothing: list(onNothing), productOnly: list(productOnly), traceable: list(traceable) },
345
+ generations: {
346
+ withoutItems: list(withoutItems), withoutDestination: list(withoutDestination),
347
+ candidates: list(openCandidates(store, ledger).map((c) => ({ id: c.key, name: c.name, why: clip(c.why, 220), documents: c.planRefs.length, numbers: c.numbers.length, carriedOn: c.carriedOn.length, work: candidateWorkLines(c) }))),
348
+ },
349
+ designs: { wide: list(wide), unplaced: list(designsUnplaced) },
350
+ requirements: { unplaced: list(requirementsUnplaced) },
351
+ duplicates: list(duplicateNumbers(store)),
352
+ misplaced: list(misplaced),
353
+ notes: { standing: list(standingOpen(store, round.id)) },
354
+ ...counted,
355
+ };
356
+ } finally {
357
+ if (opened) ledger?.close();
358
+ }
359
+ }
360
+
361
+ /**
362
+ * CZ: the numbers two items of one category carry — in their ids, or at the start of their name. Work items are one
363
+ * category; reference items go by theirs (a Requirement and the work item of the same contract share a number by design).
364
+ * Items no longer in the project's current version (`Removed`) are left out.
365
+ */
366
+ export function duplicateNumbers(store: ProjectStore): (OpenItem & { number: string; category: string; items: OpenItem[] })[] {
367
+ const groups = new Map<string, OpenItem & { number: string; category: string; items: OpenItem[] }>();
368
+ const add = (category: string, id: string, name: string, ids: readonly string[]) => {
369
+ const numbers = new Set([...ids.map((i) => i.trim().toUpperCase()).filter((i) => isNumberShaped(i)), ...(primaryIdentifier(ids, name) ? [primaryIdentifier(ids, name)!] : [])]);
370
+ for (const number of numbers) {
371
+ const k = `${category}\u0000${number}`;
372
+ const g = groups.get(k) ?? { id: `${category}:${number}`, name: `${category} ${number}`, number, category, items: [] };
373
+ g.items.push({ id, name: clip(name, 100) });
374
+ groups.set(k, g);
375
+ }
376
+ };
377
+ for (const r of store.reference.filter((x) => x.validity !== 'Removed')) add(r.category, r.id, r.name, r.ids);
378
+ for (const t of store.threads.filter((x) => x.validity !== 'Removed')) add('Work item', t.id, t.title, t.ids);
379
+ return [...groups.values()].filter((g) => g.items.length > 1).sort((a, b) => a.category.localeCompare(b.category) || a.number.localeCompare(b.number, undefined, { numeric: true }));
380
+ }
381
+
382
+ /** How many Areas a design refines before the program flags it as cut too wide (CM; CL's findings §2). */
383
+ export const WIDE_DESIGN = 3;
384
+
385
+ /** What an item's own entry names that traces it below the Product (placing.ts `entryNames`). */
386
+ export function traceableNames(store: ProjectStore, item: ReferenceItem): string[] {
387
+ return entryNames(store, item);
388
+ }
389
+
390
+ /** The lists the ledger's numbering lets the program count (CJ); empty without a ledger. */
391
+ function countedOpen(store: ProjectStore, round: Pick<ClerkRound, 'id' | 'lanes'>, ledger: Ledger | null, relations: readonly GraphRelation[]): Pick<RoundOpen, 'entries' | 'names' | 'tickets' | 'links' | 'contracts'> {
392
+ const entries = entryGaps(store, ledger);
393
+
394
+ // Items named by their number alone although the defining line has a title.
395
+ const numberOnly: (OpenItem & { suggested: string })[] = [];
396
+ if (ledger) {
397
+ const named = [...store.reference.all().map((r) => ({ id: r.id, name: r.name, sourceIds: r.sourceIds, what: `${r.category}` })), ...store.threads.all().map((t) => ({ id: t.id, name: t.title, sourceIds: [] as readonly string[], what: 'Work item' }))];
398
+ for (const x of named) {
399
+ if (!isNumberOnly(x.name)) continue;
400
+ const refusal = numberOnlyRefusal(store, ledger, x.name, x.sourceIds, 'The name');
401
+ if (!refusal) continue;
402
+ const suggested = /e\.g\. “([^”]*)”/.exec(refusal)?.[1] ?? '';
403
+ numberOnly.push({ id: x.id, name: `${x.what} ${x.name}`, suggested });
404
+ }
405
+ }
406
+
407
+ // Tickets: the numbers a task index or an execution arrangement defines.
408
+ const ticketNums = new Set<string>();
409
+ if (ledger) for (const f of requiredEntries(store, ledger)) if (f.layer === 'Task index' || f.layer === 'Execution arrangement') for (const e of f.entries) ticketNums.add(e.num.toUpperCase());
410
+ const standsIn = (title: string, n: string) => new RegExp(`(?<![A-Za-z0-9_-])${n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![A-Za-z0-9_])`).test(title);
411
+ const noIds: (OpenItem & { number: string })[] = [];
412
+ const otherNumbers: (OpenItem & { ids: string[] })[] = [];
413
+ const noContract: OpenItem[] = [];
414
+ const live = store.threads.filter((t) => !GONE.has(t.validity));
415
+ const unknownIds = ledger ? new Set(undefinedNumbers(ledger, [...new Set(live.flatMap((t) => t.ids))]).map((i) => i.toUpperCase())) : new Set<string>();
416
+ for (const t of live) {
417
+ // Not its own: another object's number (a decision's D99), or a label the project never writes (KIMI-P1, CLAUDE-D99).
418
+ const others = ledger ? t.ids.filter((i) => unknownIds.has(i.toUpperCase()) || otherObjectOf(store, ledger, i) !== null) : [];
419
+ if (others.length) otherNumbers.push({ id: t.id, name: threadName(t), ids: others });
420
+ const own = t.ids.filter((i) => !others.includes(i));
421
+ if (own.length === 0) {
422
+ const number = isNumberShaped(t.id) && ticketNums.has(t.id.toUpperCase()) ? t.id : [...ticketNums].find((n) => standsIn(t.title, n)) ?? null;
423
+ if (number) noIds.push({ id: t.id, name: threadName(t), number });
424
+ }
425
+ const isTicket = [...own, ...(own.length ? [] : [t.id])].some((i) => ticketNums.has(i.toUpperCase()));
426
+ if (isTicket && !CHECK_WORDS.test(t.title) && !relations.some((r) => r.type === 'implements' && r.from === t.id)) noContract.push({ id: t.id, name: threadName(t) });
427
+ }
428
+
429
+ // The links not confirmed yet, whichever round wrote them: a skeleton's links wait for a deepening's cross-check.
430
+ const unconfirmed = unconfirmedLinks(store).map((l) => ({
431
+ id: l.id, name: nameOf(store, l.workId), stepKind: l.stepKind, evidence: clip(l.evidence?.label ?? l.ledgerRef, 120),
432
+ }));
433
+ const suspect = suspectLinks(store).map((l) => ({
434
+ id: l.id, name: nameOf(store, l.workId), stepKind: l.stepKind, evidence: clip(l.evidence?.line ?? l.evidence?.label ?? l.ledgerRef, 120), why: clip(l.check?.why ?? 'not checked by the program', 160),
435
+ }));
436
+
437
+ // Contracts still Planned although tickets implementing them are Done, with no reason stated.
438
+ const stuck: (OpenItem & { done: string[] })[] = [];
439
+ const contractThread = (to: string): WorkThread | undefined => {
440
+ const t = store.threads.get(to);
441
+ if (t) return t;
442
+ const r = store.reference.get(to);
443
+ if (!r) return undefined;
444
+ const n = r.ids.map((i) => i.toUpperCase());
445
+ return store.threads.find((x) => x.ids.some((i) => n.includes(i.toUpperCase())));
446
+ };
447
+ const doneBy = new Map<string, string[]>();
448
+ for (const r of relations.filter((x) => x.type === 'implements')) {
449
+ const ticket = store.threads.get(r.from);
450
+ const contract = contractThread(r.to);
451
+ if (!ticket || !contract || ticket.id === contract.id || ticket.progress !== 'Done') continue;
452
+ doneBy.set(contract.id, [...new Set([...(doneBy.get(contract.id) ?? []), threadName(ticket)])]);
453
+ }
454
+ for (const [id, done] of doneBy) {
455
+ const c = store.threads.get(id)!;
456
+ if (c.progress === 'Planned' && !GONE.has(c.validity) && !c.progressWhy) stuck.push({ id, name: threadName(c), done });
457
+ }
458
+
459
+ return {
460
+ entries,
461
+ names: { numberOnly: list(numberOnly) },
462
+ // CQ (D104): a project with no contract layer (no Task contract layer, nothing implementing a contract) has no ticket missing its contract.
463
+ tickets: { noIds: list(noIds), otherNumbers: list(otherNumbers), noContract: contractLayerPresent(store) ? list(noContract) : { count: 0, items: [], notApplicable: NO_CONTRACT_LAYER } },
464
+ links: { unconfirmed: list(unconfirmed), suspect: list(suspect) },
465
+ contracts: { stuckPlanned: list(stuck) },
466
+ };
467
+ }
468
+
469
+ /** The notes standing from earlier rounds that the round gave no result yet (E153); none for a round the assets do not hold. */
470
+ function standingOpen(store: ProjectStore, roundId: string) {
471
+ const round = store.clerkRounds.get(roundId);
472
+ return round ? standingNotes(store, round).filter((s) => s.result === null).map((s) => standingItem(store, s)) : [];
473
+ }
474
+
475
+ /** The process links not confirmed yet, whichever round wrote them (lane-checked ones among them). */
476
+ export function unconfirmedLinks(store: ProjectStore): ProcessLinkLike[] {
477
+ return store.links.filter((l) => !l.confirmed);
478
+ }
479
+
480
+ /**
481
+ * The links the synthesis waits on (CM, E151): neither confirmed nor lane-checked — their evidence failed the program's
482
+ * check when they were written, or they were written before the check. On the gated run the gate counted every
483
+ * unconfirmed link, and 82 were "confirmed" in 26 seconds without a file opened.
484
+ */
485
+ export function suspectLinks(store: ProjectStore): ProcessLinkLike[] {
486
+ return store.links.filter((l) => !linkHolds(l));
487
+ }
488
+ type ProcessLinkLike = ReturnType<ProjectStore['links']['all']>[number];
489
+
490
+ const wholeSuggested = (open: RoundOpen): number => open.workItems.noModule.items.filter((i) => i.suggest).length;
491
+ /** DB: of the work in no plan, how many serve only a plan the workbench draws no band for, and which plans. */
492
+ function notCurrentNote(open: RoundOpen): string {
493
+ const listed = open.workItems.noPlan.items.filter((i) => i.notCurrent?.length);
494
+ if (!listed.length) return '';
495
+ const plans = [...new Set(listed.flatMap((i) => i.notCurrent ?? []))];
496
+ return ` (${listed.length} of them serve${listed.length === 1 ? 's' : ''} only a plan the workbench draws no band for: ${plans.slice(0, 3).join('; ')}${plans.length > 3 ? '; …' : ''})`;
497
+ }
498
+
499
+ /**
500
+ * CS: the program's placements in one clause, as counts — for the handover note and, through the synthesis' task, the
501
+ * round's Result: how many it placed in all, how many have a result (confirmed, moved), how many nobody reviewed yet.
502
+ * Empty when the program placed nothing in any round.
503
+ */
504
+ export function programPlacementsLine(c: ProgramPlacementCounts): string {
505
+ if (!c.placed) return '';
506
+ return `${c.placed} placement${c.placed === 1 ? '' : 's'} the program wrote from the records in all (basis Inferred): ${c.confirmed} confirmed, ${c.moved} moved, ${c.unreviewed} not reviewed yet${c.unreviewed ? ' (they stand as placed; pk_round_state { list: "inferredPlacements" } lists them, whichever round placed them)' : ''}`;
507
+ }
508
+
509
+ /**
510
+ * The note on what is still unplaced, counted; it refuses nothing. `pk_stage` gives it to the main agent as it hands the
511
+ * round over (D103: for its handover to say which stay unplaced and why), and the synthesis job gets it in its task.
512
+ */
513
+ export function unplacedNote(open: RoundOpen): string | null {
514
+ const parts = [
515
+ open.workItems.noPlan.count ? `${open.workItems.noPlan.count} work item${open.workItems.noPlan.count === 1 ? '' : 's'} in no plan${notCurrentNote(open)}` : '',
516
+ open.workItems.noModule.count ? `${open.workItems.noModule.count} work item${open.workItems.noModule.count === 1 ? '' : 's'} in no module${wholeSuggested(open) ? ` (${wholeSuggested(open)} of them the program reads as work for the whole plan)` : ''}` : '',
517
+ // CQ (D104): a recorded reason is "written: no plan — why". CS: the program's placements of every round are counted —
518
+ // placed in all, confirmed, moved, still unreviewed — as counts; the unreviewed ones stand as placed (U86).
519
+ programPlacementsLine(open.programPlacements),
520
+ open.workItems.noPlanWritten.count ? `${open.workItems.noPlanWritten.count} work item${open.workItems.noPlanWritten.count === 1 ? '' : 's'} written: no plan — ${open.workItems.noPlanWritten.items.slice(0, 3).map((i) => `${i.name.split(' ')[0]}: ${clip(i.why, 60)}`).join('; ')}${open.workItems.noPlanWritten.count > 3 ? '; …' : ''} (placed by the recorded reason, not counted as unplaced)` : '',
521
+ open.workItems.noAreaWritten.count ? `${open.workItems.noAreaWritten.count} work item${open.workItems.noAreaWritten.count === 1 ? '' : 's'} written: no module — ${open.workItems.noAreaWritten.items.slice(0, 3).map((i) => `${i.name.split(' ')[0]}: ${clip(i.why, 60)}`).join('; ')}${open.workItems.noAreaWritten.count > 3 ? '; …' : ''}` : '',
522
+ open.decisions.onNothing.count ? `${open.decisions.onNothing.count} decision${open.decisions.onNothing.count === 1 ? '' : 's'} placed on nothing` : '',
523
+ open.decisions.productOnly.count ? `${open.decisions.productOnly.count} decision${open.decisions.productOnly.count === 1 ? '' : 's'} placed only on the Product` : '',
524
+ open.generations.withoutItems.count ? `${open.generations.withoutItems.count} earlier generation${open.generations.withoutItems.count === 1 ? '' : 's'} with no planned item recorded` : '',
525
+ open.generations.withoutDestination.count ? `${open.generations.withoutDestination.count} item${open.generations.withoutDestination.count === 1 ? '' : 's'} of an earlier generation with no destination recorded` : '',
526
+ open.generations.candidates.count ? `${open.generations.candidates.count} candidate generation${open.generations.candidates.count === 1 ? '' : 's'} neither accepted nor rejected` : '',
527
+ open.decisions.traceable.count ? `${open.decisions.traceable.count} of the Product-only decisions and boundaries traceable (their entry names a contract, a Spec section, a module, a requirement or a plan, or placed documents cite them; pk_round_state { list: "traceable" } suggests the area)` : '',
528
+ open.designs.wide.count ? `${open.designs.wide.count} design${open.designs.wide.count === 1 ? '' : 's'} refining ${WIDE_DESIGN} areas or more (cut by chapter: one design per section that serves one area)` : '',
529
+ open.designs.unplaced.count ? `${open.designs.unplaced.count} design${open.designs.unplaced.count === 1 ? '' : 's'} placed on nothing or only on the Product with no reason` : '',
530
+ open.requirements.unplaced.count ? `${open.requirements.unplaced.count} requirement${open.requirements.unplaced.count === 1 ? '' : 's'} placed on nothing or only on the Product with no reason` : '',
531
+ open.misplaced.count ? `${open.misplaced.count} item${open.misplaced.count === 1 ? '' : 's'} naming an area in the project’s own writing but sitting elsewhere` : '',
532
+ open.names.numberOnly.count ? `${open.names.numberOnly.count} item${open.names.numberOnly.count === 1 ? '' : 's'} named by the number alone` : '',
533
+ open.tickets.noIds.count ? `${open.tickets.noIds.count} ticket${open.tickets.noIds.count === 1 ? '' : 's'} with no ids` : '',
534
+ open.tickets.otherNumbers.count ? `${open.tickets.otherNumbers.count} work item${open.tickets.otherNumbers.count === 1 ? '' : 's'} carrying another object’s number as their id` : '',
535
+ open.tickets.noContract.count ? `${open.tickets.noContract.count} ticket${open.tickets.noContract.count === 1 ? '' : 's'} recording no contract they implement` : '',
536
+ open.contracts.stuckPlanned.count ? `${open.contracts.stuckPlanned.count} contract${open.contracts.stuckPlanned.count === 1 ? '' : 's'} still Planned with Done tickets and no reason stated` : '',
537
+ ].filter(Boolean);
538
+ if (!parts.length) return null;
539
+ // CQ (D104): the defined zeros — a layer the project lacks is said in a sentence, after the counts, never as a list.
540
+ const zeros = [
541
+ open.workItems.noPlan.notApplicable ? `work in no plan: ${open.workItems.noPlan.notApplicable}` : '',
542
+ open.workItems.noModule.notApplicable ? `work in no module: ${open.workItems.noModule.notApplicable}` : '',
543
+ open.tickets.noContract.notApplicable ? `tickets without a contract: ${open.tickets.noContract.notApplicable}` : '',
544
+ ].filter(Boolean);
545
+ return `=== Unplaced on the workbench\n${[...parts, ...zeros].join('; ')}. pk_round_state lists them (open). Where the project's own records place an item the program has placed it (basis Inferred; check its chain, keep it or move it with the record); say in the Result which stay unplaced and why, each by its recorded reason (noPlanWhy, noAreaWhy — the program refuses a reason where the records lead somewhere); a decision, requirement or design that really concerns the whole product refines the Product and is written with its reason (pk_write_reference wholeProductWhy), which places it in the cross-cutting ring; a work item whose only plan is not current is in no plan — write that plan Current when the current plan document lists it as in force, or give the work the current plan it belongs to, or its reason (noPlanWhy).`;
546
+ }
547
+
548
+ // ───────────────────────── one list at a time (CM) ─────────────────────────
549
+
550
+ /**
551
+ * The open lists by one flat key each (CM, E151; CK fix 5): `pk_round_state` returns their counts, and one list in full
552
+ * when asked by its key (`{ list: "entries" }`). On the gated run the whole of `open` came back with every call — 34K and
553
+ * 57K characters, six calls, about 91K tokens — though the main agent acted on a few lists at a time.
554
+ */
555
+ export const OPEN_KEYS: Readonly<Record<string, { readonly what: string; readonly of: (o: RoundOpen) => { readonly count: number; readonly items: readonly unknown[] } }>> = {
556
+ candidates: { what: 'breakpoint candidates with no result yet (not answered by a write, not looked for, not put out, not lit), each with the briefs that name it', of: (o) => o.candidates },
557
+ noPlan: { what: 'work items in no plan, as the workbench places them: in no band, and not placed from the records by the program; `suggest` lists the current plans the records lead to, each with its chain, where they lead to several — place it by the record (pk_write_thread serves the Plan), or write noPlanWhy naming the records you read; `notCurrent` names the plans it serves that the workbench draws no band for (not current: Proposed, Deferred, Replaced, Abandoned, or held by an earlier generation) — write that plan Current when the current plan document lists it as in force, or give the work the current plan it belongs to, or noPlanWhy saying why its only plan is not in force; "not applicable" when the project has no plan layer', of: (o) => o.workItems.noPlan },
558
+ noModule: { what: `work items in no module; \`suggest\` says when a range its ticket writes spans ${WHOLE_PLAN_MODULES} modules or more — work for its whole plan is recorded with pk_write_thread wholePlanWhy and is then placed; "not applicable" when the project has no Areas`, of: (o) => o.workItems.noModule },
559
+ noPlanWritten: { what: 'work items written as in no plan, each with the recorded reason the program accepted (noPlanWhy): placed by the reason, not counted as unplaced', of: (o) => o.workItems.noPlanWritten },
560
+ noAreaWritten: { what: 'work items written as in no module, each with its recorded reason (noAreaWhy)', of: (o) => o.workItems.noAreaWritten },
561
+ inferredPlacements: { what: 'the placements the program wrote from the records, basis Inferred, that nobody has confirmed or moved yet — of this round and of earlier ones: each with where it was placed, the chain of records, and the round and stage that placed it. They stand as placed; check each against the record in its claim, and move a wrong one with replaceServes / refines. One a write keeps as its own (the same serves with your claim and basis; refines given whole with the target) or moves leaves the list', of: (o) => o.inferred },
562
+ onNothing: { what: 'decisions and boundaries placed on nothing', of: (o) => o.decisions.onNothing },
563
+ productOnly: { what: 'decisions and boundaries placed only on the Product, with no written reason', of: (o) => o.decisions.productOnly },
564
+ traceable: { what: 'of those, the ones their own entry or the documents citing them trace to an area or a plan, each with what it names, where it is cited and the areas suggested', of: (o) => o.decisions.traceable },
565
+ designsWide: { what: `designs that refine ${WIDE_DESIGN} areas or more: cut by chapter, to be cut where a section serves one area`, of: (o) => o.designs.wide },
566
+ designsUnplaced: { what: 'designs the workbench does not place: on nothing, or only on the Product with no reason (`on`) — refine the area or plan each serves, or, for one that concerns the whole product, refine the Product and write wholeProductWhy', of: (o) => o.designs.unplaced },
567
+ requirementsUnplaced: { what: 'requirements the workbench does not place: on nothing, or only on the Product with no reason (`on`) — refine the area each belongs to (its group heading or module column names it), or, for one that holds for the whole product, refine the Product and write wholeProductWhy', of: (o) => o.requirements.unplaced },
568
+ misplaced: { what: 'items whose contract, group heading or own entry names an area but which sit elsewhere', of: (o) => o.misplaced },
569
+ generationsWithoutItems: { what: 'earlier generations with no planned item recorded', of: (o) => o.generations.withoutItems },
570
+ withoutDestination: { what: 'items of an earlier generation with no destination: give each replacedBy (an id or the project’s number), a current item that depends on or carries it, or Abandoned', of: (o) => o.generations.withoutDestination },
571
+ generationCandidates: { what: 'candidate generations the program lists (archived or superseded plan, contract and module sets) waiting for pk_generation_candidate accept or reject; `work` says what each set’s plan documents list as rows, what went on under the same number, and what its other documents define (not its work)', of: (o) => o.generations.candidates },
572
+ entries: { what: 'numbers a current decision record, plan, task index or execution arrangement defines — or a current document that alone defines their family (`families`) — that no item carries and no account names, by file', of: (o) => ({ count: o.entries.count, items: o.entries.files.filter((f) => f.missing.length).map((f) => ({ path: f.path, layer: f.layer, ...(f.families ? { families: f.families } : {}), missing: f.missing.map((m) => m.num) })) }) },
573
+ duplicates: { what: 'numbers two items of one category carry: a number is written once — keep the item the document’s fill wrote (update it by its id or by the number), and merge or withdraw the other (pk_merge_work_items for work items; validity Removed has its own conditions, so say in your Result which one stands)', of: (o) => o.duplicates },
574
+ numberOnly: { what: 'items named by the number alone', of: (o) => o.names.numberOnly },
575
+ noIds: { what: 'tickets with no ids', of: (o) => o.tickets.noIds },
576
+ otherNumbers: { what: 'work items carrying another object’s number as their id', of: (o) => o.tickets.otherNumbers },
577
+ noContract: { what: 'tickets recording no contract they implement', of: (o) => o.tickets.noContract },
578
+ links: { what: 'process links neither confirmed nor lane-checked: their evidence failed the program’s check (or was never checked) — reject each, or confirm it after reading the original', of: (o) => o.links.suspect },
579
+ stuckPlanned: { what: 'contracts still Planned with Done tickets and no reason stated', of: (o) => o.contracts.stuckPlanned },
580
+ standingNotes: { what: 'notes still current from earlier rounds with no result this round (a deepening or a Follow up): the synthesis confirms each as still holding with what it read (pk_confirm_note), updates it (pk_write_note with its id), or withdraws it with why (pk_close_note)', of: (o) => o.notes.standing },
581
+ };
582
+
583
+ /** The count of every open list, by key. */
584
+ export function openCounts(open: RoundOpen): Record<string, number> {
585
+ return Object.fromEntries(Object.entries(OPEN_KEYS).map(([k, v]) => [k, v.of(open).count]));
586
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A project's schedule against its own record (Spec §3.8; CKC-07 AC-28~AC-30): the schedule in force, the scheduled
3
+ * time that is due, and the next one. The planner (clerk.ts) and the `Daily` page (keeper-page.ts) read the same
4
+ * functions, so the page says what the planner will do.
5
+ */
6
+ import type { OrganizeSchedule, Project } from '../../model/types.ts';
7
+ import type { ProjectStore } from '../../store/project-store.ts';
8
+ import { nextSlot, scheduleOf, scheduledDue } from './schedule.ts';
9
+
10
+ type ScheduleProject = Pick<Project, 'schedule' | 'organizeRhythm' | 'scheduleHandled'>;
11
+
12
+ /** When the project's latest round ended; null when none has. */
13
+ export function lastRoundEnd(store: ProjectStore): string | null {
14
+ return store.clerkRounds.all().reduce<string | null>((at, r) => (r.endedAt && (!at || r.endedAt > at) ? r.endedAt : at), null);
15
+ }
16
+
17
+ /** The schedule in force: the one saved, or the default from when the takeover finished (§3.8). */
18
+ export function scheduleInForce(project: ScheduleProject, takeoverDoneAt: string | null): OrganizeSchedule {
19
+ return scheduleOf(project, { takeoverDoneAt });
20
+ }
21
+
22
+ /**
23
+ * From when scheduled times count: the latest of when the schedule was saved, when the takeover finished, and the last
24
+ * scheduled time already dealt with. Times before it are not made up for.
25
+ */
26
+ function since(schedule: OrganizeSchedule, project: ScheduleProject, takeoverDoneAt: string | null): Date {
27
+ return new Date([schedule.savedAt, takeoverDoneAt, project.scheduleHandled?.slot].reduce<number>((at, x) => Math.max(at, x ? Date.parse(x) : 0), 0));
28
+ }
29
+
30
+ /** The scheduled time that has come and was not dealt with, with how many were missed; null when none. */
31
+ export function scheduleDueFor(_store: ProjectStore, project: ScheduleProject, takeoverDoneAt: string | null, now: number): { readonly slot: Date; readonly missed: number } | null {
32
+ const schedule = scheduleInForce(project, takeoverDoneAt);
33
+ return scheduledDue(schedule, new Date(now), since(schedule, project, takeoverDoneAt));
34
+ }
35
+
36
+ /** The next scheduled time after now; null when the schedule starts nothing by the clock (`Off`, `Continuous`). */
37
+ export function nextScheduled(project: ScheduleProject, takeoverDoneAt: string | null, now: number): Date | null {
38
+ return nextSlot(scheduleInForce(project, takeoverDoneAt), new Date(now));
39
+ }