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,934 @@
1
+ // The process view of increment K (Spec v3.0 §6.3, §2.11, §2.12, §1.18; CKC-24): the List's work cells with each
2
+ // step's did on the left and what reality gave on the right (mockup list-process-v1.html), the graph's default of
3
+ // results only with a work's branch growing on click (mockup graph-process-v0.html), breakpoints, send-backs with
4
+ // `Copy for agent`, earlier generations rolled up, and the popover's `How it got here` and versions.
5
+ //
6
+ // Everything shown comes from the views of k-data.js (src/model/views-k.ts shapes); when an endpoint is not built yet
7
+ // the surface says `No data for this yet` and nothing is invented. List and Graph share one fold rule (CKC-24 AC-11,
8
+ // AC-12; k-fold.js): every work is folded until the owner opens it, and what the owner opened or folded holds in both
9
+ // (E152, E153).
10
+ import { h, append, clear, toast, openDialog, select, state, navigate, popover, flyout, refreshProject, renderBody } from './app.js';
11
+ import * as K from './k-data.js';
12
+ import { setProcessLayer, processImage, PROC_SIZE } from './graph.js';
13
+ import { openDraft, sessionsBlock } from './k/draft-view.js';
14
+ import { fillMarkdown } from './markdown.js';
15
+ import { createFold, expandableIn, stepOfIn, foldLine, openMark } from './k-fold.js';
16
+ import { generationKey } from './list-fold.js';
17
+
18
+ const NO_DATA = K.noData();
19
+
20
+ // What the views module opens for the process view: an original source, a code territory in `Code`. Set by views.js
21
+ // (this module is imported by it, so it does not import it back).
22
+ let openers = { openSource: null };
23
+ export function setOpeners(o) { openers = { ...openers, ...o }; }
24
+
25
+ /** A session's draft, from anywhere a session is reached (§3.11; CKC-23 AC-18). */
26
+ export const draftCtx = () => ({
27
+ fetchDraft: (key) => K.getDraft(state.projectId, key),
28
+ openSource: openers.openSource,
29
+ openDraft: (key) => openDraft(key, draftCtx()),
30
+ });
31
+
32
+ /**
33
+ * `Code` with one territory brought into view (§6.17 "从过程视图的工作跳到它改过的领地"): the Project graph switches to
34
+ * its `Code` mode, and the territory's row is scrolled to, marked, and its files opened.
35
+ */
36
+ export function openTerritory(territoryId) {
37
+ popover.close('navigate');
38
+ state.graphMode = 'code';
39
+ state.codeTarget = territoryId;
40
+ if (state.view !== 'graph') navigate(state.projectId, 'graph');
41
+ else void renderBody();
42
+ }
43
+
44
+ // ── the data and the one fold rule ─────────────────────────────────────────
45
+ // undefined until the first answer arrives — a caller that comes in while the fetch is in flight must wait on
46
+ // `loading`, not take null as the answer (the first render's Expand all and branches depend on it).
47
+ let proc;
48
+ let procPid = null;
49
+ let loading = null;
50
+ // The one fold rule (CKC-24 AC-11; k-fold.js): every work folded until the owner opens it, the same choice in the List
51
+ // and in the Graph (E152, E153).
52
+ let seededFor = null;
53
+
54
+ export async function load(pid, { refresh = false } = {}) {
55
+ if (!refresh && procPid === pid && proc !== undefined) return proc;
56
+ if (!refresh && loading && procPid === pid) return loading;
57
+ procPid = pid;
58
+ loading = (async () => {
59
+ try { proc = await K.getProcess(pid); } catch { proc = null; }
60
+ if (proc && seededFor !== pid) {
61
+ seededFor = pid;
62
+ fold.reset();
63
+ }
64
+ loading = null;
65
+ return proc;
66
+ })();
67
+ return loading;
68
+ }
69
+ export function current() { return proc; }
70
+ export async function reload(pid) { return load(pid, { refresh: true }); }
71
+
72
+ const stepOf = (id) => stepOfIn(proc, id);
73
+ const expandable = (id) => expandableIn(proc, id);
74
+ export const fold = createFold(() => proc);
75
+ const listOpen = (id) => fold.has(id);
76
+ /** A row of the List opened or folded by hand. The browser also says `toggle` for a row drawn open, which changes nothing. */
77
+ const listToggled = (id, open) => { if (open !== fold.has(id)) fold.set(id, open); };
78
+
79
+ // ── §2.11 times ────────────────────────────────────────────────────────────
80
+ const shortAt = (at) => {
81
+ if (!at) return '—';
82
+ if (!at.includes('T')) return at; // a date stays a date (§1.8)
83
+ const d = new Date(at);
84
+ if (Number.isNaN(d.getTime())) return at;
85
+ const p = (n) => String(n).padStart(2, '0');
86
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
87
+ };
88
+ const BASIS_WORD = { Commit: 'committed', Session: 'session', 'Written in text': 'Text says', 'File time': 'file time', 'First observed': 'first seen' };
89
+ /** One `Occurred` as §2.11 wants it: the text's claim and the observed time are both written when they disagree. */
90
+ export function fmtOccurred(o) {
91
+ if (!o) return '—';
92
+ if (o.undated) return `Undated · first seen ${shortAt(o.at)}`;
93
+ const main = `${BASIS_WORD[o.basis] ?? o.basis} ${shortAt(o.at)}`;
94
+ if (!o.other?.at) return shortAt(o.at);
95
+ const other = `${BASIS_WORD[o.other.basis] ?? o.other.basis} ${shortAt(o.other.at)}`;
96
+ return o.basis === 'Written in text' ? `${main} · ${other}` : `${other} · ${main}`;
97
+ }
98
+ const occurredTitle = (o) => !o ? '' : o.undated ? `Undated · first seen ${shortAt(o.at)} (basis: First observed)` : `${BASIS_WORD[o.basis] ?? o.basis} ${shortAt(o.at)}${o.other?.at ? ` · ${BASIS_WORD[o.other.basis] ?? o.other.basis} ${shortAt(o.other.at)}` : ''}${o.anchor ? ` — ${o.anchor}` : ''}`;
99
+
100
+ // ── small shared pieces ────────────────────────────────────────────────────
101
+ /** `Copy for agent` (§1.18): the clipboard gets the send-back's text; a toast confirms. The checks read the hook. */
102
+ export async function copyText(text, note = 'Copied — paste it to the execution agent') {
103
+ try {
104
+ await navigator.clipboard.writeText(text);
105
+ toast(note);
106
+ } catch {
107
+ const ta = h('textarea', { class: 'input', style: { minHeight: '30vh' } }, text);
108
+ openDialog('Copy for agent', [h('p', { class: 'muted' }, 'The clipboard is unavailable; select the text below and copy it.'), ta]);
109
+ ta.select();
110
+ }
111
+ window.__ckCopied = text; // headless checks cannot always read the clipboard; the write path is the same
112
+ }
113
+
114
+ const copyBtn = (sb) => h('button', { class: 'text-btn copy', title: 'Copy this send-back for the execution agent: where the problem is, the evidence, where to send it back, the CLI command', onClick: (e) => { e.stopPropagation(); void copyText(sb.copyForAgent); } }, 'Copy for agent');
115
+
116
+ /** `No action needed` (§2.7): one reason, then the matching response endpoint; the answer stays with the object. */
117
+ export function askNoAction(kind, id, label) {
118
+ const reason = h('textarea', { class: 'input', placeholder: 'Why is no action needed?' });
119
+ const err = h('small', { class: 'faint' });
120
+ const submit = async () => {
121
+ if (!reason.value.trim()) { err.textContent = 'A reason is needed — it stays with the object.'; return; }
122
+ try {
123
+ if (kind === 'breakpoint') await K.respondBreakpoint(state.projectId, id, reason.value.trim());
124
+ else await K.respondSendBack(state.projectId, id, reason.value.trim());
125
+ document.querySelector('#dialog')?.close();
126
+ toast('Recorded: no action needed');
127
+ await refreshProject();
128
+ } catch (e) { err.textContent = e.message; }
129
+ };
130
+ openDialog('No action needed', [
131
+ h('p', { class: 'muted' }, label),
132
+ h('div', { class: 'field' }, h('label', {}, 'Reason'), reason), err,
133
+ h('div', { class: 'dialog-foot' }, h('button', { class: 'btn primary', onClick: submit }, 'Record')),
134
+ ]);
135
+ reason.focus();
136
+ }
137
+ const noActionBtn = (kind, id, label) => h('button', { class: 'text-btn copy', title: 'Answer “No action needed” with a reason; it stops lighting up and the answer stays', onClick: (e) => { e.stopPropagation(); askNoAction(kind, id, label); } }, 'No action needed');
138
+
139
+ const basisNote = (b) => b === 'Inferred' ? h('span', { class: 'tag purple', title: 'Tied to this work by a judged link, not an id' }, 'Inferred') : null;
140
+ /**
141
+ * How far a step's judged link has been checked (CM; ProcessStepView.link): a link the cross-check confirmed says
142
+ * nothing more; one that passed the program's evidence check when written counts like a confirmed one but says so
143
+ * (`Lane-checked`); a suspect one and one not checked yet say so too. Steps tied by an id carry no `link`.
144
+ */
145
+ const LINK_TAG = {
146
+ 'lane-checked': { label: 'Lane-checked', tone: '', graphTone: 'muted', title: 'The link passed the program’s evidence check when it was written; the cross-check has not confirmed it yet. It counts like a confirmed one.' },
147
+ suspect: { label: 'Suspect link', tone: 'red', graphTone: 'danger', title: 'The cross-check doubts this step belongs to this work' },
148
+ unconfirmed: { label: 'Not confirmed yet', tone: 'amber', graphTone: 'faint', title: 'A judged link nothing has checked yet' },
149
+ };
150
+ const linkNote = (link) => { const t = LINK_TAG[link]; return t ? h('span', { class: `tag link-tag ${t.tone}`.trim(), title: t.title }, t.label) : null; };
151
+
152
+ // Look up the send-backs and breakpoints hanging on an object.
153
+ const sendBacksOf = (id) => (proc?.sendBacks ?? []).filter((s) => s.targetId === id);
154
+ const breakpointsOf = (id) => (proc?.breakpoints ?? []).filter((b) => b.targetId === id);
155
+ const litOf = (id) => ({ sendBacks: sendBacksOf(id).filter((s) => s.lit), breakpoints: breakpointsOf(id).filter((b) => b.lit) });
156
+
157
+ /** A `Suggested`/`Returned` send-back is open; a `Closed` one says what evidence closed it. */
158
+ export function routeChips(step) {
159
+ const chips = [];
160
+ const sb = step?.sendBackId ? proc?.sendBacks.find((s) => s.id === step.sendBackId) : null;
161
+ if (sb) {
162
+ if (sb.stage === 'Suggested') chips.push(h('span', { class: 'route back', title: sb.what }, `↩ Suggested · back to ${sb.to === 'Plan' ? 'plan' : 'work'}`), copyBtn(sb), noActionBtn('sendback', sb.id, `Send-back ${sb.id}: ${sb.what}`));
163
+ else if (sb.stage === 'Returned') chips.push(h('span', { class: 'route back', title: `${sb.what}${sb.returned ? ` — caught by ${sb.returned.label}` : ''}` }, `↩ Returned → ${sb.returned?.label ?? '…'}`), noActionBtn('sendback', sb.id, `Send-back ${sb.id}: ${sb.what}`));
164
+ else chips.push(h('span', { class: 'route closed', title: `Closed${sb.closed?.label ? ` — ${sb.closed.label}` : ''}${sb.returned?.label ? ` · returned → ${sb.returned.label}` : ''}` }, `✓ Closed${sb.returned?.label ? ` → ${sb.returned.label}` : ''}`));
165
+ }
166
+ if (step?.kind === 'Handed to') chips.push(h('span', { class: 'route hand', title: step.result || step.did }, `→ ${step.did}`));
167
+ if (step?.kind === 'Handed in') chips.push(h('span', { class: 'route hand', title: step.did }, `← handed in`));
168
+ return chips;
169
+ }
170
+
171
+ /**
172
+ * The evidence of a breakpoint or a send-back as the owner reads it (§2.12, §1.18; CKC-24 AC-3): what each piece is and
173
+ * when, and the original line itself, verbatim — the report's line an open item was left in, the verdict as written.
174
+ */
175
+ export function evidenceList(evidence) {
176
+ if (!evidence?.length) return null;
177
+ return h('ul', { class: 'kp-evid' }, ...evidence.map((e) => h('li', {},
178
+ h('span', { class: 'kp-evid-label' }, e.label || e.id),
179
+ e.occurred ? h('span', { class: 'kp-when', title: occurredTitle(e.occurred) }, ` · ${fmtOccurred(e.occurred)}`) : null,
180
+ e.kind === 'source' && openers.openSource ? [' ', h('button', { class: 'text-btn kp-orig', title: 'Read the original where this line stands', onClick: (ev) => { ev.stopPropagation(); void openers.openSource(e.id); } }, 'Original')] : null,
181
+ e.line ? h('blockquote', { class: 'kp-line', title: 'The original line, verbatim' }, e.line) : null)));
182
+ }
183
+
184
+ /** The mark of an open send-back or a lit breakpoint, drawn on the folded line as well (D75). The mark opens the item
185
+ * beside it, with its evidence's original lines (CKC-24 AC-3). */
186
+ function flagLines(ids, { lit = true } = {}) {
187
+ const out = [];
188
+ for (const id of ids ?? []) {
189
+ const sb = proc?.sendBacks.find((s) => s.id === id);
190
+ if (sb && sb.lit === lit && sb.stage !== 'Closed') {
191
+ const open = h('button', { class: 'text-btn flag-open', title: 'Open this send-back: where the problem is, its evidence and the original lines', onClick: (e) => { e.stopPropagation(); openSendBackPopover(sb.id, () => rectOfEl(open)); } }, `↩ ${sb.stage} · back to ${sb.to === 'Plan' ? 'plan' : 'work'} · ${sb.what}`);
192
+ out.push(h('div', { class: 'flag back', dataset: { sendback: sb.id } }, open, ' ', copyBtn(sb)));
193
+ }
194
+ }
195
+ for (const id of ids ?? []) {
196
+ const bp = proc?.breakpoints.find((b) => b.id === id);
197
+ if (bp && bp.lit === lit) {
198
+ const lines = (bp.evidence ?? []).filter((e) => e.line).length;
199
+ const open = h('button', { class: 'text-btn flag-open', title: lines ? `Open this breakpoint: its evidence and the ${lines === 1 ? 'original line' : `${lines} original lines`}` : 'Open this breakpoint and its evidence', onClick: (e) => { e.stopPropagation(); openBreakpointPopover(bp.id, () => rectOfEl(open)); } }, `⚠ ${bp.kind} · ${bp.why}`);
200
+ out.push(h('div', { class: 'flag back', dataset: { breakpoint: bp.id } }, open, lines ? h('span', { class: 'kp-lines-mark', title: 'It carries the original lines — open it to read them' }, ` ❝${lines}`) : null, ' ', basisNote(bp.basis), ' ', noActionBtn('breakpoint', bp.id, `Breakpoint ${bp.kind}: ${bp.why}`)));
201
+ }
202
+ }
203
+ return out;
204
+ }
205
+ const rectOfEl = (el) => { if (!el?.isConnected) return null; const r = el.getBoundingClientRect(); return r.width || r.height ? { left: r.left, top: r.top, right: r.right, bottom: r.bottom } : null; };
206
+
207
+ // ── the List (mockup list-process-v1.html) ─────────────────────────────────
208
+ /** One step: what was done on the left, what reality gave on the right, in one row. The step's own task number
209
+ * (ProcessStepView.unit — a fix or a check that is a task of its own, e.g. AD) stands at the step's left. */
210
+ function stepRow(step) {
211
+ const verdict = step.verdict ?? null;
212
+ const tone = verdict ? (/fail|needs repair/i.test(verdict) ? 'red' : /pass/i.test(verdict) ? 'green' : 'amber') : step.kind === 'Merged' ? 'green' : '';
213
+ const resultTag = verdict ? verdict : step.kind === 'Merged' ? 'Merged' : step.kind === 'Delivered' ? 'Result' : step.kind === 'Fix' ? 'Fix' : step.kind;
214
+ return h('div', { class: `step row2${step.history ? ' history' : ''}` },
215
+ h('div', {},
216
+ h('span', { class: 'k' }, step.kind), step.unit ? h('span', { class: 'tag unit', title: 'The task of its own that made this step (a fix or a check) — it folds under this work, not a row of its own' }, step.unit) : null, step.who && step.who !== step.unit ? h('span', { class: 'who' }, step.who) : null, ' ',
217
+ // The unit badge already names the task, so the text does not repeat it ("AD" + "AD fixed …" read "ADAD fixed").
218
+ h('span', { class: 'say' }, step.unit && step.did.startsWith(`${step.unit} `) ? step.did.slice(step.unit.length + 1) : step.did), ' ', basisNote(step.basis), linkNote(step.link),
219
+ step.history ? h('span', { class: 'tag', title: 'This step lies in history (a side branch, a deleted document)' }, 'history') : null),
220
+ h('div', { title: occurredTitle(step.occurred) },
221
+ h('span', { class: `tag ${tone}` }, resultTag), ' ', h('span', { class: 'say' }, step.result), ...routeChips(step)));
222
+ }
223
+
224
+ /** The four things of §2.12 on the work's own line: execution, progress, the latest check and by whom, what is open. */
225
+ export function fourThingsTags(w) {
226
+ const four = w?.four;
227
+ if (!four) return [];
228
+ const open = (four.open.findings + four.open.sendBacks + four.open.breakpoints) || 0;
229
+ return [
230
+ four.execution ? h('span', { class: `tag ${four.execution === 'Merged' ? 'green' : four.execution === 'In progress' ? 'blue' : four.execution === 'Not merged' ? 'red' : ''}` }, four.execution) : null,
231
+ four.check && four.check !== 'Not checked'
232
+ ? h('span', { class: `tag ${/fail|needs repair/i.test(four.check.verdict) ? 'red' : 'green'}`, title: `Latest check ${four.check.occurred ? fmtOccurred(four.check.occurred) : ''}` }, `${four.check.verdict} · ${four.check.by}`)
233
+ : four.check === 'Not checked' ? h('span', { class: 'tag amber' }, 'Not checked') : null,
234
+ open ? h('span', { class: 'tag red', title: `${four.open.findings} findings · ${four.open.sendBacks} send-backs · ${four.open.breakpoints} breakpoints still open` }, `⚠ ${open} open`) : null,
235
+ ];
236
+ }
237
+
238
+ const numberTag = (w) => {
239
+ if (!w?.number) return null;
240
+ return w.number.byKeeper
241
+ ? h('span', { class: 'tag', title: 'Numbered by the Keeper — the project had no number for it' }, `${w.number.value} · Keeper's number`)
242
+ : h('span', { class: 'tag faint-num', title: 'The project’s own number' }, w.number.value);
243
+ };
244
+
245
+ /** A struck work (Replaced / Abandoned / Deferred): struck through, with what replaced it or why (§2.12). */
246
+ const struckNote = (w) => w?.struck ? h('span', { class: 'tag strike', title: `${w.struck.validity}${w.struck.why ? ` — ${w.struck.why}` : ''}` }, `${w.struck.validity}${w.struck.why ? ` · ${w.struck.why}` : ''}`) : null;
247
+
248
+ /**
249
+ * One unit of the `Work & plan` cell: folded to one line that still lights what is open, until the owner opens it
250
+ * (k-fold.js); a work whose steps are one line anyway stays a flat row (mockup: AA).
251
+ */
252
+ export function unitBlock(node, ctx) {
253
+ const w = proc?.works[node.id];
254
+ const label = ctx.nodeBtn(node);
255
+ if (!w) {
256
+ // No process assembled for this work (yet): the plain line, nothing invented.
257
+ return h('div', { class: 'unit' }, h('div', { class: 'row2' }, h('div', {}, h('span', { class: 'nocaret' }), label), h('div', {}, h('span', { class: 'faint' }, NO_DATA))));
258
+ }
259
+ const plan = proc?.plans?.[node.id];
260
+ const head = h('div', {},
261
+ w.steps.length > 1 || plan ? h('span', { class: 'caret' }) : h('span', { class: 'nocaret' }),
262
+ node.category === 'Plan' ? h('span', { class: 'tag' }, 'Plan') : null, ' ', label, ' ', numberTag(w), ' ',
263
+ w.four.progress ? h('span', { class: 'tag' }, w.four.progress) : null, ' ', ctx.acceptanceTag(node), ' ', struckNote(w),
264
+ // What is folded under it, at the end of its own line (E153: one line a unit, not a second one for the count).
265
+ w.steps.length > 1 || plan ? [' ', h('span', { class: 'wr-steps' }, w.folded)] : null);
266
+ const right = h('div', {}, ...fourThingsTags(w));
267
+ const openFlags = [...flagLines(w.sendBackIds), ...flagLines(w.breakpointIds), ...childFlagsOf(node.id)];
268
+ if (!(w.steps.length > 1 || plan)) {
269
+ const only = w.steps[0];
270
+ return h('div', { class: 'unit' },
271
+ h('div', { class: 'row2' }, head, only ? h('div', { title: occurredTitle(only.occurred) }, ...fourThingsTags(w), ' ', h('span', { class: 'say' }, only.result)) : right),
272
+ ...openFlags);
273
+ }
274
+ const body = h('div', { class: 'steps' }, ...w.steps.map(stepRow));
275
+ const det = h('details', { class: 'unit', open: listOpen(node.id) || null, onToggle: (e) => listToggled(node.id, e.currentTarget.open) },
276
+ h('summary', {},
277
+ h('div', { class: 'row2' }, head, right),
278
+ ...openFlags,
279
+ plan?.differs ? h('div', { class: 'sumline' }, `Execution differed from the plan: ${plan.differs}`) : null),
280
+ plan ? planShape(plan, ctx) : null,
281
+ w.steps.length ? body : null);
282
+ return det;
283
+ }
284
+
285
+ /**
286
+ * A work's state in one sentence, the right half of its row in the List by module (D100; Spec §6.3; CKC-24 AC-20):
287
+ * done work says what it actually delivered and how it was checked (by whom); open work says how far it is and what is
288
+ * still open (lit breakpoints, open send-backs and findings). The same four things as its node on the Graph.
289
+ */
290
+ export function stateSentence(node, w = proc?.works?.[node.id]) {
291
+ const four = w?.four;
292
+ const progress = four?.progress || node.progress || '';
293
+ if (!four) return progress ? `${progress}${node.acceptance ? ` · ${node.acceptance}` : ''}` : NO_DATA;
294
+ const steps = w.steps ?? [];
295
+ const delivered = [...steps].reverse().find((s) => s.kind === 'Merged' || s.kind === 'Delivered' || s.kind === 'Fix');
296
+ const check = four.check && four.check !== 'Not checked' ? `${four.check.verdict} · checked by ${four.check.by}` : four.check === 'Not checked' ? 'Not checked' : 'no check recorded';
297
+ const open = [];
298
+ const lit = (w.breakpointIds ?? []).map((id) => proc?.breakpoints.find((b) => b.id === id)).filter(Boolean);
299
+ if (lit.length) open.push(lit.map((b) => b.kind).join(', '));
300
+ if (four.open.sendBacks) open.push(`${four.open.sendBacks} send-back${four.open.sendBacks === 1 ? '' : 's'} open`);
301
+ if (four.open.findings) open.push(`${four.open.findings} finding${four.open.findings === 1 ? '' : 's'} open`);
302
+ if (progress === 'Done') {
303
+ const result = delivered ? `${delivered.kind === 'Merged' ? 'Merged' : delivered.kind}: ${delivered.result || delivered.did}` : four.execution ?? 'no delivery linked yet';
304
+ return [result, check, open.length ? `⚠ ${open.join(' · ')}` : null].filter(Boolean).join(' · ');
305
+ }
306
+ return [progress, four.execution !== progress ? four.execution : null, open.length ? `⚠ ${open.join(' · ')}` : 'nothing left open'].filter(Boolean).join(' · ');
307
+ }
308
+
309
+ /**
310
+ * One work item's row in the List by module (D100, the owner's drawn structure; Spec §6.3; CKC-09 AC-15, CKC-24 AC-20;
311
+ * E153). Folded — as every row is until the owner opens it, the same fold as the Graph's branch — it is one line on
312
+ * each side: on the left its number and name, its plan and where else it stands, the count of what is open and how
313
+ * many steps are folded under it; on the right one sentence of its state, cut to the line with the whole of it in the
314
+ * hover, and `Basis`. Expanded, the sentence is whole and its `Process` · `Outcome` follow — one step a line, what was
315
+ * done and what reality gave. What needs the owner is on the folded row too: the ⚠ count, and under it each open
316
+ * send-back and lit breakpoint (D75).
317
+ */
318
+ const stepLineText = (step) => `${step.kind}: ${step.result || step.did}`;
319
+ /** The line a one-step work shows beside its name (`Planned: product/PLAN.md @ 8f8d3e6 (Modified)`); null otherwise. */
320
+ export function stepText(workId) {
321
+ const w = proc?.works?.[workId];
322
+ return w && w.steps.length === 1 ? stepLineText(w.steps[0]) : null;
323
+ }
324
+
325
+ export function workRow(node, ctx) {
326
+ const w = proc?.works[node.id];
327
+ const many = Boolean(w) && w.steps.length > 1;
328
+ const only = w?.steps.length === 1 ? w.steps[0] : null;
329
+ const onlyText = only ? stepLineText(only) : null;
330
+ // What is folded under the row, or the one step it has: beside the name, the first thing to give way in a narrow row.
331
+ const stepsNote = many ? h('span', { class: 'wr-steps', title: w.folded }, w.folded)
332
+ : only ? ctx.stepLine?.(only, onlyText) ?? h('span', { class: 'wr-steps', title: onlyText }, onlyText) : null;
333
+ const mark = openMarkOf(node.id);
334
+ const sentence = ctx.sentence?.(node) ?? stateSentence(node, w);
335
+ const left = h('div', { class: 'wr-left' },
336
+ many ? h('span', { class: 'caret' }) : h('span', { class: 'nocaret' }),
337
+ h('span', { class: 'wr-name', title: node.label }, ctx.nodeBtn(node)), ctx.planTag?.(node) ?? null, ctx.acceptanceTag?.(node) ?? null, struckNote(w), ctx.extraLeft?.(node) ?? null,
338
+ mark ? h('span', { class: 'tag red wr-open', title: mark.text }, `⚠ ${mark.count} open`) : null, stepsNote);
339
+ const right = h('div', { class: 'wr-right' }, h('span', { class: 'say', title: sentence }, sentence), ctx.basisBtn?.(node) ?? null);
340
+ const flags = w ? [...flagLines(w.sendBackIds), ...flagLines(w.breakpointIds), ...childFlagsOf(node.id)] : [];
341
+ if (!many) return h('div', { class: 'unit wrow', dataset: { work: node.id } }, h('div', { class: 'row2' }, left, right), ...flags);
342
+ return h('details', { class: 'unit wrow', dataset: { work: node.id }, open: listOpen(node.id) || null, onToggle: (e) => listToggled(node.id, e.currentTarget.open) },
343
+ h('summary', {}, h('div', { class: 'row2' }, left, right), ...flags),
344
+ h('div', { class: 'proc-head row2' }, h('span', {}, 'Process'), h('span', {}, 'Outcome')),
345
+ h('div', { class: 'steps' }, ...w.steps.map(stepRow)));
346
+ }
347
+
348
+ /** A plan's execution shape (D72): the actual order as it happened, the batches as the orchestrator wrote them. */
349
+ function planShape(shape, ctx) {
350
+ const batchWork = (wid) => ctx.nodeOf(wid);
351
+ const rows = [];
352
+ let par = null;
353
+ for (const b of shape.batches) {
354
+ const row = h('div', { class: 'step row2' },
355
+ h('div', {},
356
+ h('span', { class: 'k' }, /^batch\b/i.test(b.label) ? b.label : `Batch ${b.label}`),
357
+ b.workIds.length ? h('span', { class: 'who' }, b.workIds.map((id) => batchWork(id)?.label ?? id).join(' + ')) : null, ' ',
358
+ b.agent ? h('span', { class: 'say' }, `${b.agent}${b.worktree ? ` · ${b.worktree}` : ''}`) : null,
359
+ b.parallel ? h('span', { class: 'tag blue', title: 'Could run in parallel with its sibling batch' }, '∥ parallel') : null,
360
+ b.running ? h('span', { class: 'tag amber' }, 'running') : null),
361
+ h('div', { title: b.occurred ? occurredTitle(b.occurred) : '' },
362
+ b.mergeCommit ? h('span', { class: 'tag green' }, 'Merged') : null, ' ',
363
+ h('span', { class: 'mono' }, b.mergeCommit ?? ''), b.mergeCommit ? ' ' : null));
364
+ // What a batch's work still has open lights up under it, folded or not (D75; mockup: the .sub under batch 7).
365
+ const sub = b.workIds.flatMap((wid) => [...flagLines(proc?.works[wid]?.sendBackIds ?? []), ...flagLines(proc?.works[wid]?.breakpointIds ?? [])]);
366
+ const withSub = sub.length ? h('div', {}, row, h('div', { class: 'sub' }, ...sub)) : row;
367
+ if (b.parallel) {
368
+ if (!par) { par = h('div', { class: 'par' }, h('div', { class: 'par-head' }, `Batch ${b.label} · two halves, run in parallel`)); rows.push(par); }
369
+ par.append(withSub);
370
+ } else { par = null; rows.push(withSub); }
371
+ }
372
+ return h('div', {},
373
+ shape.actualOrder ? h('div', { class: 'order' }, 'Execution order: ', h('b', {}, shape.actualOrder), shape.plannedOrder && shape.plannedOrder !== shape.actualOrder ? h('span', { class: 'faint' }, ` (planned: ${shape.plannedOrder})`) : null) : null,
374
+ ...rows,
375
+ shape.dependsOn.length ? h('div', { class: 'sumline' }, `Depends on: ${shape.dependsOn.map((d) => `${d.from} → ${d.to}`).join(', ')}`) : null);
376
+ }
377
+
378
+ /** Send-backs and breakpoints hanging on an intent object (a requirement, a decision): shown in its cell (mockup 3.5). */
379
+ export function intentFlags(objectId) {
380
+ return [...flagLines(sendBacksOf(objectId).map((s) => s.id)), ...flagLines(breakpointsOf(objectId).map((b) => b.id))];
381
+ }
382
+ /** Works that ride inside another unit's steps (a fix, a QC round) are not units of their own in the process List. */
383
+ export function absorbedIds() {
384
+ const out = new Set();
385
+ if (!proc) return out;
386
+ // The engine says so directly (WorkProcessView.stepOf): a fix or a check folds under the work it is a step of.
387
+ for (const [id, w] of Object.entries(proc.works)) if (w.stepOf) out.add(id);
388
+ for (const w of Object.values(proc.works)) {
389
+ for (const s of w.steps) for (const e of s.evidence ?? []) if (e.kind === 'object' && !proc.works[e.id]) out.add(e.id);
390
+ }
391
+ // A plan's batches are drawn inside the plan's unit, in the plan's own area.
392
+ for (const [pid_, shape] of Object.entries(proc?.plans ?? {})) {
393
+ const planArea = proc.works[pid_] ? areaOf(pid_) : null;
394
+ for (const b of shape.batches) for (const wid of b.workIds) if (!proc.works[wid] || areaOf(wid) === planArea) out.add(wid);
395
+ }
396
+ return out;
397
+ }
398
+ /** What a folded-in child (stepOf) still has open rides under the parent's unit, lit folded or not (D75). */
399
+ function childFlagsOf(id) {
400
+ const out = [];
401
+ for (const [cid, w] of Object.entries(proc?.works ?? {})) {
402
+ if (w.stepOf !== id) continue;
403
+ out.push(...flagLines(w.sendBackIds), ...flagLines(w.breakpointIds));
404
+ }
405
+ return out;
406
+ }
407
+ let nodeIndex = new Map(); // graph node id → payload node (for labels and areas while drawing)
408
+ export function indexNodes(nodes) { nodeIndex = new Map(nodes.map((n) => [n.id, n])); }
409
+ const nodeOf = (id) => nodeIndex.get(id) ?? null;
410
+ const areaOf = (id) => nodeOf(id)?.areaId ?? null;
411
+
412
+ /**
413
+ * Earlier generations (D85, §2.12): one rolled-up band per generation before the current plan — what was planned,
414
+ * done and struck, and the decision or cleanup that ended it. Unrolled it reads like the current generation.
415
+ */
416
+ export function generationBands(ctx, placed = []) {
417
+ // The generations as the story map places them (ui/placement.js: each item's destination), with what the process
418
+ // view adds (the plan documents as they stood). Without either, nothing.
419
+ const procGens = new Map((proc?.generations ?? []).map((g) => [g.id, g]));
420
+ const gens = placed.length ? placed : [...procGens.values()].map((g) => ({ ...g, items: [], destinations: null }));
421
+ if (!gens.length) return null;
422
+ // A generation's works may be gone from the current version (D82): they are drawn from the band's own data.
423
+ const genCtx = { ...ctx, nodeBtn: (n) => nodeOf(n.id) ? ctx.nodeBtn(n) : h('span', { class: 'text-btn', style: { cursor: 'default' } }, n.label) };
424
+ return h('div', { class: 'genbands' }, ...gens.map((g) => {
425
+ const pg = procGens.get(g.id);
426
+ const plans = (g.planIds ?? pg?.planIds ?? []).filter((id) => !g.workIds.includes(id));
427
+ const docs = pg?.planDocs ?? [];
428
+ const destOf = new Map((g.items ?? []).map((i) => [i.id, i.destination]));
429
+ // Folded to its row, it still lights what is open inside it and counts the notes inside (D75; E153).
430
+ const marks = ctx.insideMarks?.({ works: [...g.workIds, ...plans] }) ?? ctx.foldMarks?.([...g.workIds, ...plans]) ?? null;
431
+ // Written as where its work went, never as "abandoned" (Spec §2.12, D100).
432
+ // A generation the organizing recorded no items for says `Not organized` (owner, 2026-09-30: 「显示未整理」); its
433
+ // name, end and ending document stand as they are.
434
+ const summary = g.destinations ? ctx.destinationLine(g.destinations) : pg?.planned ? `${pg.planned} planned · ${pg?.done ?? 0} done` : 'Not organized';
435
+ const det = h('details', { class: 'genband', dataset: { generation: g.id, fold: generationKey(g.id) }, open: ctx.openGen?.(g.id) || null, onToggle: (e) => ctx.genToggled?.(g.id, e.currentTarget.open) },
436
+ h('summary', {},
437
+ h('span', { class: 'genname' }, `Earlier generation · ${g.name}`), ' ', marks, ' ',
438
+ h('span', { class: 'gendest' }, summary), ' ',
439
+ plans.length ? h('span', { class: 'tag', title: 'Its plans, drawn in the band below' }, `${plans.length} plan${plans.length === 1 ? '' : 's'}`) : null, ' ',
440
+ h('span', { class: 'faint' }, `ended ${fmtOccurred(g.ended)}${g.endedBy?.label ? ` · ${g.endedBy.label}` : ''}`)),
441
+ h('div', { class: 'genbody' },
442
+ // Its plans, as the current plan is drawn (CKC-24 AC-18): a plan object with its shape, and its plan documents as
443
+ // they stood — a deleted one from the version before its deletion (D82).
444
+ ...plans.map((id) => planOfBand(id, genCtx, pg?.plans?.find((p) => p.id === id) ?? null)),
445
+ docs.length ? h('div', { class: 'gendocs' }, h('span', { class: 'faint' }, 'Plan documents, as they stood: '),
446
+ ...docs.map((d) => h('button', { class: 'text-btn gendoc', dataset: { index: String(d.index) }, title: d.path ? `Read ${d.path} as it stood when this generation ended (a deleted document from the version before its deletion)` : 'Named by the generation; it is not a document', onClick: (e) => { e.stopPropagation(); void openGenerationPlan(g.id, d.index, `${g.name} · ${d.label}`); } }, d.label))) : null,
447
+ g.workIds.length ? null : h('div', { class: 'faint pad' }, 'Not organized: the organizing has not recorded its items or where each went.'),
448
+ ...g.workIds.map((id) => {
449
+ const w = proc?.works?.[id];
450
+ const node = nodeOf(id) ?? { id, label: [w?.number?.value, w?.struck?.why].filter(Boolean).join(' · ') || id, category: 'Work item', validity: w?.struck ? 'Replaced' : 'Current' };
451
+ // Each item says where it went: carried on, moved into the current plan, dropped by which decision.
452
+ return workRow(node, { ...genCtx, sentence: (n) => [ctx.destinationText?.(destOf.get(id)), stateSentence(n)].filter(Boolean).join(' · ') });
453
+ })));
454
+ return det;
455
+ }));
456
+ }
457
+
458
+ /** One plan of an earlier generation in its band: the plan's own unit when the process view carries it, else its name
459
+ * (and shape, when there is one) — nothing invented. `named` is the band's own naming of it (GenerationBandView.plans). */
460
+ function planOfBand(planId, ctx, named = null) {
461
+ const w = proc?.works?.[planId];
462
+ const node = nodeOf(planId) ?? { id: planId, label: named?.name || (w ? [w.number?.value, w.struck?.why].filter(Boolean).join(' · ') : '') || planId, category: 'Plan', validity: named?.validity ?? 'Replaced' };
463
+ if (w) {
464
+ const el = unitBlock(node, ctx);
465
+ el.classList.add('genplan');
466
+ el.dataset.plan = planId;
467
+ return el;
468
+ }
469
+ const shape = proc?.plans?.[planId];
470
+ return h('div', { class: 'genplan', dataset: { plan: planId } },
471
+ h('div', { class: 'row2' }, h('div', {}, h('span', { class: 'nocaret' }), h('span', { class: 'tag' }, 'Plan'), ' ', ctx.nodeBtn(node), ' ', ctx.validityTag?.(node.validity) ?? null), h('div', {})),
472
+ shape ? planShape(shape, ctx) : null);
473
+ }
474
+
475
+ // ── the Graph (mockup graph-process-v0.html): results by default, a click grows the branch ─────────────────
476
+ const graphOpen = (id) => fold.has(id);
477
+ const foldLineOf = (id) => foldLine(proc, id);
478
+ const openMarkOf = (id) => openMark(proc, id);
479
+
480
+ const trunc = (s, n = 64) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);
481
+ const stepTone = (step) => step.verdict ? (/fail|needs repair/i.test(step.verdict) ? 'danger' : /pass/i.test(step.verdict) ? 'ok' : 'accent') : step.kind === 'Merged' ? 'ok' : 'muted';
482
+
483
+ /**
484
+ * Where an open item hangs when its object is not itself on the picture (D75): the card of the fold that holds it on
485
+ * the story map — a stacked cell, a column's requirements, designs & decisions, a group of the ring, a rolled earlier
486
+ * generation. `cards` maps each folded object to its card this pass (graph.js). Null when nothing stands for it now.
487
+ */
488
+ const anchorOf = (id, byId, shown, cards) => {
489
+ if (shown.has(id)) return id;
490
+ return cards?.get?.(id) ?? null;
491
+ };
492
+ /** The ellipse of a send-back still open, and its arrow back (mockup graph-process-v0). */
493
+ const sugData = (sb, anchorId, byId) => ({ id: `ksb:${sb.id}`, kind: 'proc-suggest', procOf: anchorId, sbId: sb.id, seq: 99, areaId: byId.get(sb.targetId)?.areaId ?? null, rawLabel: `↩ ${sb.stage} · back to ${sb.to === 'Plan' ? 'plan' : 'work'}\n${trunc(sb.what, 56)}`, full: `${sb.what}\n${sb.suggestion}`, tones: ['danger', 'muted'], w: PROC_SIZE.suggest[0], h: PROC_SIZE.suggest[1] });
494
+ const sugEdge = (sb, fromId) => ({ data: { id: `ksug:${sb.id}`, source: fromId, target: `ksb:${sb.id}`, part: 'proc', elabel: `↩ ${sb.stage}`, mark: '', sbId: sb.id }, classes: `proc-sug${sb.to === 'Plan' ? ' plan' : ''}` });
495
+
496
+ /**
497
+ * The elements of one work's branch: a chain of step nodes under it; a send-back still open is an ellipse the chain
498
+ * points at (folded, it hangs on the work itself — or on the group card the work is folded into, D75); a handed-out
499
+ * finding is a curved line to the work that took it.
500
+ * Returns { nodes, edges } with stable ids (`kstep:<step.id>`, `ksb:<sb.id>`) so the graph's diffing keeps them put.
501
+ */
502
+ function branchElements(workId, byId, shown) {
503
+ const w = proc?.works[workId];
504
+ const work = byId.get(workId);
505
+ if (!w || !work) return { nodes: [], edges: [] };
506
+ const nodes = [], edges = [];
507
+ let prev = workId;
508
+ let seq = 0;
509
+ for (const step of w.steps) {
510
+ const id = `kstep:${step.id}`;
511
+ // A judged link the cross-check has not confirmed says how far it is checked, on the step's third line (CM).
512
+ const link = LINK_TAG[step.link];
513
+ nodes.push({
514
+ data: { id, kind: 'proc-step', procOf: workId, seq: seq++, areaId: work.areaId ?? null, rawLabel: `${step.kind}${step.unit ? ` ${step.unit}` : ''}${step.who ? ` · ${step.who}` : ''}\n${step.result || step.did}${link ? `\n${link.label}` : ''}`, full: `${step.did}\n${step.result}${link ? `\n${link.label}` : ''}`, tones: ['kind', stepTone(step), ...(link ? [link.graphTone] : [])], w: PROC_SIZE.step[0], h: PROC_SIZE.step[1] },
515
+ classes: `proc-step${step.basis === 'Inferred' ? ' inferred' : ''}${step.history ? ' history' : ''}`,
516
+ });
517
+ edges.push({ data: { id: `kchain:${prev}>${step.id}`, source: prev, target: id, part: 'proc', elabel: '', mark: '', sbId: step.sendBackId ?? null }, classes: step.sendBackId ? 'proc-ret' : 'proc-chain' });
518
+ prev = id;
519
+ }
520
+ // Open send-backs of this work: an ellipse the branch (or the folded work, or its group card) points back from.
521
+ for (const sb of sendBacksOf(workId)) {
522
+ if (!sb.lit || sb.stage === 'Closed') continue;
523
+ const id = `ksb:${sb.id}`;
524
+ const stepWith = w.steps.find((s) => s.sendBackId === sb.id);
525
+ nodes.push({
526
+ data: { id, kind: 'proc-suggest', procOf: workId, sbId: sb.id, seq: stepWith ? w.steps.indexOf(stepWith) + 0.5 : 99, areaId: work.areaId ?? null, rawLabel: `↩ ${sb.stage} · back to ${sb.to === 'Plan' ? 'plan' : 'work'}\n${trunc(sb.what, 56)}`, full: `${sb.what}\n${sb.suggestion}`, tones: ['danger', 'muted'], w: PROC_SIZE.suggest[0], h: PROC_SIZE.suggest[1] },
527
+ classes: `proc-suggest${sb.stage === 'Returned' ? ' returned' : ''}`,
528
+ });
529
+ const from = graphOpen(workId) && stepWith ? `kstep:${stepWith.id}` : workId;
530
+ edges.push({ data: { id: `ksug:${sb.id}`, source: from, target: id, part: 'proc', elabel: `↩ ${sb.stage}`, mark: '', sbId: sb.id }, classes: `proc-sug${sb.to === 'Plan' ? ' plan' : ''}` });
531
+ }
532
+ // A finding handed to another work: a curved line across columns (mockup: hand).
533
+ if (graphOpen(workId)) {
534
+ for (const step of w.steps) {
535
+ if (step.kind !== 'Handed to') continue;
536
+ const target = (step.evidence ?? []).find((e) => e.kind === 'object' && shown.has(e.id));
537
+ if (target) edges.push({ data: { id: `khand:${step.id}`, source: `kstep:${step.id}`, target: target.id, part: 'proc', elabel: `→ ${trunc(step.did, 30)}`, mark: '' }, classes: 'proc-hand' });
538
+ }
539
+ }
540
+ return { nodes, edges };
541
+ }
542
+
543
+ /** A plan's branch: the batches chained in the order they actually ran (D72), cross-area halves as dotted pointers. */
544
+ function planEdges(planId, byId, shown) {
545
+ const shape = proc?.plans[planId];
546
+ const plan = byId.get(planId);
547
+ if (!shape || !plan) return { nodes: [], edges: [] };
548
+ const edges = [];
549
+ const mk = (from, t, b) => {
550
+ const cross = (byId.get(t)?.areaId ?? null) !== (plan.areaId ?? null);
551
+ edges.push({
552
+ data: { id: `kplan:${planId}:${b.label}>${t}`, source: from, target: t, part: 'proc', elabel: `B${b.label}${b.mergeCommit ? ` · ${b.mergeCommit}` : ''}`, mark: '' },
553
+ classes: cross ? 'proc-ptr' : 'proc-chain',
554
+ });
555
+ };
556
+ // Consecutive batches chain; the halves of a parallel batch both hang off the batch before them, and the next batch
557
+ // hangs off every half (mockup: batch 4 → 5a and 5b, both → 7).
558
+ let prevSources = [planId];
559
+ let parSources = null, parTargets = [];
560
+ for (const b of shape.batches) {
561
+ const targets = b.workIds.filter((id) => shown.has(id));
562
+ if (!targets.length) continue;
563
+ if (b.parallel) {
564
+ if (!parSources) { parSources = prevSources; parTargets = []; }
565
+ for (const t of targets) mk(parSources[parSources.length - 1], t, b);
566
+ parTargets.push(...targets);
567
+ prevSources = parTargets;
568
+ } else {
569
+ for (const s of prevSources) for (const t of targets) mk(s, t, b);
570
+ prevSources = targets;
571
+ parSources = null; parTargets = [];
572
+ }
573
+ }
574
+ return { nodes: [], edges };
575
+ }
576
+
577
+ // The layer graph.js asks for its process elements: nothing when there is no data, so the picture is exactly as before.
578
+ setProcessLayer({
579
+ /** Which payload nodes pass the process filters (AC-16): breakpoint kind, send-back stage, one of the six things. */
580
+ match(n, f) {
581
+ if (!proc) return false;
582
+ if (f.bpKind && !proc.breakpoints.some((b) => b.kind === f.bpKind && b.targetId === n.id && b.lit)) return false;
583
+ if (f.sbStage && !proc.sendBacks.some((s) => s.stage === f.sbStage && s.targetId === n.id)) return false;
584
+ if (f.sixThing) { const t = proc.sixThings.find((x) => String(x.thing) === String(f.sixThing)); if (!t || !t.objectIds.includes(n.id)) return false; }
585
+ return true;
586
+ },
587
+ /** The second line of a work node: what is folded under it and its state, or null to leave the node as it was. */
588
+ foldLineOf,
589
+ /** The count of what is open on a work, for its top edge: { count, text }, or null. */
590
+ openMarkOf,
591
+ /** Works with a lit breakpoint or an open send-back get the alert border — the group card when the work is folded into one. */
592
+ alertIds({ shown, byId, cards } = {}) {
593
+ const out = new Set();
594
+ const put = (id) => { const a = shown ? anchorOf(id, byId, shown, cards) : id; if (a) out.add(a); };
595
+ for (const b of proc?.breakpoints ?? []) if (b.lit) put(b.targetId);
596
+ for (const s of proc?.sendBacks ?? []) if (s.lit) put(s.targetId);
597
+ return out;
598
+ },
599
+ elements({ shown, byId, cards }) {
600
+ if (!proc) return null;
601
+ const out = [];
602
+ const drawnSb = new Set();
603
+ const drawSug = (sb, anchorId, fromId) => {
604
+ const data = sugData(sb, anchorId, byId);
605
+ out.push({ data: { ...data, ...processImage(data) }, classes: `proc-suggest${sb.stage === 'Returned' ? ' returned' : ''}` });
606
+ out.push(sugEdge(sb, fromId ?? anchorId));
607
+ drawnSb.add(sb.id);
608
+ };
609
+ for (const id of Object.keys(proc.works)) {
610
+ if (stepOf(id)) continue; // a fix or a check rides in its work's branch (the steps name it, `unit`)
611
+ const anchor = anchorOf(id, byId, shown, cards);
612
+ if (!anchor) continue;
613
+ if (graphOpen(id) && anchor === id) {
614
+ const { nodes, edges } = branchElements(id, byId, shown);
615
+ for (const n of nodes) { if (n.data.sbId) drawnSb.add(n.data.sbId); out.push({ data: { ...n.data, ...processImage(n.data) }, classes: n.classes }); }
616
+ out.push(...edges);
617
+ if (proc.plans[id]) out.push(...planEdges(id, byId, shown).edges);
618
+ } else {
619
+ // Folded — or folded into a group card: the open send-backs hang off whatever stands for the work (D75).
620
+ for (const sb of sendBacksOf(id)) if (sb.lit && sb.stage !== 'Closed' && !drawnSb.has(sb.id)) drawSug(sb, anchor);
621
+ }
622
+ }
623
+ // A send-back on something that is not a work with a branch (an area, a requirement) hangs on that object — or on
624
+ // the card it is folded into.
625
+ for (const sb of proc.sendBacks ?? []) {
626
+ if (!sb.lit || sb.stage === 'Closed' || drawnSb.has(sb.id)) continue;
627
+ const anchor = anchorOf(sb.targetId, byId, shown, cards);
628
+ if (anchor) drawSug(sb, anchor);
629
+ }
630
+ return { elements: out };
631
+ },
632
+ toggle: (id) => fold.toggle(id),
633
+ isOpen: (id) => graphOpen(id),
634
+ expandable: (id) => expandable(id),
635
+ });
636
+
637
+ // ── the popover (§6.4; CKC-24 AC-14): How it got here, the versions, what is open on the object ──────────────
638
+ export async function detailsData(objectId) {
639
+ await load(state.projectId);
640
+ const [lineage, versions, sessions] = await Promise.all([
641
+ K.getLineage(state.projectId, objectId), K.getVersions(state.projectId, objectId),
642
+ // The sessions the object stands on, each with its draft (CKC-23 AC-18); none when the endpoint is not built.
643
+ K.getObjectDrafts(state.projectId, objectId).catch(() => null),
644
+ ]);
645
+ return { lineage, versions, open: litOf(objectId), sessions, territories: proc?.works?.[objectId]?.territories ?? null };
646
+ }
647
+
648
+ const LIN_KIND_TONE = { 'Send-back': 'red', Verdict: 'green', Replaced: 'strike', Now: 'blue' };
649
+ /** In a document's history a patch that withdrew the document reads as the document's (owner 2026-09-30). */
650
+ const DOC_KIND = { Replaced: 'Document superseded', Replaces: 'Document replaces' };
651
+ function stepLines(steps, { doc = false } = {}) {
652
+ const stepLine = (s) => h('div', { class: `kp-lin${s.history ? ' history' : ''}` },
653
+ h('span', { class: `tag ${LIN_KIND_TONE[s.kind] ?? ''}` }, doc ? DOC_KIND[s.kind] ?? s.kind : s.kind), ' ',
654
+ h('span', { class: 'kp-when', title: occurredTitle(s.occurred) }, fmtOccurred(s.occurred)), ' ',
655
+ h('span', { class: 'kp-what' }, s.title),
656
+ s.history ? h('span', { class: 'tag', title: 'Lies in history; not a current requirement' }, 'history') : null);
657
+ const recent = steps.slice(-3);
658
+ const earlier = steps.slice(0, -3);
659
+ return [...recent.map(stepLine),
660
+ earlier.length ? h('details', { class: 'fold kp-more' }, h('summary', {}, `All ${steps.length} steps`), h('div', { class: 'content' }, ...steps.map(stepLine))) : null];
661
+ }
662
+ function lineageBlock(steps) {
663
+ if (!steps?.length) return null;
664
+ return h('div', { class: 'kp-block' }, h('h4', {}, 'How it got here'), ...stepLines(steps));
665
+ }
666
+
667
+ function versionsBlock(versions, { doc = false } = {}) {
668
+ if (!versions) return null;
669
+ const vs = versions.versions ?? [];
670
+ if (!vs.length) return null;
671
+ const line = (v) => h('div', { class: `kp-ver${v.current ? ' current' : ''}${v.supersededBy ? ' superseded' : ''}` },
672
+ h('span', { class: 'kp-when', title: occurredTitle(v.occurred) }, fmtOccurred(v.occurred)), ' ',
673
+ v.supersededBy ? h('s', {}, v.label) : h('b', {}, v.label),
674
+ v.current ? h('span', { class: 'tag green' }, 'current') : null, ' ',
675
+ h('span', { class: 'faint' }, [
676
+ v.sections.added.length ? `+${v.sections.added.length} sections` : null,
677
+ v.sections.removed.length ? `−${v.sections.removed.length}` : null,
678
+ v.sections.changed.length ? `~${v.sections.changed.length} changed` : null,
679
+ ].filter(Boolean).join(' · ') || 'no section diff'),
680
+ // The pointer to what replaced it opens the semantic patch itself (CKC-26 AC-4: "指向替代它的内容与 patch"). Of a
681
+ // document an object sits in, it is the document's version that was superseded, not the object.
682
+ v.supersededBy ? h('div', { class: 'kp-sup' }, `${doc ? 'This version of the document superseded' : 'Superseded'}${v.supersededBy.partial ? ' in part' : ''} by `,
683
+ h('button', { class: 'text-btn kp-patch-link', title: 'Open the semantic patch: what no longer holds, what replaced it, who is affected', onClick: (e) => { e.stopPropagation(); void openPatch(v.supersededBy.patchId); } }, `${v.supersededBy.number} · ${v.supersededBy.title}`)) : null);
684
+ return h('div', { class: 'kp-block' },
685
+ h('h4', {}, `Versions (${vs.length})`),
686
+ ...vs.slice().reverse().slice(0, 4).map(line),
687
+ vs.length > 4 ? h('details', { class: 'fold kp-more' }, h('summary', {}, `All ${vs.length} versions`), h('div', { class: 'content' }, ...vs.slice().reverse().map(line))) : null,
688
+ versions.historyFrom ? h('div', { class: 'faint' }, `Under version control only from ${versions.historyFrom} — earlier versions cannot be shown.`) : null);
689
+ }
690
+
691
+ /**
692
+ * The history of the document an object sits in, labelled as that document's (owner 2026-09-30: D1's popover showed
693
+ * DECISIONS.md's 43 steps, 15 versions and the patch that archived a whole generation as if they were D1's own):
694
+ * "design/DECISIONS.md · 43 steps · 15 versions", folded in the popover, open in the full details.
695
+ */
696
+ function documentHistoryBlock(document, steps, versions, { open = false } = {}) {
697
+ const vs = versions?.versions ?? [];
698
+ if (!steps.length && !vs.length) return null;
699
+ const counts = [steps.length ? `${steps.length} step${steps.length === 1 ? '' : 's'}` : null, vs.length ? `${vs.length} version${vs.length === 1 ? '' : 's'}` : null].filter(Boolean).join(' · ');
700
+ return h('details', { class: 'kp-block kp-doc-history', open: open || null },
701
+ h('summary', { title: 'The document this sits in: its versions, commits and patches. They are the document’s history, not this object’s own.' },
702
+ h('span', { class: 'kp-doc-kicker' }, 'History of the document it sits in'), ' ', h('span', { class: 'kp-doc-name' }, `${document} · ${counts}`)),
703
+ steps.length ? h('div', { class: 'kp-doc-steps' }, ...stepLines(steps, { doc: true })) : null,
704
+ versionsBlock(versions, { doc: true }));
705
+ }
706
+
707
+ /** What is open on this object: lit breakpoints and open send-backs, each with its evidence's original lines and its
708
+ * actions (§6.4, §1.18; CKC-24 AC-3 "Observed reality 写明是哪几条、原文那一行"). */
709
+ function openItemsBlock(open) {
710
+ if (!open || (!open.breakpoints.length && !open.sendBacks.length)) return null;
711
+ return h('div', { class: 'kp-block kp-open' },
712
+ h('h4', {}, 'Open on this object'),
713
+ ...open.breakpoints.map((b) => h('div', { class: 'kp-openitem', dataset: { breakpoint: b.id } },
714
+ h('span', { class: 'tag red' }, `⚠ ${b.kind}`), ' ', basisNote(b.basis), ' ', h('span', { class: 'kp-what' }, b.why), ' ',
715
+ noActionBtn('breakpoint', b.id, `Breakpoint ${b.kind}: ${b.why}`),
716
+ evidenceList(b.evidence))),
717
+ ...open.sendBacks.map((s) => h('div', { class: 'kp-openitem', dataset: { sendback: s.id } },
718
+ h('span', { class: 'tag red' }, `↩ ${s.stage}`), ' ', h('span', { class: 'kp-what' }, s.what), ' ',
719
+ s.suggestion ? h('div', { class: 'faint' }, s.suggestion) : null,
720
+ copyBtn(s), ' ', noActionBtn('sendback', s.id, `Send-back ${s.id}: ${s.what}`),
721
+ evidenceList(s.evidence))));
722
+ }
723
+
724
+ /** The code territories a work changed (§6.17 "从过程视图的工作跳到它改过的领地"): each opens in `Code`. */
725
+ function codeChangedBlock(territories) {
726
+ if (!territories?.length) return null;
727
+ return h('div', { class: 'kp-block kp-code' },
728
+ h('h4', {}, `Code it changed (${territories.length})`),
729
+ h('div', { class: 'row wrap' }, ...territories.map((t) => h('button', {
730
+ class: 'btn small kp-terr', dataset: { territory: t.territoryId }, title: `${t.files} file${t.files === 1 ? '' : 's'} of ${t.name} changed by its commits and merges (${t.commits.join(', ')}) — open it in Code`,
731
+ onClick: (e) => { e.stopPropagation(); openTerritory(t.territoryId); },
732
+ }, `${t.name} ×${t.files}`))));
733
+ }
734
+
735
+ /** The popover's and the full details' increment-K blocks, in the order §6.4 lists them. */
736
+ export function popoverBlocks(k, { surface = 'popover' } = {}) {
737
+ if (!k) return [];
738
+ const steps = k.lineage?.steps ?? [];
739
+ const docSteps = steps.filter((st) => st.about === 'document');
740
+ const document = k.lineage?.document ?? k.versions?.document ?? null;
741
+ // An object that is the whole document (or a view that does not say whose a step is) keeps one trail: its own.
742
+ const history = document && docSteps.length
743
+ ? [lineageBlock(steps.filter((st) => st.about !== 'document')), documentHistoryBlock(document, docSteps, k.versions, { open: surface === 'full' })]
744
+ : [lineageBlock(steps), versionsBlock(k.versions)];
745
+ return [openItemsBlock(k.open), codeChangedBlock(k.territories), sessionsBlock(k.sessions, draftCtx()), ...history].filter(Boolean);
746
+ }
747
+
748
+ // ── the top bar: breakpoints still lit (§6.2, §2.12; CKC-24 AC-7) ───────────────────────────────────────────
749
+ let labelCache = null;
750
+ export async function fetchLabels() {
751
+ if (labelCache) return labelCache;
752
+ try {
753
+ const g = await (await fetch(`/api/projects/${encodeURIComponent(state.projectId)}/graph`)).json();
754
+ labelCache = new Map((g.nodes ?? []).map((n) => [n.id, n.label]));
755
+ } catch { labelCache = new Map(); }
756
+ return labelCache;
757
+ }
758
+
759
+ /** Fills `#k-bp-count` in the top bar: the lit breakpoints and open send-backs, opening to a list by kind. */
760
+ export async function updateCounts({ refresh = false } = {}) {
761
+ const mount = document.getElementById('k-bp-count');
762
+ if (!mount || !state.projectId) return;
763
+ await load(state.projectId, { refresh });
764
+ const c = proc?.counts;
765
+ const n = (c?.breakpointsLit ?? 0) + (c?.sendBacksOpen ?? 0);
766
+ const btn = h('button', { class: 'btn small kp-count', title: 'Breakpoints still lit and send-backs still open', onClick: () => flyout.toggle({ button: btn, label: 'Breakpoints and send-backs', className: 'kp-counts-panel', render: countsPanel }) },
767
+ c?.breakpointsLit ? `⚠ ${c.breakpointsLit}` : null, c?.breakpointsLit && c?.sendBacksOpen ? ' · ' : null, c?.sendBacksOpen ? `↩ ${c.sendBacksOpen}` : null);
768
+ const next = n ? btn : h('span', {});
769
+ if (mount.firstChild?.outerHTML === next.outerHTML) return;
770
+ clear(mount); mount.append(next);
771
+ }
772
+
773
+ function countsPanel() {
774
+ const lit = (proc?.breakpoints ?? []).filter((b) => b.lit);
775
+ const open = (proc?.sendBacks ?? []).filter((s) => s.lit);
776
+ // A code territory is found in `Code`, anything else on the graph.
777
+ const locate = (id, onTerritory = false) => { flyout.close(); void locateTarget(id, onTerritory); };
778
+ const byKind = new Map();
779
+ for (const b of lit) byKind.set(b.kind, [...(byKind.get(b.kind) ?? []), b]);
780
+ // The row's words open the item beside the panel, with its evidence's original lines (CKC-24 AC-3).
781
+ const opener = (text, open) => { const btn = h('button', { class: 'text-btn kp-what', title: 'Open it: its evidence and the original lines', onClick: () => { const r = rectOfEl(btn); flyout.close(); open(() => r); } }, text); return btn; };
782
+ return [
783
+ h('h4', {}, `Breakpoints still lit (${lit.length})`),
784
+ ...[...byKind.entries()].map(([kind, list]) => h('div', {},
785
+ h('div', { class: 'row', style: { gap: '6px' } }, h('span', { class: 'tag red' }, kind), h('small', { class: 'faint' }, `${list.length}`)),
786
+ ...list.map((b) => h('div', { class: 'kp-countrow' },
787
+ h('button', { class: 'text-btn', title: 'Locate the object it hangs on', onClick: () => locate(b.targetId) }, '◈'),
788
+ opener(b.why, (a) => openBreakpointPopover(b.id, a)), ' ', basisNote(b.basis), ' ',
789
+ noActionBtn('breakpoint', b.id, `Breakpoint ${b.kind}: ${b.why}`))))),
790
+ h('h4', {}, `Send-backs still open (${open.length})`),
791
+ ...open.map((s) => h('div', { class: 'kp-countrow' },
792
+ h('button', { class: 'text-btn', title: s.onTerritory ? 'Show the code territory it hangs on, in Code' : 'Locate the object it hangs on', onClick: () => locate(s.targetId, s.onTerritory) }, '◈'),
793
+ h('span', { class: `tag ${s.stage === 'Suggested' ? 'red' : 'amber'}` }, s.stage), ' ', opener(s.what, (a) => openSendBackPopover(s.id, a)), ' ',
794
+ copyBtn(s), ' ', noActionBtn('sendback', s.id, `Send-back ${s.id}: ${s.what}`))),
795
+ !lit.length && !open.length ? h('div', { class: 'faint' }, 'Nothing is lit.') : null,
796
+ ];
797
+ }
798
+
799
+ // ── the process filters (AC-16), counted beside graph-tools.js's FILTERS (whose list is pinned by its test) ──────
800
+ export const SIX_THING = { 1: 'stale', 2: 'drift', 3: 'dropped along the way', 4: 'grown by itself', 5: 'let pass', 6: 'looks residual' };
801
+ export function activeProcessFilters(f) {
802
+ const out = [];
803
+ if (f?.bpKind) out.push({ key: 'bpKind', label: 'Breakpoint kind', value: f.bpKind });
804
+ if (f?.sbStage) out.push({ key: 'sbStage', label: 'Send-back stage', value: f.sbStage });
805
+ if (f?.sixThing) out.push({ key: 'sixThing', label: 'Six things', value: `${f.sixThing} · ${SIX_THING[f.sixThing] ?? f.sixThing}` });
806
+ return out;
807
+ }
808
+
809
+ // ── misc wiring ────────────────────────────────────────────────────────────
810
+ /**
811
+ * `Expand all` and `Fold all` of the control row (AC-11, AC-12; E153): one fold rule for List and Graph. `blocks` is the
812
+ * List's own part — its blocks open and close with the works (views.js); the Graph has none. Null when there is
813
+ * nothing to open.
814
+ */
815
+ export function foldAllButtons(onChange, blocks = null) {
816
+ if (!(proc && fold.any()) && !blocks) return null;
817
+ const all = (on) => () => { fold.setAll(on); blocks?.setAll(on); onChange(); };
818
+ return [
819
+ h('button', { class: 'btn small', title: blocks ? 'Open every block of the List and every work’s process' : 'Open every work’s process', onClick: all(true) }, 'Expand all'),
820
+ h('button', { class: 'btn small', title: blocks ? 'Fold every block of the List to its head, and every work’s process' : 'Fold every work’s process again', onClick: all(false) }, 'Fold all'),
821
+ ];
822
+ }
823
+
824
+ /**
825
+ * Brings the object a breakpoint or send-back hangs on into view: a code territory in `Code`, anything else on the graph.
826
+ * Also the jump of a Follow up result's news to the object an entry is on (views.js).
827
+ */
828
+ export async function locateTarget(targetId, onTerritory = false) {
829
+ if (onTerritory) { openTerritory(targetId); return; }
830
+ const labels = await fetchLabels();
831
+ popover.close('navigate');
832
+ flyout.close();
833
+ const sel = { kind: 'node', id: targetId, label: labels.get(targetId) ?? targetId };
834
+ // From `Code` the graph comes back with the object picked (as a territory's `Built by` does).
835
+ if (state.view === 'graph' && state.graphMode === 'code') { state.graphMode = 'graph'; state.selection = sel; state.pendingPopover = {}; await renderBody(); return; }
836
+ if (state.view !== 'graph') navigate(state.projectId, 'graph');
837
+ select(sel, { reveal: true });
838
+ }
839
+
840
+ /**
841
+ * A send-back beside what was pressed (§1.18): the ellipse on the graph, a flag in the List, an anomaly in `Code`. It is
842
+ * not a graph object, so no node popover exists. `sbOrId` is the send-back itself (Code carries its own) or its id.
843
+ */
844
+ export function openSendBackPopover(sbOrId, anchor) {
845
+ const sb = typeof sbOrId === 'string' ? proc?.sendBacks.find((s) => s.id === sbOrId) : sbOrId;
846
+ if (!sb) return false;
847
+ popover.open({
848
+ key: `sendback:${sb.id}`, label: `Send-back ${sb.id}`,
849
+ anchor,
850
+ render: (mount) => append(mount,
851
+ h('div', { class: 'popover-head' }, h('div', { class: 'grow' }, h('h3', { class: 'popover-title' }, `↩ ${sb.stage} · back to ${sb.to === 'Plan' ? 'Plan' : 'Work'}`), h('div', { class: 'popover-sub' }, `Send-back · ${sb.id}${sb.sixThing ? ` · six things ${sb.sixThing}` : ''}${sb.from?.kind === 'code-anomaly' ? ' · from a code anomaly' : ''}`)), h('button', { class: 'popover-close', title: 'Close (Esc)', onClick: () => popover.close('button') }, '×')),
852
+ h('div', { class: 'popover-body' },
853
+ h('p', { class: 'popover-sentence' }, sb.what),
854
+ h('div', {}, h('h4', {}, 'Suggestion'), h('p', { class: 'popover-sentence' }, sb.suggestion)),
855
+ h('div', { class: 'kp-when', title: occurredTitle(sb.occurred) }, fmtOccurred(sb.occurred)),
856
+ sb.returned ? h('div', { class: 'faint' }, `Returned → ${sb.returned.label} · ${fmtOccurred(sb.returned.occurred)}`) : null,
857
+ sb.closed ? h('div', { class: 'faint' }, `Closed — ${sb.closed.label} · ${fmtOccurred(sb.closed.occurred)}`) : null,
858
+ sb.ownerResponse ? h('div', { class: 'faint' }, `Your answer: no action needed — ${sb.ownerResponse.reason}`) : null,
859
+ sb.evidence?.length ? h('div', { class: 'kp-block' }, h('h4', {}, 'Evidence'), evidenceList(sb.evidence)) : null,
860
+ h('div', { class: 'row wrap popover-actions' }, copyBtn(sb), sb.lit ? noActionBtn('sendback', sb.id, `Send-back ${sb.id}: ${sb.what}`) : null,
861
+ h('button', { class: 'btn small', title: sb.onTerritory ? 'Show the code territory it hangs on, in Code' : 'Locate the object this send-back hangs on', onClick: () => locateTarget(sb.targetId, sb.onTerritory) }, sb.onTerritory ? 'Show in Code' : 'Show on object'))),
862
+ ),
863
+ });
864
+ return true;
865
+ }
866
+
867
+ /**
868
+ * A breakpoint beside its flag (§2.12; CKC-24 AC-3, AC-7): which step has no trace, since when, on what basis, and its
869
+ * evidence — the ledger entries and the original lines themselves, e.g. the report's line an open item was left in.
870
+ */
871
+ export function openBreakpointPopover(bpId, anchor) {
872
+ const bp = proc?.breakpoints.find((b) => b.id === bpId);
873
+ if (!bp) return false;
874
+ const sb = bp.sendBackId ? proc.sendBacks.find((s) => s.id === bp.sendBackId) : null;
875
+ popover.open({
876
+ key: `breakpoint:${bp.id}`, label: `Breakpoint ${bp.kind}`,
877
+ anchor,
878
+ render: (mount) => append(mount,
879
+ h('div', { class: 'popover-head' }, h('div', { class: 'grow' }, h('h3', { class: 'popover-title' }, `⚠ ${bp.kind}`), h('div', { class: 'popover-sub' }, `Breakpoint · ${bp.basis}${bp.sixThing ? ` · six things ${bp.sixThing} (${SIX_THING[bp.sixThing]})` : ''}`)), h('button', { class: 'popover-close', title: 'Close (Esc)', onClick: () => popover.close('button') }, '×')),
880
+ h('div', { class: 'popover-body' },
881
+ h('p', { class: 'popover-sentence' }, bp.why),
882
+ h('div', { class: 'kp-when', title: occurredTitle(bp.since) }, `Due since ${fmtOccurred(bp.since)}`),
883
+ bp.ownerResponse ? h('div', { class: 'faint' }, `Your answer: no action needed — ${bp.ownerResponse.reason}`) : null,
884
+ h('div', { class: 'kp-block' }, h('h4', {}, 'Evidence — the original lines'), evidenceList(bp.evidence) ?? h('div', { class: 'faint' }, 'No evidence line recorded.')),
885
+ sb ? h('div', { class: 'kp-block' }, h('h4', {}, 'Send-back'), h('div', { class: 'kp-openitem' }, h('span', { class: `tag ${sb.stage === 'Closed' ? 'green' : 'red'}` }, `↩ ${sb.stage}`), ' ', h('span', { class: 'kp-what' }, sb.what), ' ', sb.stage !== 'Closed' ? copyBtn(sb) : null)) : null,
886
+ h('div', { class: 'row wrap popover-actions' }, bp.lit ? noActionBtn('breakpoint', bp.id, `Breakpoint ${bp.kind}: ${bp.why}`) : null,
887
+ h('button', { class: 'btn small', title: 'Locate the object this breakpoint hangs on', onClick: () => locateTarget(bp.targetId) }, 'Show on object'))),
888
+ ),
889
+ });
890
+ return true;
891
+ }
892
+
893
+ /** A semantic patch's details (§1.17; CKC-26 AC-4): the words `pk get SP-n` prints, and the objects it affects. */
894
+ export async function openPatch(patchId) {
895
+ let p;
896
+ try { p = await K.getPatch(state.projectId, patchId); } catch (e) { toast(`${patchId}: ${e.message}`); return; }
897
+ if (!p) { toast(`${patchId}: ${NO_DATA}`); return; }
898
+ const text = h('div', { class: 'md kp-patch-text' });
899
+ if (p.text) fillMarkdown(text, p.text);
900
+ const dialog = openDialog(`${p.number} · ${p.title}`, [
901
+ h('div', { class: 'row wrap' }, h('span', { class: `tag ${p.status === 'Confirmed' ? 'green' : p.status === 'Rejected' ? 'strike' : 'amber'}` }, p.status), p.partial ? h('span', { class: 'tag amber', title: 'Only part of the old state is withdrawn; the rest stays current' }, 'Partial') : null,
902
+ h('span', { class: 'kp-when', title: occurredTitle(p.occurred) }, fmtOccurred(p.occurred)), h('span', { class: 'tag', title: 'Numbered by the Keeper' }, "Keeper's number")),
903
+ h('div', { class: 'kp-patch-four' },
904
+ h('div', {}, h('small', { class: 'faint' }, 'No longer holds'), h('div', {}, h('s', {}, p.invalidated))),
905
+ h('div', {}, h('small', { class: 'faint' }, 'Replaced by'), h('div', {}, p.replacedBy)),
906
+ h('div', {}, h('small', { class: 'faint' }, 'Affects'), h('div', {}, p.affectsText), p.affects?.length ? h('div', { class: 'row wrap' }, ...p.affects.map((a) => h('button', { class: 'text-btn', title: 'Show it', onClick: () => { document.querySelector('#dialog')?.close(); void locateTarget(a.id); } }, a.label))) : null),
907
+ h('div', {}, h('small', { class: 'faint' }, 'Must not pass as current again'), h('div', {}, p.mustNotPassAsCurrent))),
908
+ p.text ? h('details', { class: 'fold' }, h('summary', {}, 'As the agent entry prints it (pk get)'), text) : null,
909
+ ]);
910
+ dialog.classList.add('patch-dialog');
911
+ }
912
+
913
+ /** An earlier generation's plan document as it stood (§2.12, D82): the ledger's copy, a deleted one from before its deletion. */
914
+ export async function openGenerationPlan(generationId, index, label, from = 1) {
915
+ let v;
916
+ try { v = await K.getGenerationPlan(state.projectId, generationId, index, from); } catch (e) { openDialog(label, [h('p', { class: 'muted' }, `Could not load: ${e.message}`)]); return; }
917
+ if (!v) { openDialog(label, [h('p', { class: 'muted' }, NO_DATA)]); return; }
918
+ const first = v.fromLine ?? 1;
919
+ const body = [
920
+ h('dl', { class: 'kv' },
921
+ v.path ? h('dt', {}, 'Document') : null, v.path ? h('dd', { class: 'mono' }, v.path) : null,
922
+ v.version ? h('dt', {}, 'Version read') : null, v.version ? h('dd', {}, h('span', { class: 'mono' }, v.version.commit), ' · ', h('span', { class: 'kp-when', title: occurredTitle(v.version.occurred) }, fmtOccurred(v.version.occurred))) : null,
923
+ v.deleted ? h('dt', {}, 'Deleted') : null, v.deleted ? h('dd', { class: 'kp-deleted' }, `in ${v.deleted.commit} · ${fmtOccurred(v.deleted.occurred)} — this is the version before its deletion`) : null,
924
+ h('dt', {}, 'Now'), h('dd', {}, v.current ? 'This version is still the current one' : v.deleted ? 'Gone from the current version' : 'A later version replaced it')),
925
+ v.text !== null
926
+ ? h('div', { class: 'source-quote kp-plan-text' }, ...v.text.split('\n').map((line, i) => h('div', { class: 'src-line' }, h('span', { class: 'no' }, first + i), h('span', { class: 'tx' }, line))))
927
+ : h('p', { class: 'muted' }, v.why ?? NO_DATA),
928
+ // A long document comes in pages (the ledger's own paging): the next page opens on top, with a way back.
929
+ v.truncated && v.text !== null ? h('div', { class: 'dialog-foot' }, h('span', { class: 'faint' }, `Lines ${first}–${first + v.text.split('\n').length - 1} of ${v.lines}`),
930
+ v.nextFromLine ? h('button', { class: 'btn small', onClick: () => openGenerationPlan(generationId, index, label, v.nextFromLine) }, `Lines ${v.nextFromLine}–…`) : null) : null,
931
+ ];
932
+ const dialog = openDialog(label, body);
933
+ dialog.classList.add('genplan-dialog');
934
+ }