ctxman 3.0.0 → 3.1.1

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 (224) hide show
  1. package/.ctxman/llm-profiles.json +1067 -39
  2. package/.ctxman/profiles/backend-team.json +13 -0
  3. package/.ctxman/profiles/default.json +12 -0
  4. package/.ctxman/profiles/devops-team.json +21 -0
  5. package/.ctxman/profiles/frontend-team.json +20 -0
  6. package/.ctxman/templates/bug-fix.json +17 -0
  7. package/.ctxman/templates/code-review.json +9 -0
  8. package/.ctxman/templates/documentation.json +9 -0
  9. package/.ctxman/templates/feature.json +9 -0
  10. package/.ctxman/templates/full-context.json +9 -0
  11. package/.ctxman/templates/refactoring.json +18 -0
  12. package/.ctxman/templates/testing.json +9 -0
  13. package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
  14. package/LICENSE +21 -0
  15. package/README.md +381 -136
  16. package/bin/cli.js +1880 -529
  17. package/bin/cm-ask.js +18 -67
  18. package/bin/cm-audit.js +121 -0
  19. package/bin/cm-count.js +151 -0
  20. package/bin/cm-gitingest.js +128 -115
  21. package/bin/cm-map.js +242 -0
  22. package/bin/cm-mcp.js +293 -0
  23. package/bin/cm-pack.js +405 -0
  24. package/bin/cm-policy.js +170 -0
  25. package/bin/cm-pr.js +248 -0
  26. package/bin/cm-search.js +178 -0
  27. package/bin/cm-update.js +128 -128
  28. package/bin/cm-why.js +197 -0
  29. package/bin/mcp-server.js +8 -24
  30. package/ctxman.js +237 -183
  31. package/examples/README.md +19 -4
  32. package/examples/github-actions/ctxman-check.yml +49 -0
  33. package/examples/wizard-profiles/security-audit/profile.json +7 -1
  34. package/index.d.ts +1422 -0
  35. package/index.js +55 -31
  36. package/lib/agents/pr-failure-agent.js +48 -45
  37. package/lib/analyzers/ai-suggester.js +451 -0
  38. package/lib/analyzers/dependency-scanner.js +505 -0
  39. package/lib/analyzers/duplicate-detector.js +264 -0
  40. package/lib/analyzers/go-method-analyzer.js +151 -110
  41. package/lib/analyzers/import-tracker.js +431 -0
  42. package/lib/analyzers/method-analyzer.js +1055 -434
  43. package/lib/analyzers/performance-dashboard.js +687 -0
  44. package/lib/analyzers/token-calculator.js +895 -504
  45. package/lib/analyzers/unused-exports-detector.js +336 -0
  46. package/lib/api/mcp/install.js +424 -0
  47. package/lib/api/mcp/prompts.js +281 -281
  48. package/lib/api/mcp/resources.js +300 -241
  49. package/lib/api/mcp/sandbox.js +89 -0
  50. package/lib/api/mcp/server.js +772 -312
  51. package/lib/api/mcp/start.js +125 -0
  52. package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
  53. package/lib/api/rest/server.js +105 -28
  54. package/lib/cache/CacheManager.js +9 -17
  55. package/lib/core/Analyzer.js +16 -10
  56. package/lib/core/ContextBuilder.js +19 -28
  57. package/lib/core/Reporter.js +15 -14
  58. package/lib/core/Scanner.js +81 -12
  59. package/lib/count/index.js +100 -0
  60. package/lib/formatters/format-registry.js +394 -372
  61. package/lib/formatters/gitingest-formatter.js +656 -621
  62. package/lib/formatters/toon-formatter-v1.3.js +637 -632
  63. package/lib/formatters/toon-formatter.js +239 -240
  64. package/lib/integrations/git/BlameTracker.js +4 -4
  65. package/lib/integrations/git/DiffAnalyzer.js +38 -31
  66. package/lib/integrations/git/GitClient.js +71 -53
  67. package/lib/lsp/server.js +311 -0
  68. package/lib/map/graph.js +299 -0
  69. package/lib/map/imports.js +233 -0
  70. package/lib/map/index.js +33 -0
  71. package/lib/map/rank.js +253 -0
  72. package/lib/map/render.js +346 -0
  73. package/lib/map/resolver.js +574 -0
  74. package/lib/pack/budget.js +111 -0
  75. package/lib/pack/code-lexer.js +360 -0
  76. package/lib/pack/compress.js +414 -0
  77. package/lib/pack/index.js +354 -0
  78. package/lib/pack/renderers/common.js +134 -0
  79. package/lib/pack/renderers/index.js +24 -0
  80. package/lib/pack/renderers/json.js +47 -0
  81. package/lib/pack/renderers/markdown.js +54 -0
  82. package/lib/pack/renderers/plain.js +38 -0
  83. package/lib/pack/renderers/xml.js +85 -0
  84. package/lib/pack/select.js +177 -0
  85. package/lib/pack/split.js +182 -0
  86. package/lib/pack/tree.js +35 -0
  87. package/lib/parsers/gitignore-parser.js +116 -107
  88. package/lib/parsers/method-filter-parser.js +50 -33
  89. package/lib/plugins/ExporterPlugin.js +3 -3
  90. package/lib/plugins/LanguagePlugin.js +3 -3
  91. package/lib/plugins/PluginManager.js +7 -13
  92. package/lib/policy/audit.js +312 -0
  93. package/lib/policy/enforce.js +125 -0
  94. package/lib/policy/ignore-files.js +169 -0
  95. package/lib/policy/index.js +53 -0
  96. package/lib/policy/policy.js +489 -0
  97. package/lib/pr/diff-parser.js +267 -0
  98. package/lib/pr/method-mapper.js +148 -0
  99. package/lib/pr/pr-context.js +829 -0
  100. package/lib/pr/pr-git.js +149 -0
  101. package/lib/pr/pr-renderer.js +644 -0
  102. package/lib/pr/related-tests.js +296 -0
  103. package/lib/rag/DataSourcePlugin.js +5 -5
  104. package/lib/rag/EmbeddingProvider.js +19 -8
  105. package/lib/rag/Indexer.js +14 -5
  106. package/lib/rag/VectorStoreFactory.js +3 -3
  107. package/lib/rag/ask.js +122 -0
  108. package/lib/rag/sources/FileDataSource.js +12 -9
  109. package/lib/rag/stores/BaseVectorStore.js +5 -5
  110. package/lib/rag/stores/ChromaDBStore.js +10 -10
  111. package/lib/rag/stores/LanceDBStore.js +29 -19
  112. package/lib/rag/stores/PineconeStore.js +11 -9
  113. package/lib/search/bm25.js +433 -0
  114. package/lib/search/code-units.js +201 -0
  115. package/lib/search/symbols.js +189 -0
  116. package/lib/ui/dashboard.js +120 -89
  117. package/lib/ui/index.js +1 -6
  118. package/lib/ui/init-wizard-ui.js +550 -0
  119. package/lib/ui/progress-bar.js +94 -88
  120. package/lib/ui/select-input.js +40 -32
  121. package/lib/ui/template-selector.js +99 -0
  122. package/lib/ui/wizard.js +374 -329
  123. package/lib/utils/claude-token-counter.js +151 -0
  124. package/lib/utils/cli-output.js +17 -0
  125. package/lib/utils/clipboard-utils.js +49 -50
  126. package/lib/utils/config-utils.js +81 -66
  127. package/lib/utils/context-versioning.js +532 -0
  128. package/lib/utils/error-handler.js +112 -112
  129. package/lib/utils/file-utils.js +153 -69
  130. package/lib/utils/format-converter.js +356 -283
  131. package/lib/utils/git-utils.js +366 -294
  132. package/lib/utils/llm-cost-estimator.js +281 -0
  133. package/lib/utils/llm-detector.js +242 -78
  134. package/lib/utils/logger.js +246 -237
  135. package/lib/utils/messages.js +355 -0
  136. package/lib/utils/multi-repo-manager.js +364 -0
  137. package/lib/utils/profile-manager.js +380 -0
  138. package/lib/utils/safe-read.js +61 -0
  139. package/lib/utils/secret-redactor.js +61 -0
  140. package/lib/utils/snapshot-manager.js +679 -0
  141. package/lib/utils/template-manager.js +378 -0
  142. package/lib/utils/token-utils.js +201 -170
  143. package/lib/utils/tokenizer-adapter.js +421 -377
  144. package/lib/utils/updater.js +539 -506
  145. package/lib/watch/FileWatcher.js +12 -8
  146. package/lib/watch/IncrementalAnalyzer.js +7 -7
  147. package/lib/why/coupling.js +81 -0
  148. package/lib/why/errors.js +23 -0
  149. package/lib/why/git.js +169 -0
  150. package/lib/why/history.js +108 -0
  151. package/lib/why/index.js +720 -0
  152. package/lib/why/ownership.js +79 -0
  153. package/lib/why/paths.js +207 -0
  154. package/lib/why/references.js +259 -0
  155. package/lib/why/render.js +722 -0
  156. package/lib/why/search.js +358 -0
  157. package/lib/why/target.js +389 -0
  158. package/lib/wizards/config-generator.js +257 -0
  159. package/lib/wizards/project-detector.js +572 -0
  160. package/package.json +44 -10
  161. package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
  162. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  163. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  164. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  165. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  166. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  167. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  168. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  169. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  170. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  171. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  172. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  173. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  174. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  175. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  176. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  177. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  178. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  179. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  180. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  181. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  182. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  183. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  184. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  185. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  186. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  187. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  188. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  189. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  190. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  191. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  192. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  193. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  194. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  195. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  196. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  197. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  198. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  199. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  200. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  201. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  202. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  203. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  204. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  205. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  206. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  207. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  208. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  209. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  210. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  211. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  212. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  213. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  214. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  215. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  216. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  217. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  218. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  219. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  220. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  221. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  222. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  223. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  224. package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Signature-only compression for `pack --compress`
