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
+ * What the process engine reads off a report's own words (Spec v3.0 §2.12, §2.13 row 5): the verdict exactly as the
3
+ * report wrote it, the items a passing report leaves for later, the tokens that let a later text be recognised as
4
+ * following one up, and the report's own disposition of an item. Pure functions over text; the ledger already recorded
5
+ * the lines (verdicts, findings, counts), these only read their words.
6
+ */
7
+ import { splitMarkdown } from '../sources/files.ts';
8
+
9
+ // ───────────────────────── the verdict as written ─────────────────────────
10
+
11
+ const VERDICT_TOKEN = /needs?[\s-]+(?:repair|fix(?:es|ing)?|rework)|conditional(?:ly)?\s+pass(?:ed)?|有条件通过|incomplete|未完成|不完整|partial(?:ly)?(?:\s+(?:done|pass(?:ed)?))?|部分做到|部分通过|部分成立|fail(?:ed|s|ure)?|rejected|not\s+accepted|不通过|未通过|没通过|不成立|未做到|没做到|不合格|打回|pass(?:ed|es)?|approved|accepted|lgtm|(?<![不未没非])(?:通过|成立|做到|合格)|blocked|阻塞|需修复|待修复|需要修复/i;
12
+
13
+ /** The verdict word as the line wrote it (`fail`, `Pass`, `通过`, `Needs repair`), or null. */
14
+ export function verdictAsWritten(line: string): string | null {
15
+ const field = /(?:结论|总体结论|总结论|最终结论|判定|裁决|评定|verdict|result|结果|status)\s*\**\s*[::]\s*[*_`「“"'【\[((\s]*(?:[✅❌⚠️✔✖✗✘]\s*)?/iu.exec(line);
16
+ const from = field ? line.slice(field.index + field[0].length) : line;
17
+ const m = VERDICT_TOKEN.exec(from) ?? VERDICT_TOKEN.exec(line);
18
+ return m ? m[0].trim() : null;
19
+ }
20
+
21
+ /**
22
+ * A normalised verdict that says the check found the work failing: `fail` (fail, 不通过, 未通过, rejected, 打回 …),
23
+ * `needs-repair` (needs repair, 需修复 …) and `partial` (部分做到, 部分通过: the result has gaps). Only a check's failing
24
+ * verdict starts a send-back (§1.18: 结果有缺口、失败,QC 判 fail → 送回 Work).
25
+ */
26
+ export const FAILING = new Set(['fail', 'needs-repair', 'partial']);
27
+ /**
28
+ * A normalised verdict that says the check has not concluded: a review that did not finish (`incomplete(审核进行中)`) or
29
+ * could not go on (`blocked`). It is a check still in progress, not a failure: it starts no send-back and fixes nothing.
30
+ */
31
+ export const IN_PROGRESS = new Set(['incomplete', 'blocked']);
32
+
33
+ /** The verdict word with the gloss the report wrote right after it (`incomplete(审核进行中)`), when there is one. */
34
+ export function withGloss(line: string, word: string): string {
35
+ const at = line.toLowerCase().indexOf(word.toLowerCase());
36
+ if (at < 0) return word;
37
+ const m = /^[\s*_`」”"']*([((][^))\r\n]{1,40}[))])/.exec(line.slice(at + word.length));
38
+ return m ? `${word}${m[1]}` : word;
39
+ }
40
+
41
+ // ───────────────────────── items left for later ─────────────────────────
42
+
43
+ /** A section of a report that lists what it leaves open: gaps, leftovers, unresolved concerns, deferred items. */
44
+ const OPEN_HEADING = /缺口|遗留|未完成|未解决|未处理|待办|后续|暂缓|延后|以后再|之后再|留给|不在本轮|本轮不做|顾虑|\bopen (?:items?|issues?|questions?|points?)\b|known (?:issues?|gaps?)|\bgaps?\b|follow[- ]?ups?|\bdeferred\b|\bremaining\b|\btodo\b|to[- ]do|not done|left open|unresolved/i;
45
+ /** A line that says something is left for later. */
46
+ export const DEFER_WORDS = /之后再|以后再|后续再|下一(?:批|轮|版|阶段|个里程碑)|留给|留到|暂不|暂缓|延后|记下|不单开|不另开|不开新|先不|\bdefer(?:red)?\b|\blater\b|follow[- ]?up|next (?:round|batch|iteration|milestone)|\btodo\b|not (?:now|in this round)/i;
47
+ /** The report's own words that dispose of an item: not a product problem, accepted, the account's or the environment's. */
48
+ export const DISPOSE_WORDS = /不是产品|非产品|不属于产品|产品未改|不是新(?:限制|规则|要求)|原有的|不算|不处理|不修|无需|不需要|可接受|接受|已接受|账号侧|环境问题|操作恢复|误贴|不是缺陷|not a (?:product |real )?(?:bug|defect|issue|problem)|won'?t fix|wontfix|by design|as designed|\baccepted\b|\bacceptable\b|no action (?:needed|required)/i;
49
+ const NONE = /^(?:无|没有|暂无|none|n\/a|nothing)[。.!!]?$/i;
50
+
51
+ export interface OpenItem {
52
+ /** 1-based line in the report's current version. */
53
+ readonly line: number;
54
+ readonly text: string;
55
+ readonly section: string;
56
+ }
57
+
58
+ /** List items of the report's open-item sections (§2.12 `Passed with open items`: "之后再说"的条目). */
59
+ export function openItemsOf(text: string): OpenItem[] {
60
+ const out: OpenItem[] = [];
61
+ for (const s of splitMarkdown(text)) {
62
+ const own = s.headingPath[s.headingPath.length - 1] ?? '';
63
+ if (!own || !OPEN_HEADING.test(own)) continue;
64
+ const lines = s.text.split(/\r?\n/);
65
+ for (let i = 1; i < lines.length; i++) {
66
+ const raw = lines[i]!;
67
+ const m = /^\s{0,3}(?:[-*+]|\d{1,3}[.)、])\s+(.+)$/.exec(raw);
68
+ if (!m) continue;
69
+ const body = m[1]!.trim();
70
+ if (!body || NONE.test(body.replace(/[*_`]/g, ''))) continue;
71
+ out.push({ line: s.lineStart + i, text: raw.trim(), section: own });
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+
77
+ /** Lines outside the open-item sections that defer something (a disposition in a root comment: 「记下,批次 5 处理」). */
78
+ export function deferLinesOf(text: string): OpenItem[] {
79
+ const out: OpenItem[] = [];
80
+ let inFence = false;
81
+ const sections = splitMarkdown(text);
82
+ const sectionAt = (ln: number) => sections.find((s) => ln >= s.lineStart && ln <= s.lineEnd);
83
+ text.split(/\r?\n/).forEach((raw, i) => {
84
+ if (/^\s*(```|~~~)/.test(raw)) { inFence = !inFence; return; }
85
+ if (inFence || /^\s{0,3}#/.test(raw) || !raw.trim()) return;
86
+ if (!DEFER_WORDS.test(raw)) return;
87
+ const s = sectionAt(i + 1);
88
+ const heading = s?.headingPath[s.headingPath.length - 1] ?? '';
89
+ if (heading && OPEN_HEADING.test(heading)) return;
90
+ out.push({ line: i + 1, text: raw.trim(), section: heading });
91
+ });
92
+ return out;
93
+ }
94
+
95
+ // ───────────────────────── tokens that recognise a follow-up ─────────────────────────
96
+
97
+ const STOP = new Set(['owner', 'root', 'agent', 'worker', 'model', 'system', 'commit', 'report', 'test', 'tests', 'debug', 'release', 'build', 'main', 'branch', 'Graph', 'README', 'TODO']);
98
+
99
+ /**
100
+ * The words of a line distinctive enough to find it again in a later text: code in backticks, quoted phrases, file
101
+ * names, identifiers with capitals or underscores, long numbers, and the project's finding numbers.
102
+ */
103
+ export function tokensOf(line: string): string[] {
104
+ const out = new Set<string>();
105
+ for (const m of line.matchAll(/`([^`\n]{3,80})`/g)) out.add(m[1]!.trim().replace(/:\d+(?:-\d+)?$/, ''));
106
+ for (const m of line.matchAll(/[「“"『]([^」”"』\n]{3,40})[」”"』]/g)) out.add(m[1]!.trim());
107
+ for (const m of line.matchAll(/\b[\w-]+\.(?:dart|ts|tsx|js|py|md|json|yaml|yml|kt|swift|java|go|rs|sql)\b/g)) out.add(m[0]);
108
+ for (const m of line.matchAll(/\b(?:[a-z]+[A-Z][A-Za-z0-9]*|[A-Za-z]+_[A-Za-z0-9_]+|[A-Z][a-z]{3,}[A-Za-z0-9]*|[A-Z]{2,}[a-z][A-Za-z]*)\b/g)) if (m[0].length >= 5 && !STOP.has(m[0])) out.add(m[0]);
109
+ for (const m of line.matchAll(/(?<![\d.])\d{4,6}(?![\d.])/g)) if (!/^(?:19|20)\d\d$/.test(m[0])) out.add(m[0]);
110
+ for (const m of line.matchAll(/(?<![A-Za-z0-9_-])[A-Z]{1,3}-\d{1,3}(?![A-Za-z0-9_]|-\d)/g)) out.add(m[0]);
111
+ return [...out].filter((t) => t.length >= 3).slice(0, 8);
112
+ }
113
+
114
+ /** Whether a number stands on its own in a text (not `F-6` inside `F-6-3`, not `AB` inside `ABC`). */
115
+ export function standsAlone(text: string, num: string): boolean {
116
+ const re = new RegExp(`(?<![A-Za-z0-9_])${num.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![A-Za-z0-9_]|-\\d|\\.\\d)`);
117
+ return re.test(text);
118
+ }
119
+
120
+ /** One line of a text (1-based), or null. */
121
+ export function lineOf(text: string | null | undefined, line: number | null | undefined): string | null {
122
+ if (!text || !line) return null;
123
+ return text.split(/\r?\n/)[line - 1]?.trim() ?? null;
124
+ }
125
+
126
+ /**
127
+ * The receipt's own account of what was done — recorded as a claim (§2.4): the first line of its summary section
128
+ * (`## 做了什么`, `## 结论`, `## Summary`), else its status line (`status: submitted`), else its title.
129
+ */
130
+ export function claimLineOf(text: string | null): string | null {
131
+ if (!text) return null;
132
+ const lines = text.split(/\r?\n/);
133
+ const at = lines.findIndex((l) => /^\s{0,3}#{1,6}\s+(?:\d+[.、]\s*)?(?:做了什么|完成了什么|结论|总结|总体结论|交付|summary|what (?:was )?done|result|outcome)\s*[::]?\s*$/i.test(l));
134
+ if (at >= 0) {
135
+ const first = lines.slice(at + 1).map((l) => l.trim()).find((l) => l && !/^#/.test(l) && !/^[|>-]{3}/.test(l));
136
+ if (first) return first;
137
+ }
138
+ const status = lines.slice(0, 12).map((l) => l.trim()).find((l) => /^status\s*[::]\s*\S/i.test(l));
139
+ if (status) return status;
140
+ const title = lines.find((l) => /^#\s/.test(l));
141
+ return title ? title.replace(/^#\s+/, '').trim() : null;
142
+ }
143
+
144
+ /** The host's evidence about a unit: its first line that names the unit (what the evidence is, and why it exists). */
145
+ export function evidenceLineOf(text: string | null, num: string): string | null {
146
+ if (!text) return null;
147
+ const lines = text.split(/\r?\n/).slice(0, 15).map((l) => l.trim()).filter((l) => l && !/^---$/.test(l) && !/^[A-Za-z_]+\s*:\s/.test(l));
148
+ return lines.find((l) => standsAlone(l, num) && !/^#/.test(l)) ?? null;
149
+ }
150
+
151
+ /** What the host wrote in the report's `Root comment` (or disposition) section: its first line. */
152
+ export function rootCommentLineOf(text: string | null): string | null {
153
+ if (!text) return null;
154
+ const lines = text.split(/\r?\n/);
155
+ const at = lines.findIndex((l) => /^\s{0,3}#{1,6}\s+(?:root comment|root disposition|处置|root 的处置|disposition)/i.test(l));
156
+ if (at < 0) return null;
157
+ return lines.slice(at + 1).map((l) => l.trim()).find((l) => l && !/^#/.test(l)) ?? null;
158
+ }
159
+
160
+ /** The clauses of a line (a root comment packs several dispositions into one line, one per clause). */
161
+ export const clausesOf = (line: string): string[] => line.split(/[;;。]/).map((x) => x.trim()).filter(Boolean);
162
+
163
+ /** An item's own words that dispose of it (narrower than a later line's: an item describing a mis-paste is still a gap). */
164
+ export const SELF_DISPOSE_WORDS = /产品未改|不是产品|非产品|不属于产品|not a (?:product |real )?(?:bug|defect|issue|problem)|by design|as designed|无需处理|不需要处理/i;
165
+
166
+ /** A test count as a report writes it next to its test words (`14/14 通过`, `134 passed`), not any pair of numbers. */
167
+ export function isTestCount(text: string, value: string | null): boolean {
168
+ if (!value) return false;
169
+ if (/\d\s+(?:passed|failed|passing|failing|pass|fail|通过|失败)$/i.test(value)) return true;
170
+ const flat = text.replace(/\s+/g, '');
171
+ const v = value.replace(/\s+/g, '');
172
+ const at = flat.indexOf(v);
173
+ if (at < 0) return false;
174
+ const around = flat.slice(Math.max(0, at - 8), at) + flat.slice(at + v.length, at + v.length + 10);
175
+ return /测试|用例|通过|失败|绿|passed|failed|passing|failing|tests?|specs?|cases?|pass|fail/i.test(around);
176
+ }
177
+
178
+ /** A severity label at the head of a finding (`【严重 · …】`, `P1`, `major`), or null. */
179
+ export function severityOf(line: string): string | null {
180
+ const m = /【\s*(严重|中等|轻微|提示|阻塞|P[0-3])/.exec(line) ?? /\b(P[0-3]|critical|blocker|major|minor|nit)\b/i.exec(line);
181
+ return m ? m[1]! : null;
182
+ }
183
+
184
+ /** Clip a line for a step's result; the full line stays in the evidence. */
185
+ export const clip = (s: string, n = 140): string => { const t = s.replace(/\s+/g, ' ').trim(); return t.length > n ? `${t.slice(0, n - 1)}…` : t; };