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,504 @@
1
+ /** Program-computed code-map facts. All counts come from the ledger's git and code tables. */
2
+ import { linkHolds } from '../model/k-types.ts';
3
+ import type { ProjectStore } from '../store/project-store.ts';
4
+ import type { Project } from '../model/types.ts';
5
+ import type { CodeTerritory, CurrentUse, EvidenceRef, Generation } from '../model/k-types.ts';
6
+ import type { CodeLanguageView, TerritoryView, WorkTerritoryView } from '../model/views-k.ts';
7
+ import type { Ledger } from '../ledger/index.ts';
8
+ import type { BlameSegment } from '../ledger/blame.ts';
9
+ import { LANG_BY_EXT, NOT_CODE, compilerReads, fileNameOf, namingOf, namingThatCounts, type Naming } from '../ledger/code.ts';
10
+ import { ledgerGit } from '../ledger/git-read.ts';
11
+ import { endsWithPath, pathKey } from '../util/paths.ts';
12
+ import { earlierWork } from '../keeper/organize/carried-on.ts';
13
+
14
+ export interface CodeMapFile {
15
+ readonly path: string; readonly lines: number | null; readonly blob: string | null;
16
+ readonly generated: number; readonly classification: string | null; readonly test: number;
17
+ readonly lang: string | null; readonly last_commit: string | null; readonly last_at: string | null;
18
+ readonly test_cases: number;
19
+ /** Who reads its references (`code_files.reader`): `compiler` (the TypeScript compiler), `engine` (the code engine), or nobody (null). */
20
+ readonly reader: string | null;
21
+ /** The reader's name for its language (`code_files.ref_lang`); null when nothing read it. */
22
+ readonly ref_lang: string | null;
23
+ /**
24
+ * The files naming it though no counted reference reaches it, and how (`code_files.named_by`, ledger/code.ts
25
+ * `NamingKind`): a build or manifest file (a named entry point), code (a reference no reader resolved), other text (a mention).
26
+ */
27
+ readonly naming: readonly Naming[];
28
+ }
29
+
30
+ /**
31
+ * What counts as code (Spec §1.19; D98, D98 补), decided by the ledger rather than a list of languages: a file whose
32
+ * references a reader reads — the TypeScript compiler, or the code engine for every other language — or one in a language
33
+ * the ledger knows is code though nothing reads it (code.ts `NOT_CODE` names what is prose or data). The same rule
34
+ * `Ledger.referenceReach` weighs a residual judgement by. Generated, third-party and test files are no territory's code.
35
+ */
36
+ export const isCodeFile = (f: Pick<CodeMapFile, 'generated' | 'classification' | 'test' | 'lang' | 'reader'>): boolean =>
37
+ !f.generated && !f.classification && !f.test && (Boolean(f.reader) || (f.lang !== null && !NOT_CODE.has(f.lang)));
38
+ /** A file whose own references a reader reads: that nothing reaches it is then a computed fact. */
39
+ const isRead = (f: CodeMapFile): boolean => Boolean(f.reader);
40
+ /** The language a person reads for a file: its reader's name for it, else the ledger's by extension. */
41
+ const languageOf = (f: CodeMapFile): string => f.ref_lang ?? f.lang ?? '(no extension)';
42
+ /** The languages the ledger knows are code: its extension table, prose and data left out. */
43
+ const CODE_LANGUAGES: ReadonlySet<string> = new Set(Object.values(LANG_BY_EXT).filter((l) => !NOT_CODE.has(l)));
44
+
45
+ /** One language of the ledger's measured coverage (`Ledger.coverage().languages`). */
46
+ type CoverageLanguage = ReturnType<Ledger['coverage']>['languages'][number];
47
+
48
+ /**
49
+ * How deep the ledger reads each language of the code (Spec §1.16 "看得见", §6.17; D98 补), from the coverage it measured:
50
+ * every language a reader reads, and every language the ledger knows is code but reads nothing of; prose and data are no
51
+ * code. The compiler reads to symbols; the code engine to symbols as far as it resolved them — the imports it resolved to a
52
+ * file of the repository, those it left unresolved though they name one, and the files other files name that no counted
53
+ * reference reaches; a language not read has its files and sizes only. `complete` when every file is read and nothing was
54
+ * measured unresolved: only there can a residual call rest on computed references alone (CKC-25 AC-5).
55
+ */
56
+ export function codeLevels(languages: readonly CoverageLanguage[]): CodeLanguageView[] {
57
+ return languages.filter((l) => l.reader !== null || CODE_LANGUAGES.has(l.language)).map((l) => ({
58
+ language: l.language, files: l.files, read: l.read, readBy: l.reader,
59
+ imports: l.reader === 'engine' ? { resolved: l.reach?.imports.resolved ?? 0, unresolvedNamingRepoFiles: l.reach?.imports.unresolvedNamingRepoFiles ?? 0 } : null,
60
+ namedButUnreferenced: l.reach?.namedButUnreferenced ?? 0, parseErrors: l.reach?.parseErrors ?? 0,
61
+ complete: l.reader !== null && l.read === l.files && l.gaps.length === 0, gaps: l.gaps,
62
+ }));
63
+ }
64
+ export interface CodeMapEdge { readonly src: string; readonly dst: string; readonly kind: string }
65
+ interface Commit { hash: string; parents: string; subject: string; body: string; author_at: string; author_ms: number; first_parent_trunk: number; on_trunk: number }
66
+ interface Change { hash: string; path: string; old_path: string | null }
67
+ type Work = { id: string; label: string };
68
+ export interface GenerationOrigin { readonly generationId: string | null; readonly name: string; readonly lines: number; readonly share: number; readonly work: readonly { readonly workId: string | null; readonly label: string; readonly lines: number }[] }
69
+ export type CodeAnomalyCandidate = {
70
+ readonly kind: 'Unreferenced file' | 'Unreferenced block' | 'Residual name, referenced' | 'Unclaimed integrated commit' | 'Territory path gone';
71
+ readonly repo: string; readonly territoryId: string | null; readonly path: string | null;
72
+ readonly commit: string | null; readonly detail: string; readonly evidence: readonly EvidenceRef[];
73
+ };
74
+
75
+ export const within = (paths: readonly string[], path: string): boolean => paths.some((raw) => {
76
+ const prefix = raw.replace(/\\/g, '/').replace(/\/+$/, '');
77
+ return path === prefix || path.startsWith(`${prefix}/`);
78
+ });
79
+ const idIn = (text: string, id: string): boolean => new RegExp(`(^|[^A-Za-z0-9])${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=$|[^A-Za-z0-9])`, 'i').test(text);
80
+ const fileEvidence = (path: string, repo: string): EvidenceRef => ({ kind: 'ledger', id: `file:${path}`, label: path, repo });
81
+ const commitEvidence = (hash: string): EvidenceRef => ({ kind: 'ledger', id: `commit:${hash.slice(0, 12)}`, label: hash.slice(0, 12) });
82
+ const dateMs = (at: string): number => Date.parse(/^\d{4}-\d\d-\d\d$/.test(at) ? `${at}T23:59:59.999Z` : at);
83
+ /** A file with at least 95% older lines is an older-generation importer. */
84
+ export const PREVIOUS_GENERATION_THRESHOLD = 0.95;
85
+ const entrypoint = (path: string): boolean => /(?:^|\/)(?:main|app|index|cli|bootstrap|application|routes?|router)\.[^.\/]+$/i.test(path);
86
+ // Generic words only: whether a name looks residual in one project (a folder still called after the
87
+ // product's first idea) is the cross-check's judgement from that project's history, never a rule written for every project (QC AY).
88
+ const residualName = (path: string): boolean => /(?:^|[\/_.-])(legacy|old|deprecated|backup|v1)(?=$|[\/_.-])/i.test(path);
89
+
90
+ /** One index per repository per view. A fresh index uses the current ledger and current workbench assets. */
91
+ export class CodeMapIndex {
92
+ readonly files: readonly CodeMapFile[];
93
+ readonly edges: readonly CodeMapEdge[];
94
+ readonly repo: string;
95
+ private readonly ledger: Ledger;
96
+ private readonly store: ProjectStore;
97
+ private readonly fileByPath: Map<string, CodeMapFile>;
98
+ private readonly commits: Map<string, Commit>;
99
+ private readonly changes: readonly Change[];
100
+ private readonly work = new Map<string, Work>();
101
+ private readonly blame: Map<string, BlameSegment[]>;
102
+ private readonly oldGenerations: readonly Generation[];
103
+ private readonly unused: Set<string>;
104
+ /** Unused code that nothing references by no computed fact, with the named file it is reached from (`findUncertain`). */
105
+ private readonly uncertain: Map<string, string>;
106
+
107
+ constructor(ledger: Ledger, store: ProjectStore, repo: string) {
108
+ this.ledger = ledger;
109
+ this.store = store;
110
+ this.repo = repo;
111
+ // A ledger not rebuilt since D98 has no `reader` column: the TypeScript compiler's files by extension, as the ledger
112
+ // itself reads it then (Ledger.readerOf); the next rebuild records every reader.
113
+ const recorded = ledger.db.prepare('SELECT * FROM code_files WHERE repo = ?').all(repo) as unknown as (Omit<CodeMapFile, 'reader' | 'ref_lang' | 'naming'> & { reader?: string | null; ref_lang?: string | null; named_by?: string | null })[];
114
+ this.files = recorded.map((r) => ({
115
+ path: r.path, lines: r.lines, blob: r.blob, generated: r.generated, classification: r.classification, test: r.test, lang: r.lang,
116
+ last_commit: r.last_commit, last_at: r.last_at, test_cases: r.test_cases,
117
+ reader: r.reader !== undefined ? r.reader : compilerReads(r.path) ? 'compiler' : null, ref_lang: r.ref_lang ?? null, naming: namingOf(r.named_by),
118
+ }));
119
+ this.fileByPath = new Map(this.files.map((f) => [f.path, f]));
120
+ this.edges = ledger.db.prepare('SELECT src, dst, kind FROM code_deps WHERE repo = ? AND external = 0').all(repo) as unknown as CodeMapEdge[];
121
+ const rows = ledger.db.prepare('SELECT hash, parents, subject, body, author_at, author_ms, first_parent_trunk, on_trunk FROM commits WHERE repo = ?').all(repo) as unknown as Commit[];
122
+ this.commits = new Map(rows.map((r) => [r.hash, r]));
123
+ this.changes = ledger.db.prepare('SELECT hash, path, old_path FROM commit_files WHERE repo = ?').all(repo) as unknown as Change[];
124
+ const cached = ledger.db.prepare('SELECT b.path, b.segments FROM code_blame b JOIN code_files f ON f.repo = b.repo AND f.path = b.path AND f.blob = b.blob WHERE b.repo = ?').all(repo) as { path: string; segments: string }[];
125
+ this.blame = new Map(cached.map((r) => [r.path, JSON.parse(r.segments) as BlameSegment[]]));
126
+ this.oldGenerations = store.generations.filter((g) => g.projectId === store.projectId).sort((a, b) => dateMs(a.ended.at) - dateMs(b.ended.at));
127
+ this.assignWork();
128
+ const { unused, outgoing } = this.findUnused();
129
+ this.unused = unused;
130
+ this.uncertain = this.findUncertain(outgoing);
131
+ }
132
+
133
+ private assignWork(): void {
134
+ const names = new Map<string, Work>();
135
+ for (const t of this.store.threads.all()) {
136
+ const label = `${t.ids[0] ?? ''} ${t.title}`.trim();
137
+ for (const id of t.ids) names.set(id.toLowerCase(), { id: t.id, label });
138
+ }
139
+ for (const n of this.store.numbers.all()) {
140
+ if (n.objectKind !== 'work') continue;
141
+ const t = this.store.threads.get(n.objectId);
142
+ if (!t) continue;
143
+ for (const id of [n.number, n.projectNumber].filter((x): x is string => Boolean(x))) names.set(id.toLowerCase(), { id: t.id, label: `${t.ids[0] ?? n.number} ${t.title}`.trim() });
144
+ }
145
+ const uniqueMatch = (text: string): Work | null => {
146
+ const found = new Map<string, Work>();
147
+ for (const [id, w] of names) if (idIn(text, id)) found.set(w.id, w);
148
+ return found.size === 1 ? [...found.values()][0]! : null;
149
+ };
150
+ const numbers = this.ledger.db.prepare("SELECT commit_hash, num FROM nums WHERE repo = ? AND kind = 'commit'").all(this.repo) as { commit_hash: string; num: string }[];
151
+ const byHash = new Map<string, string[]>();
152
+ for (const n of numbers) byHash.set(n.commit_hash, [...(byHash.get(n.commit_hash) ?? []), n.num]);
153
+ for (const c of this.commits.values()) {
154
+ const ledgerNums = byHash.get(c.hash)?.map((n) => names.get(n.toLowerCase())).filter((w): w is Work => Boolean(w)) ?? [];
155
+ const fromNums = new Map(ledgerNums.map((w) => [w.id, w]));
156
+ const fromText = uniqueMatch(`${c.subject}\n${c.body}`);
157
+ if (fromText) this.work.set(c.hash, fromText);
158
+ else if (fromNums.size === 1) this.work.set(c.hash, [...fromNums.values()][0]!);
159
+ }
160
+ // Confirmed process links take precedence over textual guesses.
161
+ for (const link of this.store.links.all()) {
162
+ if (!linkHolds(link) || !this.store.threads.has(link.workId)) continue;
163
+ const ref = link.ledgerRef.replace(/^commit:/, '').toLowerCase();
164
+ if (!/^[0-9a-f]{7,40}$/.test(ref)) continue;
165
+ const commit = [...this.commits.keys()].find((h) => h.startsWith(ref));
166
+ if (commit) { const t = this.store.threads.get(link.workId)!; this.work.set(commit, { id: t.id, label: `${t.ids[0] ?? ''} ${t.title}`.trim() }); }
167
+ }
168
+ const branches = this.ledger.db.prepare('SELECT name, tip FROM branches WHERE repo = ? AND merged = 1').all(this.repo) as { name: string; tip: string }[];
169
+ const branchWork = new Map<string, Work>();
170
+ for (const b of branches) { const w = uniqueMatch(b.name); if (w) branchWork.set(b.tip, w); }
171
+ // A retained branch identifies its tip. On a merge it also identifies the merge that integrated the tip.
172
+ for (const [tip, w] of branchWork) {
173
+ if (!this.work.has(tip)) this.work.set(tip, w);
174
+ for (const c of this.commits.values()) if (c.first_parent_trunk && c.parents.split(' ').slice(1).includes(tip) && !this.work.has(c.hash)) this.work.set(c.hash, w);
175
+ }
176
+ const repoPath = this.ledger.repos().find((r) => r.id === this.repo)?.path;
177
+ const trunk = this.ledger.repos().find((r) => r.id === this.repo)?.trunkTip;
178
+ const ordered = repoPath && trunk ? ledgerGit(repoPath, ['rev-list', '--first-parent', trunk]) : null;
179
+ const first = ordered?.ok ? ordered.out.trim().split(/\s+/).filter(Boolean) : [...this.commits.values()].filter((c) => c.first_parent_trunk).sort((a, b) => b.author_ms - a.author_ms).map((c) => c.hash);
180
+ // Fast-forward branches have no merge commit. Attribute the contiguous run back to the previous integration or
181
+ // explicitly labelled work, never across a merge or a different known work item.
182
+ for (const [tip, w] of branchWork) {
183
+ if (!this.commits.get(tip)?.first_parent_trunk) continue;
184
+ const start = first.indexOf(tip);
185
+ for (let i = start + 1; start >= 0 && i < first.length; i++) {
186
+ const c = this.commits.get(first[i]!);
187
+ if (!c || c.parents.split(' ').length > 1 || (this.work.has(c.hash) && this.work.get(c.hash)?.id !== w.id)) break;
188
+ if (!this.work.has(c.hash)) this.work.set(c.hash, w);
189
+ }
190
+ }
191
+ // A side commit's blame belongs to the work integrated by the first-parent merge that brought it in.
192
+ for (const hash of [...first].reverse()) {
193
+ const merge = this.commits.get(hash);
194
+ const parents = merge?.parents.split(' ').filter(Boolean) ?? [];
195
+ const w = this.work.get(hash);
196
+ if (!w || parents.length < 2) continue;
197
+ const base = new Set<string>();
198
+ const visit = (start: string, into: Set<string>) => {
199
+ const stack = [start];
200
+ while (stack.length) { const h = stack.pop()!; if (into.has(h)) continue; into.add(h); stack.push(...(this.commits.get(h)?.parents.split(' ').filter(Boolean) ?? [])); }
201
+ };
202
+ visit(parents[0]!, base);
203
+ const side = new Set<string>();
204
+ for (const p of parents.slice(1)) visit(p, side);
205
+ for (const h of side) if (!base.has(h) && !this.work.has(h)) this.work.set(h, w);
206
+ }
207
+ }
208
+
209
+ workForCommit(hash: string | null): Work | null { return hash ? this.work.get(hash) ?? null : null; }
210
+ generationForCommit(hash: string): Generation | null {
211
+ const commit = this.commits.get(hash);
212
+ if (!commit) return null;
213
+ return this.oldGenerations.find((g) => /^\d{4}-\d\d-\d\d$/.test(g.ended.at)
214
+ ? commit.author_at.slice(0, 10) <= g.ended.at : commit.author_ms <= dateMs(g.ended.at)) ?? null;
215
+ }
216
+ private linesOf(path: string): BlameSegment[] { return this.blame.get(path) ?? []; }
217
+ private oldShare(path: string): number {
218
+ const segments = this.linesOf(path);
219
+ const total = segments.reduce((n, s) => n + s.end - s.start + 1, 0);
220
+ return total ? segments.reduce((n, s) => n + (this.generationForCommit(s.commit) ? s.end - s.start + 1 : 0), 0) / total : 0;
221
+ }
222
+ /**
223
+ * Code no reference walk from an anchor reaches, over every code file: what a reader reads counts exactly as TypeScript
224
+ * does, and code nothing reads can still be reached by a reference from code that is read. That nothing reaches a file
225
+ * whose own references nobody reads is a candidate, not a computed fact (`currentUse`, `anomalies`).
226
+ */
227
+ private findUnused(): { unused: Set<string>; outgoing: Map<string, string[]> } {
228
+ const source = new Set(this.files.filter(isCodeFile).map((f) => f.path));
229
+ const outgoing = new Map<string, string[]>();
230
+ for (const e of this.edges) if (source.has(e.src) && source.has(e.dst)) outgoing.set(e.src, [...(outgoing.get(e.src) ?? []), e.dst]);
231
+ const roots = new Set(this.files.filter((f) => source.has(f.path) && entrypoint(f.path)).map((f) => f.path));
232
+ // Entry points and files directly owned by current work are explicit anchors. Without either, an isolated component
233
+ // remains a candidate: one a build or manifest file names says so (`findUncertain`); model review can recognise it,
234
+ // and dynamic registration, reflection or manually run scripts.
235
+ for (const f of this.files) if (source.has(f.path) && f.last_commit && this.workForCommit(f.last_commit)
236
+ && !this.generationForCommit(f.last_commit) && !this.oldWork().has(this.workForCommit(f.last_commit)!.id)) roots.add(f.path);
237
+ const reached = new Set<string>();
238
+ const stack = [...roots];
239
+ while (stack.length) { const path = stack.pop()!; if (reached.has(path)) continue; reached.add(path); stack.push(...(outgoing.get(path) ?? [])); }
240
+ return { unused: new Set([...source].filter((p) => !reached.has(p))), outgoing };
241
+ }
242
+ /**
243
+ * Unused code of which it is no computed fact that nothing uses it (ledger/code.ts `NamingKind`): what a build or manifest
244
+ * file names (a named entry point), what code the walk reaches names (a reference its reader did not resolve), and the
245
+ * unused code only such files reach through file references — each with the named file it is reached from (itself, when
246
+ * it is named). Code named only by code nothing reaches, or by tests, stays unreferenced as computed: whatever that
247
+ * reference is, it comes from code that is not live. A mention in a document or other text references nothing.
248
+ */
249
+ private findUncertain(outgoing: ReadonlyMap<string, readonly string[]>): Map<string, string> {
250
+ const live = (n: Naming) => n.kind === 'entry' || (n.kind === 'reference' && !this.unused.has(n.path) && !this.fileByPath.get(n.path)?.test);
251
+ const from = new Map<string, string>();
252
+ const stack: string[] = [];
253
+ for (const f of this.files) if (this.unused.has(f.path) && f.naming.some(live)) { from.set(f.path, f.path); stack.push(f.path); }
254
+ while (stack.length) {
255
+ const path = stack.pop()!;
256
+ for (const next of outgoing.get(path) ?? []) if (this.unused.has(next) && !from.has(next)) { from.set(next, from.get(path)!); stack.push(next); }
257
+ }
258
+ return from;
259
+ }
260
+ isUnused(path: string): boolean { return this.unused.has(path); }
261
+ /**
262
+ * What an Unreferenced file candidate says beyond the walk: that nothing reads its references; that a build or manifest
263
+ * file names it — a named entry point, "named in AndroidManifest.xml"; that code names it — a reference its reader did not
264
+ * resolve; that only such files reach it. Any of these makes a judgement there Inferred (Ledger.referenceReach). A
265
+ * mention in a document or other text is said too; it references nothing.
266
+ */
267
+ private namedWords(f: CodeMapFile): string {
268
+ const of = (kind: Naming['kind']) => f.naming.filter((n) => n.kind === kind).map((n) => n.path);
269
+ const entry = of('entry');
270
+ const code = of('reference');
271
+ const mention = of('mention');
272
+ const via = this.uncertain.get(f.path);
273
+ const why = [
274
+ isRead(f) ? null : `The ledger reads no references of this ${languageOf(f)} file, so a use from code nobody reads would not show.`,
275
+ entry.length ? `It is a named entry point: named in ${entry.map((p) => `${fileNameOf(p)} (${p})`).join(', ')}, a build or manifest file, which compiles, launches, loads or runs it.` : null,
276
+ code.length ? `Named in ${code.map((p) => `${p}${this.unused.has(p) ? ', which nothing reaches either' : ''}`).join('; ')}: a reference its reader did not resolve.` : null,
277
+ via && via !== f.path ? `Only files other files name reach it, from ${via} (${[...new Set(namingThatCounts(this.fileByPath.get(via)?.naming ?? []).map((n) => `named in ${fileNameOf(n.path)}`))].join(', ')}).` : null,
278
+ ].filter((x): x is string => x !== null);
279
+ return `${why.length ? ` ${why.join(' ')} A judgement here is Inferred.` : ''}${mention.length ? ` Mentioned in ${mention.join(', ')}, which reference${mention.length === 1 ? 's' : ''} nothing.` : ''}`;
280
+ }
281
+ /**
282
+ * Code that nothing reaches, some of it read by a reader, none of it named by live files: nothing reaching code whose
283
+ * references are read is a computed fact. Code nothing reads and nothing reaches is not shown to be unreferenced — nobody
284
+ * reads the references that would; nor is code holding a named entry point, a file live code names, or code only they
285
+ * reach (`findUncertain`): it may be entered from there.
286
+ */
287
+ private allUnused(files: readonly CodeMapFile[]): boolean {
288
+ return files.length > 0 && files.every((f) => this.unused.has(f.path)) && files.some(isRead) && !files.some((f) => this.uncertain.has(f.path));
289
+ }
290
+
291
+ builtBy(paths: readonly string[]): TerritoryView['builtBy'] {
292
+ const rows = new Map<string, { workId: string | null; label: string; commits: Set<string>; files: Set<string> }>();
293
+ for (const change of this.changes) {
294
+ if (!this.commits.get(change.hash)?.first_parent_trunk || !(within(paths, change.path) || (change.old_path && within(paths, change.old_path)))) continue;
295
+ const w = this.workForCommit(change.hash);
296
+ const key = w?.id ?? `commit:${change.hash}`;
297
+ const c = this.commits.get(change.hash)!;
298
+ const row = rows.get(key) ?? { workId: w?.id ?? null, label: w?.label ?? `Unclaimed · ${c.subject}`, commits: new Set<string>(), files: new Set<string>() };
299
+ row.commits.add(change.hash.slice(0, 12));
300
+ row.files.add(change.path);
301
+ rows.set(key, row);
302
+ }
303
+ return [...rows.values()].sort((a, b) => b.files.size - a.files.size || a.label.localeCompare(b.label)).map((r) => ({ workId: r.workId, label: r.label, commits: [...r.commits], files: r.files.size }));
304
+ }
305
+
306
+ generationOrigins(paths: readonly string[]): GenerationOrigin[] {
307
+ const groups = new Map<string, { generationId: string | null; name: string; lines: number; work: Map<string, { workId: string | null; label: string; lines: number }> }>();
308
+ let total = 0;
309
+ for (const f of this.files) {
310
+ if (!within(paths, f.path) || !isCodeFile(f)) continue;
311
+ // The rebuild blames the files a reader reads (ledger/blame.ts): code nothing reads adds no lines here.
312
+ for (const s of this.linesOf(f.path)) {
313
+ const n = s.end - s.start + 1;
314
+ const generation = this.generationForCommit(s.commit);
315
+ const key = generation?.id ?? 'current';
316
+ const row = groups.get(key) ?? { generationId: generation?.id ?? null, name: generation?.name ?? 'Current generation', lines: 0, work: new Map() };
317
+ const w = this.workForCommit(s.commit);
318
+ const wk = w?.id ?? `commit:${s.commit}`;
319
+ const wr = row.work.get(wk) ?? { workId: w?.id ?? null, label: w?.label ?? `Unclaimed · ${s.commit.slice(0, 12)}`, lines: 0 };
320
+ wr.lines += n; row.work.set(wk, wr); row.lines += n; total += n;
321
+ groups.set(key, row);
322
+ }
323
+ }
324
+ return [...groups.values()].sort((a, b) => b.lines - a.lines).map((r) => ({ generationId: r.generationId, name: r.name, lines: r.lines, share: total ? r.lines / total : 0, work: [...r.work.values()].sort((a, b) => b.lines - a.lines) }));
325
+ }
326
+
327
+ /**
328
+ * Whether the current plan still uses this code (§1.19, D85). The references say whether current code reaches it; the
329
+ * plan's own objects say whether anything current points at it: `In current plan` needs both (QC AY: it had been given
330
+ * on the references alone). `paths` are a territory's; the territory itself (its area) is found by them in the store.
331
+ */
332
+ currentUse(paths: readonly string[]): CurrentUse {
333
+ const mine = this.files.filter((f) => within(paths, f.path) && isCodeFile(f));
334
+ // No code here: its use can be neither shown nor ruled out (§1.16).
335
+ if (!mine.length) return 'Not known';
336
+ if (this.allUnused(mine)) return 'Unreferenced';
337
+ // Nothing reaches it, and nobody reads the references of any of it, or it holds a named entry point or code live files
338
+ // name (`findUncertain`): not shown either way.
339
+ if (mine.every((f) => this.unused.has(f.path))) return 'Not known';
340
+ const incoming = this.edges.filter((e) => within(paths, e.dst) && !within(paths, e.src) && !this.fileByPath.get(e.src)?.test && !this.unused.has(e.src));
341
+ if (incoming.length && incoming.every((e) => this.oldShare(e.src) >= PREVIOUS_GENERATION_THRESHOLD)) return 'Previous generation only';
342
+ return this.pointedAtByPlan(paths) ? 'In current plan' : 'In use, not in current plan';
343
+ }
344
+
345
+ /**
346
+ * The work items of earlier generations (`Generation.workIds`): not current work, whatever their progress. CZ: less
347
+ * the items carried on under the same number, which are the current plan's.
348
+ */
349
+ private oldWork(): Set<string> {
350
+ return earlierWork(this.store);
351
+ }
352
+
353
+ /** A work item that is current: in force, and not planned in an earlier generation. */
354
+ private currentWork(workId: string, old = this.oldWork()): boolean {
355
+ const t = this.store.threads.get(workId);
356
+ return Boolean(t && t.validity === 'Current' && !old.has(workId));
357
+ }
358
+
359
+ /**
360
+ * Current work or requirements point at this code (§1.19 "被现行的工作和需求指着"): a current work item of the current
361
+ * generation changed it (its trunk commits and merges, as `Built by` counts them), or the area the territory serves —
362
+ * mainly or also — has requirements, designs, decisions, plans or work items in force. The territory's area is the
363
+ * cross-check's judgement; the objects in force are the assets'.
364
+ */
365
+ pointedAtByPlan(paths: readonly string[]): boolean {
366
+ const old = this.oldWork();
367
+ for (const change of this.changes) {
368
+ if (!this.commits.get(change.hash)?.first_parent_trunk || !(within(paths, change.path) || (change.old_path && within(paths, change.old_path)))) continue;
369
+ const w = this.workForCommit(change.hash);
370
+ if (w && this.currentWork(w.id, old) && !this.generationForCommit(change.hash)) return true;
371
+ }
372
+ const territory = this.territoryOf(paths);
373
+ const areas = new Set([territory?.areaId, ...(territory?.alsoServes ?? [])].filter((a): a is string => Boolean(a)));
374
+ if (!areas.size) return false;
375
+ const PLAN = new Set(['Requirement', 'Design', 'Decision', 'Plan', 'Work item']);
376
+ return this.store.nodes.find((n) => n.areaId !== null && areas.has(n.areaId) && PLAN.has(n.category) && n.validity === 'Current'
377
+ && (n.category !== 'Work item' || this.currentWork(n.refId, old))) !== undefined;
378
+ }
379
+
380
+ /** The territory of this repository drawn on exactly these paths, when there is one (the unmapped directories have none).
381
+ * Its repository is matched as `Code` matches it (ledger/views.ts `repoOf`): by id, path or the path's last part, else the main one. */
382
+ private territoryOf(paths: readonly string[]): CodeTerritory | undefined {
383
+ const norm = (ps: readonly string[]) => [...ps].map((p) => p.replace(/\\/g, '/').replace(/\/+$/, '')).sort().join('\n');
384
+ const key = norm(paths);
385
+ const repos = this.ledger.repos();
386
+ const repoOf = (t: CodeTerritory) => (repos.find((r) => r.id === t.repo || pathKey(r.path) === pathKey(t.repo) || endsWithPath(r.path, t.repo)) ?? repos[0])?.id;
387
+ return this.store.territories.find((t) => norm(t.paths) === key && repoOf(t) === this.repo);
388
+ }
389
+
390
+ /** The paths the current version has no file at or under: deleted or moved since the territory was drawn (CKC-25 AC-10). */
391
+ gonePaths(paths: readonly string[]): string[] {
392
+ return paths.filter((p) => !this.files.some((f) => within([p], f.path)));
393
+ }
394
+
395
+ /**
396
+ * The territories each work item changed (§6.17 "从过程视图的工作跳到它改过的领地"), by work id: the files its trunk
397
+ * commits and merges changed in each, counted exactly as `builtBy` counts them — so a territory's `Built by` and the
398
+ * work's jump back to it give the same number. Most files first.
399
+ */
400
+ territoriesByWork(territories: readonly Pick<CodeTerritory, 'id' | 'name' | 'paths'>[]): Map<string, WorkTerritoryView[]> {
401
+ const acc = new Map<string, Map<string, { name: string; files: Set<string>; commits: Set<string> }>>();
402
+ for (const change of this.changes) {
403
+ if (!this.commits.get(change.hash)?.first_parent_trunk) continue;
404
+ const w = this.workForCommit(change.hash);
405
+ if (!w) continue;
406
+ for (const t of territories) {
407
+ if (!(within(t.paths, change.path) || (change.old_path && within(t.paths, change.old_path)))) continue;
408
+ const byTerritory = acc.get(w.id) ?? new Map();
409
+ const row = byTerritory.get(t.id) ?? { name: t.name, files: new Set<string>(), commits: new Set<string>() };
410
+ row.files.add(change.path);
411
+ row.commits.add(change.hash);
412
+ byTerritory.set(t.id, row);
413
+ acc.set(w.id, byTerritory);
414
+ }
415
+ }
416
+ const byTime = (a: string, b: string) => (this.commits.get(a)?.author_ms ?? 0) - (this.commits.get(b)?.author_ms ?? 0);
417
+ return new Map([...acc].map(([workId, rows]) => [workId, [...rows].map(([territoryId, r]) => ({
418
+ territoryId, name: r.name, files: r.files.size, commits: [...r.commits].sort(byTime).map((h) => h.slice(0, 12)),
419
+ })).sort((a, b) => b.files - a.files || a.name.localeCompare(b.name))]));
420
+ }
421
+
422
+ dependencyCounts(paths: readonly string[], other: readonly { id: string; paths: readonly string[] }[]): { dependsOn: Record<string, number>; dependedBy: Record<string, number> } {
423
+ const dependsOn: Record<string, number> = {};
424
+ const dependedBy: Record<string, number> = {};
425
+ const seen = new Set<string>();
426
+ for (const e of this.edges) {
427
+ if (this.fileByPath.get(e.src)?.test || this.fileByPath.get(e.dst)?.test) continue;
428
+ const pair = `${e.src}\0${e.dst}`;
429
+ if (seen.has(pair)) continue;
430
+ seen.add(pair);
431
+ for (const o of other) {
432
+ if (within(paths, e.src) && within(o.paths, e.dst) && !within(paths, e.dst)) dependsOn[o.id] = (dependsOn[o.id] ?? 0) + 1;
433
+ if (within(o.paths, e.src) && within(paths, e.dst) && !within(paths, e.src)) dependedBy[o.id] = (dependedBy[o.id] ?? 0) + 1;
434
+ }
435
+ }
436
+ return { dependsOn, dependedBy };
437
+ }
438
+
439
+ anomalies(territories: readonly Pick<CodeTerritory, 'id' | 'paths'>[]): CodeAnomalyCandidate[] {
440
+ const out: CodeAnomalyCandidate[] = [];
441
+ const territory = (path: string) => territories.find((t) => within(t.paths, path))?.id ?? null;
442
+ for (const f of this.files.filter(isCodeFile)) {
443
+ // Code nothing reads is a candidate too (D98 补): what would show its use may be in references nobody reads, so the
444
+ // detail says so and the judgement's basis follows Ledger.referenceReach (Inferred there). So does code other files
445
+ // name: a named entry point, a reference no reader resolved, and what only such code reaches.
446
+ if (this.unused.has(f.path)) out.push({ kind: 'Unreferenced file', repo: this.repo, territoryId: territory(f.path), path: f.path, commit: null,
447
+ detail: `No path from a conventional entry point or currently claimed work through non-test file references.${this.namedWords(f)}`,
448
+ evidence: [fileEvidence(f.path, this.repo), ...f.naming.map((n) => fileEvidence(n.path, this.repo))] });
449
+ const refs = this.edges.filter((e) => e.dst === f.path && !this.fileByPath.get(e.src)?.test);
450
+ if (residualName(f.path) && refs.length) out.push({ kind: 'Residual name, referenced', repo: this.repo, territoryId: territory(f.path), path: f.path, commit: null,
451
+ detail: `${refs.length} non-test file references`, evidence: [fileEvidence(f.path, this.repo), ...refs.map((e) => fileEvidence(e.src, this.repo))] });
452
+ }
453
+ for (const t of territories) {
454
+ // A territory drawn on a path the current version no longer has: its numbers shrink to what is left, so the round
455
+ // redraws it on the current paths rather than let it count nothing in silence (CKC-25 AC-10; QC AY B13).
456
+ const gone = this.gonePaths(t.paths);
457
+ if (gone.length) {
458
+ const history = gone.map((p) => ({ p, last: [...this.changes].filter((c) => within([p], c.path) || (c.old_path !== null && within([p], c.old_path))).sort((a, b) => (this.commits.get(b.hash)?.author_ms ?? 0) - (this.commits.get(a.hash)?.author_ms ?? 0))[0] ?? null }));
459
+ out.push({ kind: 'Territory path gone', repo: this.repo, territoryId: t.id, path: gone.join(', '), commit: history.find((h) => h.last)?.last?.hash.slice(0, 12) ?? null,
460
+ detail: `${gone.length === t.paths.length ? `none of its paths is in the current version any more (${gone.join(', ')})` : `${gone.length} of its ${t.paths.length} paths ${gone.length === 1 ? 'is' : 'are'} no longer in the current version (${gone.join(', ')})`}; redraw it on the paths its code has now (pk_write_territory with its id), or let it go if its code went elsewhere`,
461
+ evidence: history.flatMap((h) => (h.last ? [commitEvidence(h.last.hash)] : [])) });
462
+ }
463
+ const files = this.files.filter((f) => within(t.paths, f.path) && isCodeFile(f));
464
+ // As `currentUse` says `Unreferenced`: nothing reaches it, some of it is read, and nothing in it is named by live files.
465
+ const notRead = files.filter((f) => !isRead(f)).length;
466
+ if (this.allUnused(files)) out.push({ kind: 'Unreferenced block', repo: this.repo, territoryId: t.id, path: t.paths.join(', '), commit: null,
467
+ detail: `${files.length} current source files are outside the non-test reference closure${notRead ? `; the ledger reads no references of ${notRead} of them` : ''}`, evidence: files.map((f) => fileEvidence(f.path, this.repo)) });
468
+ if (t.paths.some(residualName)) {
469
+ const referenced = files.filter((f) => this.edges.some((e) => e.dst === f.path && !this.fileByPath.get(e.src)?.test && !within(t.paths, e.src)));
470
+ if (referenced.length) out.push({ kind: 'Residual name, referenced', repo: this.repo, territoryId: t.id, path: t.paths.join(', '), commit: null,
471
+ detail: `${referenced.length} current source files have non-test external references`, evidence: referenced.map((f) => fileEvidence(f.path, this.repo)) });
472
+ }
473
+ }
474
+ const unclaimed = new Map<string, { hash: string; territoryId: string | null; paths: Set<string> }>();
475
+ for (const c of this.changes) {
476
+ if (!this.commits.get(c.hash)?.first_parent_trunk || this.workForCommit(c.hash)) continue;
477
+ const tid = territory(c.path);
478
+ const key = `${c.hash}\0${tid ?? ''}`;
479
+ const row = unclaimed.get(key) ?? { hash: c.hash, territoryId: tid, paths: new Set<string>() };
480
+ row.paths.add(c.path);
481
+ unclaimed.set(key, row);
482
+ }
483
+ for (const row of unclaimed.values()) {
484
+ const paths = [...row.paths].sort();
485
+ out.push({ kind: 'Unclaimed integrated commit', repo: this.repo, territoryId: row.territoryId, path: paths[0] ?? null, commit: row.hash.slice(0, 12),
486
+ detail: `${this.commits.get(row.hash)!.subject}: ${paths.length} changed files on the trunk without a work item match`,
487
+ evidence: [commitEvidence(row.hash), ...paths.map((p) => fileEvidence(p, this.repo))] });
488
+ }
489
+ return out;
490
+ }
491
+ }
492
+
493
+ /** The cross-check's input; these are candidates, never the model's anomaly judgement. */
494
+ export function codeAnomalyCandidates(ledger: Ledger, store: ProjectStore, project: Project): CodeAnomalyCandidate[] {
495
+ const out: CodeAnomalyCandidate[] = [];
496
+ for (const repo of ledger.repos()) {
497
+ const t = store.territories.filter((x) => x.projectId === project.id && (x.repo === repo.id || x.repo === repo.path));
498
+ const index = new CodeMapIndex(ledger, store, repo.id);
499
+ const blocks = t.length ? t : [...new Set(index.files.filter(isCodeFile).map((f) => f.path.split('/').slice(0, -1).join('/')).filter(Boolean))]
500
+ .map((path) => ({ id: `directory:${repo.id}:${path}`, paths: [path] }));
501
+ out.push(...index.anomalies(blocks));
502
+ }
503
+ return out;
504
+ }