3
+ *
4
+ * Keeps everything outside function bodies (imports, class headers, top-level code,
5
+ * signatures) and replaces each body with a one-line placeholder in the language's comment
6
+ * syntax: `{ /* … 42 lines *\/ }` for brace languages, `... # 42 lines` for Python and
7
+ * `# … 42 lines` for Ruby.
8
+ *
9
+ * Which functions exist comes from the existing method analyzers (names and declaration
10
+ * lines). Their `extractMethodSources()` only returns text slices from the declaration line
11
+ * and scans braces from there without knowing whether that line starts inside a string, so
12
+ * body ranges are located here with a string/comment-aware lexer instead. The rules are
13
+ * conservative: a body is elided only when its braces are unambiguous, and a file whose
14
+ * brackets do not balance under the lexer is left whole.
15
+ */
16
+
17
+ import MethodAnalyzer from '../analyzers/method-analyzer.js';
18
+ import { lexCode, familyOf, lineAt, CODE, COMMENT, STRING } from './code-lexer.js';
19
+
20
+ const analyzer = new MethodAnalyzer();
21
+
22
+ // Lines between `)` and a body's `{` (multi-line `where`/`throws` clauses, init lists)
23
+ const MAX_SIGNATURE_LINES = 6;
24
+ // How a signature line may continue on the next line before the body's `{`
25
+ const CONTINUATION = /^(?:where\b|throws\b|->|:|const\b|noexcept\b|override\b|final\b|requires\b)/;
26
+ // The `where` keyword, checked from the character before it
27
+ const WHERE = /^[^\w$]where\b/;
28
+
29
+ // What may follow a declared name up to the `(` of its parameter list
30
+ const GENERIC_TAIL = /[ \t]*(?:<[^<>()\n]*(?:<[^<>()\n]*>[^<>()\n]*)*>)?[ \t]*\(/y;
31
+ const SWIFT_TAIL = /[?!]?[ \t]*(?:<[^<>()\n]*(?:<[^<>()\n]*>[^<>()\n]*)*>)?[ \t]*\(/y;
32
+ const JS_TAILS = [
33
+ GENERIC_TAIL,
34
+ /[ \t]*:[ \t]*(?:async[ \t]+)?function[ \t]*\*?[ \t]*\(/y, // name: function (
35
+ /[ \t]*(?::[^=;()\n]+)?=[ \t]*(?:async[ \t]*)?\(/y, // const name = (...) =>
36
+ ];
37
+
38
+ const plural = (count) => `${count} ${count === 1 ? 'line' : 'lines'}`;
39
+ const escapeRegex = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
40
+
41
+ /**
42
+ * Compress one file
43
+ * @param {string} content - File content
44
+ * @param {string} filePath - Path (its extension picks the language)
45
+ * @returns {{text: string, lineMap: number[]|null, elided: number, supported: boolean}}
46
+ * text: compressed content; lineMap: original 1-based line number of every output line
47
+ * (null when nothing changed); elided: number of bodies replaced; supported: whether the
48
+ * language has a method analyzer
49
+ */
50
+ export function compressCode(content, filePath) {
51
+ const family = familyOf(filePath);
52
+ const unchanged = { text: content, lineMap: null, elided: 0, supported: family !== null };
53
+ if (!family) return unchanged;
54
+
55
+ let methods;
56
+ try {
57
+ methods = analyzer.extractMethods(content, filePath);
58
+ } catch (_error) {
59
+ return unchanged;
60
+ }
61
+ if (!methods.length) return unchanged;
62
+
63
+ let replacements;
64
+ if (family === 'ruby') {
65
+ replacements = rubyReplacements(content, methods);
66
+ } else {
67
+ const lexed = lexCode(content, family);
68
+ if (!lexed.balanced) return unchanged;
69
+ replacements = bodyReplacements(content, lexed, methods, family);
70
+ }
71
+ if (!replacements.length) return unchanged;
72
+
73
+ const compressed = applyReplacements(content, replacements);
74
+ // Last line of defence: brackets that no longer balance mean a range was wrong
75
+ if (family !== 'ruby' && !lexCode(compressed.text, family).balanced) return unchanged;
76
+ return { ...compressed, elided: replacements.length, supported: true };
77
+ }
78
+
79
+ /**
80
+ * Placeholders for brace and Python bodies, outermost first; bodies nested in an elided
81
+ * body are left to it
82
+ */
83
+ function bodyReplacements(src, lexed, methods, family) {
84
+ const declarations = [];
85
+ const seen = new Set();
86
+ for (const method of methods) {
87
+ const name = method.name.split(/\s+/).pop(); // "get value" -> "value"
88
+ const declaration = locateDeclaration(src, lexed, method.line, name, family);
89
+ if (declaration && !seen.has(declaration.parenIdx)) {
90
+ seen.add(declaration.parenIdx);
91
+ declarations.push(declaration);
92
+ }
93
+ }
94
+ declarations.sort((a, b) => a.nameIdx - b.nameIdx);
95
+
96
+ const replacements = [];
97
+ let coveredUntil = -1;
98
+ for (const declaration of declarations) {
99
+ if (declaration.nameIdx < coveredUntil) continue; // nested in an elided body
100
+ const replacement =
101
+ family === 'python'
102
+ ? pythonBody(src, lexed, declaration)
103
+ : braceBody(src, lexed, declaration, family);
104
+ if (!replacement) continue;
105
+ // Only when it is shorter: a one-line body is cheaper than its placeholder
106
+ if (replacement.end - replacement.start <= replacement.text.length) continue;
107
+ replacements.push(replacement);
108
+ coveredUntil = replacement.end;
109
+ }
110
+ return replacements;
111
+ }
112
+
113
+ /**
114
+ * Position of a declared name on its line and of the `(` that opens its parameters
115
+ * @returns {{nameIdx: number, parenIdx: number}|null}
116
+ */
117
+ function locateDeclaration(src, lexed, line, name, family) {
118
+ const { kinds, lineStarts } = lexed;
119
+ if (!name || line < 1 || line > lineStarts.length) return null;
120
+ const start = lineStarts[line - 1];
121
+ const stop = line < lineStarts.length ? lineStarts[line] - 1 : src.length;
122
+ const lineText = src.slice(start, stop);
123
+ const tails = family === 'js' ? JS_TAILS : [family === 'swift' ? SWIFT_TAIL : GENERIC_TAIL];
124
+ const pattern = new RegExp(`(?<![\\w$])${escapeRegex(name)}(?![\\w$])`, 'g');
125
+
126
+ for (const found of lineText.matchAll(pattern)) {
127
+ const nameIdx = start + found.index;
128
+ if (kinds[nameIdx] !== CODE) continue;
129
+ const before = lineText.slice(0, found.index);
130
+ if (family === 'python' && !/(?:^|[^\w])def\s+$/.test(before)) continue;
131
+ // A call, not a declaration: `new Foo(...) {`, `obj.run(...)`, `$this->run(...)`.
132
+ // Kotlin extension functions (`fun String.trimAll(`) are declared with a dot.
133
+ if (/\bnew\s+$/.test(before) || /->\s*$/.test(before)) continue;
134
+ if (family !== 'kotlin' && /\.\s*$/.test(before)) continue;
135
+ for (const tail of tails) {
136
+ tail.lastIndex = nameIdx + name.length;
137
+ if (!tail.test(src)) continue;
138
+ const parenIdx = tail.lastIndex - 1;
139
+ if (kinds[parenIdx] === CODE && lexed.match[parenIdx] > parenIdx) {
140
+ return { nameIdx, parenIdx };
141
+ }
142
+ }
143
+ }
144
+ return null;
145
+ }
146
+
147
+ /** Index of the next code character that is not whitespace, or -1 */
148
+ function nextCode(src, kinds, from) {
149
+ for (let i = from; i < src.length; i++) {
150
+ if (kinds[i] === CODE && !/\s/.test(src[i])) return i;
151
+ }
152
+ return -1;
153
+ }
154
+
155
+ /** Whether only whitespace and comments follow `from` on its line */
156
+ function restOfLineIsTrivia(src, kinds, from) {
157
+ for (let i = from; i < src.length && src[i] !== '\n'; i++) {
158
+ if (kinds[i] === STRING) return false;
159
+ if (kinds[i] === CODE && !/\s/.test(src[i])) return false;
160
+ }
161
+ return true;
162
+ }
163
+
164
+ /** Whether `index` is the first non-whitespace character of its line */
165
+ function startsLine(src, index) {
166
+ for (let i = index - 1; i >= 0 && src[i] !== '\n'; i--) {
167
+ if (!/\s/.test(src[i])) return false;
168
+ }
169
+ return true;
170
+ }
171
+
172
+ /**
173
+ * The body braces after a parameter list, or null when they are not certain. The `{` must
174
+ * end its line and follow the `)` on the same line, alone on the next line (Allman style),
175
+ * or after continuation lines (`where`, `throws`, `:` init lists). Inline brace groups such
176
+ * as TypeScript object types or C++ `x_{x}` initializers are skipped; `;`, `=` (except
177
+ * `=> {` and Scala's `= {`) or a closing bracket mean there is no body.
178
+ */
179
+ function findBodyBraces(src, lexed, closeParen, family) {
180
+ const { kinds, match, lineStarts } = lexed;
181
+ let lineBreaks = 0;
182
+ let atLineStart = false;
183
+ // After `where`, every line up to the body is a bound (`T: Clone,`)
184
+ let inWhereClause = false;
185
+
186
+ for (let i = closeParen + 1; i < src.length; i++) {
187
+ if (kinds[i] !== CODE) continue;
188
+ const ch = src[i];
189
+ if (ch === '\n') {
190
+ if (++lineBreaks > MAX_SIGNATURE_LINES) return null;
191
+ atLineStart = true;
192
+ continue;
193
+ }
194
+ if (/\s/.test(ch)) continue;
195
+ if (atLineStart) {
196
+ atLineStart = false;
197
+ if (ch !== '{' && !inWhereClause && !CONTINUATION.test(src.slice(i, i + 9))) return null;
198
+ }
199
+ if (ch === 'w' && WHERE.test(src.slice(i - 1, i + 6))) inWhereClause = true;
200
+
201
+ if (ch === '{') {
202
+ const close = match[i];
203
+ if (close < 0) return null;
204
+ if (lineAt(lineStarts, close) === lineAt(lineStarts, i)) {
205
+ i = close; // inline group: a type literal or an initializer
206
+ continue;
207
+ }
208
+ if (!restOfLineIsTrivia(src, kinds, i + 1)) return null;
209
+ // `{ ... } {`: the group was a multi-line return type, not the body
210
+ const after = nextCode(src, kinds, close + 1);
211
+ if (
212
+ after !== -1 &&
213
+ src[after] === '{' &&
214
+ lineAt(lineStarts, after) === lineAt(lineStarts, close)
215
+ ) {
216
+ return null;
217
+ }
218
+ return { open: i, close };
219
+ }
220
+ if (ch === '(' || ch === '[') {
221
+ if (match[i] < 0) return null;
222
+ i = match[i];
223
+ continue;
224
+ }
225
+ if (ch === ';' || ch === '}' || ch === ')' || ch === ']') return null;
226
+ if (ch === '=') {
227
+ const next = src[i + 1];
228
+ const arrow = next === '>';
229
+ if (!arrow && !(family === 'scala' && next !== '=')) return null;
230
+ const target = nextCode(src, kinds, i + (arrow ? 2 : 1));
231
+ if (target === -1 || src[target] !== '{') return null;
232
+ i = target - 1;
233
+ }
234
+ }
235
+ return null;
236
+ }
237
+
238
+ /** Placeholder for a brace body: `{ /* … N lines *\/ }` */
239
+ function braceBody(src, lexed, declaration, family) {
240
+ const closeParen = lexed.match[declaration.parenIdx];
241
+ const braces = findBodyBraces(src, lexed, closeParen, family);
242
+ if (!braces) return null;
243
+
244
+ const { lineStarts } = lexed;
245
+ const openLine = lineAt(lineStarts, braces.open);
246
+ const closeLine = lineAt(lineStarts, braces.close);
247
+ const count = closeLine - openLine - 1 + (startsLine(src, braces.close) ? 0 : 1);
248
+ if (count < 1) return null;
249
+
250
+ return { start: braces.open + 1, end: braces.close, text: ` /* … ${plural(count)} */ ` };
251
+ }
252
+
253
+ /** Indentation width and kind of the first non-blank character of a line */
254
+ function describeLine(src, lexed, line) {
255
+ const { kinds, lineStarts } = lexed;
256
+ const start = lineStarts[line];
257
+ const stop = line + 1 < lineStarts.length ? lineStarts[line + 1] - 1 : src.length;
258
+ // Inside a string or comment that began on an earlier line
259
+ const continued = start > 0 && kinds[start - 1] !== CODE;
260
+ let i = start;
261
+ while (i < stop && (src[i] === ' ' || src[i] === '\t' || src[i] === '\r')) i++;
262
+ return {
263
+ continued,
264
+ blank: i >= stop,
265
+ indent: i - start,
266
+ kind: kinds[i],
267
+ stop: src[stop - 1] === '\r' ? stop - 1 : stop,
268
+ };
269
+ }
270
+
271
+ /** Placeholder for a Python body: an indented `... # N lines` line */
272
+ function pythonBody(src, lexed, declaration) {
273
+ const { kinds, match, lineStarts } = lexed;
274
+ const closeParen = match[declaration.parenIdx];
275
+
276
+ // The `:` that ends the signature (after an optional `-> type`) must end its line
277
+ let colon = -1;
278
+ for (let i = closeParen + 1; i < src.length; i++) {
279
+ if (kinds[i] !== CODE) continue;
280
+ const ch = src[i];
281
+ if (ch === '(' || ch === '[' || ch === '{') {
282
+ if (match[i] < 0) return null;
283
+ i = match[i];
284
+ } else if (ch === ':') {
285
+ colon = i;
286
+ break;
287
+ } else if (ch === '\n' || ch === ';' || ch === ')' || ch === ']' || ch === '}') {
288
+ return null;
289
+ }
290
+ }
291
+ if (colon === -1 || !restOfLineIsTrivia(src, kinds, colon + 1)) return null;
292
+
293
+ const colonLine = lineAt(lineStarts, colon);
294
+ const base = describeLine(src, lexed, lineAt(lineStarts, declaration.nameIdx)).indent;
295
+ let first = -1;
296
+ let last = -1;
297
+ for (let line = colonLine + 1; line < lineStarts.length; line++) {
298
+ const info = describeLine(src, lexed, line);
299
+ if (info.continued) {
300
+ if (first !== -1) last = line; // rest of a multi-line string in the body
301
+ continue;
302
+ }
303
+ if (info.blank) continue;
304
+ if (info.kind === COMMENT) {
305
+ if (first !== -1 && info.indent > base) last = line;
306
+ continue;
307
+ }
308
+ if (info.indent <= base) break;
309
+ if (first === -1) first = line;
310
+ last = line;
311
+ }
312
+ if (first === -1) return null;
313
+
314
+ const firstInfo = describeLine(src, lexed, first);
315
+ const firstStart = lineStarts[first];
316
+ const indent = src.slice(firstStart, firstStart + firstInfo.indent);
317
+ // A docstring is documentation, like the comment above a function elsewhere: keep it
318
+ let keptUntil = colonLine;
319
+ if (firstInfo.kind === STRING) {
320
+ let end = firstStart + firstInfo.indent;
321
+ while (end < src.length && kinds[end] === STRING) end++;
322
+ keptUntil = lineAt(lineStarts, end - 1);
323
+ if (!restOfLineIsTrivia(src, kinds, end) || keptUntil >= last) return null;
324
+ }
325
+ return {
326
+ start: lineStarts[keptUntil + 1],
327
+ end: describeLine(src, lexed, last).stop,
328
+ text: `${indent}... # ${plural(last - keptUntil)}`,
329
+ };
330
+ }
331
+
332
+ /** Placeholders for Ruby bodies (`def` ... `end`), found by indentation like the analyzer */
333
+ function rubyReplacements(src, methods) {
334
+ const lines = src.split('\n');
335
+ const starts = [];
336
+ for (let i = 0, offset = 0; i < lines.length; offset += lines[i].length + 1, i++) {
337
+ starts.push(offset);
338
+ }
339
+ const lineStop = (line) => {
340
+ const stop = starts[line] + lines[line].length;
341
+ return src[stop - 1] === '\r' ? stop - 1 : stop;
342
+ };
343
+
344
+ const replacements = [];
345
+ let coveredUntil = -1;
346
+ const ordered = [...new Set(methods.map((method) => method.line - 1))].sort((a, b) => a - b);
347
+ for (const start of ordered) {
348
+ if (start <= coveredUntil) continue; // nested in an elided body
349
+ const declaration = lines[start] ?? '';
350
+ if (!/^\s*def\s/.test(declaration)) continue;
351
+ if (/\bdef\s+[\w.]+[?!]?\s*(?:\([^)]*\))?\s*=/.test(declaration)) continue; // endless method
352
+ const opened = (declaration.match(/\(/g) || []).length;
353
+ if (opened !== (declaration.match(/\)/g) || []).length) continue; // multi-line parameters
354
+
355
+ if (/\bend\b/.test(declaration.replace(/#.*$/, ''))) continue; // one-line `def x() y end`
356
+
357
+ const end = analyzer.findRubyBlockEnd(lines, start);
358
+ if (end <= start + 1) continue;
359
+ // Every body line must be indented deeper than `def` (`rescue`/`ensure`/`else` aside),
360
+ // otherwise the `end` found may close something else
361
+ const base = declaration.length - declaration.trimStart().length;
362
+ const body = lines.slice(start + 1, end);
363
+ const misplaced = body.some((line) => {
364
+ const text = line.trim();
365
+ if (text === '') return false;
366
+ return line.length - line.trimStart().length <= base && !/^(rescue|ensure|else)\b/.test(text);
367
+ });
368
+ if (misplaced) continue;
369
+ const first = body.findIndex((line) => line.trim() !== '');
370
+ if (first === -1) continue;
371
+ const indent = lines[start + 1 + first].match(/^[ \t]*/)[0];
372
+ const replacement = {
373
+ start: starts[start + 1],
374
+ end: lineStop(end - 1),
375
+ text: `${indent}# … ${plural(end - start - 1)}`,
376
+ };
377
+ if (replacement.end - replacement.start <= replacement.text.length) continue;
378
+ replacements.push(replacement);
379
+ coveredUntil = end;
380
+ }
381
+ return replacements;
382
+ }
383
+
384
+ /**
385
+ * Apply non-overlapping replacements (sorted by start) and map every output line to the
386
+ * original line it starts on
387
+ * @returns {{text: string, lineMap: number[]}}
388
+ */
389
+ function applyReplacements(src, replacements) {
390
+ let text = '';
391
+ const lineMap = [1];
392
+ let line = 1;
393
+ let cursor = 0;
394
+
395
+ const keep = (from, to) => {
396
+ const segment = src.slice(from, to);
397
+ for (let i = segment.indexOf('\n'); i !== -1; i = segment.indexOf('\n', i + 1)) {
398
+ lineMap.push(++line);
399
+ }
400
+ text += segment;
401
+ };
402
+
403
+ for (const { start, end, text: placeholder } of replacements) {
404
+ keep(cursor, start);
405
+ text += placeholder;
406
+ const removed = src.slice(start, end);
407
+ for (let i = removed.indexOf('\n'); i !== -1; i = removed.indexOf('\n', i + 1)) line++;
408
+ cursor = end;
409
+ }
410
+ keep(cursor, src.length);
411
+ return { text, lineMap };
412
+ }
413
+
414
+ export default { compressCode };