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,243 @@
1
+ /**
2
+ * Materials: the units material organizing works on (Spec §3.3 "一份材料或一段会话"). A file is
3
+ * one material (all its sections), a session segment is one material, the commits of a repository
4
+ * are one material. Materials are tiered: intent material first (product reference), then the
5
+ * active slice the first usable picture is formed from (chosen by relevance, bounded, D37) plus
6
+ * whatever appeared after the takeover started, then history (Spec §3.7 stage 2 and 4, §3.8).
7
+ */
8
+ import { statSync } from 'node:fs';
9
+ import { sep } from 'node:path';
10
+ import type { OrganizingPlanContent, Project, ProjectRule, Source } from '../../model/types.ts';
11
+ // The material rules themselves reach the sources they cover in intake/material-rules.ts (a Recovery only place is
12
+ // History only, a Reference only one is Reference only); what is left here is how the planner follows them.
13
+ import type { ProjectStore } from '../../store/project-store.ts';
14
+ import { isRulesSource, isTestFixturePath, selectFirstUsable, type Candidate, type FrameRole } from './takeover.ts';
15
+ import { isWithin, samePath } from '../../util/paths.ts';
16
+ import { isDocumentPath, readingOf, skippedSegment } from '../../scope/skip.ts';
17
+ import { planEntryFor } from '../rules.ts';
18
+
19
+ export type Tier = 'intent' | 'active' | 'history';
20
+
21
+ export interface Material {
22
+ readonly key: string;
23
+ readonly kind: 'file' | 'session' | 'commits';
24
+ readonly label: string;
25
+ /** Coverage reference: path, native session file, or repository. */
26
+ readonly ref: string;
27
+ /** Batching group: directory, or host:session. */
28
+ readonly group: string;
29
+ readonly sourceIds: readonly string[];
30
+ readonly chars: number;
31
+ readonly recency: string;
32
+ readonly tier: Tier;
33
+ readonly intent: boolean;
34
+ readonly scopeItemId: string | null;
35
+ /** Path inside its scope item (relevance and level rules look at this, never at the directories above the project). */
36
+ readonly rel: string;
37
+ /** Its part in the round-1 frame (Spec §3.7 stage 2), or null when round 1 does not read it. */
38
+ readonly frame: FrameRole | null;
39
+ /** One of the places the project writes its rules down (Spec §1.15, §3.7): read before anything else in round 1. */
40
+ readonly rulesSource?: boolean;
41
+ }
42
+
43
+ const INTENT_RE = /(^|[\\/])(design|designs|product|docs?|spec|specs|prd|plan|plans|planning|decision|decisions|contracts?|requirements?|adr|rfc|roadmap|vision|readme|agents|claude)([\\/.]|$)|(prd|spec|plan|decision|readme|roadmap|requirement|module|product|design)[^\\/]*\.md$/i;
44
+
45
+ /** Intent is judged on the path inside the project (root: the scope item's path), never on the directories above it. */
46
+ export function isIntentPath(path: string, root: string | null = null): boolean {
47
+ const inside = root && path.toLowerCase().startsWith(root.toLowerCase()) ? path.slice(root.length) : path;
48
+ const lower = inside.replace(/\\/g, '/').toLowerCase();
49
+ // Build output, dependencies and vendored code are the skip list's (scope/skip.ts); archives and trash are this one's;
50
+ // what the tests keep as fixtures is the test side's (takeover.ts).
51
+ if (skippedSegment(lower) !== null || isTestFixturePath(lower) || /(^|\/)(archive|archived|old|legacy|trash|deleted?|superseded)(\/|$)/.test(lower)) return false;
52
+ return INTENT_RE.test(lower);
53
+ }
54
+
55
+ export function materialKeyOf(source: Source): string | null {
56
+ const a = source.anchor;
57
+ if (a.kind === 'file') return `file:${a.path}`;
58
+ if (a.kind === 'session') return `session:${source.id}`;
59
+ if (a.kind === 'commit') return `commits:${a.repo}`;
60
+ return null;
61
+ }
62
+
63
+ function mtimeOf(path: string, fallback: string): string {
64
+ try { return new Date(statSync(path).mtimeMs).toISOString(); } catch { return fallback; }
65
+ }
66
+
67
+ /**
68
+ * When a commit was made: the time intake records on its anchor (sources/gitobs.ts), never parsed out of the excerpt it
69
+ * writes for people to read. A commit source written before the anchor carried the time is not read again — intake
70
+ * reads only the commits since its last pass, and an unchanged source is kept as it is — so for such a record the
71
+ * "at:" line the intake of that time wrote into its excerpt stands in; that excerpt is never rewritten. With no time at
72
+ * all, null: when a commit was read says nothing about when it was made.
73
+ */
74
+ function commitTime(s: Source): string | null {
75
+ if (s.anchor.kind !== 'commit') return null;
76
+ const recorded = s.anchor.at !== undefined ? s.anchor.at : /^at: (.+)$/m.exec(s.excerpt)?.[1]?.trim();
77
+ const at = Date.parse(recorded ?? '');
78
+ return Number.isNaN(at) ? null : new Date(at).toISOString();
79
+ }
80
+
81
+ export function listMaterials(store: ProjectStore, project: Project, options: { readonly now?: number } = {}): Material[] {
82
+ const now = options.now ?? Date.now();
83
+ const byKey = new Map<string, { kind: Material['kind']; label: string; ref: string; group: string; ids: string[]; chars: number; recency: string; intent: boolean; scopeItemId: string | null; rel: string }>();
84
+ const roots = new Map(project.scope.map((i) => [i.id, i.path]));
85
+ // The project's own root comes first: a file inside the project keeps its place in it (design/archive/… stays
86
+ // archived) even when a directory below it is also a scope item of its own.
87
+ const under = (path: string, root: string) => root.length > 0 && path.toLowerCase().startsWith(root.toLowerCase());
88
+ const relOf = (path: string, scopeItemId: string): string => {
89
+ const own = project.locations.filter((l) => under(path, l)).sort((a, b) => a.length - b.length)[0];
90
+ const root = own ?? roots.get(scopeItemId);
91
+ return root && under(path, root) ? path.slice(root.length).replace(/^[\\/]+/, '') : fromTheDrive(path);
92
+ };
93
+ // A directory the owner excluded is out of the project even when its files were read before it was excluded
94
+ // (2026-09-18: copies of the Keeper's own context packs under subagent/runs were organized as project material).
95
+ const excluded = project.scope.filter((i) => i.relation === 'Excluded').map((i) => i.path);
96
+ for (const s of store.sources.all()) {
97
+ // What exists only in history (kept for recovery only, or read from version history) is never material to organize
98
+ // (§1.2, D61); nor is what is for reference only, which is looked up and cited when needed (§1.2, §1.15).
99
+ if (s.availability === 'No longer available' || s.usedAs === 'History only' || s.usedAs === 'Reference only') continue;
100
+ const filePath = s.anchor.kind === 'file' ? s.anchor.path : null;
101
+ if (filePath !== null && excluded.some((root) => isWithin(root, filePath))) continue;
102
+ // What the scope reads from where (Spec §1.1; scope/skip.ts): nothing from generated output or a place left out,
103
+ // nothing current from a place kept for recovery only, and only the documents of third-party material.
104
+ if (filePath !== null && !readAsMaterial(project, filePath)) continue;
105
+ const key = materialKeyOf(s);
106
+ if (!key) continue;
107
+ const a = s.anchor;
108
+ let entry = byKey.get(key);
109
+ if (!entry) {
110
+ if (a.kind === 'file') {
111
+ const dir = a.path.replace(/[\\/][^\\/]*$/, '');
112
+ entry = { kind: 'file', label: a.path, ref: a.path, group: dir, ids: [], chars: 0, recency: mtimeOf(a.path, s.version.readAt), intent: isIntentPath(relOf(a.path, s.scopeItemId)), scopeItemId: s.scopeItemId, rel: relOf(a.path, s.scopeItemId) };
113
+ } else if (a.kind === 'session') {
114
+ // The owner's messages in the Keeper conversation are handled in the turn itself, not organized again.
115
+ if (a.host === 'pi') continue;
116
+ entry = { kind: 'session', label: s.title, ref: a.file, group: `${a.host}:${a.sessionId}`, ids: [], chars: 0, recency: a.at ?? s.version.readAt, intent: false, scopeItemId: s.scopeItemId, rel: s.title };
117
+ } else if (a.kind === 'commit') {
118
+ entry = { kind: 'commits', label: `Commits of ${a.repo}`, ref: a.repo, group: `commits:${a.repo}`, ids: [], chars: 0, recency: '', intent: false, scopeItemId: s.scopeItemId, rel: `commits of ${a.repo}` };
119
+ } else continue;
120
+ byKey.set(key, entry);
121
+ }
122
+ entry.ids.push(s.id);
123
+ entry.chars += s.excerpt.length;
124
+ // A repository's commits are as recent as its latest commit, not as the intake that read them: commits made before
125
+ // the takeover are history, and wait for the depth the owner chooses like any other (Spec §3.7).
126
+ if (a.kind === 'commit') { const at = commitTime(s); if (at !== null && at > entry.recency) entry.recency = at; }
127
+ }
128
+ // The first usable slice (Spec §3.7 stage 2, D37): chosen by relevance, bounded in count. Material that appeared after
129
+ // the takeover started is daily work, not history; older material outside the slice is history and waits for the depth.
130
+ // A worktree, copy or experiment beside the main project repeats the main project's documents, so they do not frame
131
+ // it. One of the project's own locations is the project, whatever its relation says: a project may itself be a copy of
132
+ // another repository (CKC-04 AC-2), and then its documents are the only ones there are (trial of 2026-09-21: none of
133
+ // them reached round 1).
134
+ const ownLocation = (path: string) => project.locations.some((l) => samePath(l, path));
135
+ const secondaryItems = new Set(project.scope.filter((i) => (i.relation === 'Worktree of main repo' || i.relation === 'Copy of another project' || i.relation === 'Experiment') && !ownLocation(i.path)).map((i) => i.id));
136
+ const candidates: Candidate[] = [...byKey].map(([key, e]) => ({ key, kind: e.kind, ref: e.ref, rel: e.rel, group: e.group, recency: e.recency, chars: e.chars, intent: e.intent, secondary: e.scopeItemId !== null && secondaryItems.has(e.scopeItemId) }));
137
+ const chosen = selectFirstUsable(candidates, now);
138
+ const takeoverStart = project.createdAt;
139
+ const out: Material[] = [];
140
+ for (const [key, e] of byKey) {
141
+ const tier: Tier = e.intent && chosen.has(key) ? 'intent' : chosen.has(key) || e.recency >= takeoverStart ? 'active' : 'history';
142
+ out.push({ key, kind: e.kind, label: e.label, ref: e.ref, group: e.group, sourceIds: e.ids, chars: e.chars, recency: e.recency, tier, intent: e.intent, scopeItemId: e.scopeItemId, rel: e.rel, frame: chosen.get(key) ?? null, rulesSource: e.kind === 'file' && isRulesSource(e.rel) });
143
+ }
144
+ return sortMaterials(out);
145
+ }
146
+
147
+ /** Whether a file is read as material, given what the scope reads from where (Spec §1.1; scope/skip.ts `treatmentOf`). */
148
+ function readAsMaterial(project: Project, path: string): boolean {
149
+ const { treatment } = readingOf(project.scope, path);
150
+ if (treatment === 'none' || treatment === 'history') return false;
151
+ return treatment !== 'documents' || isDocumentPath(path);
152
+ }
153
+
154
+ /** A file's path inside the project: from the project's own location that holds it, else from the drive. */
155
+ export function projectRelPath(project: Project, path: string): string {
156
+ const under = (root: string) => root.length > 0 && path.toLowerCase().startsWith(root.toLowerCase());
157
+ const own = project.locations.filter(under).sort((a, b) => a.length - b.length)[0];
158
+ return own ? path.slice(own.length).replace(/^[\\/]+/, '') : fromTheDrive(path);
159
+ }
160
+
161
+ /** A path of no project location, without what it starts from: the drive on Windows, the root where paths start at `/`. */
162
+ function fromTheDrive(path: string): string {
163
+ return sep === '/' ? path.replace(/^\/+/, '') : path.replace(/^[A-Za-z]:[\\/]+/, '');
164
+ }
165
+
166
+ // ───────────────────────── what the project's rules settle (Spec §1.15, §3.7; D62) ─────────────────────────
167
+
168
+ /** The `Used as` values a rule settles a material into without anyone reading it closely (§1.2, §1.15). */
169
+ const SETTLING_USED_AS: ReadonlySet<string> = new Set(['Reference only', 'History only']);
170
+
171
+ /** Whether a rule is in force: only such a rule settles anything. */
172
+ const inForce = (rule: ProjectRule | undefined): rule is ProjectRule => rule !== undefined && rule.validity === 'Current';
173
+
174
+ /** Sources that state one of the project's rules in force: a rule's own text is read again whenever it changes (§3.3). */
175
+ function ruleTextSources(store: ProjectStore): Set<string> {
176
+ return new Set(store.rules.filter((r) => r.validity === 'Current').flatMap((r) => r.sourceIds));
177
+ }
178
+
179
+ /**
180
+ * Whether the project's own rules settle this material directly, so no one reads it closely (Spec §3.7 "按规矩直接
181
+ * 判定"; CKC-13 AC-30): the organizing plan names its place under a rule in force (`plan`), or a rule in force set how
182
+ * every one of its sources is used — reference only, history only (`sources`). A material that states one of the rules
183
+ * is never settled away: when it changes, the rules are recognised again. Null when no rule settles it.
184
+ */
185
+ export function ruleSettlement(store: ProjectStore, plan: OrganizingPlanContent | undefined | null, material: { readonly kind: string; readonly rel: string; readonly sourceIds: readonly string[] }): { ruleId: string; via: 'plan' | 'sources' } | null {
186
+ if (material.sourceIds.length === 0) return null;
187
+ const ruleText = ruleTextSources(store);
188
+ if (material.sourceIds.some((id) => ruleText.has(id))) return null;
189
+ if (plan && material.kind === 'file') {
190
+ const entry = planEntryFor(plan, material.rel);
191
+ if (entry && inForce(store.rules.get(entry.ruleId))) return { ruleId: entry.ruleId, via: 'plan' };
192
+ }
193
+ const sources = material.sourceIds.map((id) => store.sources.get(id));
194
+ const ruleIds = [...new Set(sources.map((s) => (s && SETTLING_USED_AS.has(s.usedAs ?? '') ? s.usedAsByRuleId ?? '' : '')))];
195
+ if (ruleIds.length === 1 && ruleIds[0] && inForce(store.rules.get(ruleIds[0]))) return { ruleId: ruleIds[0], via: 'sources' };
196
+ return null;
197
+ }
198
+
199
+ /**
200
+ * Material the project's rules took out of what is organized — its place is kept for recovery only, or a rule set every
201
+ * one of its sources reference only or history only — as the coverage counts it (`Settled by rule`, §1.11): it is the
202
+ * project's material, judged by a rule, not waiting for anyone. Only file material; what `listMaterials` lists is not
203
+ * repeated here. It runs on every planning pass, so it reads no file: `recency` is when the material was last read.
204
+ */
205
+ export function settledAwayMaterials(store: ProjectStore, project: Project, listed: ReadonlySet<string>): { key: string; kind: 'file'; rel: string; chars: number; recency: string; ruleId: string }[] {
206
+ const byKey = new Map<string, { rel: string; chars: number; recency: string; ruleIds: Set<string>; unsettled: boolean }>();
207
+ const ruleText = ruleTextSources(store);
208
+ for (const s of store.sources.all()) {
209
+ if (s.anchor.kind !== 'file' || s.availability === 'No longer available') continue;
210
+ const key = `file:${s.anchor.path}`;
211
+ if (listed.has(key)) continue;
212
+ const { item, treatment } = readingOf(project.scope, s.anchor.path);
213
+ const recovery = treatment === 'history' ? (item?.coveredBy ?? []).find((c) => c.category === 'Recovery only' && inForce(store.rules.get(c.ruleId)))?.ruleId ?? null : null;
214
+ const byRule = SETTLING_USED_AS.has(s.usedAs ?? '') && s.usedAsByRuleId && inForce(store.rules.get(s.usedAsByRuleId)) ? s.usedAsByRuleId : null;
215
+ const ruleId = ruleText.has(s.id) ? null : recovery ?? byRule;
216
+ const e = byKey.get(key) ?? { rel: projectRelPath(project, s.anchor.path), chars: 0, recency: s.version.readAt, ruleIds: new Set<string>(), unsettled: false };
217
+ e.chars += s.excerpt.length;
218
+ if (s.version.readAt > e.recency) e.recency = s.version.readAt;
219
+ if (ruleId) e.ruleIds.add(ruleId); else e.unsettled = true;
220
+ byKey.set(key, e);
221
+ }
222
+ const out: { key: string; kind: 'file'; rel: string; chars: number; recency: string; ruleId: string }[] = [];
223
+ for (const [key, e] of byKey) if (!e.unsettled && e.ruleIds.size > 0) out.push({ key, kind: 'file', rel: e.rel, chars: e.chars, recency: e.recency, ruleId: [...e.ruleIds][0]! });
224
+ return out;
225
+ }
226
+
227
+ const TIER_ORDER: Record<Tier, number> = { intent: 0, active: 1, history: 2 };
228
+
229
+ export function sortMaterials(materials: readonly Material[]): Material[] {
230
+ return [...materials].sort((a, b) => TIER_ORDER[a.tier] - TIER_ORDER[b.tier] || a.group.localeCompare(b.group) || b.recency.localeCompare(a.recency) || a.key.localeCompare(b.key));
231
+ }
232
+
233
+ export type OrganizedState = 'none' | 'current' | 'pending';
234
+
235
+ /** Is this material covered by fact records that are still current? */
236
+ export function organizedState(store: ProjectStore, material: Material): OrganizedState {
237
+ const ids = new Set(material.sourceIds);
238
+ const facts = store.facts.filter((f) => f.aboutSourceIds.some((id) => ids.has(id)));
239
+ if (facts.length === 0) return 'none';
240
+ if (facts.some((f) => f.pendingSourceIds.length > 0)) return 'pending';
241
+ const covered = new Set(facts.flatMap((f) => f.aboutSourceIds));
242
+ return material.sourceIds.every((id) => covered.has(id)) ? 'current' : 'pending';
243
+ }