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,191 @@
1
+ /**
2
+ * The owner's words a piece of work traces up to, and the adjustments made on the way from them to this work (D66;
3
+ * Spec §7.1, §7.4 item 1; CKC-12 AC-40–AC-42). Deterministic; it reads the assets and writes nothing.
4
+ *
5
+ * The way up follows the work's relations: the areas, requirements and designs it serves, the plan it is in, the
6
+ * decisions it carries out, and from each of them `refines`, up to the `Owner's words` layer (§1.3). Owner's words kept
7
+ * in a decision record count too (§1.3): an item on the way that carries the owner's words in `quote` gives them.
8
+ * The product item is where the way stops: what the owner said about the whole product belongs to the start pack's
9
+ * `Purpose`, and comes with a piece of work only when it names that work.
10
+ *
11
+ * An adjustment is a change on the way that altered what the work is — a decision, a hand-over, a change of plan — or a
12
+ * decision on the way that a role made. Building the code, a test result and finishing the work are progress, not
13
+ * adjustments; a proposal nobody adopted altered nothing; an adjustment a later change superseded is not listed (§7.4).
14
+ */
15
+ import type { Attribution, ChangeItem, ChangeRecord, EntryMark, ReferenceItem, WorkThread } from '../model/types.ts';
16
+ import { OWNER_WORDS } from '../model/vocab.ts';
17
+ import type { ProjectStore } from '../store/project-store.ts';
18
+ import { itemsOf } from '../keeper/adjustment.ts';
19
+ import { materialDate } from '../model/time.ts';
20
+
21
+ export interface TracedWords {
22
+ /** The `Owner's words` item, or the item on the way whose `quote` holds the owner's words. */
23
+ readonly item: ReferenceItem;
24
+ readonly quote: string;
25
+ /** When the owner said them, from the session they were said in; null when the source gives no time. */
26
+ readonly saidOn: string | null;
27
+ }
28
+
29
+ export interface Adjustment {
30
+ readonly on: string | null; // the day it happened, as the material gives it
31
+ readonly material: string | null;
32
+ readonly effect: string | null;
33
+ readonly title: string;
34
+ readonly summary: string;
35
+ readonly before: string | null;
36
+ readonly after: string | null;
37
+ readonly why: string | null;
38
+ readonly changeId: string | null; // the change record, fetched by id
39
+ readonly refId: string | null; // a decision on the way with no change record of its own
40
+ /** Who decided it, in the three layers of §1.9: `decided by the owner`, `decided by <role>, within its remit`,
41
+ * `decided by <role> without the owner …`; a report or an unnamed author is said as such. */
42
+ readonly decidedBy: string;
43
+ readonly withoutOwner: boolean;
44
+ readonly sourceIds: readonly string[];
45
+ /** The objects it changed (a change item's subjects, or the decision itself). */
46
+ readonly subjects: readonly string[];
47
+ }
48
+
49
+ export interface OwnerWordsTrace {
50
+ readonly words: readonly TracedWords[];
51
+ /** The product descriptions and decisions between the work and the owner's words (never the product item). */
52
+ readonly path: readonly ReferenceItem[];
53
+ readonly adjustments: readonly Adjustment[];
54
+ /** `Layer drift` where what the work rests on departs from the owner's words (§1.3). */
55
+ readonly drift: readonly EntryMark[];
56
+ /** Owner's words the owner later replaced, on the way up: `Do not revive` material, never the direction. */
57
+ readonly replacedWords: readonly ReferenceItem[];
58
+ /** Every asset on the way, for the entries a work pack carries because they bear on this work (§7.4 item 11). */
59
+ readonly related: ReadonlySet<string>;
60
+ }
61
+
62
+ export interface TraceOptions {
63
+ /** Names and numbers of the work and its areas: product-wide words that name one of them come along. */
64
+ readonly scopeWords: readonly string[];
65
+ /** Whether a mark can be written into a pack at all (it says what differs and names what it was checked against). */
66
+ readonly checked: (m: EntryMark) => boolean;
67
+ }
68
+
69
+ const text = (s: string | null | undefined) => (s ?? '').trim();
70
+ const GONE = new Set(['Removed', 'Replaced', 'Abandoned']);
71
+ const DESCRIPTIONS = new Set(['Goal', 'Area', 'Requirement', 'Design']);
72
+
73
+ /** The day a time falls on, the same for every channel that delivers the pack: a date stays a date (§1.8). */
74
+ export const dayOf = (at: string | null | undefined): string | null => (text(at) ? materialDate(text(at), 'UTC') : null);
75
+
76
+ /** When the owner said what this item holds: the first session it cites that gives a time. */
77
+ function saidOn(store: ProjectStore, r: ReferenceItem): string | null {
78
+ for (const id of r.sourceIds) {
79
+ const a = store.sources.get(id)?.anchor;
80
+ if (a?.kind === 'session' && a.at) return dayOf(a.at);
81
+ }
82
+ return null;
83
+ }
84
+
85
+ const isOwners = (by: Attribution | undefined): boolean => by?.author?.kind === 'owner' || by?.identity === 'Decision';
86
+ const nameOf = (by: Attribution | undefined): string | null => by?.author?.name ?? by?.holder?.role ?? by?.author?.host ?? null;
87
+
88
+ /** Who decided, in the words the pack uses (§1.9's three layers). */
89
+ export function decidedBy(by: Attribution | undefined, withoutOwner: EntryMark | undefined): { phrase: string; without: boolean } {
90
+ if (isOwners(by)) return { phrase: 'decided by the owner', without: false };
91
+ const name = nameOf(by);
92
+ if (withoutOwner) return { phrase: `decided by ${withoutOwner.decidedBy?.who ?? name ?? 'a role'} without the owner (also under \`Pending owner decisions\`)`, without: true };
93
+ if (by?.identity === 'Artifact') return { phrase: `decided by ${name ?? 'a role'}, within its remit`, without: false };
94
+ if (by?.identity === 'Report') return { phrase: `as reported by ${name ?? 'a report'}`, without: false };
95
+ return { phrase: name ? `by ${name}` : 'by an author the records do not name', without: false };
96
+ }
97
+
98
+ export function traceOwnerWords(store: ProjectStore, thread: WorkThread, opts: TraceOptions): OwnerWordsTrace {
99
+ const openWithoutOwner = (id: string) => store.marks.find((m) => m.targetId === id && m.kind === 'Decided without owner' && !m.closed);
100
+
101
+ // ── the way up ──
102
+ const starts = [
103
+ ...thread.serves.map((s) => s.referenceId),
104
+ ...store.relations.filter((r) => r.from === thread.id && (r.type === 'serves' || r.type === 'refines' || r.type === 'carries out' || r.type === 'implements')).map((r) => r.to),
105
+ ...store.reference.filter((r) => r.carryOut?.workIds.includes(thread.id) === true).map((r) => r.id),
106
+ ];
107
+ const seen = new Set<string>();
108
+ const queue = [...starts];
109
+ const words = new Map<string, TracedWords>();
110
+ const replaced = new Map<string, ReferenceItem>();
111
+ const path: ReferenceItem[] = [];
112
+ const products: ReferenceItem[] = [];
113
+ const addWords = (first: ReferenceItem) => {
114
+ let r = first;
115
+ // Words the owner later replaced lead to the words that replaced them (§1.3: the later ones are current).
116
+ for (let i = 0; r.validity === 'Replaced' && r.replacedBy && i < 8; i++) {
117
+ replaced.set(r.id, r);
118
+ const next = store.reference.get(r.replacedBy);
119
+ if (!next) return;
120
+ r = next;
121
+ }
122
+ if (r.validity !== 'Current' || !text(r.quote) || words.has(r.id)) return;
123
+ words.set(r.id, { item: r, quote: text(r.quote), saidOn: saidOn(store, r) });
124
+ };
125
+ while (queue.length) {
126
+ const id = queue.shift()!;
127
+ if (seen.has(id)) continue;
128
+ seen.add(id);
129
+ const r = store.reference.get(id);
130
+ if (!r) continue;
131
+ if (r.category === OWNER_WORDS) { addWords(r); continue; }
132
+ if (r.validity === 'Replaced' && r.replacedBy) { queue.push(r.replacedBy); continue; }
133
+ if (GONE.has(r.validity)) continue;
134
+ if (r.category === 'Product') { products.push(r); continue; }
135
+ path.push(r);
136
+ if (text(r.quote) && r.validity === 'Current') words.set(r.id, { item: r, quote: text(r.quote), saidOn: saidOn(store, r) });
137
+ queue.push(...r.refines, ...store.relations.filter((x) => x.from === r.id && x.type === 'refines').map((x) => x.to));
138
+ }
139
+ // What the owner said about the whole product comes along only when it names this work (§7.4 item 1).
140
+ const names = opts.scopeWords.map((w) => w.toLowerCase()).filter((w) => w.length > 2);
141
+ for (const p of products) {
142
+ for (const up of p.refines) {
143
+ const r = store.reference.get(up);
144
+ if (!r || r.category !== OWNER_WORDS || words.has(r.id)) continue;
145
+ const said = `${r.name} ${r.text} ${r.quote ?? ''}`.toLowerCase();
146
+ if (names.some((w) => said.includes(w))) addWords(r);
147
+ }
148
+ }
149
+
150
+ // ── adjustments on the way ──
151
+ const onTheWay = new Set<string>([thread.id, ...path.map((r) => r.id), ...words.keys(), ...replaced.keys()]);
152
+ const superseded = new Set(store.propagation.filter((j) => onTheWay.has(j.nodeId)).flatMap((j) => j.closed.filter((c) => c.close === 'Superseded by a later change').map((c) => `${c.changeId}|${c.itemId}`)));
153
+ const validityOf = (id: string) => store.reference.get(id)?.validity ?? store.threads.get(id)?.validity ?? null;
154
+ const adjustments: Adjustment[] = [];
155
+ const covered = new Set<string>();
156
+ const consider = (c: ChangeRecord, it: ChangeItem) => {
157
+ if (!it.affects.some((id) => onTheWay.has(id))) return;
158
+ if (it.effect === 'Completed' || it.material === 'Code change' || it.material === 'Test result') return;
159
+ if (it.by?.identity === 'Proposal') return;
160
+ if (superseded.has(`${c.id}|${it.id}`)) return;
161
+ const states = it.affects.map(validityOf).filter((v): v is NonNullable<typeof v> => v !== null);
162
+ // Everything it set down has since been replaced, given up or removed: a later change took its place.
163
+ if (states.length && states.every((v) => GONE.has(v))) return;
164
+ const sources = it.sourceIds.map((id) => store.sources.get(id)).filter((s) => s !== undefined);
165
+ if (sources.length && sources.every((s) => s.usedAs === 'History only')) return;
166
+ const without = it.affects.map(openWithoutOwner).find((m) => m !== undefined);
167
+ const who = decidedBy(it.by, without);
168
+ for (const id of it.affects) covered.add(id);
169
+ adjustments.push({ on: dayOf(it.at), material: it.material, effect: it.effect, title: text(it.title), summary: text(it.summary), before: it.before, after: it.after, why: text(it.why) || null, changeId: c.id, refId: null, decidedBy: who.phrase, withoutOwner: who.without, sourceIds: it.sourceIds, subjects: it.affects });
170
+ };
171
+ for (const c of store.changes.all()) for (const it of itemsOf(c)) consider(c, it);
172
+ // A decision on the way that a role made is an adjustment even when no change record holds it (for example one
173
+ // that was already in place when the project was taken over: a first judgement is not a change, §2.1).
174
+ for (const r of path) {
175
+ if ((r.category !== 'Decision' && r.category !== 'Boundary') || r.validity !== 'Current' || isOwners(r.attribution) || text(r.quote) || covered.has(r.id)) continue;
176
+ const who = decidedBy(r.attribution, openWithoutOwner(r.id));
177
+ adjustments.push({ on: saidOn(store, r), material: r.category, effect: null, title: r.name, summary: text(r.text), before: null, after: null, why: null, changeId: null, refId: r.id, decidedBy: who.phrase, withoutOwner: who.without, sourceIds: r.sourceIds, subjects: [r.id] });
178
+ }
179
+ adjustments.sort((a, b) => (a.on === null ? 1 : 0) - (b.on === null ? 1 : 0) || (a.on ?? '').localeCompare(b.on ?? ''));
180
+
181
+ // ── drift from the owner's words ──
182
+ const wordSources = new Set([...words.values()].flatMap((w) => w.item.sourceIds));
183
+ const describes = new Set(path.filter((r) => DESCRIPTIONS.has(r.category)).map((r) => r.id));
184
+ const planOrWork = new Set([thread.id, ...path.filter((r) => r.category === 'Plan').map((r) => r.id)]);
185
+ const drift = store.marks.filter((m) => !m.closed && m.kind === 'Layer drift' && opts.checked(m)
186
+ && (describes.has(m.targetId) || (planOrWork.has(m.targetId) && m.clueSourceIds.some((s) => wordSources.has(s)))));
187
+
188
+ const ordered = [...words.values()].sort((a, b) => (a.saidOn === null ? 1 : 0) - (b.saidOn === null ? 1 : 0) || (a.saidOn ?? '').localeCompare(b.saidOn ?? ''));
189
+ const related = new Set<string>([...onTheWay, ...adjustments.flatMap((a) => a.subjects)]);
190
+ return { words: ordered, path, adjustments, drift, replacedWords: [...replaced.values()], related };
191
+ }
@@ -0,0 +1,342 @@
1
+ /**
2
+ * A made-up project for the context-pack tests of Spec §7.1–§7.4 and §7.10 (CKC-12 AC-3, AC-4, AC-18, AC-22,
3
+ * AC-35–AC-43; CKC-21 AC-9, AC-10), and for the sample work contexts handed to the Product architect.
4
+ *
5
+ * "Orchard Log" is a field notebook for small orchard growers: record tree inspections under the tree, send the co-op a
6
+ * season report. Nothing here comes from any real project. Its assets are written straight into a store, the way the
7
+ * organizing rounds will write them (fixture first: the owner's words and `refines` get real data from the organizing
8
+ * method later). It holds, on purpose:
9
+ * - the owner's words at three levels — product-wide, a goal's, an area's red line — one of them replaced by later words;
10
+ * - a requirement that drifted from the owner's words (`Layer drift`), a decision a role set without the owner, and
11
+ * decisions carried out, partly carried out, and recorded as carried out by work that has not started;
12
+ * - adjustments by the owner, by a role within its remit and by a role without the owner, one superseded by a later one;
13
+ * - claims from a receipt, one of them contradicted by the code, and a code observation;
14
+ * - code files, a config file, a QC report with its own code block, commits;
15
+ * - the project's rules in all three groups, one inferred, one replaced, one set without the owner, one obsolete;
16
+ * - history (CKC-12 AC-35): a removed requirement and work item that current work still rests on, the change that took
17
+ * them out, a history-only source and a change record built only from history — never current content, given as
18
+ * lineage where they bear on the work — and a mark on a removed object, which no pack writes;
19
+ * - two records that disagree: a plan recorded as done over unfinished work, and one task number on two work items.
20
+ */
21
+ import { mkdtempSync } from 'node:fs';
22
+ import { tmpdir } from 'node:os';
23
+ import { join } from 'node:path';
24
+ import { ProjectStore } from '../store/project-store.ts';
25
+ import type {
26
+ Attribution, ChangeItem, ChangeRecord, EntryMark, FactRecord, GraphRelation, JobInputs, Note, ObjectJudgement, Project,
27
+ ProjectRule, ReferenceItem, Source, Statement, WorkThread,
28
+ } from '../model/types.ts';
29
+ import type { ChangeEffect, ChangeMaterial, Identity, MaterialRule, Progress, ReferenceCategory, RuleGroup } from '../model/vocab.ts';
30
+
31
+ export const ORCHARD_ID = 'p_orchard';
32
+ export const ORCHARD_AT = '2026-09-12T08:00:00.000Z';
33
+ const READ_AT = '2026-09-11T20:00:00.000Z';
34
+ const HEAD = 'c0ffee5a1b2c3d4e5f60718293a4b5c6d7e8f901';
35
+ /** Where the invented project lies: a rooted path of the system the tests run on, so that what is under it is under it
36
+ * there too. `D:\orchard` is a path on Windows and one odd file name anywhere else. */
37
+ export const ORCHARD_ROOT = process.platform === 'win32' ? 'D:\\orchard' : '/orchard';
38
+ const ROOT = ORCHARD_ROOT;
39
+
40
+ /** What the owner said, word for word, as the sessions hold it. */
41
+ export const WORDS = {
42
+ underAMinute: 'An inspection has to be done in under a minute, standing under the tree, with one hand free.',
43
+ csv: 'The season report is a CSV the co-op can open in a spreadsheet.',
44
+ pdfLine1: 'The co-op stopped taking CSV. The season report is a PDF with every inspected tree on it, each with the date it was inspected.',
45
+ pdfLine2: 'And never round the counts: the co-op pays per tree.',
46
+ forWhom: 'Orchard Log is for growers with ten to two hundred trees, not for estates.',
47
+ redLine: 'Nothing that leaves the device may show where a grower’s trees stand unless the grower has said yes.',
48
+ noSignal: 'It has to save with no signal — the orchards have none.',
49
+ todayCount: 'I want to see how many trees I did today, on the first screen.',
50
+ } as const;
51
+
52
+ /** The code files' own lines. None of them may reach an execution agent through a pack, an original or an answer (D65). */
53
+ export const CODE_LINES = {
54
+ build: [
55
+ "import { renderPdf } from './pdf';",
56
+ 'export function buildSeasonReport(trees: InspectedTree[]): SeasonReport {',
57
+ ' const pages = chunk(trees, BATCH_SIZE);',
58
+ ' const counts = pages.map((p) => Math.round(p.length / 10) * 10);',
59
+ ' return { pages, counts, file: renderPdf(pages, counts) };',
60
+ '}',
61
+ ],
62
+ form: [
63
+ "export const FORM_FIELDS = ['tree', 'health', 'fruit', 'pests', 'note'] as const;",
64
+ 'export function saveInspection(entry: Inspection): void { outbox.push(entry); }',
65
+ ],
66
+ config: ['{', ' "batchSize": 500,', ' "roundCounts": true', '}'],
67
+ } as const;
68
+
69
+ /** A QC report's own code block: the report's text, given like any other original (Spec §7.1). */
70
+ export const QC_BLOCK_LINE = 'node scripts/sample-report.js --trees 40 --check-dates';
71
+
72
+ const inputs: JobInputs = { jobId: 'fixture', sourceIds: [], factRecordIds: [], threadIds: [], areaIds: [], referenceIds: [], noteIds: [], location: 'Orchard Log' };
73
+ const OWNER: Attribution = { author: { kind: 'owner', name: null, window: null, host: null, model: null }, holder: null, identity: 'Decision' };
74
+ const role = (name: string, identity: Identity = 'Artifact', holder: string | null = null): Attribution =>
75
+ ({ author: { kind: 'role', name, window: null, host: null, model: null }, holder: holder ? { role: holder, window: null } : null, identity });
76
+
77
+ export const orchardProject: Project = {
78
+ id: ORCHARD_ID, name: 'Orchard Log', locations: [ROOT],
79
+ scope: [{ id: 'scope_main', path: ROOT, category: 'Repository', relation: 'Main project', reason: 'the project the owner added', reasonSourceIds: [], sessionHost: null, readOnly: false, copyOf: null, worktreeOf: null, versionControl: 'git', missing: null, addedBy: 'owner' }],
80
+ scopeQuestions: [], keeperFiles: [], roles: ['Lead', 'Builder', 'Checker'], language: 'en', organizingPaused: false,
81
+ createdAt: '2026-09-01T00:00:00.000Z', lastOpenedAt: null, lastScopedAt: null,
82
+ };
83
+
84
+ function fileSource(id: string, rel: string, heading: readonly string[], lines: readonly [number, number], excerpt: string, over: Partial<Source> = {}): Source {
85
+ const path = join(ROOT, ...rel.split('/'));
86
+ return {
87
+ id, projectId: ORCHARD_ID, title: heading.length ? heading[heading.length - 1]! : rel.split('/').pop()!,
88
+ anchor: { kind: 'file', path, headingPath: heading, lineStart: lines[0], lineEnd: lines[1] }, ids: [],
89
+ version: { fingerprint: `sha256:${id}`, readAt: READ_AT, commit: HEAD }, excerpt, usedAs: null, usedAsBy: null,
90
+ availability: null, movedTo: null, scopeItemId: 'scope_main', hasCredential: false, bytes: excerpt.length, ...over,
91
+ };
92
+ }
93
+ function sessionSource(id: string, sessionId: string, at: string, excerpt: string): Source {
94
+ return {
95
+ id, projectId: ORCHARD_ID, title: `Owner session ${at.slice(0, 10)}`,
96
+ anchor: { kind: 'session', host: 'claude', sessionId, file: `C:\\sessions\\${sessionId}.jsonl`, cwd: ROOT, messageStart: 4, messageEnd: 9, at },
97
+ ids: [], version: { fingerprint: `sha256:${id}`, readAt: READ_AT, commit: null }, excerpt, usedAs: 'Session', usedAsBy: 'keeper',
98
+ availability: null, movedTo: null, scopeItemId: 'scope_main', hasCredential: false, bytes: excerpt.length,
99
+ };
100
+ }
101
+ function commitSource(id: string, hash: string, subject: string, author: string, at: string, files: readonly string[]): Source {
102
+ const excerpt = `${subject}\n\nauthor: ${author}\nat: ${at}\nfiles:\n${files.map((f) => ` ${f}`).join('\n')}`;
103
+ return {
104
+ id, projectId: ORCHARD_ID, title: `${hash.slice(0, 8)} ${subject}`, anchor: { kind: 'commit', repo: ROOT, commit: hash }, ids: [],
105
+ version: { fingerprint: `sha256:${id}`, readAt: READ_AT, commit: hash }, excerpt, usedAs: 'Code', usedAsBy: 'keeper',
106
+ availability: null, movedTo: null, scopeItemId: 'scope_main', hasCredential: false, bytes: excerpt.length,
107
+ };
108
+ }
109
+ function ref(id: string, category: ReferenceCategory, name: string, text: string, over: Partial<ReferenceItem> = {}): ReferenceItem {
110
+ return {
111
+ id, projectId: ORCHARD_ID, category, name, ids: [], text, quote: null, basis: 'Explicit', validity: 'Current', progress: null,
112
+ attribution: role('Lead'), sourceIds: [], refines: [], replacedBy: null, inputs: null, asOf: ORCHARD_AT, updatedAt: ORCHARD_AT, ...over,
113
+ };
114
+ }
115
+ function ownerWords(id: string, name: string, quote: string, sourceId: string, over: Partial<ReferenceItem> = {}): ReferenceItem {
116
+ return ref(id, "Owner's words", name, name, { quote, attribution: OWNER, sourceIds: [sourceId], ...over });
117
+ }
118
+ function work(id: string, title: string, progress: Progress, serves: readonly (string | readonly [string, string])[], over: Partial<WorkThread> = {}): WorkThread {
119
+ return {
120
+ id, projectId: ORCHARD_ID, title, ids: [title.split(' · ')[0]!], doing: '', changed: '', results: '', unresolved: '',
121
+ executionFacts: [], qcFacts: [], factRecordIds: [],
122
+ serves: serves.map((s) => (typeof s === 'string' ? { referenceId: s, claim: '', basis: 'Explicit' as const } : { referenceId: s[0], claim: s[1], basis: 'Explicit' as const })),
123
+ dependsOn: [], progress, validity: 'Current', replacedBy: null,
124
+ attribution: { author: { kind: 'unknown', name: null, window: null, host: null, model: null }, holder: null, identity: 'Artifact' },
125
+ inputs, asOf: ORCHARD_AT, updatedAt: ORCHARD_AT, pendingSourceIds: [], ...over,
126
+ };
127
+ }
128
+ function rule(id: string, group: RuleGroup, category: MaterialRule | null, summary: string, over: Partial<ProjectRule> = {}): ProjectRule {
129
+ return {
130
+ id, projectId: ORCHARD_ID, group, category, summary, excerpt: null, sourceIds: ['src_agents'], appliesTo: [], basis: 'Explicit',
131
+ validity: 'Current', replacedBy: null, ownerSystem: null, differsInPractice: [], ownerConfirmation: null, jobId: null, asOf: ORCHARD_AT, updatedAt: ORCHARD_AT, ...over,
132
+ };
133
+ }
134
+ function item(id: string, at: string, material: ChangeMaterial, effect: ChangeEffect, title: string, summary: string, by: Attribution, affects: readonly string[], sourceIds: readonly string[], why: string | null, before: string | null = null, after: string | null = null): ChangeItem {
135
+ return { id, at, atSource: 'material', material, effect, title, summary, before, after, sourceIds, by, why, affects };
136
+ }
137
+ function change(id: string, label: string, items: readonly ChangeItem[]): ChangeRecord {
138
+ const first = items[0]!;
139
+ return {
140
+ id, projectId: ORCHARD_ID, at: first.at, atSource: 'material', material: first.material, effect: first.effect, title: label,
141
+ summary: first.summary, before: null, after: null, sourceIds: [...new Set(items.flatMap((i) => i.sourceIds))], by: first.by,
142
+ affects: [...new Set(items.flatMap((i) => i.affects))], propagation: [], segment: null, createdInJobId: null, updatedAt: first.at,
143
+ work: { kind: 'Session', label, sessionId: null, startedAt: first.at, endedAt: first.at, openEnded: false }, items, notJudged: [],
144
+ };
145
+ }
146
+ function mark(id: string, kind: EntryMark['kind'], targetId: string, clue: string, clueSourceIds: readonly string[], over: Partial<EntryMark> = {}): EntryMark {
147
+ return { id, projectId: ORCHARD_ID, kind, targetId, clueSourceIds, clue, since: ORCHARD_AT, noteId: null, closed: null, ...over };
148
+ }
149
+ function relation(id: string, type: GraphRelation['type'], from: string, to: string, claim: string, sourceIds: readonly string[]): GraphRelation {
150
+ return { id, projectId: ORCHARD_ID, type, from, to, claim, basis: 'Explicit', evidence: { sourceIds, factRecordIds: [], factsSoFar: '' }, assessment: 'Not assessed', assessedAt: null, assessedInJobId: null, updatedAt: ORCHARD_AT };
151
+ }
152
+ const claimed = (id: string, text: string, who: string, at: string, sourceIds: readonly string[]): Statement => ({ id, type: 'Claimed', text, sourceIds, claimedBy: { who, at, untrustedRuleId: null } });
153
+ const observed = (id: string, text: string, sourceIds: readonly string[]): Statement => ({ id, type: 'Observed', text, sourceIds });
154
+
155
+ /** The section titles of a pack, in order. */
156
+ export function headings(md: string): string[] {
157
+ return md.split('\n').filter((l) => l.startsWith('## ')).map((l) => l.slice(3).trim());
158
+ }
159
+ /** One section's body (without its title), or '' when the pack has no such section. */
160
+ export function sectionOf(md: string, title: string): string {
161
+ const start = md.indexOf(`\n## ${title}\n`);
162
+ if (start < 0) return '';
163
+ const body = md.slice(start + title.length + 5);
164
+ const end = body.indexOf('\n## ');
165
+ return end < 0 ? body : body.slice(0, end);
166
+ }
167
+
168
+ /** The Orchard Log assets in a fresh store (a temporary home unless one is given). */
169
+ export function orchardStore(home: string = mkdtempSync(join(tmpdir(), 'pk-orchard-'))): ProjectStore {
170
+ const store = ProjectStore.open(ORCHARD_ID, home);
171
+
172
+ // ── sources ──
173
+ store.sources.putMany([
174
+ sessionSource('src_sess_0901', 'a1f0-0901', '2026-09-01T09:12:00.000Z', `Owner: ${WORDS.underAMinute}\nOwner: ${WORDS.forWhom}\nOwner: ${WORDS.noSignal}\nOwner: ${WORDS.todayCount}`),
175
+ sessionSource('src_sess_0903', 'b2e1-0903', '2026-09-03T14:05:00.000Z', `Owner: ${WORDS.csv}\nOwner: ${WORDS.redLine}`),
176
+ sessionSource('src_sess_0906', 'c3d2-0906', '2026-09-06T10:30:00.000Z', `Owner: ${WORDS.pdfLine1}\n${WORDS.pdfLine2}\nOwner: and the form has to work for left-handed growers too.`),
177
+ fileSource('src_prd_intro', 'docs/PRD.md', ['1 · Product'], [1, 12], 'Orchard Log is a field notebook for small orchard growers.'),
178
+ fileSource('src_prd_a1', 'docs/PRD.md', ['2 · Inspections'], [13, 39], 'Inspections: a short form under the tree.'),
179
+ fileSource('src_prd_req3', 'docs/PRD.md', ['3 · Season reports', 'REQ-3'], [40, 58], 'REQ-3: the PDF lists each inspected tree with its date; counts per page are rounded to the nearest ten.'),
180
+ fileSource('src_plan', 'docs/PLAN.md', ['Plan'], [1, 40], 'P1 · Field inspections — done. P2 · Season reports — in progress.'),
181
+ fileSource('src_tasks', 'docs/TASKS.md', ['Tasks'], [1, 30], '| R-7 | Build the season report | In progress |\n| R-8 | Send the report to the co-op server | Planned |'),
182
+ fileSource('src_decisions', 'docs/DECISIONS.md', ['DEC-4'], [30, 41], 'DEC-4 (Lead, 2026-09-08): reports are built in batches of 500 trees.'),
183
+ fileSource('src_receipt', 'runs/R-7-receipt.md', ['Receipt', 'Counts'], [1, 8], 'The counts are exact; nothing is rounded. — Builder, 2026-09-10'),
184
+ fileSource('src_receipt_trees', 'runs/R-7-receipt.md', ['Receipt', 'Trees'], [9, 16], 'The PDF lists every inspected tree with its date. — Builder, 2026-09-10'),
185
+ fileSource('src_qc', 'runs/R-7-qc.md', ['Findings'], [1, 14], `Sample run, 40 trees:\n\n\`\`\`\n${QC_BLOCK_LINE}\n\`\`\`\n\nAll 40 trees listed with their dates.`, { usedAs: 'QC', usedAsBy: 'keeper' }),
186
+ fileSource('src_code_build', 'app/src/report/build.ts', [], [1, 6], CODE_LINES.build.join('\n'), { usedAs: 'Code', usedAsBy: 'keeper' }),
187
+ fileSource('src_code_form', 'app/src/inspect/form.ts', [], [1, 2], CODE_LINES.form.join('\n')),
188
+ fileSource('src_config', 'app/report.config.json', [], [1, 4], CODE_LINES.config.join('\n')),
189
+ commitSource('src_commit_r7', 'a1b2c3d4e5f60718293a4b5c6d7e8f9012345678', 'R-7: build the PDF season report', 'Builder', '2026-09-09T16:20:00+00:00', ['app/src/report/build.ts', 'app/src/report/pdf.ts']),
190
+ commitSource('src_commit_i2', 'b2c3d4e5f60718293a4b5c6d7e8f901234567890', 'I-2: cut the inspection form to five fields', 'Builder', '2026-09-05T11:00:00+00:00', ['app/src/inspect/form.ts']),
191
+ commitSource('src_commit_other', 'd4e5f60718293a4b5c6d7e8f9012345678901234', 'T-1: move the build to the new toolchain', 'Builder', '2026-09-11T09:00:00+00:00', ['tools/build.sh']),
192
+ fileSource('src_crew', 'CREW.md', ['Crew'], [1, 25], 'One Lead plans; Builders build; the Checker checks every item once, on a fresh clone, before the owner sees it.'),
193
+ fileSource('src_agents', 'AGENTS.md', ['Working here'], [1, 40], 'New tasks take the next free number in TASKS.md. Do not push to main; the Lead merges. Put what you built in dist/, named after the task.'),
194
+ fileSource('src_handover', 'HANDOVER.md', ['Next'], [1, 10], 'R-5 went through review twice; the second round was the Lead’s.'),
195
+ fileSource('src_vendor', 'docs/vendor-pdf/api.md', ['Pages'], [1, 30], 'renderPdf(pages) renders one page per item.', { usedAs: 'Reference only', usedAsBy: 'keeper', usedAsByRuleId: 'rule_vendor' }),
196
+ fileSource('src_old_export', 'docs/old/export-v1.md', ['Layout'], [1, 20], 'HISTORY-ONLY-TEXT: the v1 export put every tree on one CSV line.', { usedAs: 'History only', usedAsBy: 'keeper', usedAsByRuleId: 'rule_recovery' }),
197
+ fileSource('src_sync_gone', 'docs/SYNC.md', ['Sync'], [1, 12], 'REMOVED-TEXT: two-way sync with the co-op server.', { availability: 'No longer available' }),
198
+ fileSource('src_build_doc', 'docs/BUILD.md', ['Pipeline'], [1, 18], 'The build runs through the new toolchain script.'),
199
+ fileSource('src_layout_v1', 'docs/LAYOUT-v1.md', ['Layout'], [1, 16], 'The first report layout: one table, no dates.'),
200
+ ]);
201
+
202
+ // ── the owner's words and the product reference hanging from them ──
203
+ store.reference.putMany([
204
+ ownerWords('ref_ow_minute', 'Inspections take under a minute', WORDS.underAMinute, 'src_sess_0901'),
205
+ ownerWords('ref_ow_csv', 'The season report is a CSV', WORDS.csv, 'src_sess_0903', { validity: 'Replaced', replacedBy: 'ref_ow_pdf' }),
206
+ ownerWords('ref_ow_pdf', 'The season report is a PDF with exact counts', `${WORDS.pdfLine1}\n${WORDS.pdfLine2}`, 'src_sess_0906'),
207
+ ownerWords('ref_ow_whom', 'For small growers, not estates', WORDS.forWhom, 'src_sess_0901'),
208
+ ownerWords('ref_ow_redline', 'Tree locations leave the device only with a yes', WORDS.redLine, 'src_sess_0903'),
209
+ ref('ref_product', 'Product', 'Orchard Log', 'A field notebook for small orchard growers: record tree inspections in the field and send the co-op a season report it accepts.', { attribution: role('Lead'), sourceIds: ['src_prd_intro'], refines: ['ref_ow_whom'] }),
210
+ ref('ref_g1', 'Goal', 'G1 · Record an inspection in under a minute', 'A grower records a tree’s inspection before walking to the next tree.', { ids: ['G1'], sourceIds: ['src_prd_a1'], refines: ['ref_product', 'ref_ow_minute'] }),
211
+ ref('ref_g2', 'Goal', 'G2 · Send the co-op a season report it accepts', 'The co-op pays on the season report, so it has to accept it as sent.', { ids: ['G2'], sourceIds: ['src_prd_req3'], refines: ['ref_product'] }),
212
+ ref('ref_g3', 'Goal', 'G3 · Keep the app buildable by one person', 'One person can build and ship the app.', { ids: ['G3'], sourceIds: ['src_build_doc'], refines: ['ref_product'] }),
213
+ ref('ref_a1', 'Area', 'A1 · Inspections', 'Recording an inspection under the tree.', { ids: ['A1'], sourceIds: ['src_prd_a1'], refines: ['ref_g1'] }),
214
+ ref('ref_a2', 'Area', 'A2 · Season reports', 'Building the season report and getting it to the co-op.', { ids: ['A2'], sourceIds: ['src_prd_req3'], refines: ['ref_g2', 'ref_ow_redline'] }),
215
+ ref('ref_a3', 'Area', 'A3 · Tooling', 'The build and release tooling.', { ids: ['A3'], sourceIds: ['src_build_doc'], refines: ['ref_g3'] }),
216
+ ref('ref_req3', 'Requirement', 'REQ-3 · The season report lists every inspected tree', 'The PDF lists each inspected tree with its inspection date; tree counts per page are rounded to the nearest ten so the summary reads easily.', { ids: ['REQ-3'], sourceIds: ['src_prd_req3'], refines: ['ref_a2', 'ref_ow_pdf'] }),
217
+ ref('ref_des7', 'Design', 'DES-7 · Build pipeline', 'The app is built by one script on the new toolchain.', { ids: ['DES-7'], attribution: role('Builder'), sourceIds: ['src_build_doc'], refines: ['ref_a3'] }),
218
+ ref('ref_p1', 'Plan', 'P1 · Field inspections', 'The inspection form and saving without a network.', { ids: ['P1'], progress: 'Done', sourceIds: ['src_plan'], refines: ['ref_g1'] }),
219
+ ref('ref_p2', 'Plan', 'P2 · Season reports', 'The PDF season report, then sending it to the co-op.', { ids: ['P2'], progress: 'In progress', sourceIds: ['src_plan'], refines: ['ref_g2'] }),
220
+ ref('ref_dec2', 'Decision', 'DEC-2 · Inspections save without a network', 'An inspection is saved on the device first and sent when there is a network.', { ids: ['DEC-2'], quote: WORDS.noSignal, attribution: OWNER, sourceIds: ['src_sess_0901'], refines: ['ref_a1'], carryOut: { status: 'Carried out', remaining: null, workIds: ['thread_i1'], evidenceSourceIds: ['src_code_form'], at: ORCHARD_AT, jobId: null } }),
221
+ ref('ref_dec4', 'Decision', 'DEC-4 · Reports are built in batches of 500 trees', 'The report is built 500 trees at a time.', { ids: ['DEC-4'], attribution: role('Lead'), sourceIds: ['src_decisions'], refines: ['ref_a2'], carryOut: { status: 'Partly carried out', remaining: 'the summary page still counts all trees in one batch', workIds: ['thread_r7'], evidenceSourceIds: ['src_code_build'], at: ORCHARD_AT, jobId: null } }),
222
+ ref('ref_dec5', 'Decision', 'DEC-5 · Today’s count on the first screen', 'The first screen shows how many trees were inspected today.', { ids: ['DEC-5'], quote: WORDS.todayCount, attribution: OWNER, sourceIds: ['src_sess_0901'], refines: ['ref_a1'], carryOut: { status: 'Carried out', remaining: null, workIds: ['thread_i5'], evidenceSourceIds: [], at: ORCHARD_AT, jobId: null } }),
223
+ ref('ref_bound', 'Boundary', 'Not in v1: e-mailing the report', 'The report is not e-mailed from the app in v1.', { attribution: OWNER, sourceIds: ['src_sess_0903'], refines: ['ref_a2'] }),
224
+ ref('ref_layout1', 'Design', 'DES-2 · First report layout', 'One table, no dates.', { ids: ['DES-2'], validity: 'Replaced', replacedBy: 'ref_req3', validityByRuleId: 'rule_obsolete', sourceIds: ['src_layout_v1'], refines: ['ref_a2'] }),
225
+ ref('ref_photos', 'Requirement', 'REQ-11 · Photos in the season report', 'Each tree’s photo is embedded in the report.', { ids: ['REQ-11'], validity: 'Deferred', sourceIds: ['src_plan'], refines: ['ref_a2'] }),
226
+ ref('ref_sync', 'Requirement', 'REQ-9 · Two-way sync with the co-op server', 'REMOVED-TEXT: inspections sync both ways.', { ids: ['REQ-9'], validity: 'Removed', sourceIds: ['src_sync_gone'], refines: ['ref_a2'] }),
227
+ ]);
228
+
229
+ // ── work ──
230
+ store.facts.put({
231
+ id: 'fact_r7', projectId: ORCHARD_ID, title: 'R-7 receipt and code', aboutSourceIds: ['src_receipt', 'src_vendor', 'src_code_build'],
232
+ statements: [claimed('st_r7_exact', 'The counts are exact; nothing is rounded.', 'Builder, R-7 receipt', '2026-09-10', ['src_receipt'])],
233
+ decisions: [], changes: [], openQuestions: ['Does the co-op accept a report split into batches?'], executionFacts: [], language: 'en', inputs, asOf: ORCHARD_AT, updatedAt: ORCHARD_AT, pendingSourceIds: [],
234
+ } satisfies FactRecord);
235
+ store.facts.put({
236
+ id: 'fact_i2', projectId: ORCHARD_ID, title: 'I-2 form', aboutSourceIds: ['src_tasks', 'src_code_form'],
237
+ statements: [], decisions: [], changes: [], openQuestions: [], executionFacts: [], language: 'en', inputs, asOf: ORCHARD_AT, updatedAt: ORCHARD_AT, pendingSourceIds: [],
238
+ } satisfies FactRecord);
239
+ store.threads.putMany([
240
+ work('thread_i1', 'I-1 · Save inspections offline', 'Done', ['ref_a1', 'ref_p1']),
241
+ work('thread_i2', 'I-2 · One-hand inspection form', 'In progress', [['ref_a1', 'lets a grower finish an inspection with one thumb'], 'ref_p1'], {
242
+ attribution: role('Lead', 'Artifact', 'Builder'), progressByRuleId: 'rule_authority', factRecordIds: ['fact_i2'],
243
+ doneMeans: 'Five fields, one thumb, saved in under a minute on a mid-range handset.', results: 'The form has five fields; a trial inspection takes about fifty seconds.',
244
+ executionFacts: [observed('st_i2_fields', 'The form has five fields.', ['src_code_form']), claimed('st_i2_time', 'A trial inspection takes about fifty seconds.', 'Builder, I-2 note', '2026-09-05', ['src_tasks'])],
245
+ }),
246
+ work('thread_i4', 'I-4 · A photo per tree', 'Done', ['ref_a1']),
247
+ work('thread_i4b', 'I-4 · Photos (from the hand-over)', 'Planned', ['ref_a1'], { ids: ['I-4'] }),
248
+ work('thread_i5', 'I-5 · Today’s count on the first screen', 'Planned', ['ref_a1']),
249
+ work('thread_r7', 'R-7 · Build the season report', 'In progress', [['ref_req3', 'lists every inspected tree for the co-op'], 'ref_p2'], {
250
+ attribution: role('Lead', 'Artifact', 'Builder'), progressByRuleId: 'rule_authority', factRecordIds: ['fact_r7'],
251
+ doneMeans: 'The PDF lists every inspected tree with its date and exact counts, and the co-op’s sample file opens.',
252
+ results: 'The PDF builds for the 40-tree sample; the summary page is in place.', unresolved: 'Whether the counts are exact is in question.',
253
+ executionFacts: [
254
+ claimed('st_r7_exact', 'The counts are exact; nothing is rounded.', 'Builder, R-7 receipt', '2026-09-10', ['src_receipt']),
255
+ claimed('st_r7_every', 'The PDF lists every inspected tree with its date.', 'Builder, R-7 receipt', '2026-09-10', ['src_receipt_trees']),
256
+ observed('st_r7_batches', 'The report is built one batch of pages at a time.', ['src_code_build']),
257
+ ],
258
+ qcFacts: [observed('st_r7_qc', 'The Checker’s run of the 40-tree sample listed all 40 trees with their dates.', ['src_qc'])],
259
+ }),
260
+ work('thread_r8', 'R-8 · Send the report to the co-op server', 'Planned', ['ref_a2', 'ref_p2', 'ref_sync'], {
261
+ dependsOn: [{ threadId: 'thread_r7', claim: 'needs the finished report', basis: 'Explicit' }, { threadId: 'thread_s1', claim: 'needs the sync engine', basis: 'Explicit' }],
262
+ }),
263
+ work('thread_s1', 'S-1 · Sync engine', 'In progress', ['ref_sync'], { validity: 'Removed', doing: 'REMOVED-TEXT: the sync engine.' }),
264
+ work('thread_t1', 'T-1 · Move the build to the new toolchain', 'In progress', ['ref_des7']),
265
+ work('thread_r2', 'R-2 · CSV export', 'Done', ['ref_a2'], { validity: 'Replaced', replacedBy: 'thread_r7' }),
266
+ ]);
267
+
268
+ // ── the project's rules ──
269
+ store.rules.putMany([
270
+ rule('rule_crew', 'How work is organized', null, 'Multi-agent: a Lead plans, Builders implement, and a Checker reviews each item once before the owner sees it', {
271
+ ownerSystem: 'Crew handbook', sourceIds: ['src_crew'], excerpt: 'One Lead plans; Builders build; the Checker checks every item once, on a fresh clone, before the owner sees it.',
272
+ differsInPractice: [{ text: 'R-5 went through review twice, the second round by the Lead', sourceIds: ['src_handover'] }],
273
+ }),
274
+ rule('rule_qc', 'How work is organized', null, 'Independent QC: one round by the Checker, on a fresh clone of the repository', { ownerSystem: 'Crew handbook', sourceIds: ['src_crew'], excerpt: 'the Checker checks every item once, on a fresh clone', appliesTo: ['Review'] }),
275
+ rule('rule_who', 'How work is organized', null, 'The Lead sets the order of tasks; product scope and anything a grower sees is the owner’s call', { basis: 'Inferred', sourceIds: ['src_decisions', 'src_handover'] }),
276
+ rule('rule_numbers', 'Working rules', null, 'Task numbers come from docs/TASKS.md; the next free number is R-9', { appliesTo: ['docs/TASKS.md'], excerpt: 'New tasks take the next free number in TASKS.md.' }),
277
+ rule('rule_push', 'Working rules', null, 'Builders never push to main; the Lead merges', { appliesTo: ['main branch'], excerpt: 'Do not push to main; the Lead merges.' }),
278
+ rule('rule_push_old', 'Working rules', null, 'Builders push straight to main', { validity: 'Replaced', replacedBy: 'rule_push' }),
279
+ rule('rule_deliver', 'Working rules', null, 'A finished build goes into dist/, named after its task', { appliesTo: ['Builder'], excerpt: 'Put what you built in dist/, named after the task.' }),
280
+ rule('rule_findings', 'Working rules', null, 'The Checker files findings in runs/<task>-qc.md', { appliesTo: ['Checker'] }),
281
+ rule('rule_skip', 'Working rules', null, 'The Checker may skip the review of a one-line fix', { appliesTo: ['Checker'], sourceIds: ['src_handover'] }),
282
+ rule('rule_vendor', 'Material rules', 'Reference only', 'docs/vendor-pdf/ is the PDF library’s own documentation, for reference only', { appliesTo: ['docs/vendor-pdf/'] }),
283
+ rule('rule_recovery', 'Material rules', 'Recovery only', 'The archive branch and docs/old/ are kept for recovery only', { appliesTo: ['archive branch', 'docs/old/'] }),
284
+ rule('rule_authority', 'Material rules', 'Authoritative', 'The Status column of docs/TASKS.md decides a task’s progress', { appliesTo: ['docs/TASKS.md'], sourceIds: ['src_tasks'] }),
285
+ rule('rule_untrusted', 'Material rules', 'Untrusted', 'Receipts signed “auto” were written by a script and are unverified', { appliesTo: ['runs/'] }),
286
+ rule('rule_obsolete', 'Material rules', 'Obsolete', 'The first report layout (docs/LAYOUT-v1.md) is withdrawn; REQ-3 replaces it', { appliesTo: ['docs/LAYOUT-v1.md'] }),
287
+ rule('rule_open', 'Material rules', 'Declared open', 'Whether season reports go out weekly or once at the end of the season is still open', { appliesTo: ['season reports'] }),
288
+ ]);
289
+
290
+ // ── what changed on the way ──
291
+ const by = { lead: role('Lead'), builder: role('Builder'), owner: OWNER };
292
+ store.changes.putMany([
293
+ change('chg_round', 'Lead session, 5 Sep', [item('it_round', '2026-09-05', 'Decision', 'Added', 'Counts on the summary page are rounded to tens', 'The summary page rounds each count to the nearest ten.', by.lead, ['ref_req3'], ['src_prd_req3'], 'the summary read badly with odd counts')]),
294
+ change('chg_ow_pdf', 'Owner session, 6 Sep', [item('it_pdf', '2026-09-06', 'Owner statement', 'Replaced', 'The season report becomes a PDF; CSV is dropped', 'The owner said the co-op no longer takes CSV and that the counts must never be rounded.', by.owner, ['ref_ow_pdf', 'ref_ow_csv', 'ref_req3'], ['src_sess_0906'], 'the co-op stopped taking CSV and pays per tree', 'a CSV the co-op opens in a spreadsheet', 'a PDF listing every inspected tree with its date, counts exact')]),
295
+ change('chg_photos', 'Lead session, 7 Sep', [item('it_photos', '2026-09-07', 'Plan update', 'Deferred', 'Photos leave the season report for P3', 'R-7 no longer embeds photos; REQ-11 moves to P3.', by.lead, ['thread_r7', 'ref_photos'], ['src_plan'], 'the PDF grew past 20 MB on the 40-tree sample', 'photos embedded in the PDF', 'no photos; REQ-11 deferred to P3')]),
296
+ change('chg_dec4', 'Lead session, 8 Sep', [item('it_dec4', '2026-09-08', 'Decision', 'Added', 'DEC-4: reports are built in batches of 500 trees', 'The report is built 500 trees at a time.', by.lead, ['ref_dec4', 'thread_r7'], ['src_decisions'], 'the PDF library runs out of memory past 600 pages')]),
297
+ change('chg_code_r7', 'Builder run, 9 Sep', [item('it_code_r7', '2026-09-09', 'Code change', 'Added', 'The PDF season report is built', 'report/build.ts and report/pdf.ts build the PDF.', by.builder, ['thread_r7'], ['src_commit_r7'], 'R-7 needs the PDF path')]),
298
+ change('chg_form', 'Lead session, 4 Sep', [item('it_form', '2026-09-04', 'Decision', 'Replaced', 'The inspection form goes from nine fields to five', 'Four fields leave the form.', by.lead, ['thread_i2'], ['src_plan'], 'the nine-field form took two minutes in the field trial', 'nine fields', 'five fields')]),
299
+ change('chg_mirror', 'Owner session, 6 Sep (form)', [item('it_mirror', '2026-09-06', 'Owner statement', 'Corrected', 'The form works for left-handed growers too', 'The owner asked for the form to work with either hand.', by.owner, ['ref_a1', 'thread_i2'], ['src_sess_0906'], 'left-handed growers could not reach the save button')]),
300
+ change('chg_code_i2', 'Builder run, 5 Sep', [item('it_code_i2', '2026-09-05', 'Code change', 'Added', 'The five-field form is built', 'inspect/form.ts has the five fields.', by.builder, ['thread_i2'], ['src_commit_i2'], 'the field trial')]),
301
+ change('chg_done_i1', 'Builder run, 2 Sep', [item('it_done_i1', '2026-09-02', 'Status report', 'Completed', 'Offline save is done', 'I-1 is finished.', by.builder, ['thread_i1'], ['src_tasks'], null)]),
302
+ change('chg_history', 'History, November', [item('it_history', '2025-11-03', 'Decision', 'Replaced', 'HISTORY-ONLY-CHANGE: the v1 export layout was dropped', 'HISTORY-ONLY-TEXT', by.lead, [], ['src_old_export'], null)]),
303
+ change('chg_sync_gone', 'Lead session, 4 Sep (sync)', [item('it_sync_gone', '2026-09-04', 'Plan update', 'Abandoned', 'Sync leaves the plan', 'Two-way sync and its engine leave the plan.', by.lead, ['ref_sync', 'thread_s1'], ['src_plan'], 'the co-op server takes no uploads')]),
304
+ ]);
305
+ // The round that judged R-7 closed the rounding item: the owner's later words superseded it (Spec §2.10).
306
+ store.rounds.put({ id: 'round_1', projectId: ORCHARD_ID, number: 1, startedAt: ORCHARD_AT, endedAt: ORCHARD_AT, mainJobId: null, result: null });
307
+ store.propagation.put({
308
+ id: 'round_1:thread_r7', projectId: ORCHARD_ID, nodeId: 'thread_r7', roundId: 'round_1', state: 'Updated', sourceOrReason: 'R-7 follows the owner’s PDF words',
309
+ covers: [{ changeId: 'chg_round', itemId: 'it_round' }], followed: [], lacks: [],
310
+ closed: [{ changeId: 'chg_round', itemId: 'it_round', close: 'Superseded by a later change', reason: 'the owner said never round the counts', supersededByChangeId: 'chg_ow_pdf' }],
311
+ objectUpdatedAt: ORCHARD_AT, jobId: null, at: ORCHARD_AT,
312
+ } satisfies ObjectJudgement);
313
+
314
+ // ── relations, marks, notes ──
315
+ store.relations.putMany([
316
+ relation('rel_impl_r7', 'implements', 'src_code_build', 'thread_r7', 'builds the season report PDF (buildSeasonReport)', ['src_code_build']),
317
+ relation('rel_config_r7', 'implements', 'src_config', 'thread_r7', 'sets the batch size and whether counts are rounded (batchSize, roundCounts)', ['src_config']),
318
+ relation('rel_qc_r7', 'verifies', 'src_qc', 'thread_r7', 'the Checker’s 40-tree sample run', ['src_qc']),
319
+ relation('rel_impl_i2', 'implements', 'src_code_form', 'thread_i2', 'the inspection form and its save (FORM_FIELDS, saveInspection)', ['src_code_form']),
320
+ relation('rel_contra_r7', 'contradicts', 'src_code_build', 'thread_r7', 'The code rounds each page’s tree count to the nearest ten before it prints it (buildSeasonReport, app/src/report/build.ts line 4).', ['src_code_build', 'src_receipt']),
321
+ ]);
322
+ store.marks.putMany([
323
+ mark('mark_drift_req3', 'Layer drift', 'ref_req3', 'REQ-3 has the counts rounded to the nearest ten; the owner said never round the counts', ['src_prd_req3', 'src_sess_0906']),
324
+ mark('mark_stale_r7', 'Suspected stale', 'thread_r7', 'R-7’s progress rests on the receipt’s word that the counts are exact; the code rounds them', ['src_code_build']),
325
+ mark('mark_dwo_dec4', 'Decided without owner', 'ref_dec4', 'The batch size decides what a grower with more than 500 trees gets in one report — something a grower sees, which is the owner’s call unless the crew handbook gives it to the Lead', ['src_decisions'], { decidedBy: { who: 'Lead', at: '2026-09-08' } }),
326
+ mark('mark_dwo_skip', 'Decided without owner', 'rule_skip', 'Skipping a review changes the QC arrangement the owner set in the crew handbook', ['src_handover'], { decidedBy: { who: 'Lead', at: '2026-09-12' } }),
327
+ mark('mark_hist_a2', 'Suspected stale', 'ref_a2', 'The area still speaks of a CSV layout the history shows was dropped', ['src_old_export']),
328
+ mark('mark_on_removed', 'Suspected stale', 'thread_s1', 'REMOVED-MARK: the sync engine is behind', ['src_sync_gone']),
329
+ ]);
330
+ const note = (id: string, title: string, preview: string, mountIds: readonly string[], ask: Note['versions'][number]['ask']): Note => ({
331
+ id, projectId: ORCHARD_ID, mount: { kind: mountIds.length ? 'node' : 'project', ids: mountIds }, status: 'Current', ownerResponse: null,
332
+ versions: [{ version: 1, at: ORCHARD_AT, title, preview, body: { currentView: null, whyItMatters: null, facts: [], otherExplanations: null, keepAdjust: null, whatWouldSettleIt: null }, ask, judgementRecordId: 'jdg_fixture', reason: 'first' }],
333
+ discussion: [], followUps: [], author: { agent: 'pi', model: null }, resolvedReason: null, withdrawnReason: null, delegatedTo: null, language: 'en', updatedAt: ORCHARD_AT,
334
+ });
335
+ store.notes.putMany([
336
+ note('note_round', 'Should the season report round counts?', 'REQ-3 rounds them; the owner said never round.', ['thread_r7'], 'For your decision'),
337
+ note('note_wide', 'The co-op’s own file format may change next season', 'Nothing to do yet.', [], 'For information'),
338
+ ]);
339
+
340
+ store.setCoverage({ ...store.coverage, scopes: [{ id: 'project', kind: 'project', label: 'Orchard Log', coverage: 'Up to date', asOf: ORCHARD_AT, commit: null, pending: [], organizing: [], failed: [], lastRelookAt: null }] });
341
+ return store;
342
+ }