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,185 @@
1
+ /**
2
+ * 发生的时间 (Spec §2.11; CKC-22 AC-10, AC-15): every ledger entry carries when the thing it records happened, with the
3
+ * basis the program read it from — never a model-written date. The shape is the workbench's own (`model/k-types.ts`
4
+ * `Occurred`), so an entry can be handed to the views and the evidence resolver as it is.
5
+ *
6
+ * Precision is kept as the source gives it: a date-only value stays a date (`2026-09-26`); a clock time is stored as its
7
+ * instant in UTC (model/time.ts). When the text writes its own date beside the commit time, both are kept (`other`), so
8
+ * a reader sees where they disagree. When nothing gives a time, the entry is `First observed` at the moment the program
9
+ * first saw it, marked `undated`.
10
+ *
11
+ * Dates written in a text (`Written in text`) are read by one rule, used everywhere a line needs one (`dateContext`):
12
+ * 1. a date written on the line itself — `2026-09-26`, `2026/9/26`, `2026年9月26日`; `09-26` and `9月26日` only when an
13
+ * earlier full date of the same text gives the year (and `09-26` only zero-padded, so `5-6` is never a date);
14
+ * 2. else the stated date of the entry the line belongs to: the first `日期:…` / `date: …` / `时间:…` line of the entry
15
+ * (an entry begins at a Markdown heading or at a line that opens in bold, e.g. `**D83 · …**`);
16
+ * 3. else a date written on the entry's own first line (`**D77 补(2026-09-26)**`, `### 2026-09-17 round two`);
17
+ * 4. else the document's stated date: a `date` / `created` / `日期` field in the front matter or in the first 20 lines
18
+ * (a "last updated" line is not the date of what the document says, so it never counts);
19
+ * 5. else none.
20
+ */
21
+ import type { Occurred, OccurredBasis } from '../model/k-types.ts';
22
+ import { normalizeMaterialTime } from '../model/time.ts';
23
+
24
+ export type { Occurred, OccurredBasis };
25
+
26
+ /** The time to store: a date stays a date, a clock time becomes its UTC instant; null when it is not a time. */
27
+ export function materialTime(raw: string | null | undefined): string | null {
28
+ if (!raw) return null;
29
+ return normalizeMaterialTime(raw);
30
+ }
31
+
32
+ /** Milliseconds for ordering and range filters; a date-only value orders at its UTC midnight (never displayed). */
33
+ export const msOf = (at: string | null | undefined): number | null => {
34
+ if (!at) return null;
35
+ const ms = Date.parse(/^\d{4}-\d{2}-\d{2}$/.test(at) ? `${at}T00:00:00Z` : at);
36
+ return Number.isNaN(ms) ? null : ms;
37
+ };
38
+
39
+ /** A range bound given by a caller: a date means the start of that day (since) or its end (until), in UTC. */
40
+ export function boundMs(value: string | null | undefined, end: boolean): number | null {
41
+ const v = (value ?? '').trim();
42
+ if (!v) return null;
43
+ if (/^\d{4}-\d{2}-\d{2}$/.test(v)) return Date.parse(`${v}T${end ? '23:59:59.999' : '00:00:00.000'}Z`);
44
+ const ms = Date.parse(v);
45
+ return Number.isNaN(ms) ? null : ms;
46
+ }
47
+
48
+ /** Date-only prefix, for grouping by day. */
49
+ export const dayOf = (at: string | null | undefined): string | null => (at ? at.slice(0, 10) : null);
50
+
51
+ /**
52
+ * A dated occurrence, with the other time the source gives kept beside it (§2.11: a date the text writes is recorded as
53
+ * well — "正文写明日期的,另记"). Only an other time that is the very same value is left out. An other time on the same
54
+ * day used to be dropped, which lost a text's own date whenever it named the commit's day, and compared a local date
55
+ * with the UTC day of an instant, so a commit made just after midnight east of UTC hid a real one-day disagreement
56
+ * (QC AY, CKC-22 AC-10). Whether the two disagree is for the reader, who sees both.
57
+ */
58
+ export function occurred(at: string, basis: OccurredBasis, anchor: string | null, other?: { at: string; basis: OccurredBasis; anchor: string | null } | null): Occurred {
59
+ const keepOther = other && other.at !== at ? other : null;
60
+ return { at, basis, anchor, ...(keepOther ? { other: keepOther } : {}) };
61
+ }
62
+
63
+ /** Nothing gives a time: `Undated · first seen <at>`. */
64
+ export function undated(firstSeen: string, anchor: string | null): Occurred {
65
+ return { at: firstSeen, basis: 'First observed', anchor, undated: true };
66
+ }
67
+
68
+ // ───────────────────────── dates written in a text ─────────────────────────
69
+
70
+ const pad = (n: number): string => String(n).padStart(2, '0');
71
+ function realDate(y: number, m: number, d: number): string | null {
72
+ if (y < 1990 || y > 2100 || m < 1 || m > 12 || d < 1 || d > 31) return null;
73
+ const check = new Date(Date.UTC(y, m - 1, d));
74
+ if (check.getUTCMonth() !== m - 1 || check.getUTCDate() !== d) return null;
75
+ return `${y}-${pad(m)}-${pad(d)}`;
76
+ }
77
+
78
+ const FULL = /(?<![\d.])((?:19|20)\d{2})[-/.](\d{1,2})[-/.](\d{1,2})(?![\d])/g;
79
+ const CN_FULL = /((?:19|20)\d{2})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*[日号]/g;
80
+ const SHORT = /(?<![\d:./-])(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])(?![\d:])/g;
81
+ const CN_SHORT = /(?<![\d年])(\d{1,2})\s*月\s*(\d{1,2})\s*[日号]/g;
82
+
83
+ /** The full dates written on one line, in order; with `year`, also the month-day forms. */
84
+ export function datesOnLine(line: string, year: number | null): { at: string; raw: string; index: number }[] {
85
+ const out: { at: string; raw: string; index: number }[] = [];
86
+ for (const m of line.matchAll(FULL)) { const at = realDate(Number(m[1]), Number(m[2]), Number(m[3])); if (at) out.push({ at, raw: m[0], index: m.index! }); }
87
+ for (const m of line.matchAll(CN_FULL)) { const at = realDate(Number(m[1]), Number(m[2]), Number(m[3])); if (at) out.push({ at, raw: m[0], index: m.index! }); }
88
+ if (year !== null) {
89
+ const taken = (i: number) => out.some((o) => i >= o.index && i < o.index + o.raw.length);
90
+ for (const m of line.matchAll(SHORT)) { if (taken(m.index!)) continue; const at = realDate(year, Number(m[1]), Number(m[2])); if (at) out.push({ at, raw: m[0], index: m.index! }); }
91
+ for (const m of line.matchAll(CN_SHORT)) { if (taken(m.index!)) continue; const at = realDate(year, Number(m[1]), Number(m[2])); if (at) out.push({ at, raw: m[0], index: m.index! }); }
92
+ }
93
+ return out.sort((a, b) => a.index - b.index);
94
+ }
95
+
96
+ /** Every full date written anywhere in a text (for the import-style root rule: does the content predate the commit?). */
97
+ export function fullDatesIn(text: string): string[] {
98
+ const out: string[] = [];
99
+ for (const m of text.matchAll(FULL)) { const at = realDate(Number(m[1]), Number(m[2]), Number(m[3])); if (at) out.push(at); }
100
+ for (const m of text.matchAll(CN_FULL)) { const at = realDate(Number(m[1]), Number(m[2]), Number(m[3])); if (at) out.push(at); }
101
+ return out;
102
+ }
103
+
104
+ /**
105
+ * A stated date: a `日期:` / `date:` field at the start of a line or after a separator (`编写:…。日期:2026-09-26。`).
106
+ * "最后更新:…" / "updated: …" never counts: the separator before the word keeps `更新日期` out.
107
+ */
108
+ const STATED = /(?:^|[\s。;;,,((|*>])(?:日期|发生时间|时间|date|Date|DATE)\**\s*[::]/;
109
+ const DOC_STATED = /(?:^|[\s。;;,,((|*>])(?:日期|创建于|创建|date|Date|DATE|created(?:_at)?|Created)\**\s*[::]/;
110
+ const ENTRY_START = (l: string): boolean => /^\s{0,3}#{1,6}\s/.test(l) || /^\s{0,3}\*\*\S/.test(l);
111
+
112
+ export interface WrittenAt {
113
+ readonly at: string;
114
+ /** 1-based line the date was read from. */
115
+ readonly line: number;
116
+ readonly how: 'line' | 'entry' | 'entry-head' | 'document';
117
+ }
118
+
119
+ export interface DateContext {
120
+ /** The written date governing a 1-based line, by the rule above, or null. */
121
+ at(line: number): WrittenAt | null;
122
+ /** The document's own stated date (rule 4), or null. */
123
+ readonly document: WrittenAt | null;
124
+ }
125
+
126
+ /** Read a text's written dates once; `at(line)` then answers in constant time. */
127
+ export function dateContext(text: string): DateContext {
128
+ const lines = text.split(/\r?\n/);
129
+ const n = lines.length;
130
+ const lineDate: (WrittenAt | null)[] = new Array(n).fill(null);
131
+ const stated: (string | null)[] = new Array(n).fill(null);
132
+ let year: number | null = null;
133
+ let inFence = false;
134
+ let frontMatter = n > 0 && /^---\s*$/.test(lines[0]!);
135
+ // The document's own date is stated in its front matter or its preamble (before the first entry below the title).
136
+ let preamble = true;
137
+ let document: WrittenAt | null = null;
138
+ for (let i = 0; i < n; i++) {
139
+ const l = lines[i]!;
140
+ if (frontMatter && i > 0 && /^---\s*$/.test(l)) frontMatter = false;
141
+ if (!frontMatter && ENTRY_START(l) && !/^\s{0,3}#\s/.test(l)) preamble = false;
142
+ if (/^\s*(```|~~~)/.test(l)) inFence = !inFence;
143
+ const found = datesOnLine(l, year);
144
+ if (found[0]) {
145
+ lineDate[i] = { at: found[0].at, line: i + 1, how: 'line' };
146
+ const full = found.find((f) => /^\d{4}/.test(f.raw));
147
+ if (full) year = Number(full.at.slice(0, 4));
148
+ const s = inFence ? null : STATED.exec(l);
149
+ if (s) stated[i] = (found.find((f) => f.index >= s.index) ?? found[0]).at;
150
+ if (!document && ((preamble && i < 20) || (frontMatter && i < 60)) && DOC_STATED.test(l)) {
151
+ const m = DOC_STATED.exec(l)!;
152
+ // The date the field names: the first one after the field word.
153
+ const after = found.find((f) => f.index >= m.index) ?? found[0];
154
+ document = { at: after.at, line: i + 1, how: 'document' };
155
+ }
156
+ }
157
+ }
158
+ // Entry blocks: the entry each line belongs to, its stated date and its head's date.
159
+ const entryOf: number[] = new Array(n).fill(-1);
160
+ const entryStated = new Map<number, WrittenAt>();
161
+ let current = -1;
162
+ inFence = false;
163
+ for (let i = 0; i < n; i++) {
164
+ const l = lines[i]!;
165
+ if (/^\s*(```|~~~)/.test(l)) inFence = !inFence;
166
+ if (!inFence && ENTRY_START(l)) current = i;
167
+ entryOf[i] = current;
168
+ if (current >= 0 && stated[i] && !entryStated.has(current)) entryStated.set(current, { at: stated[i]!, line: i + 1, how: 'entry' });
169
+ }
170
+ return {
171
+ document,
172
+ at(line: number): WrittenAt | null {
173
+ const i = Math.min(Math.max(0, Math.floor(line) - 1), n - 1);
174
+ if (i < 0) return document;
175
+ if (lineDate[i]) return lineDate[i];
176
+ const e = entryOf[i]!;
177
+ if (e >= 0) {
178
+ const s = entryStated.get(e);
179
+ if (s) return s;
180
+ if (lineDate[e]) return { ...lineDate[e]!, how: 'entry-head' };
181
+ }
182
+ return document;
183
+ },
184
+ };
185
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Symbol-level TypeScript queries, on demand (Spec §1.16 row 7, CKC-03 AC-27): who references a
3
+ * symbol, who implements it, and its call hierarchy, answered by a LanguageService over the current
4
+ * checkout. Nothing is precomputed into the database; the LanguageService is created lazily per
5
+ * repository and kept for the process. Every other language is answered by the ledger's code engine in the
6
+ * same shape (code-engine.ts `engineSymbol`; D98).
7
+ */
8
+ import { existsSync, readFileSync, statSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import * as ts from 'typescript';
11
+ import { normalizePath } from '../util/paths.ts';
12
+
13
+ export interface SymbolLocation {
14
+ readonly file: string; // repo-relative, forward slashes
15
+ readonly line: number;
16
+ readonly character: number;
17
+ readonly preview: string;
18
+ }
19
+
20
+ export interface SymbolResult {
21
+ readonly symbol: string;
22
+ readonly file: string;
23
+ readonly line: number;
24
+ readonly locations?: readonly SymbolLocation[];
25
+ readonly incoming?: readonly { readonly from: string; readonly file: string; readonly line: number }[];
26
+ readonly outgoing?: readonly { readonly to: string; readonly file: string; readonly line: number }[];
27
+ }
28
+
29
+ export type SymbolQueryKind = 'references' | 'implementations' | 'callers' | 'callees';
30
+
31
+ interface Host { readonly service: ts.LanguageService; readonly files: readonly string[]; readonly root: string; readonly signature: string }
32
+ const hosts = new Map<string, Host>();
33
+
34
+ /** One LanguageService per checkout, made again when the set of files changes; a file's version is its modification time. */
35
+ function serviceFor(root: string, relFiles: readonly string[]): Host {
36
+ const key = normalizePath(root).toLowerCase();
37
+ const signature = `${relFiles.length}:${relFiles.join('\n').length}`;
38
+ const existing = hosts.get(key);
39
+ if (existing && existing.signature === signature) return existing;
40
+ existing?.service.dispose();
41
+ const files = relFiles.map((f) => join(root, ...f.split('/')));
42
+ const host: ts.LanguageServiceHost = {
43
+ getCompilationSettings: () => ({
44
+ allowJs: true, checkJs: false, noEmit: true, module: ts.ModuleKind.NodeNext, moduleResolution: ts.ModuleResolutionKind.NodeNext,
45
+ target: ts.ScriptTarget.ES2024, strict: false, skipLibCheck: true, allowImportingTsExtensions: true,
46
+ }),
47
+ getScriptFileNames: () => [...files],
48
+ getScriptVersion: (f) => { try { return String(statSync(f).mtimeMs); } catch { return '0'; } },
49
+ getScriptSnapshot: (f) => {
50
+ try { return ts.ScriptSnapshot.fromString(readFileSync(f, 'utf8')); } catch { return undefined; }
51
+ },
52
+ getCurrentDirectory: () => root,
53
+ getDirectories: (d) => {
54
+ try { return ts.sys.getDirectories ? ts.sys.getDirectories(d) : []; } catch { return []; }
55
+ },
56
+ getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o),
57
+ fileExists: (f) => existsSync(f),
58
+ readFile: (f) => {
59
+ try { return readFileSync(f, 'utf8'); } catch { return undefined; }
60
+ },
61
+ readDirectory: (d, ext, excl, incl, depth) => ts.sys.readDirectory(d, ext, excl, incl, depth),
62
+ directoryExists: (d) => {
63
+ try { return ts.sys.directoryExists(d); } catch { return false; }
64
+ },
65
+ realpath: (p) => p,
66
+ useCaseSensitiveFileNames: () => false,
67
+ getNewLine: () => '\n',
68
+ };
69
+ const created: Host = { service: ts.createLanguageService(host), files, root, signature };
70
+ hosts.set(key, created);
71
+ return created;
72
+ }
73
+
74
+ const clip = (s: string, n: number): string => (s.length > n ? `${s.slice(0, n)}…` : s);
75
+
76
+ /** A file as a repository-relative path with forward slashes (TypeScript and Windows spell the separator differently). */
77
+ const relOf = (root: string, file: string): string => {
78
+ const f = normalizePath(file).split('\\').join('/');
79
+ const r = normalizePath(root).split('\\').join('/');
80
+ return f.toLowerCase().startsWith(`${r.toLowerCase()}/`) ? f.slice(r.length + 1) : f;
81
+ };
82
+
83
+ /** The program's source file for an absolute path (paths compare case-insensitively here). */
84
+ function sourceFileOf(service: ts.LanguageService, file: string): ts.SourceFile | undefined {
85
+ const program = service.getProgram();
86
+ const direct = program?.getSourceFile(file);
87
+ if (direct) return direct;
88
+ const wanted = normalizePath(file).toLowerCase();
89
+ return program?.getSourceFiles().find((f) => normalizePath(f.fileName).toLowerCase() === wanted);
90
+ }
91
+
92
+ /** First declaration of a name in a file, via the navigation tree. */
93
+ function findByName(service: ts.LanguageService, file: string, name: string): number | null {
94
+ const tree = service.getNavigationTree(file);
95
+ const needle = name.trim();
96
+ let best: number | null = null;
97
+ const visit = (node: ts.NavigationTree): void => {
98
+ if (best !== null) return;
99
+ if (node.text === needle) {
100
+ // The name itself, not the start of the declaration (its `export` keyword): callers and callees are asked at a
101
+ // position, and the declaration's start answered nothing (BM found 0 callers of rebuildLedgerInPlace against 12).
102
+ best = node.nameSpan?.start ?? node.spans[0]?.start ?? null;
103
+ return;
104
+ }
105
+ for (const child of node.childItems ?? []) visit(child);
106
+ };
107
+ if (tree) visit(tree);
108
+ return best;
109
+ }
110
+
111
+ /**
112
+ * Answer one symbol query. `file` is repo-relative; the symbol is located by 1-based `line` and
113
+ * `character`, or by `name` (first declaration in the file). Returns an error string on misuse.
114
+ */
115
+ export function symbolQuery(
116
+ root: string, relFiles: readonly string[], kind: SymbolQueryKind,
117
+ input: { file: string; line?: number; character?: number; name?: string },
118
+ ): SymbolResult | string {
119
+ const rel = input.file.split('\\').join('/').replace(/^\.?\//, '');
120
+ if (!rel || rel.split('/').some((s) => s === '..')) return `Give a file inside the repository (got “${input.file}”).`;
121
+ const abs = join(root, ...rel.split('/'));
122
+ if (!existsSync(abs)) return `${rel} is not in the current checkout of ${root}. Symbol queries read the working tree.`;
123
+ if (!/\.(tsx?|jsx?|mjs|cts|mts)$/i.test(rel)) return `${rel} is not a TypeScript/JavaScript file; symbol queries cover TS/JS (see ledger coverage for the level of each language).`;
124
+
125
+ const { service } = serviceFor(root, relFiles);
126
+ const sf = sourceFileOf(service, abs);
127
+ if (!sf) return `${rel} is not among the project's TypeScript files in the ledger.`;
128
+ let pos: number;
129
+ if (typeof input.line === 'number' && typeof input.character === 'number') {
130
+ pos = sf.getPositionOfLineAndCharacter(input.line - 1, input.character - 1);
131
+ } else if (input.name) {
132
+ const found = findByName(service, abs, input.name);
133
+ if (found === null) return `No declaration named “${input.name}” in ${rel}.`;
134
+ pos = found;
135
+ } else {
136
+ return 'Give either line + character (1-based) or a symbol name.';
137
+ }
138
+ const lc = ts.getLineAndCharacterOfPosition(sf, pos);
139
+ const symbolName = input.name ?? service.getQuickInfoAtPosition(abs, pos)?.displayParts?.map((p) => p.text).join('') ?? '';
140
+ const base: SymbolResult = { symbol: clip(symbolName.trim(), 120), file: rel, line: lc.line + 1 };
141
+
142
+ const locOf = (file: string, span: ts.TextSpan): SymbolLocation => {
143
+ const target = sourceFileOf(service, file);
144
+ const l = target ? ts.getLineAndCharacterOfPosition(target, span.start) : { line: 0, character: 0 };
145
+ const lineText = target ? (target.text.split(/\r?\n/)[l.line] ?? '') : '';
146
+ return { file: relOf(root, file), line: l.line + 1, character: l.character + 1, preview: clip(lineText.trim(), 200) };
147
+ };
148
+ const spanStartLine = (file: string, span: ts.TextSpan): number => {
149
+ const target = sourceFileOf(service, file);
150
+ return target ? ts.getLineAndCharacterOfPosition(target, span.start).line + 1 : 0;
151
+ };
152
+
153
+ if (kind === 'references') {
154
+ const refs = service.findReferences(abs, pos) ?? [];
155
+ const locations: SymbolLocation[] = [];
156
+ for (const sym of refs) for (const refEntry of sym.references) locations.push(locOf(refEntry.fileName, refEntry.textSpan));
157
+ return { ...base, locations };
158
+ }
159
+ if (kind === 'implementations') {
160
+ const impls = service.getImplementationAtPosition(abs, pos) ?? [];
161
+ return { ...base, locations: impls.map((i) => locOf(i.fileName, i.textSpan)) };
162
+ }
163
+ if (kind === 'callers') {
164
+ const incoming = service.provideCallHierarchyIncomingCalls(abs, pos) ?? [];
165
+ return {
166
+ ...base,
167
+ incoming: incoming.map((c) => ({ from: c.from.name, file: relOf(root, c.from.file), line: spanStartLine(c.from.file, c.from.span) })),
168
+ };
169
+ }
170
+ const outgoing = service.provideCallHierarchyOutgoingCalls(abs, pos) ?? [];
171
+ return {
172
+ ...base,
173
+ outgoing: outgoing.map((c) => ({ to: c.to.name, file: relOf(root, c.to.file), line: spanStartLine(c.to.file, c.to.span) })),
174
+ };
175
+ }
176
+
177
+ /** The tracked TS/JS files of a repo's current version, for the LanguageService. */
178
+ export function tsFilesOf(relFiles: readonly string[]): string[] {
179
+ return relFiles.filter((f) => /\.(tsx?|jsx?|mjs|cts|mts)$/i.test(f));
180
+ }