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,148 @@
1
+ /**
2
+ * A project's route (Spec §6.10 "每一步用哪个模型", "备用与并行", §3.10; D38, D105; CKC-03 AC-29, AC-35): the main model —
3
+ * which key, which model, how hard it thinks — that the owner's conversation and requests use and that every step of a
4
+ * round follows unless set apart; each step's own model and thinking, or "follows"; and the order of the keys that stand
5
+ * in when one runs out of quota or is rate-limited.
6
+ *
7
+ * Each project chooses its own (U98). A project that has not saved one runs on the machine's settings (`workspace.json`:
8
+ * `model`, `modelBackups`, `steps`), as every project did before D105, so an existing home goes on as it was.
9
+ *
10
+ * Which key runs a job: the keys of the route in order — the main key, then the backups — and after them any other key
11
+ * that carries a model a step is set to. A job that follows the main model goes to the keys that have that model, spread
12
+ * over all of them. A step set to a model on a key, or on a provider's keys (the page's "glm-5.3-flash · Z.AI", "· 2
13
+ * keys"), runs on those; when they are all out of quota or rate-limited it goes on to the other keys of the order that
14
+ * carry the same model (a switch, recorded). Only when no usable key carries the model does it go to the other keys of the
15
+ * order, on the model each has (the stand-in model of that backup). Pure functions: runtime.ts asks them, with what pi
16
+ * knows of the keys.
17
+ */
18
+ import type { RoundStepKind } from '../model/k-types.ts';
19
+ import type { ProjectRoute, RouteModel as ModelChoice, StepRoute } from '../model/types.ts';
20
+ import type { WorkspaceSettings } from '../store/workspace.ts';
21
+ import { STEP_SETTING_AS } from './clerk-steps.ts';
22
+
23
+ export type { ProjectRoute, StepRoute };
24
+
25
+ /** The route in force for a project: its own, or the machine's. */
26
+ export interface Route {
27
+ readonly main: ModelChoice | null;
28
+ readonly backups: readonly ModelChoice[];
29
+ readonly steps: Readonly<Partial<Record<string, StepRoute>>>;
30
+ /** Whether the project saved a route of its own. */
31
+ readonly own: boolean;
32
+ }
33
+
34
+ /** The steps of a round the owner sets apart (Spec §6.10): session drafts, the main agent, its lanes, the synthesis, the spot-check. */
35
+ export const ROUTE_STEPS = ['session-drafts', 'main', 'lane', 'synthesis', 'spot-check'] as const satisfies readonly RoundStepKind[];
36
+
37
+ export function routeOf(project: { readonly route?: ProjectRoute | null } | null | undefined, settings: Pick<WorkspaceSettings, 'model' | 'modelBackups' | 'steps'>, fallbackMain: ModelChoice | null = null): Route {
38
+ const own = project?.route ?? null;
39
+ if (own) return { main: own.model, backups: own.backups, steps: own.steps, own: true };
40
+ return { main: settings.model ?? fallbackMain, backups: settings.modelBackups ?? [], steps: settings.steps ?? {}, own: false };
41
+ }
42
+
43
+ /** What a job wants: a step's own model and thinking, or the main model's. `follows` says whose setting it is when not its own. */
44
+ export interface Want {
45
+ readonly provider: string | null;
46
+ readonly model: string | null;
47
+ readonly thinking: string | null;
48
+ /** `null` when the step is set itself; otherwise the step it follows (`main` for the main agent's), or `'main model'`. */
49
+ readonly follows: string | null;
50
+ /**
51
+ * Where a step set to a model of its own runs (or the step it follows, D103): `at` is a key id or a provider, whose keys
52
+ * then all count. A step set by model alone sits on the main key's provider (`loose`), when that provider carries the
53
+ * model. `null` for work that follows the main model: it goes to every key that has the model.
54
+ */
55
+ readonly pin: Pin | null;
56
+ }
57
+ export interface Pin { readonly at: string; readonly loose: boolean }
58
+
59
+ /**
60
+ * The model a step runs on (§6.10: a step not set apart says whom it follows; the main model changed, the steps that
61
+ * follow it change with it). Each part a step leaves unset comes from the step it defaults to (D103: the synthesis from
62
+ * the main agent), then from the main model. A job outside a round's steps (the owner's, an investigation) wants the main model.
63
+ */
64
+ export function wantOf(route: Route, step: string | null | undefined): Want {
65
+ const main = route.main;
66
+ const own = step ? route.steps[step] : undefined;
67
+ const asKind = step ? STEP_SETTING_AS[step as RoundStepKind] : undefined;
68
+ const as = asKind ? route.steps[asKind] : undefined;
69
+ const model = own?.model ?? as?.model ?? null;
70
+ const provider = own?.model ? own.provider : as?.model ? as.provider : undefined;
71
+ const set = own?.model || as?.model;
72
+ const pin: Pin | null = !set ? null : provider ? { at: provider, loose: false } : main?.provider ? { at: main.provider, loose: true } : null;
73
+ return {
74
+ provider: provider ?? main?.provider ?? null,
75
+ model: model ?? main?.id ?? null,
76
+ thinking: own?.thinking ?? as?.thinking ?? main?.thinking ?? null,
77
+ follows: own?.model ? null : asKind ?? 'main model',
78
+ pin,
79
+ };
80
+ }
81
+
82
+ /** Whether a key is one a pin names: the key itself, or a key of the provider it names (of the main key's provider, for a loose pin). */
83
+ export function onPin(pin: Pin, key: string, familyOf: (key: string) => string = (k) => k): boolean {
84
+ return pin.loose ? familyOf(key) === familyOf(pin.at) : key === pin.at || familyOf(key) === pin.at;
85
+ }
86
+
87
+ /** A key of the route, in order, with the model it runs when the wanted one is not among its models. */
88
+ export interface RouteKey { readonly provider: string; readonly id: string; readonly thinking: string | null }
89
+
90
+ /**
91
+ * The keys of a route in order: the main key, the backups, then any other key (from `others`, in the order given) that
92
+ * carries a model one of the steps is set to — a step set to a provider's model runs there even when that key is not in
93
+ * the backup order. One entry per key.
94
+ */
95
+ export function routeKeys(route: Route, others: readonly string[], hasModel: (key: string, model: string) => boolean): RouteKey[] {
96
+ const out: RouteKey[] = [];
97
+ const add = (c: { provider: string; id: string; thinking?: string | null }) => { if (!out.some((x) => x.provider === c.provider)) out.push({ provider: c.provider, id: c.id, thinking: c.thinking ?? route.main?.thinking ?? null }); };
98
+ if (route.main) add(route.main);
99
+ for (const b of route.backups) add(b);
100
+ const stepModels = [...new Set(Object.values(route.steps).map((s) => s?.model).filter((m): m is string => Boolean(m)))];
101
+ for (const key of others) for (const m of stepModels) if (hasModel(key, m)) { add({ provider: key, id: m, thinking: null }); break; }
102
+ return out;
103
+ }
104
+
105
+ export interface JobKeys {
106
+ readonly keys: readonly RouteKey[];
107
+ /** No usable key carries the wanted model: the keys run it on their own stand-in model. */
108
+ readonly standIn: boolean;
109
+ /** The keys the step is set to cannot be used now: it runs on other keys of the order that carry the same model. */
110
+ readonly switched: boolean;
111
+ }
112
+ export interface KeyLook {
113
+ /** The provider a key runs for (an own key's provider, an extra key's `like`); a provider id stays itself. */
114
+ readonly familyOf?: (key: string) => string;
115
+ /** Whether a usable key can take work now: not resting after a rate limit. */
116
+ readonly ready?: (key: string) => boolean;
117
+ /** Every key with credentials, usable or not: a loose pin holds only while its provider has a key that carries the model. */
118
+ readonly all?: readonly string[];
119
+ }
120
+
121
+ /**
122
+ * The keys a job may go to now, best first (§3.10 路由), from the usable keys (credentials, not out of quota) in the
123
+ * route's order. Work that follows the main model: the keys that have the model it wants. A step set to a model on a key
124
+ * or a provider's keys: those; when none of them can take work now (out of quota, rate-limited, removed), the other keys
125
+ * that carry the same model and can (`switched`); when every key with the model is resting, those, to wait for the first
126
+ * back. Only when no usable key carries the model: every usable key of the order, on its own stand-in model.
127
+ */
128
+ export function keysForJob(usable: readonly RouteKey[], want: Want, hasModel: (key: string, model: string) => boolean, look: KeyLook = {}): JobKeys {
129
+ const withModel = want.model ? usable.filter((k) => hasModel(k.provider, want.model!)) : [...usable];
130
+ if (!withModel.length) return { keys: usable, standIn: true, switched: false };
131
+ const familyOf = look.familyOf ?? ((k: string) => k);
132
+ const ready = look.ready ?? (() => true);
133
+ let pin = want.pin;
134
+ // A step set by model alone sits on the main key's provider only when that provider carries the model at all.
135
+ if (pin?.loose && want.model && !(look.all ?? usable.map((k) => k.provider)).some((k) => onPin(pin!, k, familyOf) && hasModel(k, want.model!))) pin = null;
136
+ if (!pin) return { keys: withModel, standIn: false, switched: false };
137
+ const pinned = withModel.filter((k) => onPin(pin!, k.provider, familyOf));
138
+ if (pinned.some((k) => ready(k.provider))) return { keys: pinned, standIn: false, switched: false };
139
+ const others = withModel.filter((k) => !onPin(pin!, k.provider, familyOf) && ready(k.provider));
140
+ if (others.length) return { keys: others, standIn: false, switched: true };
141
+ return { keys: pinned.length ? pinned : withModel, standIn: false, switched: false };
142
+ }
143
+
144
+ /** The model and thinking a job runs on, on the key it was given. */
145
+ export function modelOnKey(key: RouteKey, want: Want, hasModel: (key: string, model: string) => boolean): { readonly id: string; readonly thinking: string | null; readonly standIn: boolean } {
146
+ if (want.model && hasModel(key.provider, want.model)) return { id: want.model, thinking: want.thinking ?? key.thinking, standIn: false };
147
+ return { id: key.id, thinking: key.thinking, standIn: true };
148
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The project's own rules (Spec §1.15; D62, D64; CKC-21): the checks that keep an object judged "by a rule" honest,
3
+ * and small pure helpers for the views and the context to read the rules through. Deterministic; no model.
4
+ */
5
+ import type { OrganizingPlanContent, ProjectRule } from '../model/types.ts';
6
+ import { RULE_GROUP, type MaterialRule, type RuleGroup } from '../model/vocab.ts';
7
+ import type { ProjectStore } from '../store/project-store.ts';
8
+
9
+ /**
10
+ * What an object is judged to be by a rule, and which rule categories say so (§1.15, "怎样施行"). `Other` is "apply as
11
+ * the rule says", so it can stand behind any of them. Uses the Spec does not tie to a category accept any rule.
12
+ */
13
+ const RULE_USE = {
14
+ 'Reference only': { label: 'Reference only by a rule', needs: ['Reference only', 'Other'] },
15
+ 'History only': { label: 'History only by a rule', needs: ['Recovery only', 'Other'] },
16
+ 'Removed': { label: 'Removed by a rule', needs: ['Recovery only', 'Other'] },
17
+ 'Progress': { label: 'Progress by a rule', needs: ['Authoritative', 'Other'] },
18
+ 'Untrusted claim': { label: 'A claim from an untrusted source', needs: ['Untrusted'] },
19
+ 'Validity': { label: 'Validity by a rule', needs: null },
20
+ 'Used as': { label: 'Used as by a rule', needs: null },
21
+ } as const satisfies Record<string, { label: string; needs: readonly MaterialRule[] | null }>;
22
+ export type RuleUse = keyof typeof RULE_USE;
23
+
24
+ /** Why this rule cannot be the basis of that judgement, or null when it can. */
25
+ export function ruleUseRefusal(store: ProjectStore, ruleId: string, use: RuleUse): string | null {
26
+ const rule = store.rules.get(ruleId);
27
+ if (!rule) return `${ruleId} names no rule in the assets. Record the project’s rule first (pk_write_rule), then judge by it.`;
28
+ if (rule.validity !== 'Current') {
29
+ return `${ruleId} (“${rule.summary}”) is ${rule.validity}, not in force${rule.replacedBy ? `: ${rule.replacedBy} replaced it` : ''}. Judge by the rule in force (a changed rule is Replaced and points to the new one).`;
30
+ }
31
+ const { label, needs } = RULE_USE[use];
32
+ if (needs && !(rule.category && (needs as readonly string[]).includes(rule.category))) {
33
+ const wanted = needs[0]!;
34
+ return `${label} needs ${/^[AEIOU]/.test(wanted) ? 'an' : 'a'} ${wanted} rule${needs.length > 1 ? ` (or one of category ${needs.slice(1).join(', ')})` : ''}; ${ruleId} (“${rule.summary}”) is ${rule.category ? `a ${rule.category} rule` : `one of ${rule.group}`}.`;
35
+ }
36
+ return null;
37
+ }
38
+
39
+ /**
40
+ * Whether pointing `id` at `replacedBy` would close a circle: following `replacedBy` from the new successor leads back
41
+ * to `id`. A rule is never replaced by itself or, through others, by something it replaced.
42
+ */
43
+ export function replacementCircle(store: ProjectStore, id: string, replacedBy: string): boolean {
44
+ let current: string | null = replacedBy;
45
+ const seen = new Set<string>();
46
+ while (current) {
47
+ if (current === id) return true;
48
+ if (seen.has(current)) return false;
49
+ seen.add(current);
50
+ current = store.rules.get(current)?.replacedBy ?? null;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ export interface RuleGroupView {
56
+ readonly group: RuleGroup;
57
+ /** The rules shown: in force or proposed, the owner's own summarized way of working first. */
58
+ readonly rules: readonly ProjectRule[];
59
+ /** Replaced rules, struck through and pointing to their successors; shown collapsed. */
60
+ readonly replaced: readonly ProjectRule[];
61
+ }
62
+
63
+ /**
64
+ * The rules in the three fixed groups and their order (§6.7, §7.3; CKC-21 AC-9, AC-11). What the owner summarized
65
+ * (`ownerSystem`) comes first in its group; a group the project has no rule in does not appear. Rules given up
66
+ * otherwise (`Abandoned`, `Removed`) are not shown.
67
+ */
68
+ export function rulesByGroup(rules: readonly ProjectRule[]): RuleGroupView[] {
69
+ const shown = (r: ProjectRule) => r.validity !== 'Replaced' && r.validity !== 'Abandoned' && r.validity !== 'Removed';
70
+ return RULE_GROUP.map((group) => {
71
+ const mine = rules.filter((r) => r.group === group);
72
+ const inForce = mine.filter(shown);
73
+ return {
74
+ group,
75
+ rules: [...inForce.filter((r) => r.ownerSystem), ...inForce.filter((r) => !r.ownerSystem)],
76
+ replaced: mine.filter((r) => r.validity === 'Replaced'),
77
+ };
78
+ }).filter((g) => g.rules.length > 0 || g.replaced.length > 0);
79
+ }
80
+
81
+ const normalizePath = (p: string) => p.trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '').toLowerCase();
82
+
83
+ /**
84
+ * The plan entry that settles this material by rule (§3.7, D62), or null: a target covers its own path and everything
85
+ * under it when it is a directory. Paths are relative to the project, compared without regard to slash direction or
86
+ * case. A target that names a branch or anything else that is not a path covers no file.
87
+ */
88
+ export function planEntryFor(plan: OrganizingPlanContent, relPath: string): OrganizingPlanContent['byRule'][number] | null {
89
+ const path = normalizePath(relPath);
90
+ for (const entry of plan.byRule) {
91
+ for (const t of entry.targets) {
92
+ const target = normalizePath(t);
93
+ if (target && (path === target || path.startsWith(`${target}/`))) return entry;
94
+ }
95
+ }
96
+ return null;
97
+ }