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,103 @@
1
+ /**
2
+ * Where a project's takeover stands (Spec §3.7, §6.10; D105; CKC-13 AC-19, AC-20, AC-23).
3
+ *
4
+ * A project that was added does nothing until the owner chooses a depth on the `Takeover` page and presses `Start`.
5
+ * From then on the three depths cannot be pressed; when the takeover is done the page says which depth ran. That is read
6
+ * from the rounds — a deepening records the depth it started at — never from the last button pressed: on 2026-10-02 the
7
+ * owner pressed `Focused` after a `Full` deepening had finished, no round ran, and the page then said `Focused`.
8
+ * A depth deeper than the one that ran stays selectable and continues from what is done; the same and shallower ones
9
+ * need a `Clear` first.
10
+ */
11
+ import type { KeeperJob, Project } from '../../model/types.ts';
12
+ import type { ClerkRound } from '../../model/k-types.ts';
13
+ import type { TakeoverDepth } from '../../model/vocab.ts';
14
+ import { TAKEOVER_DEPTH } from '../../model/vocab.ts';
15
+ import type { ProjectStore } from '../../store/project-store.ts';
16
+
17
+ export const DEPTH_RANK: Readonly<Record<TakeoverDepth, number>> = { 'First picture only': 0, Focused: 1, Full: 2 };
18
+ const deeper = (a: TakeoverDepth, b: TakeoverDepth | null): boolean => b === null || DEPTH_RANK[a] > DEPTH_RANK[b];
19
+ const isDepth = (v: unknown): v is TakeoverDepth => (TAKEOVER_DEPTH as readonly unknown[]).includes(v);
20
+
21
+ export type TakeoverPhase = 'Not started' | 'Under way' | 'Done';
22
+
23
+ export interface TakeoverState {
24
+ readonly phase: TakeoverPhase;
25
+ /** The depth the takeover runs to: what the owner pressed `Start` with. Null before the start. */
26
+ readonly chosen: TakeoverDepth | null;
27
+ /** The deepest depth that actually finished, from the rounds. Null until the first usable round is done. */
28
+ readonly ran: TakeoverDepth | null;
29
+ readonly startedAt: string | null;
30
+ /** When the chosen depth was reached: the end of the round that reached it. */
31
+ readonly completedAt: string | null;
32
+ /** The takeover's rounds, oldest first. */
33
+ readonly rounds: readonly ClerkRound[];
34
+ /** Each depth: whether it can be chosen now, and when not, why — said on the page beside it. */
35
+ readonly options: readonly { readonly depth: TakeoverDepth; readonly selectable: boolean; readonly why: string | null; readonly ran: boolean; readonly chosen: boolean }[];
36
+ }
37
+
38
+ /**
39
+ * The depth a deepening round ran at: recorded on the round since D105; on a round from before, read from the label its
40
+ * own record was given when it started (`Takeover round 2: deepening (Full)`), which was written from the depth in force
41
+ * then and not touched since.
42
+ */
43
+ export function roundDepth(round: ClerkRound, rootJob: KeeperJob | undefined): TakeoverDepth | null {
44
+ if (round.kind !== 'Deepen') return null;
45
+ if (isDepth(round.depth)) return round.depth;
46
+ const m = /deepening \(([^)]+)\)/.exec(rootJob?.scope.label ?? '');
47
+ return m && isDepth(m[1]) ? m[1] : null;
48
+ }
49
+
50
+ type StateProject = Pick<Project, 'takeoverDepth' | 'takeoverStartedAt' | 'takeoverDepthChosenAt' | 'createdAt' | 'takenOverByTable'>;
51
+
52
+ export function takeoverState(store: ProjectStore, project: StateProject): TakeoverState {
53
+ const rounds = store.clerkRounds.filter((r) => r.kind === 'First usable' || r.kind === 'Deepen').sort((a, b) => a.startedAt.localeCompare(b.startedAt));
54
+ const first = rounds.filter((r) => r.kind === 'First usable').pop();
55
+ const firstDone = first?.status === 'Done';
56
+ // The deepest depth a finished round reached. A deepening from before D105 whose depth cannot be read was `Full` or
57
+ // `Focused`; it is taken as the shallower, so a deeper choice is never refused on a guess.
58
+ let ran: TakeoverDepth | null = firstDone ? 'First picture only' : null;
59
+ let reachedAt: string | null = firstDone ? first!.endedAt : null;
60
+ // D107: a table an agent filled and `pk import` took in is a whole takeover: the agent read as deep as it judged.
61
+ const table = project.takenOverByTable ?? null;
62
+ if (table && deeper('Full', ran)) { ran = 'Full'; reachedAt = table.at; }
63
+ if (firstDone) {
64
+ for (const r of rounds.filter((x) => x.kind === 'Deepen' && x.status === 'Done')) {
65
+ const d = roundDepth(r, store.jobs.get(r.rootJobId)) ?? 'Focused';
66
+ if (deeper(d, ran)) { ran = d; reachedAt = r.endedAt; }
67
+ }
68
+ }
69
+ const started = Boolean(project.takeoverStartedAt) || rounds.length > 0 || table !== null;
70
+ // What the takeover runs to. A home from before D105 whose first usable round ran with no depth chosen stood at the
71
+ // old depth question, which behaved as `First picture only`; one whose field was overwritten after a deeper round
72
+ // finished runs to what ran.
73
+ let chosen: TakeoverDepth | null = started ? (isDepth(project.takeoverDepth) ? project.takeoverDepth : 'First picture only') : null;
74
+ if (chosen && ran && deeper(ran, chosen)) chosen = ran;
75
+ const phase: TakeoverPhase = !started ? 'Not started' : ran !== null && chosen !== null && DEPTH_RANK[ran] >= DEPTH_RANK[chosen] ? 'Done' : 'Under way';
76
+ const options = TAKEOVER_DEPTH.map((depth) => {
77
+ const base = { depth, ran: ran === depth && phase === 'Done', chosen: chosen === depth };
78
+ if (phase === 'Not started') return { ...base, selectable: true, why: null };
79
+ if (phase === 'Under way') return { ...base, selectable: false, why: `The takeover is under way at ${chosen}. Stop the round to pause it, or Clear to choose again.` };
80
+ if (deeper(depth, ran)) return { ...base, selectable: true, why: null };
81
+ return { ...base, selectable: false, why: depth === ran ? `${depth} has run. To run it again from the beginning, Clear first.` : `${ran} has run, which goes deeper than ${depth}. To go shallower, Clear first.` };
82
+ });
83
+ return {
84
+ phase, chosen, ran, rounds, options,
85
+ startedAt: started ? project.takeoverStartedAt ?? rounds[0]?.startedAt ?? table?.at ?? null : null,
86
+ completedAt: phase === 'Done' ? reachedAt : null,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Whether `Start` may be pressed with this depth now, and when not, why (§3.7 规则; CKC-13 AC-19, AC-23). `keyUsable`:
92
+ * whether a model with a usable key is there; without one the takeover would start and do nothing.
93
+ */
94
+ export function startRefusal(state: TakeoverState, depth: unknown, keyUsable: boolean): string | null {
95
+ if (!isDepth(depth)) return `Choose a depth: ${TAKEOVER_DEPTH.join(', ')}`;
96
+ const option = state.options.find((o) => o.depth === depth)!;
97
+ if (!option.selectable) return option.why ?? `${depth} cannot be chosen now`;
98
+ if (!keyUsable) return 'No usable key: set a key and a model in Model provider first';
99
+ return null;
100
+ }
101
+
102
+ /** Whether the takeover is done, so the schedule and Follow up apply (§3.8; CKC-07 AC-17, AC-28). */
103
+ export const takeoverDone = (store: ProjectStore, project: StateProject): boolean => takeoverState(store, project).phase === 'Done';
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Takeover depth (Spec v2.1 §1.11, §3.7; CKC-13 AC-1, AC-2, AC-8, AC-10, AC-19~AC-24; D36, D37).
3
+ *
4
+ * The first usable picture is formed from a bounded slice of the material chosen by relevance to the current
5
+ * direction and the work in progress, so its time does not grow with the project's history (D37). Everything
6
+ * older than the takeover that the slice leaves out is history; how much of it gets organized is the owner's
7
+ * choice (`Full`, `Focused`, `First picture only`), made on the `Takeover` page before `Start` (D105; takeover-state.ts).
8
+ * The organizing level of each material is computed from the planner's own record of what it did.
9
+ */
10
+ import type { Project } from '../../model/types.ts';
11
+ import type { OrganizingLevel, TakeoverDepth } from '../../model/vocab.ts';
12
+ import type { ProjectStore } from '../../store/project-store.ts';
13
+ import { skippedSegment } from '../../scope/skip.ts';
14
+
15
+ /**
16
+ * Round 1 reads documents only (owner 2026-09-17): first the places the project writes its own rules down (D62), then
17
+ * the product overview, the plan, design, decision and contract documents, and the documents that report how the plan
18
+ * went. Counts, never minutes, bound it, so the first picture does not grow with history (D37). Code, sessions,
19
+ * commits and run records wait for the deepening rounds, however few documents a project has, and so do the documents
20
+ * the tests keep as fixtures, which are the test side's as code is: the owner's own words reach round 1 through
21
+ * pk_owner_utterances, never as whole sessions (Spec §3.7).
22
+ */
23
+ export const FIRST_USABLE_LIMITS = { rules: 30, overview: 6, intent: 80, status: 40 } as const;
24
+
25
+ /**
26
+ * A document's part in the frame: the product overview (stage 0), a plan/design/decision/contract (stage 1), a report
27
+ * on how the plan went (stage 2), or — for a note that plays none of those parts — a place the project writes its rules
28
+ * down (`rules`: what an archive, recovery or deletion place holds). A document of the first three kinds can also be a
29
+ * rules source; `isRulesSource` says so.
30
+ */
31
+ export type FrameRole = 'overview' | 'intent' | 'status' | 'rules';
32
+
33
+ export interface Candidate {
34
+ readonly key: string;
35
+ readonly kind: 'file' | 'session' | 'commits';
36
+ readonly ref: string;
37
+ /** Path inside the scope item: the rules below look at it, never at the directories above the project. */
38
+ readonly rel: string;
39
+ readonly group: string;
40
+ readonly recency: string;
41
+ readonly chars: number;
42
+ readonly intent: boolean;
43
+ /** In a worktree or copy of the main project: its documents duplicate the main project's and do not frame the project. */
44
+ readonly secondary?: boolean;
45
+ }
46
+
47
+ const CODE_RE = /\.(ts|tsx|js|jsx|mjs|cjs|py|rb|go|rs|java|kt|swift|c|h|cc|cpp|hpp|cs|php|scala|sh|ps1|psm1|bat|cmd|sql|css|scss|less|html|htm|vue|svelte|lock|json|jsonl|yaml|yml|toml|ini|cfg|xml|csv|tsv|map|svg)$/i;
48
+ /** Material whose name says it carries conclusions: handovers, summaries, reports, decisions, status, reviews, QC. */
49
+ const CONCLUSION_RE = /(^|[\\/_.\- ])(handoffs?|handover|takeover|summary|summaries|milestones?|reports?|decisions?|status|progress|changelog|retro|retrospective|reviews?|qc|conclusions?|results?|findings|postmortem|verdict|acceptance)([\\/_.\- ]|$)/i;
50
+ /** Execution records: prompts, runs, logs, traces, outputs. Their conclusions matter; their course does not (Spec §3.7 `Conclusions only`). */
51
+ const EXECUTION_RE = /(^|[\\/_.\- ])(runs?|logs?|prompts?|execution|executions|dispatch|queue|jobs?|traces?|outputs?|artifacts?|scratch|tmp|temp)([\\/_.\- ]|$)/i;
52
+ const DOC_RE = /\.(md|markdown|mdx|txt|rst|adoc|org)$/i;
53
+ const ARCHIVED_RE = /(^|\/)(archive|archived|old|legacy|superseded[^/]*|deprecated)(\/|$)/i;
54
+ /** A place for what is thrown away or scratch; build output, dependencies and vendored code are the skip list's (scope/skip.ts). */
55
+ const TRASH_RE = /(^|\/)(delete|deleted|trash|tmp|temp)(\/|$)/i;
56
+ const trashed = (p: string) => TRASH_RE.test(p) || skippedSegment(p) !== null;
57
+ /** A place a project keeps what it archived, keeps for recovery or deleted: the note that says what it holds is a rules source. */
58
+ const KEPT_PLACE_RE = /(^|\/)(archive|archived|old|legacy|superseded[^/]*|deprecated|delete|deleted|trash|recycle|attic|归档|删除|废弃|作废|回收)(\/|$)/i;
59
+ /** The instruction files a project writes for agents (AGENTS.md, CLAUDE.md and the like) and its contribution rules. */
60
+ const INSTRUCTION_RE = /^(agents|claude|gemini|copilot-instructions|conventions|contributing)(\.[a-z]+)?$/i;
61
+ const INDEX_RE = /(^|[-_. ])(index|catalog|catalogue)([-_. ]|$)|索引/i;
62
+ const HANDOVER_RE = /(^|[-_. /])(handoffs?|handover|takeover)([-_. /]|$)|交接/i;
63
+ /** The name of a note that says what a place holds: a README, an index, notes, an explanation. */
64
+ const NOTE_NAME_RE = /^(readme|index|notes?|about|说明|索引|介绍)([-_. ]|$)/i;
65
+ /** A document at the top whose name says it is about what was archived, withdrawn or deleted. */
66
+ const DISPOSAL_NAME_RE = /(archive|deprecat|obsolete|superseded|deleted?|withdrawn|归档|删除|废弃|作废)/i;
67
+
68
+ /**
69
+ * Whether a document is one of the places a project writes its own rules down (Spec §1.15, §3.7; CKC-13 AC-25): its
70
+ * instruction files for agents, its README (at the top or one level down), its indexes, its hand-over documents, and
71
+ * the notes that say what an archive, recovery or deletion place holds. The first round reads these before anything
72
+ * else, so every later material is judged by the rules they state. Judged on the path inside the project.
73
+ */
74
+ export function isRulesSource(rel: string): boolean {
75
+ const p = slash(rel).replace(/^\.\//, '');
76
+ if (!DOC_RE.test(p) || skippedSegment(p) !== null || RUN_DIR_RE.test(p) || TEST_FIXTURE_RE.test(p)) return false;
77
+ const parts = p.split('/');
78
+ const name = parts[parts.length - 1]!;
79
+ const depth = parts.length;
80
+ if (KEPT_PLACE_RE.test(parts.slice(0, -1).join('/'))) return NOTE_NAME_RE.test(name) && depth <= 4;
81
+ if (INSTRUCTION_RE.test(name)) return depth <= 3;
82
+ if (/^readme([-_. ]|$)/i.test(name)) return depth <= 2;
83
+ if (HANDOVER_RE.test(p) || INDEX_RE.test(name)) return depth <= 4;
84
+ return depth === 1 && DISPOSAL_NAME_RE.test(name);
85
+ }
86
+ const RUN_DIR_RE = /(^|\/)(runs?|logs?|outputs?|artifacts?|scratch|traces?)\//i;
87
+ /**
88
+ * Folders a project's tests keep their fixtures in, by the usual names. What is there — often a whole made-up project
89
+ * with its own plan, status and README — belongs to the tests, as the code beside it does: it is read, but it never
90
+ * frames the project, is never where the project writes its rules, and is organized as code is (Spec §3.7: code does
91
+ * not enter round 1). Unlike the skip list (scope/skip.ts), nothing here is left unread.
92
+ */
93
+ const TEST_FIXTURE_RE = /(^|\/)(fixtures?|__fixtures__|testdata|test[-_]data)\//i;
94
+ /** Whether a path inside the project lies in a folder the tests keep fixtures in. */
95
+ export const isTestFixturePath = (rel: string): boolean => TEST_FIXTURE_RE.test(slash(rel));
96
+ /** A document that reports how the plan went: status, summaries, handovers, execution plans, QC and review reports. */
97
+ const STATUS_RE = /(^|[\/_.\- ])(handoffs?|handover|summary|summaries|milestones?|reports?|status|progress|changelog|retro|retrospective|reviews?|qc|findings|postmortem|verdict|observations?|execution)([\/_.\- ]|$)/i;
98
+ /** A product overview's own name starts with the word: `product.md`, `4-PRODUCT-AND-MODULES.md`, `PRD.md`, `README.md`. */
99
+ const OVERVIEW_RE = /^(\d+[-_. ])?(product|prd|vision|overview|readme)([-_. ]|$)/i;
100
+ /** Task contracts, requirements and specs are intent whatever their file name says (a contract named "handover" is not a status report). */
101
+ const CONTRACT_DIR_RE = /(^|\/)(contracts?|requirements?|specs?|tasks?|adr|rfcs?)\//i;
102
+
103
+ export const isCodePath = (path: string): boolean => CODE_RE.test(path);
104
+ const slash = (p: string) => p.replace(/\\/g, '/');
105
+
106
+ /** The part a file plays in the frame, from its path inside the project; null when it is not a frame document. */
107
+ export function frameRole(rel: string, intent: boolean): FrameRole | null {
108
+ const p = slash(rel);
109
+ if (!DOC_RE.test(p) || trashed(p) || RUN_DIR_RE.test(p) || ARCHIVED_RE.test(p) || TEST_FIXTURE_RE.test(p)) return null;
110
+ const name = p.split('/').pop()!;
111
+ const contract = CONTRACT_DIR_RE.test(p);
112
+ const status = STATUS_RE.test(p) && !contract;
113
+ if (!intent && !status) return null;
114
+ if (!status && !contract && OVERVIEW_RE.test(name) && p.split('/').length <= 3) return 'overview';
115
+ return status ? 'status' : 'intent';
116
+ }
117
+ const overviewRank = (rel: string): number => { const n = slash(rel).split('/').pop()!.toLowerCase(); return (/product/.test(n) ? 0 : /prd/.test(n) ? 1 : /vision|overview/.test(n) ? 2 : 3) * 10 + slash(rel).split('/').length; };
118
+
119
+ /**
120
+ * The first usable slice: the frame documents, bounded per role and newest first, and the places the project writes
121
+ * its own rules down (`isRulesSource`), which the first round reads before anything else (D62). A note that plays no
122
+ * other part in the frame — what an archive or deletion place holds — joins as `rules`. Nothing else joins it, however
123
+ * few documents there are: a project with almost none is framed from those, its rules and the owner's words, and a
124
+ * fallback that once added its commit history and latest sessions went when the Spec kept them out of round 1 (§3.7).
125
+ */
126
+ export function selectFirstUsable(candidates: readonly Candidate[], now = Date.now()): Map<string, FrameRole | null> {
127
+ const chosen = selectFrameDocuments(candidates, now);
128
+ const rules = candidates.filter((c) => c.kind === 'file' && !c.secondary && !chosen.has(c.key) && isRulesSource(c.rel))
129
+ .sort((a, b) => slash(a.rel).split('/').length - slash(b.rel).split('/').length || b.recency.localeCompare(a.recency));
130
+ for (const c of rules.slice(0, FIRST_USABLE_LIMITS.rules)) chosen.set(c.key, 'rules');
131
+ return chosen;
132
+ }
133
+
134
+ function selectFrameDocuments(candidates: readonly Candidate[], _now: number): Map<string, FrameRole | null> {
135
+ const chosen = new Map<string, FrameRole | null>();
136
+ const byRecency = (a: Candidate, b: Candidate) => b.recency.localeCompare(a.recency);
137
+ const docs = candidates.filter((c) => c.kind === 'file' && !c.secondary).map((c) => ({ c, role: frameRole(c.rel, c.intent) })).filter((x) => x.role !== null);
138
+ const overview = docs.filter((x) => x.role === 'overview').sort((a, b) => overviewRank(a.c.rel) - overviewRank(b.c.rel) || byRecency(a.c, b.c));
139
+ for (const x of overview.slice(0, FIRST_USABLE_LIMITS.overview)) chosen.set(x.c.key, 'overview');
140
+ const intent = [...overview.slice(FIRST_USABLE_LIMITS.overview), ...docs.filter((x) => x.role === 'intent')].sort((a, b) => byRecency(a.c, b.c));
141
+ for (const x of intent.slice(0, FIRST_USABLE_LIMITS.intent)) chosen.set(x.c.key, 'intent');
142
+ for (const x of docs.filter((d) => d.role === 'status').sort((a, b) => byRecency(a.c, b.c)).slice(0, FIRST_USABLE_LIMITS.status)) chosen.set(x.c.key, 'status');
143
+ return chosen;
144
+ }
145
+
146
+ // ---- levels under `Focused` ------------------------------------------------------------
147
+
148
+ export interface SampledGroup { readonly rule: string; readonly samples: readonly string[]; readonly members: readonly string[] }
149
+
150
+ const patternOf = (path: string): string => path.replace(/\\/g, '/').split('/').pop()!.replace(/\d+/g, '#').toLowerCase();
151
+
152
+ /**
153
+ * Structurally similar history files (same directory, same name pattern) are read by sample: the newest and the
154
+ * oldest are organized, the rest are classified by the rule (Spec §3.7 `Sampled`). Files that duplicate the main
155
+ * project inside a worktree or copy are one such group.
156
+ */
157
+ export function sampledGroups(store: ProjectStore, project: Project, history: readonly { key: string; kind: string; ref: string; group: string; recency: string; sourceIds: readonly string[] }[]): SampledGroup[] {
158
+ const out: SampledGroup[] = [];
159
+ const groups = new Map<string, typeof history[number][]>();
160
+ for (const m of history.filter((x) => x.kind === 'file')) { const k = `${m.group}::${patternOf(m.ref)}`; const list = groups.get(k) ?? []; list.push(m); groups.set(k, list); }
161
+ for (const [k, members] of groups) {
162
+ if (members.length < 6) continue;
163
+ const sorted = [...members].sort((a, b) => b.recency.localeCompare(a.recency));
164
+ out.push({ rule: `${members.length} files like ${k.split('::')[1]} in ${members[0]!.group}: the newest and the oldest are read, the rest follow the rule`, samples: [sorted[0]!.key, sorted[sorted.length - 1]!.key], members: members.map((m) => m.key) });
165
+ }
166
+ // Worktree or copy files identical to the main project: the main project's copy is the sample.
167
+ const main = project.scope.find((i) => i.relation === 'Main project' && !i.missing);
168
+ const secondary = project.scope.filter((i) => (i.worktreeOf || i.copyOf) && i.relation !== 'Excluded');
169
+ if (main && secondary.length) {
170
+ const fingerprints = new Map<string, string>(); // relative path → joined fingerprints of the main project's file
171
+ for (const s of store.sources.all()) if (s.anchor.kind === 'file' && s.scopeItemId === main.id) fingerprints.set(rel(main.path, s.anchor.path), `${fingerprints.get(rel(main.path, s.anchor.path)) ?? ''}|${s.version.fingerprint}`);
172
+ const grouped = new Set(out.flatMap((g) => g.members));
173
+ for (const item of secondary) {
174
+ const dup: string[] = [];
175
+ for (const m of history) {
176
+ if (m.kind !== 'file' || grouped.has(m.key) || !m.ref.startsWith(item.path)) continue;
177
+ const own = m.sourceIds.map((id) => store.sources.get(id)?.version.fingerprint ?? '').reduce((acc, f) => `${acc}|${f}`, '');
178
+ if (fingerprints.get(rel(item.path, m.ref)) === own) dup.push(m.key);
179
+ }
180
+ if (dup.length) out.push({ rule: `${dup.length} files in ${item.path} identical to the main project: the main project's copies are the samples`, samples: [], members: dup });
181
+ }
182
+ }
183
+ return out;
184
+ }
185
+ const rel = (root: string, path: string): string => path.slice(root.length).replace(/^[\\/]+/, '').replace(/\\/g, '/');
186
+
187
+ /** The level `Focused` organizes a history material to (Spec §3.7 rules). */
188
+ export function focusedLevel(m: { kind: string; rel: string; key: string }, sampled: readonly SampledGroup[]): OrganizingLevel {
189
+ const g = sampled.find((x) => x.members.includes(m.key));
190
+ if (g) return g.samples.includes(m.key) ? 'Read in full' : 'Sampled';
191
+ if (m.kind === 'commits') return 'Read in full';
192
+ if (m.kind === 'session') return 'Conclusions only'; // the owner's own words stay in full; the agent's course is reduced to conclusions
193
+ if (isCodePath(m.rel) || isTestFixturePath(m.rel)) return 'Indexed'; // a test fixture's documents go as the code beside them
194
+ if (CONCLUSION_RE.test(m.rel)) return 'Read in full';
195
+ if (EXECUTION_RE.test(m.rel)) return 'Conclusions only';
196
+ return 'Indexed';
197
+ }
198
+
199
+ /**
200
+ * Which level the chosen depth plans for a history material; `Indexed` means it is not organized under this depth.
201
+ * Material the project's rules settle (`settled`) is `Settled by rule` at every depth: it is not read closely (§3.7).
202
+ */
203
+ export function plannedLevel(depth: TakeoverDepth | null, m: { kind: string; rel: string; key: string; settled?: boolean }, sampled: readonly SampledGroup[]): OrganizingLevel {
204
+ if (m.settled) return 'Settled by rule';
205
+ if (depth === 'Full') return 'Read in full';
206
+ if (depth === 'Focused') return focusedLevel(m, sampled);
207
+ return 'Indexed';
208
+ }
209
+ export const READ_LEVELS: readonly OrganizingLevel[] = ['Read in full', 'Conclusions only'];
210
+
211
+ /**
212
+ * The depth question's note of a home from before D105 (§3.7, D36): the depth was asked once the first picture existed.
213
+ * Since D105 the depth is chosen on the `Takeover` page before `Start` and no note asks for it; one left in an existing
214
+ * home is withdrawn when the home is opened (App `migrateTakeover`).
215
+ */
216
+ export const DEPTH_NOTE_ID = 'note_takeover-depth';