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,493 @@
1
+ /**
2
+ * The owner's lines no position stands on yet (Spec v3.0 §3.11, §1.15, §1.3; CKC-21 — how the project works; CKC-23
3
+ * AC-18 — session drafts). A session draft keeps the owner's words verbatim and says which lines decide or confirm
4
+ * something; a line reaches the workbench only when a step writes it — a rule of how this project works, an Owner's words
5
+ * item, a decision. The program lists the owner's lines that nothing cites yet (any label, CM), with the session
6
+ * segment each stands in, so the steps that write those positions judge every one of them.
7
+ *
8
+ * Found in test-C-1: the orchestrator's session draft held the owner's answers of 2026-09-27 verbatim, as a Confirmation —
9
+ * 「让各自commit就可以」 (each role commits its own documents) and 「可以,需要的时候就即时更新」 (update pi when needed) — and
10
+ * neither became a rule, an Owner's words item or anything an incoming agent reads.
11
+ *
12
+ * A line is cited when a rule or a product-reference item carries its words: a part of the rule's excerpt or of the
13
+ * owner's confirmation, or of the item's quote (the parts a quote's ellipses leave between them), stands in the line, or
14
+ * the line stands in it. A short part (「可以」, 「对的」) stands in too many lines to tell them apart: it counts only when the
15
+ * item also cites the session segment the line is in. An item that cites the segment with no words of its own is named
16
+ * beside the line, for the step to judge whether it stands on it.
17
+ *
18
+ * DA (E156; the flash run): a line has three outcomes, not two. On that run the program showed all 246 lines as a debt
19
+ * with no way to settle one except by citing it, and the main agent ordered every owner message recorded — 248 Owner's
20
+ * words items, 「继续」 among them. A line for its moment only is now judged to need nothing, by its line reference with
21
+ * why (`pk_judge_owner_lines`, many lines in one call): it leaves the list, is not given again in a later round while
22
+ * its words stand, and stays on the round's record, where the spot-check samples it. And a quote too short to stand
23
+ * alone (every part of it under `DISTINCT_WORDS`) becomes an Owner's words item only with the message it answers
24
+ * (Spec §3.11: 「owner 说“可以”时,要看它答的是什么」; `ownerWordsContextRefusal`, called from `pk_write_reference`).
25
+ */
26
+ import { Type } from 'typebox';
27
+ import { defineTool, type ToolDefinition } from '@earendil-works/pi-coding-agent';
28
+ import { Ledger } from '../../ledger/index.ts';
29
+ import type { ClerkRound, OwnerLineJudgement, OwnerLineKind, SessionDraft } from '../../model/k-types.ts';
30
+ import type { ProjectRule, ReferenceItem, Source } from '../../model/types.ts';
31
+ import type { ProjectStore } from '../../store/project-store.ts';
32
+ import type { ToolContext } from '../tools.ts';
33
+ import { checkVerbatim } from '../verbatim.ts';
34
+
35
+ /** A folded part this long (letters, digits and CJK characters) tells one owner line from another by itself. */
36
+ export const DISTINCT_WORDS = 8;
37
+
38
+ /** Only the words: no spacing, punctuation, quotation marks or Markdown; lower case; full-width forms as plain ones. */
39
+ export function fold(text: string): string {
40
+ return text.normalize('NFKC').toLowerCase().replace(/[\s\p{P}\p{S}\p{Cc}\p{Cf}]/gu, '');
41
+ }
42
+
43
+ /** The parts of a quote: what stands between its ellipses and between the separate utterances it strings together (「…」「…」). */
44
+ export function wordParts(quote: string): string[] {
45
+ return quote.split(/(?:…|⋯|\.{3,})+|」\s*「|”\s*“|"\s+"/u).map(fold).filter(Boolean);
46
+ }
47
+
48
+ export interface OwnerLineRef {
49
+ readonly draftId: string;
50
+ readonly host: string;
51
+ readonly sessionId: string;
52
+ readonly ref: string;
53
+ readonly at: string;
54
+ /** How the session draft labelled it: a reading, never a filter (CM: a line naming two generations was labelled Chat and dropped). */
55
+ readonly kind: OwnerLineKind;
56
+ readonly text: string;
57
+ readonly confirms: string | null;
58
+ /** The agent message it answers, as the session draft took it from the ledger (a confirmation's); null when the draft has none. */
59
+ readonly answers: string | null;
60
+ /** The session segment(s) it stands in: the sources a rule or an item cites for it. */
61
+ readonly sourceIds: readonly string[];
62
+ }
63
+
64
+ export interface CitingItem {
65
+ readonly id: string;
66
+ readonly kind: 'rule' | 'reference';
67
+ readonly label: string;
68
+ readonly words: readonly string[];
69
+ readonly sourceIds: readonly string[];
70
+ }
71
+
72
+ export interface UncitedLine extends OwnerLineRef {
73
+ /** Items that cite its session segment with no words of their own: they may stand on it. */
74
+ readonly segmentCitedBy: readonly CitingItem[];
75
+ }
76
+
77
+ /** A line judged to need nothing (DA): the judgement, and the round it was made in. */
78
+ export interface JudgedLine extends OwnerLineRef {
79
+ readonly why: string;
80
+ readonly by: 'main' | 'lane';
81
+ readonly lane: string | null;
82
+ readonly judgedAt: string;
83
+ readonly roundId: string;
84
+ }
85
+
86
+ export interface OwnerLinesResult {
87
+ /** The lines not looked at yet: cited by nothing, and not judged to need nothing. Newest first. */
88
+ readonly lines: readonly UncitedLine[];
89
+ /** Every owner's line of the session drafts, whatever the draft labelled it, and the drafts they are in. */
90
+ readonly considered: number;
91
+ readonly drafts: number;
92
+ /** Those cited, with what cites them. */
93
+ readonly cited: readonly { readonly line: OwnerLineRef; readonly by: readonly string[] }[];
94
+ /** Those judged to need nothing and cited by nothing (DA), newest first. */
95
+ readonly judged: readonly JudgedLine[];
96
+ }
97
+
98
+ /** A line's reference, as the program's lists name it and `pk_judge_owner_lines` takes it: `<draft id>:<ref>`. */
99
+ export const lineKey = (draftId: string, ref: string): string => `${draftId}:${ref}`;
100
+
101
+ /** How a line's words begin, folded: what a judgement is held against, so a line whose words changed is looked at again. */
102
+ const HEAD_WORDS = 80;
103
+ const headOf = (text: string): string => fold(text).slice(0, HEAD_WORDS);
104
+
105
+ /** Whether a quote or a line stands alone: some part of it is long enough to tell one owner's line from another. */
106
+ export function standsAlone(words: string): boolean {
107
+ return wordParts(words).some((p) => p.length >= DISTINCT_WORDS);
108
+ }
109
+
110
+ /** The judgements "needs nothing" of every round, by line reference; a later one of the same line replaces an earlier. */
111
+ export function lineJudgements(store: ProjectStore): Map<string, OwnerLineJudgement & { readonly roundId: string }> {
112
+ const out = new Map<string, OwnerLineJudgement & { readonly roundId: string }>();
113
+ for (const round of store.clerkRounds.all().sort((a, b) => a.startedAt.localeCompare(b.startedAt))) {
114
+ for (const j of round.ownerLineJudgements ?? []) out.set(lineKey(j.draftId, j.ref), { ...j, roundId: round.id });
115
+ }
116
+ return out;
117
+ }
118
+
119
+ /** Where a session line stands: its message position, from the line's ref or the ledger's message entry. */
120
+ function positionOf(ref: string, ledger: Ledger | null): number | null {
121
+ const n = /^\[?(\d+)\]?$/.exec(ref.trim())?.[1];
122
+ if (n !== undefined) return Number(n);
123
+ if (!ledger || !/^msg:[0-9a-f]+$/i.test(ref.trim())) return null;
124
+ try {
125
+ const row = ledger.db.prepare('SELECT idx FROM session_messages WHERE key = ?').get(ref.trim()) as { idx: number } | undefined;
126
+ return row ? Number(row.idx) : null;
127
+ } catch { return null; }
128
+ }
129
+
130
+ /** The session sources that hold a draft's line: by its message position, else by its words, else by its time. */
131
+ export function segmentsOf(sources: readonly Source[], draft: SessionDraft, line: SessionDraft['ownerLines'][number], ledger: Ledger | null): string[] {
132
+ const own = sources.filter((s) => s.anchor.kind === 'session' && s.anchor.host === draft.session.host && s.anchor.sessionId === draft.session.sessionId);
133
+ if (!own.length) return [];
134
+ const at = positionOf(line.ref, ledger);
135
+ if (at !== null) {
136
+ const hit = own.filter((s) => s.anchor.kind === 'session' && s.anchor.messageStart <= at && at <= s.anchor.messageEnd);
137
+ if (hit.length) return hit.map((s) => s.id);
138
+ }
139
+ const words = fold(line.text).slice(0, 60);
140
+ if (words.length >= DISTINCT_WORDS) {
141
+ const hit = own.filter((s) => fold(s.excerpt).includes(words));
142
+ if (hit.length) return hit.map((s) => s.id);
143
+ }
144
+ const byTime = own.filter((s) => s.anchor.kind === 'session' && (s.anchor.at ?? '') <= line.at)
145
+ .sort((a, b) => ((b.anchor.kind === 'session' ? b.anchor.at ?? '' : '').localeCompare(a.anchor.kind === 'session' ? a.anchor.at ?? '' : '')))[0];
146
+ return byTime ? [byTime.id] : [];
147
+ }
148
+
149
+ function itemsOf(store: ProjectStore): CitingItem[] {
150
+ const rules = store.rules.all().map((r: ProjectRule): CitingItem => ({
151
+ id: r.id, kind: 'rule', label: r.summary,
152
+ words: [r.excerpt ?? '', r.ownerConfirmation?.quote ?? ''].filter(Boolean),
153
+ sourceIds: [...r.sourceIds, ...(r.ownerConfirmation ? [r.ownerConfirmation.sourceId] : [])],
154
+ }));
155
+ const refs = store.reference.all().map((r: ReferenceItem): CitingItem => ({
156
+ id: r.id, kind: 'reference', label: `${r.category} ${r.name}`, words: r.quote ? [r.quote] : [], sourceIds: r.sourceIds,
157
+ }));
158
+ return [...rules, ...refs];
159
+ }
160
+
161
+ /** Whether an item carries the line's words (see the module comment). */
162
+ export function carries(item: CitingItem, lineWords: string, lineSources: ReadonlySet<string>): boolean {
163
+ const onSegment = item.sourceIds.some((s) => lineSources.has(s));
164
+ for (const w of item.words) {
165
+ const whole = fold(w);
166
+ if (lineWords.length >= DISTINCT_WORDS && whole.includes(lineWords)) return true;
167
+ if (onSegment && whole && (whole === lineWords || lineWords.includes(whole) || whole.includes(lineWords))) return true;
168
+ for (const part of wordParts(w)) {
169
+ if (!lineWords.includes(part)) continue;
170
+ if (part.length >= DISTINCT_WORDS || onSegment) return true;
171
+ }
172
+ }
173
+ return false;
174
+ }
175
+
176
+ /**
177
+ * The owner's lines by their outcome (DA): cited — a rule, an Owner's words item or a decision carries their words —,
178
+ * judged to need nothing, or not looked at yet (`lines`, newest first). Every line the session drafts hold counts,
179
+ * whatever they labelled it (CM, E151: the draft labelled the owner's 「这两代」 message Chat, the program left it out,
180
+ * and orientation folded the two generations into one). The label stays on each line as the draft's reading. A cited
181
+ * line is cited whatever was judged of it; a judgement stands while the line's words do.
182
+ */
183
+ export function uncitedOwnerLines(store: ProjectStore, ledger: Ledger | null): OwnerLinesResult {
184
+ const sources = store.sources.filter((s) => s.anchor.kind === 'session');
185
+ const items = itemsOf(store);
186
+ const drafts = store.drafts.all();
187
+ const judgements = lineJudgements(store);
188
+ const out: UncitedLine[] = [];
189
+ const cited: { line: OwnerLineRef; by: string[] }[] = [];
190
+ const judged: JudgedLine[] = [];
191
+ let considered = 0;
192
+ for (const d of drafts) {
193
+ for (const l of d.ownerLines) {
194
+ if (!l.text.trim()) continue;
195
+ considered++;
196
+ const sourceIds = segmentsOf(sources, d, l, ledger);
197
+ const line: OwnerLineRef = { draftId: d.id, host: d.session.host, sessionId: d.session.sessionId, ref: l.ref, at: l.at, kind: l.kind ?? 'Chat', text: l.text, confirms: l.confirms, answers: l.answers ?? null, sourceIds };
198
+ const words = fold(l.text);
199
+ const onIt = new Set(sourceIds);
200
+ const by = items.filter((i) => carries(i, words, onIt)).map((i) => i.id);
201
+ if (by.length) { cited.push({ line, by }); continue; }
202
+ const j = judgements.get(lineKey(d.id, l.ref));
203
+ if (j && j.head === headOf(l.text)) { judged.push({ ...line, why: j.why, by: j.by, lane: j.lane ?? null, judgedAt: j.at, roundId: j.roundId }); continue; }
204
+ out.push({ ...line, segmentCitedBy: items.filter((i) => i.words.length === 0 && i.sourceIds.some((s) => onIt.has(s))) });
205
+ }
206
+ }
207
+ const newestFirst = (a: OwnerLineRef, b: OwnerLineRef) => b.at.localeCompare(a.at) || a.ref.localeCompare(b.ref);
208
+ return { lines: out.sort(newestFirst), considered, drafts: drafts.length, cited, judged: judged.sort(newestFirst) };
209
+ }
210
+
211
+ /** The program's count, in the words every block gives it: "n of N … are cited, m judged to need nothing, k not looked at yet". */
212
+ export function ownerLinesTally(r: OwnerLinesResult): string {
213
+ return `${r.cited.length} of the ${r.considered} owner's lines in the ${r.drafts} session drafts are cited, ${r.judged.length} judged to need nothing, ${r.lines.length} not looked at yet`;
214
+ }
215
+
216
+ /** The agent message an owner's line answers: the session draft's, else the ledger's (the message right before it). */
217
+ export function answeredBy(line: Pick<OwnerLineRef, 'ref' | 'answers'>, ledger: Ledger | null): string | null {
218
+ if (line.answers?.trim()) return line.answers;
219
+ if (!ledger || !/^msg:[0-9a-f]+$/i.test(line.ref.trim())) return null;
220
+ try { return ledger.message(line.ref.trim())?.answers?.text ?? null; } catch { return null; }
221
+ }
222
+
223
+ // ───────────────────────── the block a step is given ─────────────────────────
224
+
225
+ const when = (at: string): string => (at ? at.replace('T', ' ').slice(0, 16) : 'time unknown');
226
+
227
+ const clip = (s: string, n: number): string => { const flat = s.trim().replace(/\s+/g, ' '); return flat.length > n ? `${flat.slice(0, n)}…` : flat; };
228
+
229
+ function lineHead(l: UncitedLine): string {
230
+ return `${when(l.at)} · ${l.kind} · ${l.host} session ${l.sessionId.slice(0, 8)} · line ${lineKey(l.draftId, l.ref)} · cite ${l.sourceIds.length ? l.sourceIds.join(', ') : 'no session source holds it'}`;
231
+ }
232
+
233
+ /** How much of the message a short line answers the block shows. */
234
+ const ANSWERS_SHOWN = 400;
235
+
236
+ export interface OwnerLinesBlockOptions {
237
+ /** A Follow up: lines said since this time in full; the earlier ones still cited by nothing, one line each. */
238
+ readonly since?: string | null;
239
+ }
240
+
241
+ /**
242
+ * The block for the lane that writes the Owner's words items (a lane given the slot reference:Owner's words, CM): every
243
+ * owner's line not looked at yet (any label), verbatim, with its line reference and the segment to cite. What each
244
+ * becomes — a rule, an Owner's words item, both, or a line judged to need nothing — is the step's judgement; the method
245
+ * says how. A line too short to stand alone comes with the message it answers (DA).
246
+ */
247
+ export function ownerLinesBlock(store: ProjectStore, ledger: Ledger | null, opts: OwnerLinesBlockOptions = {}): string {
248
+ const head = "=== The owner's lines no position cites yet (the program's list from the session drafts, whatever label the draft gave a line. A line is settled one of two ways: cited — a rule's excerpt or owner's confirmation, or a product-reference item's quote, carries its words — or judged to need nothing — a line for its moment only: pk_judge_owner_lines { lines: [its line reference, …], why }, many lines in one call; it is not listed again)";
249
+ const r = uncitedOwnerLines(store, ledger);
250
+ if (r.drafts === 0) return `${head}\nNo session draft yet.`;
251
+ if (r.lines.length === 0) return `${head}\nNone left: ${ownerLinesTally(r)}.`;
252
+ const since = opts.since ?? '';
253
+ const recent = since ? r.lines.filter((l) => l.at >= since) : r.lines;
254
+ const earlier = since ? r.lines.filter((l) => l.at < since) : [];
255
+ const lines = [head, `${ownerLinesTally(r)}${since ? `: ${recent.length} said since ${when(since)} (in full), ${earlier.length} earlier` : ''}. Those not looked at yet follow, newest first; the owner's words are verbatim, the label is the draft's reading (not a filter: judge each line yourself), and the session segment named is the source to cite.`];
256
+ for (const l of recent) {
257
+ lines.push(`- ${lineHead(l)}`);
258
+ // Verbatim, each line of the message indented so it stays inside its entry (indentation makes no difference to a quote).
259
+ lines.push(` the owner's message: 「${l.text.trim().split(/\r?\n/).join('\n ')}」`);
260
+ if (l.confirms) lines.push(` what it confirms (the draft's words, not the owner's): ${l.confirms.trim()}`);
261
+ if (!standsAlone(l.text)) {
262
+ const answered = answeredBy(l, ledger);
263
+ lines.push(answered
264
+ ? ` too short to stand alone — it answers (the agent's message before it, as the program has it): 「${clip(answered, ANSWERS_SHOWN)}」 An item that quotes this line gives what it answers (answers).`
265
+ : ' too short to stand alone, and the program has no message it answers: read its segment. An item that quotes this line gives what it answers (answers).');
266
+ }
267
+ if (l.segmentCitedBy.length) lines.push(` its segment is cited, with no words of its own, by: ${l.segmentCitedBy.map((i) => `${i.id} (${i.label.slice(0, 80)})`).join('; ')} — see whether it stands on this line`);
268
+ }
269
+ if (earlier.length) {
270
+ lines.push(`Said before ${when(since)} and still not looked at (${earlier.length}; pk_read_assets kind draft for any of them in full):`);
271
+ for (const l of earlier) lines.push(`- ${lineHead(l)}: 「${clip(l.text, 160)}」`);
272
+ }
273
+ return lines.join('\n');
274
+ }
275
+
276
+ // ───────────────────────── the main agent's count (CM) ─────────────────────────
277
+
278
+ /**
279
+ * Words that name versions, generations or a supersession (CM, E151): 「这两代」, 「上一代」, 「v2.0–v3.x」, 「取代」, "generation",
280
+ * "superseded", an archive. A line with them can change how orientation cuts the generations, so the main agent sees it
281
+ * even though the full list goes to a lane.
282
+ */
283
+ export const GENERATION_WORDS = /(?:[这两几上前旧新首每各][一二三四五六七八九十两\d]?代(?![码表理替价言谢])|上一?代|第[一二三四五六七八九十\d]+代|generations?\b|版本史|(?<![A-Za-z])v\d+(?:\.\d+)*(?:\.x)?\s*[–—\-~~到至]\s*v?\d+(?:\.\d+)*(?:\.x)?|supersed\w*|取代|作废|归档|archiv(?:e|ed|ing)\b|重写|rewrit\w*)/i;
284
+
285
+ /** The narrower words that name a generation or a span of versions itself (not merely a supersession): 「这两代」, 「上一代」, "generation", 「v2.0–v3.x」. */
286
+ export const GENERATION_NAMED = /(?:[这两几上前旧新首每各][一二三四五六七八九十两\d]?代(?![码表理替价言谢])|上一?代|第[一二三四五六七八九十\d]+代|generations?\b|版本史|(?<![A-Za-z])v\d+(?:\.\d+)*(?:\.x)?\s*[–—\-~~到至]\s*v?\d+(?:\.\d+)*(?:\.x)?)/i;
287
+
288
+ /** How many of the lines that name generations the main agent's count shows. */
289
+ export const NAMING_SHOWN = 12;
290
+
291
+ /** Up to `max` stretches of a text around the words that name generations, each with some words either side. */
292
+ export function generationExcerpts(text: string, max = 3, around = 70): string[] {
293
+ const flat = text.replace(/\s+/g, ' ').trim();
294
+ const re = new RegExp(GENERATION_WORDS.source, 'gi');
295
+ const out: { from: number; to: number }[] = [];
296
+ for (const m of flat.matchAll(re)) {
297
+ const from = Math.max(0, m.index! - around);
298
+ const to = Math.min(flat.length, m.index! + m[0].length + around);
299
+ const last = out[out.length - 1];
300
+ if (last && from <= last.to) { last.to = Math.max(last.to, to); continue; }
301
+ if (out.length >= max) break;
302
+ out.push({ from, to });
303
+ }
304
+ return out.map((x) => `${x.from > 0 ? '…' : ''}${flat.slice(x.from, x.to)}${x.to < flat.length ? '…' : ''}`);
305
+ }
306
+
307
+ /**
308
+ * What the main agent is given of the owner's lines (CM, E151; CK fix 1): a count, and the lines that name versions,
309
+ * generations or a supersession, each as the stretches around those words with where to read it whole. The full list —
310
+ * 96K characters on the gated run, pasted four times into the main session — goes to the lane that writes the `Owner's
311
+ * words` items (a lane given the slot `reference:Owner's words`), with its brief.
312
+ */
313
+ export function ownerLinesCountBlock(store: ProjectStore, ledger: Ledger | null, opts: { readonly naming?: boolean } = {}): string {
314
+ const head = "=== The owner's lines no position cites yet (a count; the full list goes to the lane you give the slot reference:Owner's words)";
315
+ const r = uncitedOwnerLines(store, ledger);
316
+ if (r.drafts === 0) return `${head}\nNo session draft yet.`;
317
+ if (r.lines.length === 0) return `${head}\nNone left: ${ownerLinesTally(r)}.`;
318
+ const kinds = r.lines.reduce((m, l) => m.set(l.kind, (m.get(l.kind) ?? 0) + 1), new Map<string, number>());
319
+ const naming = r.lines.filter((l) => GENERATION_WORDS.test(l.text));
320
+ const lines = [head, `${ownerLinesTally(r)} (the drafts labelled those ${[...kinds].map(([k, n]) => `${n} ${k}`).join(', ')}). Give their judging to a lane with the slot reference:Owner's words — the program gives that lane the whole list with each line verbatim and the segment to cite; by its own skill a line becomes an item, a working rule it reports for you to write, or — a line for its moment only — is judged to need nothing.`];
321
+ // The lines that name generations are read in orientation, where the generations are cut; later stages get the count.
322
+ if (naming.length && opts.naming === false) lines.push(`${naming.length} of them name versions, generations or a supersession (pk_round_state { list: "ownerLines" } lists every line).`);
323
+ else if (naming.length) {
324
+ // Those that name a generation or a span of versions itself come first; a line that only says something was superseded after.
325
+ const ranked = [...naming].sort((a, b) => Number(GENERATION_NAMED.test(b.text)) - Number(GENERATION_NAMED.test(a.text)) || b.at.localeCompare(a.at));
326
+ lines.push(`${naming.length} of them name versions, generations or a supersession — read them before you cut the generations (pk_read_assets kind draft for a line in full${naming.length > NAMING_SHOWN ? `; ${NAMING_SHOWN} shown, those that name a generation first — pk_round_state { list: "ownerLines" } for all` : ''}):`);
327
+ for (const l of ranked.slice(0, NAMING_SHOWN)) lines.push(`- ${when(l.at)} · ${l.kind} · ${l.draftId}, ${l.ref}: ${generationExcerpts(l.text, 2).map((x) => `「${x}」`).join(' ')}`);
328
+ }
329
+ return lines.join('\n');
330
+ }
331
+
332
+ // ───────────────────────── the third outcome: judged, needs nothing (DA) ─────────────────────────
333
+
334
+ const text = (v: unknown): string => (typeof v === 'string' ? v.trim() : '');
335
+ const ok = (value: unknown) => ({ content: [{ type: 'text' as const, text: JSON.stringify(value, null, 1) }], details: {} });
336
+ const fail = (message: string) => ({ content: [{ type: 'text' as const, text: `ERROR: ${message}` }], details: {}, isError: true });
337
+ const bare = (ref: string): string => ref.trim().replace(/^\[|\]$/g, '');
338
+
339
+ /** The line a reference names: `<draft id>:<ref>` as the lists give it (a comma or a space between them is read too). */
340
+ function lineNamed(drafts: readonly SessionDraft[], input: string): { readonly draft: SessionDraft; readonly line: SessionDraft['ownerLines'][number] } | null {
341
+ const named = input.trim().replace(/^\(|\)$/g, '').replace(/^line\s+/i, '');
342
+ for (const draft of drafts) {
343
+ if (!named.startsWith(draft.id)) continue;
344
+ const ref = bare(named.slice(draft.id.length).replace(/^[\s:,]+/, ''));
345
+ const line = draft.ownerLines.find((l) => bare(l.ref) === ref);
346
+ if (line) return { draft, line };
347
+ }
348
+ return null;
349
+ }
350
+
351
+ /** The slot whose lane judges the owner's lines (round-blocks.ts `OWNER_WORDS_SLOT`), lower case. */
352
+ const JUDGING_SLOT = "reference:owner's words";
353
+
354
+ /**
355
+ * `pk_judge_owner_lines` (DA): the third outcome of an owner's line. Which job is offered it is the stage gate's
356
+ * (clerk-steps.ts: a lane with the slot `reference:Owner's words`, the main agent in reconcile and the cross-check).
357
+ */
358
+ export function ownerLineTools(ctx: ToolContext): ToolDefinition[] {
359
+ const { store } = ctx;
360
+ return [defineTool({
361
+ name: 'pk_judge_owner_lines', label: 'Judge owner’s lines as needing nothing',
362
+ description: 'The third outcome of an owner’s line: judged, needs nothing. A line that is for its moment only — a go-ahead, a question answered then, a relay, thanks — forms no item and no rule: record it here by its line reference (<draft id>:<ref>, as the program’s list of the owner’s lines names it), many lines in one call, with why. Such a line leaves the list and is not given again while its words stand; the judgement stays on the round’s record, and the spot-check samples it. A line about what the product is, a decision, a correction or a working rule is not for its moment only: write it or report it. A short answer (“ok”, “right”, 「可以」) is judged by what it answers.',
363
+ parameters: Type.Object({
364
+ lines: Type.Array(Type.String(), { description: 'each line by its reference, <draft id>:<ref>, as the list names it' }),
365
+ why: Type.String({ description: 'why these lines need nothing: what they were, in a few words' }),
366
+ }),
367
+ execute: async (_id, raw) => {
368
+ const p = raw as Record<string, unknown>;
369
+ const step = ctx.step;
370
+ if (!step || (step.kind !== 'main' && step.kind !== 'lane')) return fail(`pk_judge_owner_lines is a round's: the lane with the slot reference:Owner's words, or the main agent in reconcile or the cross-check. This job is ${step ? `the round's ${step.kind}` : 'no step of a round'}, so nothing was recorded.`);
371
+ if (step.kind === 'lane' && !(step.lane?.slots ?? []).some((s) => s.toLowerCase() === JUDGING_SLOT)) return fail(`pk_judge_owner_lines is the lane's that holds the slot reference:Owner's words; this lane (${step.path ?? ''}) writes ${(step.lane?.slots ?? []).join(', ') || 'none'}, so nothing was recorded. Put the lines in your Report for the main agent.`);
372
+ const round = store.clerkRounds.get(step.roundId);
373
+ if (!round) return fail(`The round ${step.roundId} is not in the assets.`);
374
+ const why = text(p.why);
375
+ if (!why) return fail('why is empty: say why these lines need nothing — what they were. Nothing was recorded.');
376
+ const named = [...new Set((Array.isArray(p.lines) ? p.lines : []).map(text).filter(Boolean))];
377
+ if (!named.length) return fail('lines is empty: name each line by its reference (<draft id>:<ref>), as the list of the owner’s lines gives it. Nothing was recorded.');
378
+ const drafts = store.drafts.all();
379
+ let ledger: Ledger | null = null;
380
+ try { ledger = Ledger.openDir(store.dir); } catch { ledger = null; }
381
+ try {
382
+ const before = uncitedOwnerLines(store, ledger);
383
+ const open = new Set(before.lines.map((l) => lineKey(l.draftId, l.ref)));
384
+ const citedBy = new Map(before.cited.map((c) => [lineKey(c.line.draftId, c.line.ref), c.by]));
385
+ const judgedAlready = new Set(before.judged.map((l) => lineKey(l.draftId, l.ref)));
386
+ const at = new Date().toISOString();
387
+ const fresh: OwnerLineJudgement[] = [];
388
+ const unknown: string[] = [];
389
+ const alreadyCited: string[] = [];
390
+ const alreadyJudged: string[] = [];
391
+ for (const name of named) {
392
+ const hit = lineNamed(drafts, name);
393
+ if (!hit) { unknown.push(name); continue; }
394
+ const key = lineKey(hit.draft.id, hit.line.ref);
395
+ if (citedBy.has(key)) { alreadyCited.push(`${key} (cited by ${citedBy.get(key)!.slice(0, 3).join(', ')})`); continue; }
396
+ if (judgedAlready.has(key)) { alreadyJudged.push(key); continue; }
397
+ if (!open.has(key) || fresh.some((j) => lineKey(j.draftId, j.ref) === key)) continue;
398
+ fresh.push({ draftId: hit.draft.id, ref: hit.line.ref, head: headOf(hit.line.text), why, by: step.kind === 'main' ? 'main' : 'lane', lane: step.kind === 'lane' ? step.path ?? null : null, jobId: ctx.jobId, at });
399
+ }
400
+ if (!fresh.length) {
401
+ return fail(`No line was recorded${unknown.length ? `; not a line of the session drafts: ${unknown.slice(0, 12).join(', ')}${unknown.length > 12 ? ` and ${unknown.length - 12} more` : ''} — name a line as the list does, <draft id>:<ref>` : ''}${alreadyCited.length ? `; cited already, so they need no judgement: ${alreadyCited.slice(0, 12).join('; ')}` : ''}${alreadyJudged.length ? `; judged already: ${alreadyJudged.slice(0, 12).join(', ')}` : ''}.`);
402
+ }
403
+ const current: ClerkRound = store.clerkRounds.get(round.id) ?? round;
404
+ store.clerkRounds.put({ ...current, ownerLineJudgements: [...(current.ownerLineJudgements ?? []), ...fresh], updatedAt: at }, { jobId: ctx.jobId, basisSourceIds: [], summary: `Owner's lines judged to need nothing: ${fresh.length} — ${why}` });
405
+ const after = uncitedOwnerLines(store, ledger);
406
+ return ok({
407
+ judged: fresh.length, cited: after.cited.length, judgedToNeedNothing: after.judged.length, notLookedAtYet: after.lines.length,
408
+ ...(unknown.length ? { unknown } : {}), ...(alreadyCited.length ? { alreadyCited } : {}), ...(alreadyJudged.length ? { alreadyJudged } : {}),
409
+ });
410
+ } finally {
411
+ ledger?.close();
412
+ }
413
+ },
414
+ })];
415
+ }
416
+
417
+ /** How many of the round's judged lines the spot-check is given (all of them stay on the round's record). */
418
+ export const JUDGED_SAMPLE = 12;
419
+
420
+ /**
421
+ * What a round's spot-check is given of the lines judged to need nothing (DA): those judged since the last spot-check
422
+ * of an earlier round began, whichever round judged them — a first usable round, where most lines are judged, has no
423
+ * spot-check of its own. How many, each reason with how many lines it was given for, and a sample — the lines the session
424
+ * draft labelled a decision or a confirmation first, then the longest, where a wrong judgement costs most. Null when
425
+ * none was judged since.
426
+ */
427
+ export function judgedLinesBlock(store: ProjectStore, round: Pick<ClerkRound, 'id'>, ledger: Ledger | null): string | null {
428
+ const lastCheck = store.jobs.filter((j) => j.step?.kind === 'spot-check' && j.step.roundId !== round.id).map((j) => j.queuedAt).sort().at(-1) ?? '';
429
+ const judged = uncitedOwnerLines(store, ledger).judged.filter((l) => l.judgedAt > lastCheck);
430
+ if (!judged.length) return null;
431
+ const reasons = new Map<string, number>();
432
+ for (const l of judged) reasons.set(l.why, (reasons.get(l.why) ?? 0) + 1);
433
+ const weight = (l: JudgedLine): number => (l.kind === 'Chat' ? 0 : 1);
434
+ const sample = [...judged].sort((a, b) => weight(b) - weight(a) || fold(b.text).length - fold(a.text).length || b.at.localeCompare(a.at)).slice(0, JUDGED_SAMPLE);
435
+ const lines = [
436
+ `=== The owner's lines judged to need nothing since the last spot-check (${judged.length}; the program's record — a sample of ${sample.length} to check)`,
437
+ 'A line for its moment only forms no item. A line about what the product is, a decision, a correction or a working rule should have become one: where a sampled line is such a line, write what it should have been (pk_write_reference for an Owner’s words item, with the segment as its source — a cited line leaves this record) and record the check as Wrong; else Right. Record each with pk_record_spot_check, target { collection: "drafts", id: <its draft id> }, kind "owner’s line judged to need nothing".',
438
+ `The reasons given: ${[...reasons].map(([why, n]) => `“${clip(why, 160)}” (${n})`).join('; ')}.`,
439
+ ];
440
+ for (const l of sample) lines.push(`- ${when(l.at)} · ${l.kind} · line ${lineKey(l.draftId, l.ref)} · cite ${l.sourceIds.join(', ') || 'no session source holds it'} · judged by ${l.by === 'lane' ? `the lane ${l.lane ?? ''}`.trim() : 'the main agent'}: “${clip(l.why, 120)}”\n the owner's message: 「${clip(l.text, 400)}」`);
441
+ return lines.join('\n');
442
+ }
443
+
444
+ // ───────────────────────── a short quote needs what it answers (DA; Spec §3.11) ─────────────────────────
445
+
446
+ /**
447
+ * The agent messages the owner's lines a quote stands on answer: the lines of the session drafts that hold every part of
448
+ * the quote and stand in a cited session segment, each with the message before it as the program has it.
449
+ */
450
+ function answeredMessages(store: ProjectStore, ledger: Ledger | null, quote: string, sourceIds: readonly string[]): string[] {
451
+ const parts = wordParts(quote);
452
+ if (!parts.length) return [];
453
+ const cited = new Set(sourceIds);
454
+ const sources = store.sources.filter((s) => s.anchor.kind === 'session');
455
+ const out: string[] = [];
456
+ for (const d of store.drafts.all()) {
457
+ for (const l of d.ownerLines) {
458
+ const words = fold(l.text);
459
+ if (!parts.every((part) => words.includes(part))) continue;
460
+ if (!segmentsOf(sources, d, l, ledger).some((id) => cited.has(id))) continue;
461
+ const answered = answeredBy({ ref: l.ref, answers: l.answers ?? null }, ledger);
462
+ if (answered?.trim()) out.push(answered);
463
+ }
464
+ }
465
+ return out;
466
+ }
467
+
468
+ /**
469
+ * Why an Owner's words item is refused for its context, or null (DA; Spec §3.11: 「owner 说“可以”时,要看它答的是什么」).
470
+ * A quote too short to stand alone — every part of it under `DISTINCT_WORDS` — is accepted only with the message it
471
+ * answers (`answers`). What is given as `answers` is copied, not written: it stands in the sources cited, or in the agent
472
+ * message the program has before the owner's line. There is no filter on length beyond that: 「对的」 can be a decision.
473
+ */
474
+ export function ownerWordsContextRefusal(store: ProjectStore, input: { readonly quote: string; readonly answers: string; readonly sourceIds: readonly string[] }): string | null {
475
+ const short = !standsAlone(input.quote);
476
+ const answers = input.answers.trim();
477
+ if (!short && !answers) return null;
478
+ let ledger: Ledger | null = null;
479
+ try { ledger = Ledger.openDir(store.dir); } catch { ledger = null; }
480
+ try {
481
+ const answered = answeredMessages(store, ledger, input.quote, input.sourceIds);
482
+ if (!answers) {
483
+ const known = answered[0] ? ` The message before it, as the program has it: 「${clip(answered[0], ANSWERS_SHOWN)}」` : '';
484
+ return `「${clip(input.quote, 60)}」 is too short to stand alone as an Owner's words item (when the owner says “ok” or 「可以」, look at what it answers). Give answers: the message it answers, copied from the session or the record cited (…… where you leave words out).${known} A line that was for its moment only forms no item: judge it with pk_judge_owner_lines instead. Nothing was written.`;
485
+ }
486
+ const parts = wordParts(answers);
487
+ if (parts.length && answered.some((m) => { const words = fold(m); return parts.every((part) => words.includes(part)); })) return null;
488
+ if (checkVerbatim(store, answers, input.sourceIds).missing.length === 0) return null;
489
+ return `answers is the message the owner's words answer, copied from where it was said (…… where you leave words out): what you gave stands neither in the sources cited nor in the agent's message before the owner's line${answered[0] ? `, which the program has as 「${clip(answered[0], ANSWERS_SHOWN)}」` : ''}. Copy it; a summary of yours goes in text. Nothing was written.`;
490
+ } finally {
491
+ ledger?.close();
492
+ }
493
+ }