micro-models-agent 2.11.0 → 2.13.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 (356) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/assets/app.js +22 -2
  3. package/dist/assets/index.html +39 -9
  4. package/dist/assets/js/dom.js +15 -0
  5. package/dist/assets/js/feed.js +129 -10
  6. package/dist/assets/js/sidebar.js +512 -0
  7. package/dist/assets/js/tabs.js +30 -5
  8. package/dist/assets/setup.js +267 -8
  9. package/dist/assets/styles.css +168 -3
  10. package/dist/cli/cache-line.js +1 -0
  11. package/dist/cli/command-suggest.js +5 -4
  12. package/dist/cli/commands.js +231 -122
  13. package/dist/cli/completer.js +132 -31
  14. package/dist/cli/index.js +1 -0
  15. package/dist/cli/json-payload.js +4 -4
  16. package/dist/cli/main.js +166 -61
  17. package/dist/cli/onboarding-commands.js +126 -0
  18. package/dist/cli/plugin-commands.js +2 -1
  19. package/dist/cli/relaunch.js +16 -13
  20. package/dist/cli/repl-commands-lsp.js +75 -0
  21. package/dist/cli/repl-commands-skills.js +105 -0
  22. package/dist/cli/repl-commands.js +589 -266
  23. package/dist/cli/repl.js +436 -177
  24. package/dist/cli/run-result.js +5 -6
  25. package/dist/cli/security-commands.js +37 -28
  26. package/dist/cli/setup-order.js +4 -55
  27. package/dist/cli/setup-prompt.js +3 -2
  28. package/dist/cli/setup.js +27 -107
  29. package/dist/cli/web-command.js +98 -0
  30. package/dist/config/budget.js +5 -5
  31. package/dist/config/config.js +103 -96
  32. package/dist/config/defaults.js +38 -8
  33. package/dist/config/domains.js +24 -27
  34. package/dist/config/env.js +46 -0
  35. package/dist/config/experts.js +2 -0
  36. package/dist/config/index.js +1 -0
  37. package/dist/config/paths.js +56 -0
  38. package/dist/config/redact.js +44 -0
  39. package/dist/config/registry-extra.js +334 -0
  40. package/dist/config/registry.js +584 -0
  41. package/dist/config/security.js +35 -36
  42. package/dist/config/timeouts.js +74 -0
  43. package/dist/config/utils.js +16 -15
  44. package/dist/core/agent/artifact-progress.js +59 -0
  45. package/dist/core/agent/artifact-resolve.js +79 -0
  46. package/dist/core/agent/audit-gate.js +7 -3
  47. package/dist/core/agent/compaction.js +2 -2
  48. package/dist/core/agent/constants.js +33 -4
  49. package/dist/core/agent/context-log.js +454 -0
  50. package/dist/core/agent/context-renderer.js +5 -0
  51. package/dist/core/agent/critic-gate.js +132 -0
  52. package/dist/core/agent/hallucination-gate.js +148 -38
  53. package/dist/core/agent/loop-state.js +15 -0
  54. package/dist/core/agent/prefix-monitor.js +1 -1
  55. package/dist/core/agent/reasoning-resolver.js +10 -1
  56. package/dist/core/agent/token-tracker.js +7 -4
  57. package/dist/core/agent/tool-batch.js +93 -18
  58. package/dist/core/agent/tool-images.js +18 -0
  59. package/dist/core/agent/tool-output.js +65 -11
  60. package/dist/core/agent/tool-pairing.js +60 -0
  61. package/dist/core/agent-moe.js +25 -24
  62. package/dist/core/agent.js +429 -108
  63. package/dist/core/bootstrap/agent-phase.js +66 -0
  64. package/dist/core/bootstrap/config-phase.js +86 -0
  65. package/dist/core/bootstrap/modules-phase.js +269 -0
  66. package/dist/core/bootstrap/provider-phase.js +183 -0
  67. package/dist/core/bootstrap/session-phase.js +204 -0
  68. package/dist/core/bootstrap/state.js +94 -0
  69. package/dist/core/bootstrap/workspace-phase.js +115 -0
  70. package/dist/core/bootstrap.js +92 -660
  71. package/dist/core/crash-handler.js +17 -7
  72. package/dist/core/environment.js +37 -12
  73. package/dist/core/host-plan.js +43 -0
  74. package/dist/core/host.js +23 -0
  75. package/dist/core/index.js +1 -0
  76. package/dist/core/prompt-builder.js +17 -1
  77. package/dist/core/prompt-overflow.js +42 -43
  78. package/dist/core/session-logger.js +122 -30
  79. package/dist/core/system-prompt.js +117 -0
  80. package/dist/core/version.js +6 -4
  81. package/dist/core/web-host.js +34 -0
  82. package/dist/core/workspace.js +19 -15
  83. package/dist/i18n/en.json +89 -3
  84. package/dist/i18n/index.js +37 -3
  85. package/dist/i18n/ru.json +89 -3
  86. package/dist/index.js +7 -3
  87. package/dist/llm/image-utils.js +55 -64
  88. package/dist/llm/llm-errors.js +9 -0
  89. package/dist/llm/model-loader.js +10 -32
  90. package/dist/llm/models-dev.js +121 -0
  91. package/dist/llm/openai-compat.js +146 -73
  92. package/dist/llm/orchestrator.js +33 -11
  93. package/dist/llm/provider-budget.js +2 -2
  94. package/dist/llm/provider-context.js +233 -0
  95. package/dist/llm/provider.js +1 -1
  96. package/dist/llm/response.js +5 -0
  97. package/dist/llm/token-counter.js +27 -11
  98. package/dist/logger/app-logger.js +48 -22
  99. package/dist/logger/file-log.js +28 -19
  100. package/dist/logger/index.js +1 -0
  101. package/dist/main.js +888 -139
  102. package/dist/migration/backup.js +14 -3
  103. package/dist/migration/detect.js +12 -3
  104. package/dist/migration/index.js +1 -0
  105. package/dist/modules/artifacts/store.js +9 -8
  106. package/dist/modules/browser/actions.js +6 -0
  107. package/dist/modules/browser/bridge-client.js +14 -12
  108. package/dist/modules/browser/bridge-path.js +4 -4
  109. package/dist/modules/browser/console-buffer.js +37 -0
  110. package/dist/modules/browser/cookie-store.js +5 -0
  111. package/dist/modules/browser/driver-types.js +1 -0
  112. package/dist/modules/browser/driver.js +19 -11
  113. package/dist/modules/browser/index.js +3 -1
  114. package/dist/modules/browser/module.js +2 -1
  115. package/dist/modules/browser/session.js +10 -30
  116. package/dist/modules/browser/snapshot.js +19 -13
  117. package/dist/modules/browser/types.js +1 -0
  118. package/dist/modules/certification/cli.js +28 -20
  119. package/dist/modules/certification/fact-checker.js +9 -1
  120. package/dist/modules/certification/loader.js +9 -2
  121. package/dist/modules/certification/manifest.js +83 -21
  122. package/dist/modules/certification/runner.js +23 -13
  123. package/dist/modules/certification/scenarios.js +20 -20
  124. package/dist/modules/certification/syntax-scenarios.js +1 -2
  125. package/dist/modules/context/chunk-query.js +11 -9
  126. package/dist/modules/context/fact-extractor.js +37 -39
  127. package/dist/modules/context/index.js +1 -0
  128. package/dist/modules/context/manager.js +106 -80
  129. package/dist/modules/critic/client.js +176 -0
  130. package/dist/modules/critic/evidence.js +81 -0
  131. package/dist/modules/critic/provider.js +45 -0
  132. package/dist/modules/critic/tools.js +78 -0
  133. package/dist/modules/critic/types.js +1 -0
  134. package/dist/modules/execution/audit-runners.js +28 -34
  135. package/dist/modules/execution/auditor.js +53 -49
  136. package/dist/modules/execution/execution-plugin.js +142 -102
  137. package/dist/modules/execution/index.js +1 -1
  138. package/dist/modules/execution/input-from.js +37 -13
  139. package/dist/modules/execution/module.js +368 -191
  140. package/dist/modules/execution/moe-executor.js +53 -26
  141. package/dist/modules/execution/plan-actions.js +320 -202
  142. package/dist/modules/execution/plan-checks.js +35 -0
  143. package/dist/modules/execution/plan-coverage.js +36 -33
  144. package/dist/modules/execution/plan-steps.js +11 -3
  145. package/dist/modules/execution/plan-store.js +83 -17
  146. package/dist/modules/execution/plan-tool.js +89 -35
  147. package/dist/modules/execution/plan-validator.js +30 -10
  148. package/dist/modules/execution/planner.js +22 -13
  149. package/dist/modules/execution/stuck-detector.js +196 -93
  150. package/dist/modules/execution/tracker.js +22 -8
  151. package/dist/modules/execution/verifier.js +69 -48
  152. package/dist/modules/execution/windows-commands.js +12 -12
  153. package/dist/modules/hallucination/artifact-claim.js +93 -0
  154. package/dist/modules/hallucination/confidence.js +13 -11
  155. package/dist/modules/hallucination/consistency.js +10 -2
  156. package/dist/modules/hallucination/detector.js +10 -5
  157. package/dist/modules/hallucination/factual.js +43 -26
  158. package/dist/modules/hallucination/index.js +1 -0
  159. package/dist/modules/hallucination/js-identifiers.js +44 -45
  160. package/dist/modules/hallucination/llm-judge.js +17 -12
  161. package/dist/modules/hallucination/success-claim.js +59 -0
  162. package/dist/modules/indexer/cache.js +52 -4
  163. package/dist/modules/indexer/index.js +1 -0
  164. package/dist/modules/indexer/map-command.js +2 -4
  165. package/dist/modules/indexer/map-select.js +16 -16
  166. package/dist/modules/indexer/module.js +47 -12
  167. package/dist/modules/indexer/project-profile.js +8 -7
  168. package/dist/modules/indexer/symbols.js +15 -15
  169. package/dist/modules/indexer/walker.js +159 -29
  170. package/dist/modules/lsp/check-tool.js +7 -5
  171. package/dist/modules/lsp/client.js +39 -39
  172. package/dist/modules/lsp/command.js +17 -24
  173. package/dist/modules/lsp/config.js +12 -11
  174. package/dist/modules/lsp/index.js +1 -0
  175. package/dist/modules/lsp/module.js +48 -46
  176. package/dist/modules/lsp/probe.js +13 -14
  177. package/dist/modules/lsp/project-root.js +14 -17
  178. package/dist/modules/lsp/startup-check.js +18 -16
  179. package/dist/modules/mcp/client.js +59 -62
  180. package/dist/modules/mcp/index.js +1 -0
  181. package/dist/modules/mcp/module.js +66 -37
  182. package/dist/modules/mcp/registry.js +5 -0
  183. package/dist/modules/memory/index.js +1 -0
  184. package/dist/modules/memory/module.js +12 -8
  185. package/dist/modules/memory/search.js +4 -1
  186. package/dist/modules/memory/store.js +40 -2
  187. package/dist/modules/onboarding/init.js +47 -0
  188. package/dist/modules/onboarding/meet.js +30 -0
  189. package/dist/modules/onboarding/mode.js +30 -0
  190. package/dist/modules/onboarding/prompts.js +43 -0
  191. package/dist/modules/pipelines/engine.js +10 -0
  192. package/dist/modules/pipelines/index.js +1 -0
  193. package/dist/modules/pipelines/parser.js +4 -1
  194. package/dist/modules/pipelines/template.js +4 -0
  195. package/dist/modules/plugins/builtin/lint-on-write.js +80 -67
  196. package/dist/modules/plugins/builtin/notify.js +2 -0
  197. package/dist/modules/plugins/index.js +1 -0
  198. package/dist/modules/plugins/loader.js +18 -3
  199. package/dist/modules/plugins/manager.js +37 -17
  200. package/dist/modules/pricing/index.js +10 -11
  201. package/dist/modules/pricing/prices.js +12 -12
  202. package/dist/modules/processes/index.js +1 -0
  203. package/dist/modules/processes/kill-tree.js +27 -31
  204. package/dist/modules/processes/registry.js +44 -25
  205. package/dist/modules/processes/runner.js +4 -4
  206. package/dist/modules/providers/capabilities.js +28 -0
  207. package/dist/modules/providers/create.js +3 -4
  208. package/dist/modules/providers/factory.js +7 -7
  209. package/dist/modules/providers/fallback.js +12 -12
  210. package/dist/modules/providers/health.js +4 -4
  211. package/dist/modules/providers/manager.js +50 -19
  212. package/dist/modules/providers/naming.js +15 -0
  213. package/dist/modules/providers/preflight.js +71 -0
  214. package/dist/modules/providers/presets.js +39 -8
  215. package/dist/modules/providers/registry.js +7 -2
  216. package/dist/modules/reasoning/policy.js +8 -6
  217. package/dist/modules/reasoning/probe.js +30 -25
  218. package/dist/modules/security/audit-log.js +19 -36
  219. package/dist/modules/security/audit-notifier.js +35 -75
  220. package/dist/modules/security/command-validator.js +99 -51
  221. package/dist/modules/security/content-scanner.js +8 -8
  222. package/dist/modules/security/data-sanitizer.js +9 -16
  223. package/dist/modules/security/encryption.js +32 -52
  224. package/dist/modules/security/index.js +2 -1
  225. package/dist/modules/security/network-validator.js +120 -43
  226. package/dist/modules/security/path-validator.js +61 -46
  227. package/dist/modules/security/protected-paths.js +73 -0
  228. package/dist/modules/security/rate-limiter.js +17 -49
  229. package/dist/modules/security/security-policies.js +25 -57
  230. package/dist/modules/security/session-encryption.js +32 -71
  231. package/dist/modules/security/session-isolation.js +15 -28
  232. package/dist/modules/session/auto-name.js +63 -0
  233. package/dist/modules/session/export.js +98 -0
  234. package/dist/modules/session/index.js +1 -0
  235. package/dist/modules/session/manager.js +79 -28
  236. package/dist/modules/session/module.js +4 -3
  237. package/dist/modules/session/store.js +57 -20
  238. package/dist/modules/setup/answers.js +35 -0
  239. package/dist/modules/setup/apply.js +79 -0
  240. package/dist/modules/setup/discovery.js +72 -0
  241. package/dist/modules/skills/index.js +1 -0
  242. package/dist/modules/skills/loader.js +9 -1
  243. package/dist/modules/skills/module.js +11 -0
  244. package/dist/modules/updater/changelog-reader.js +15 -16
  245. package/dist/modules/updater/checker.js +9 -4
  246. package/dist/modules/updater/dev-detect.js +7 -6
  247. package/dist/modules/updater/index.js +1 -0
  248. package/dist/modules/updater/module.js +17 -15
  249. package/dist/modules/user-profile/compressor.js +2 -0
  250. package/dist/modules/user-profile/index.js +1 -0
  251. package/dist/modules/user-profile/profile.js +10 -1
  252. package/dist/modules/webui/answer-api.js +29 -0
  253. package/dist/modules/webui/assets.js +50 -0
  254. package/dist/modules/webui/body.js +21 -0
  255. package/dist/modules/webui/commands-settings.js +209 -0
  256. package/dist/modules/webui/commands-shared.js +60 -0
  257. package/dist/modules/webui/commands.js +174 -0
  258. package/dist/modules/webui/config-api.js +134 -0
  259. package/dist/modules/webui/file-api.js +202 -0
  260. package/dist/modules/webui/forward.js +159 -0
  261. package/dist/modules/webui/history-api.js +112 -0
  262. package/dist/modules/webui/host-state.js +60 -0
  263. package/dist/modules/webui/i18n-api.js +9 -0
  264. package/dist/modules/webui/limits.js +6 -0
  265. package/dist/modules/webui/log-forward.js +20 -0
  266. package/dist/modules/webui/memory-commands.js +64 -0
  267. package/dist/modules/webui/model-cache.js +18 -0
  268. package/dist/modules/webui/module.js +148 -0
  269. package/dist/modules/webui/persist.js +24 -0
  270. package/dist/modules/webui/prompt-io.js +97 -0
  271. package/dist/modules/webui/providers-api.js +337 -0
  272. package/dist/modules/webui/providers-view.js +56 -0
  273. package/dist/modules/webui/reasoning-view.js +15 -0
  274. package/dist/modules/webui/run-agent.js +81 -0
  275. package/dist/modules/webui/server.js +284 -0
  276. package/dist/modules/webui/sessions-api.js +178 -0
  277. package/dist/modules/webui/settings-api.js +561 -0
  278. package/dist/modules/webui/setup-api.js +201 -0
  279. package/dist/modules/webui/state.js +74 -0
  280. package/dist/modules/webui/util.js +261 -0
  281. package/dist/output/bus.js +7 -2
  282. package/dist/output/channel.js +80 -28
  283. package/dist/output/format.js +2 -1
  284. package/dist/output/index.js +1 -0
  285. package/dist/output/json-sink.js +5 -3
  286. package/dist/output/machine.js +4 -3
  287. package/dist/output/session-sink.js +4 -4
  288. package/dist/tools/approve.js +4 -0
  289. package/dist/tools/attach-image.js +5 -4
  290. package/dist/tools/auto-fixer.js +26 -44
  291. package/dist/tools/auto-tags.js +83 -0
  292. package/dist/tools/bash.js +193 -116
  293. package/dist/tools/browser.js +76 -31
  294. package/dist/tools/chunk-query.js +14 -6
  295. package/dist/tools/create-dir.js +9 -5
  296. package/dist/tools/delete-file.js +9 -5
  297. package/dist/tools/download-file.js +32 -7
  298. package/dist/tools/edit-file.js +52 -14
  299. package/dist/tools/enable-tools.js +6 -6
  300. package/dist/tools/executor.js +132 -27
  301. package/dist/tools/file-info.js +3 -1
  302. package/dist/tools/filter-tools.js +3 -3
  303. package/dist/tools/glob-tool.js +4 -1
  304. package/dist/tools/grep-tool.js +13 -10
  305. package/dist/tools/hidden-tools-block.js +6 -6
  306. package/dist/tools/index.js +16 -8
  307. package/dist/tools/list-dir.js +4 -2
  308. package/dist/tools/load-skill.js +7 -0
  309. package/dist/tools/mcp-call.js +15 -4
  310. package/dist/tools/meet-done.js +14 -0
  311. package/dist/tools/move-file.js +14 -7
  312. package/dist/tools/path-utils.js +29 -29
  313. package/dist/tools/pipeline-run.js +29 -7
  314. package/dist/tools/preview.js +1 -1
  315. package/dist/tools/process-ids.js +31 -0
  316. package/dist/tools/process-kill.js +9 -16
  317. package/dist/tools/process-list.js +1 -0
  318. package/dist/tools/process-log.js +11 -16
  319. package/dist/tools/process-wait.js +63 -0
  320. package/dist/tools/prompt-io.js +4 -3
  321. package/dist/tools/question.js +172 -25
  322. package/dist/tools/read-file.js +55 -34
  323. package/dist/tools/recall.js +14 -8
  324. package/dist/tools/registry.js +6 -5
  325. package/dist/tools/remember.js +8 -4
  326. package/dist/tools/scope-request.js +11 -6
  327. package/dist/tools/search-history.js +5 -2
  328. package/dist/tools/session-info.js +2 -0
  329. package/dist/tools/set-thinking.js +8 -4
  330. package/dist/tools/subagent.js +59 -39
  331. package/dist/tools/syntax-validator.js +292 -90
  332. package/dist/tools/user-input.js +22 -18
  333. package/dist/tools/web-browse.js +9 -3
  334. package/dist/tools/web-fetch.js +12 -23
  335. package/dist/tools/web-search.js +7 -5
  336. package/dist/tools/write-file.js +45 -15
  337. package/dist/ui/action-log.js +13 -0
  338. package/dist/ui/box.js +4 -3
  339. package/dist/ui/colors.js +2 -0
  340. package/dist/ui/diff.js +6 -5
  341. package/dist/ui/index.js +1 -0
  342. package/dist/ui/line-editor.js +230 -122
  343. package/dist/ui/line-math.js +10 -2
  344. package/dist/ui/md-formatter.js +57 -37
  345. package/dist/ui/output.js +8 -3
  346. package/dist/ui/plan-view.js +21 -10
  347. package/dist/ui/renderer.js +393 -67
  348. package/dist/ui/spinner.js +16 -5
  349. package/dist/ui/table.js +15 -17
  350. package/dist/ui/theme.js +143 -0
  351. package/dist/utils/child-run.js +20 -0
  352. package/dist/utils/error.js +1 -1
  353. package/dist/utils/index.js +2 -0
  354. package/dist/utils/retry.js +2 -2
  355. package/dist/utils/truncate.js +1 -1
  356. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { toForwardSlash } from "../../tools/path-utils";
2
2
  import { isCodeLanguage } from "./symbols";
3
- /** Lockfiles and generated manifests: no structural value in the map. */
3
+ /** Локфайлы и генерированные манифесты: структурной ценности в карте нет. */
4
4
  const NOISE_BASENAMES = new Set([
5
5
  "package-lock.json",
6
6
  "bun.lock",
@@ -12,6 +12,7 @@ const NOISE_BASENAMES = new Set([
12
12
  "composer.lock",
13
13
  "poetry.lock",
14
14
  ]);
15
+ /** True, если файл — локфайловый шум (не попадает в карту). */
15
16
  export function isNoiseFile(file) {
16
17
  const base = toForwardSlash(file.path).split("/").pop()?.toLowerCase() ?? "";
17
18
  return NOISE_BASENAMES.has(base);
@@ -24,7 +25,7 @@ function isTestFile(path) {
24
25
  path.includes("__tests__/") ||
25
26
  /\.(test|spec)\.[a-z0-9]+$/i.test(path));
26
27
  }
27
- /** Lower rank = more useful for understanding the project structure. */
28
+ /** Ниже ранг — полезнее для понимания структуры проекта. */
28
29
  function rank(file) {
29
30
  const path = toForwardSlash(file.path).toLowerCase();
30
31
  if (isTestFile(path))
@@ -33,13 +34,13 @@ function rank(file) {
33
34
  return 0;
34
35
  return 2;
35
36
  }
36
- /** Top-level segment of a path (`src/core/x.ts` → `src`; root file → its name). */
37
+ /** Верхний сегмент пути (`src/core/x.ts` → `src`; файл корня → его имя). */
37
38
  function topDir(path) {
38
39
  const normalized = toForwardSlash(path);
39
40
  const slash = normalized.indexOf("/");
40
41
  return slash >= 0 ? normalized.slice(0, slash) : normalized;
41
42
  }
42
- /** How many source-code files live under each top-level directory. */
43
+ /** Сколько файлов исходного кода под каждым верхнеуровневым каталогом. */
43
44
  function codeCountByTopDir(files) {
44
45
  const counts = new Map();
45
46
  for (const file of files) {
@@ -51,19 +52,18 @@ function codeCountByTopDir(files) {
51
52
  return counts;
52
53
  }
53
54
  /**
54
- * Pick the files to list in the project-map prompt block.
55
+ * Выбирает файлы для блока карты проекта в промпте.
55
56
  *
56
- * The index is built by an arbitrary `readdir` walk, so a naive
57
- * `files.slice(0, N)` can end up listing only docs, config and tests while the
58
- * actual source tree (`src/`) never appears — the model then sees a "project
59
- * map" with no code in it. The selection:
60
- * 1. drops lockfile noise,
61
- * 2. ranks source before tests, tests before docs/config,
62
- * 3. within a rank, puts directories with the most source files first (the
63
- * densest code root is almost always the real source tree — no directory
64
- * names hardcoded),
65
- * 4. then shallower paths, then alphabetical — deterministic so the block
66
- * does not change between turns and the prefix KV-cache survives.
57
+ * Индекс строится произвольным обходом `readdir`, так что наивный
58
+ * `files.slice(0, N)` может перечислить одни доки, конфиги и тесты, а реальное
59
+ * дерево исходников (`src/`) не показать — модель увидит "карту проекта" без
60
+ * кода. Отбор:
61
+ * 1. выкидывает локфайловый шум,
62
+ * 2. ранжирует исходники выше тестов, тесты выше доков/конфигов,
63
+ * 3. внутри ранга первыми — каталоги с плотнейшим кодом (плотнейший корень
64
+ * почти всегда настоящее дерево исходников — имён каталогов не захардкожено),
65
+ * 4. затем мельче пути, затем алфавит — детерминированно, чтобы блок не
66
+ * менялся между ходами и выживал префиксный KV-кэш.
67
67
  */
68
68
  export function selectMapFiles(files, max) {
69
69
  const codeByDir = codeCountByTopDir(files);
@@ -1,3 +1,4 @@
1
+ // Модуль индексатора: построение карты проекта, тул project_map, вотчер.
1
2
  import { dirname } from "path";
2
3
  import { Indexer } from "./walker";
3
4
  import { IndexCache } from "./cache";
@@ -6,20 +7,29 @@ import { buildProjectProfileLine } from "./project-profile";
6
7
  import { t } from "../../i18n/index";
7
8
  import { toForwardSlash } from "../../tools/path-utils";
8
9
  import { estimateTokens } from "../../llm/token-counter";
9
- /** Max files listed in the project-map prompt block (code first; see map-select). */
10
+ /** Макс. файлов в блоке карты проекта (сначала код; см. map-select). */
10
11
  const MAP_FILE_LIMIT = 80;
12
+ /** Модуль индексатора: кэш, карта проекта для промпта, тул project_map, вотчер. */
11
13
  export class IndexerModule {
12
14
  name = "indexer";
13
15
  indexer;
14
16
  cache;
15
17
  baseDir;
16
18
  index = null;
19
+ statsCache = null;
17
20
  rebuildTimeout = null;
18
21
  constructor(opts) {
19
22
  this.baseDir = opts.baseDir;
20
23
  this.indexer = new Indexer(opts.baseDir);
21
- this.cache = new IndexCache(opts.cacheDir);
24
+ // Привязываем кэш к корню проекта + текущей сигнатуре дерева, чтобы протухший
25
+ // `index-cache.json` (файлы добавлены/удалены/правлены после обхода) не
26
+ // утёк устаревшей картой в системный промпт, а перестроился.
27
+ this.cache = new IndexCache(opts.cacheDir, {
28
+ root: opts.baseDir,
29
+ signature: () => this.indexer.scanSignature(),
30
+ });
22
31
  }
32
+ /** Строит индекс (кэш → обход), null при неустранимой ошибке. */
23
33
  async buildIndex() {
24
34
  if (this.index)
25
35
  return this.index;
@@ -38,11 +48,13 @@ export class IndexerModule {
38
48
  return null;
39
49
  }
40
50
  }
51
+ /** Принудительно перестраивает индекс (сброс кэша + обход). */
41
52
  async refresh() {
42
53
  this.cache.invalidate();
43
54
  this.index = null;
44
55
  return this.buildIndex();
45
56
  }
57
+ /** Ищет файлы по подстроке пути/экспорта (пусто без индекса). */
46
58
  find(query) {
47
59
  if (!this.index)
48
60
  return [];
@@ -52,7 +64,7 @@ export class IndexerModule {
52
64
  return path.includes(q) || f.exports.some((e) => e.toLowerCase().includes(q));
53
65
  });
54
66
  }
55
- /** Human/model-readable result of a `find` query (shared by the tool and `mma map`). */
67
+ /** Человеко/модельно-читаемый результат `find` (общий для тула и `mma map`). */
56
68
  formatFind(query) {
57
69
  const matches = this.find(query);
58
70
  if (matches.length === 0) {
@@ -67,10 +79,11 @@ export class IndexerModule {
67
79
  .join("\n");
68
80
  return t("indexer.find_results", { count: matches.length, results: lines });
69
81
  }
70
- /** The exact project-map text injected into the system prompt (null if not indexed). */
82
+ /** Точный текст карты проекта для системного промпта (null без индекса). */
71
83
  getPromptMap() {
72
84
  return this.index ? this.formatMap(this.index) : null;
73
85
  }
86
+ /** Подписка на изменения файлов с дебаунсом перестройки (best-effort). */
74
87
  watch() {
75
88
  this.unwatch();
76
89
  try {
@@ -85,9 +98,10 @@ export class IndexerModule {
85
98
  });
86
99
  }
87
100
  catch {
88
- // Watching is best-effort; manual refresh still works.
101
+ // Вотчинг best-effort; ручной refresh всё равно работает.
89
102
  }
90
103
  }
104
+ /** Отписывается от вотчера и чистит дебаунс-таймер. */
91
105
  unwatch() {
92
106
  if (this.rebuildTimeout) {
93
107
  clearTimeout(this.rebuildTimeout);
@@ -95,6 +109,7 @@ export class IndexerModule {
95
109
  }
96
110
  this.indexer.unwatch();
97
111
  }
112
+ /** Блок системного промпта с картой проекта (null без индекса). */
98
113
  getSystemPromptBlock() {
99
114
  if (!this.index)
100
115
  return null;
@@ -107,9 +122,30 @@ export class IndexerModule {
107
122
  kind: "project-map",
108
123
  };
109
124
  }
125
+ /**
126
+ * Компактная статистика карты для сайдбара хоста (`null` до конца обхода).
127
+ * Кэшируется на объект индекса, чтобы поллинг `/api/state` не ререндерил
128
+ * всю карту.
129
+ */
130
+ stats() {
131
+ if (!this.index)
132
+ return null;
133
+ if (this.statsCache && this.statsCache.index === this.index)
134
+ return this.statsCache.value;
135
+ const symbols = this.index.files.reduce((n, f) => n + f.exports.length, 0);
136
+ const value = {
137
+ files: this.index.files.length,
138
+ symbols,
139
+ tokens: this.estimateTokens(this.formatMap(this.index)),
140
+ };
141
+ this.statsCache = { index: this.index, value };
142
+ return value;
143
+ }
144
+ /** Определения тулов модуля (project_map). */
110
145
  getToolDefinitions() {
111
146
  return [this.createProjectMapTool()];
112
147
  }
148
+ /** Плагин модуля: индекс + вотчер на старт сессии, отписка на конец. */
113
149
  getPlugin() {
114
150
  return {
115
151
  name: "indexer",
@@ -147,18 +183,17 @@ export class IndexerModule {
147
183
  ...(stackLine ? [stackLine] : []),
148
184
  `${t("indexer.top_directories")}: ${topDirs}`,
149
185
  ...(duplicates.length > 0 ? [t("indexer.duplicates", { names: duplicates.join(", ") })] : []),
150
- `${t("indexer.files")}:` + (fileLines.length > 0 ? "" : " " + t("indexer.empty")),
186
+ `${t("indexer.files")}:${fileLines.length > 0 ? "" : ` ${t("indexer.empty")}`}`,
151
187
  ...fileLines,
152
188
  more,
153
189
  ].join("\n");
154
190
  }
155
191
  /**
156
- * Find files that share a basename across different extensions in the same
157
- * directory (e.g. `ProductCard.js` + `ProductCard.tsx`). This is a purely
158
- * structural fact about the file tree — a stale transpiled twin may shadow
159
- * the real source at build time. Flagging it is not task classification, so
160
- * it does not violate the no-keyword-matching rule; the model decides what
161
- * to do with the information.
192
+ * Находит файлы с общим basename при разных расширениях в одном каталоге
193
+ * (напр. `ProductCard.js` + `ProductCard.tsx`). Чисто структурный факт дерева —
194
+ * протухший транспилированный близнец может затенять реальный исходник на
195
+ * сборке. Подсветить его — не классификация задач, правило no-keyword-matching
196
+ * не нарушает; что делать с информацией — решает модель.
162
197
  */
163
198
  findDuplicateBasenames(result) {
164
199
  const byDir = {};
@@ -1,3 +1,4 @@
1
+ // Профиль проекта: чтение манифеста (package.json/pyproject/Cargo/go.mod) в одну строку.
1
2
  import { readFileSync, existsSync } from "fs";
2
3
  import { join } from "path";
3
4
  import { t } from "../../i18n/index";
@@ -12,7 +13,7 @@ const MAX_DEPS = 5;
12
13
  const MAX_DEV_DEPS = 3;
13
14
  const MAX_SCRIPTS = 3;
14
15
  /**
15
- * Find the first existing manifest in priority order (best-effort, root only).
16
+ * Находит первый существующий манифест по приоритету (best-effort, только корень).
16
17
  */
17
18
  export function detectManifest(baseDir) {
18
19
  for (const manifest of MANIFEST_ORDER) {
@@ -21,7 +22,7 @@ export function detectManifest(baseDir) {
21
22
  }
22
23
  return null;
23
24
  }
24
- /** Strip version specifiers from a manifest dependency entry. */
25
+ /** Чистит спеки версий из записи зависимости манифеста (имя без версии). */
25
26
  function cleanDependency(entry) {
26
27
  let name = entry.trim();
27
28
  const eq = name.indexOf("=");
@@ -135,8 +136,8 @@ function readRequirements(baseDir) {
135
136
  }
136
137
  }
137
138
  /**
138
- * Read a compact project profile from the workspace manifest (best-effort).
139
- * Returns null when no readable manifest is present.
139
+ * Читает компактный профиль проекта из манифеста (best-effort).
140
+ * Null, если читаемого манифеста нет.
140
141
  */
141
142
  export function readProjectProfile(baseDir) {
142
143
  const manifest = detectManifest(baseDir);
@@ -157,7 +158,7 @@ export function readProjectProfile(baseDir) {
157
158
  return null;
158
159
  }
159
160
  }
160
- /** One-line stack-neutral summary of the project profile. */
161
+ /** Однострочная стек-нейтральная сводка профиля проекта. */
161
162
  export function formatProjectProfile(profile) {
162
163
  const parts = [];
163
164
  if (profile.deps.length > 0) {
@@ -174,8 +175,8 @@ export function formatProjectProfile(profile) {
174
175
  return `[Stack: ${profile.runtime}${name} — ${parts.join(" | ")}]`;
175
176
  }
176
177
  /**
177
- * Convenience: detect + read + format in one call. Returns null when the
178
- * workspace has no readable manifest.
178
+ * Удобно: detect + read + format одним вызовом. Null, если в проекте нет
179
+ * читаемого манифеста.
179
180
  */
180
181
  export function buildProjectProfileLine(baseDir) {
181
182
  const profile = readProjectProfile(baseDir);
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Language registry and symbol extraction for the project index.
2
+ * Реестр языков и извлечение символов для индекса проекта.
3
3
  *
4
- * Adding a language = one entry in `RULES` + one or more extensions in
5
- * `EXT_TO_LANGUAGE`. Each rule declares whether the language is source code
6
- * (used for map ranking) and how to pull public/exported symbol names out of
7
- * a file. Extraction is regex-based on purpose (no parser dependency): it is
8
- * cheap, runs on every indexed file, and only needs enough structure to make
9
- * the project map useful — not a full AST.
4
+ * Новый язык = одна запись в `RULES` + одно или больше расширений в
5
+ * `EXT_TO_LANGUAGE`. Каждое правило объявляет, код ли это (для ранжирования
6
+ * карты) и как тянуть публичные/экспортные имена из файла. Извлечение
7
+ * regex-based осознанно (без зависимости от парсера): дёшево, гоняется на
8
+ * каждом индексируемом файле, и структуры хватает для полезности карты —
9
+ * полный AST не нужен.
10
10
  */
11
- /** Cap per file so a single generated file cannot blow up the map. */
11
+ /** Кап на файл: один генерированный файл не раздувает карту. */
12
12
  export const MAX_SYMBOLS_PER_FILE = 40;
13
- /** Unique preserved-order names. */
13
+ /** Уникальные имена с сохранением порядка. */
14
14
  function dedupe(names) {
15
15
  const seen = new Set();
16
16
  const out = [];
@@ -22,7 +22,7 @@ function dedupe(names) {
22
22
  }
23
23
  return out;
24
24
  }
25
- /** Collect captured group 1 from a list of global (multiline) regexes. */
25
+ /** Собирает группу 1 совпадений по списку глобальных (многострочных) regex'ов. */
26
26
  function collect(content, patterns) {
27
27
  const names = [];
28
28
  for (const re of patterns) {
@@ -37,7 +37,7 @@ function collect(content, patterns) {
37
37
  }
38
38
  return names;
39
39
  }
40
- /** Names from `export { a, b as c }` / `export type { X }` clauses. */
40
+ /** Имена из клауз `export { a, b as c }` / `export type { X }`. */
41
41
  function collectExportClauses(content) {
42
42
  const names = [];
43
43
  const re = /export\s+(?:type\s+)?\{([^}]*)\}/g;
@@ -168,22 +168,22 @@ const EXT_TO_LANGUAGE = {
168
168
  ".scss": "css",
169
169
  ".less": "css",
170
170
  };
171
- /** Language id for a file extension (lowercase, with dot), or null if not indexed. */
171
+ /** Id языка по расширению (lowercase, с точкой), или null если не индексируем. */
172
172
  export function languageIdForExt(ext) {
173
173
  return EXT_TO_LANGUAGE[ext.toLowerCase()] ?? null;
174
174
  }
175
- /** True when the language is source code (affects project-map ranking). */
175
+ /** True, если язык — исходный код (влияет на ранжирование карты). */
176
176
  export function isCodeLanguage(languageId) {
177
177
  return RULES[languageId]?.code ?? false;
178
178
  }
179
- /** Extract public/exported symbol names from file content. */
179
+ /** Извлекает публичные/экспортные имена символов из содержимого файла. */
180
180
  export function extractSymbols(content, languageId) {
181
181
  const rule = RULES[languageId];
182
182
  if (!rule)
183
183
  return [];
184
184
  return dedupe(rule.extract(content)).slice(0, MAX_SYMBOLS_PER_FILE);
185
185
  }
186
- /** All indexed extensions (for docs/tests). */
186
+ /** Все индексируемые расширения (для доков/тестов). */
187
187
  export function indexedExtensions() {
188
188
  return Object.keys(EXT_TO_LANGUAGE).sort();
189
189
  }
@@ -1,14 +1,16 @@
1
- import { readdirSync, readFileSync, statSync, lstatSync, existsSync, watch } from "fs";
1
+ // Обход дерева проекта: индекс файлов с символами, вотчер, сигнатура дерева.
2
+ import { readdirSync, readFileSync, statSync, lstatSync, existsSync, watch, } from "fs";
3
+ import { createHash } from "node:crypto";
2
4
  import { join, relative, extname } from "path";
3
5
  import { Logger } from "../../logger/app-logger";
4
6
  import { languageIdForExt, extractSymbols } from "./symbols";
5
7
  import { t } from "../../i18n/index";
6
8
  const logger = new Logger("warn", "indexer");
7
9
  /**
8
- * Directories that never hold project source but would flood the index:
9
- * VCS/build output, package caches and virtual environments across
10
- * ecosystems (Node, Python, Rust, Go/PHP/Ruby, JVM, .NET, Apple, IDE).
11
- * Stored lowercase — matching is case-insensitive (Windows/macOS).
10
+ * Каталоги, где исходников проекта не бывает, но индекс бы утонул:
11
+ * VCS/артефакты сборки, кэши пакетов и виртуалки всех экосистем
12
+ * (Node, Python, Rust, Go/PHP/Ruby, JVM, .NET, Apple, IDE).
13
+ * Хранятся в lowercase — сравнение регистронезависимо (Windows/macOS).
12
14
  */
13
15
  const IGNORE_DIRS = new Set([
14
16
  // VCS / MMA
@@ -16,7 +18,13 @@ const IGNORE_DIRS = new Set([
16
18
  ".hg",
17
19
  ".svn",
18
20
  ".mma",
19
- // Node / JS bundlers
21
+ // Каталоги dev/agent/тулинга MMA — не исходники проекта (зеркало .gitignore:
22
+ // карта dev-чекаута не тонет в тестовых песочницах / агентском скратче).
23
+ "_testing",
24
+ ".superpowers",
25
+ ".opencode",
26
+ ".fallow",
27
+ // Node / JS-бандлеры
20
28
  "node_modules",
21
29
  "dist",
22
30
  "build",
@@ -51,22 +59,45 @@ const IGNORE_DIRS = new Set([
51
59
  // Apple
52
60
  "pods",
53
61
  "deriveddata",
54
- // IDE / editor
62
+ // IDE / редакторы
55
63
  ".idea",
56
64
  ".vscode",
57
65
  ]);
58
- /** True when a directory name is ignored (case-insensitive). */
66
+ /** True, если имя каталога игнорируется (регистронезависимо). */
59
67
  export function isIgnoredDirName(name) {
60
68
  return IGNORE_DIRS.has(name.toLowerCase());
61
69
  }
70
+ /** Локфайлы — генерированы, не исходники проекта. */
71
+ const IGNORE_FILE_NAMES = new Set([
72
+ "package-lock.json",
73
+ "bun.lock",
74
+ "bun.lockb",
75
+ "yarn.lock",
76
+ "pnpm-lock.yaml",
77
+ // Вендоренные веб-бандлы (минифицированы; их символы — шум).
78
+ "hljs.js",
79
+ "smd.js",
80
+ ]);
81
+ /** Семейства конфигов/бейзлайнов тулинга — не исходники проекта. */
82
+ const IGNORE_FILE_PREFIXES = [".fallow"];
83
+ /** True, если имя файла игнорируется (регистронезависимый префикс/точное совпадение). */
84
+ export function isIgnoredFileName(name) {
85
+ const lower = name.toLowerCase();
86
+ return (IGNORE_FILE_NAMES.has(lower) ||
87
+ lower.endsWith(".min.js") ||
88
+ IGNORE_FILE_PREFIXES.some((p) => lower.startsWith(p)));
89
+ }
62
90
  /**
63
- * True when any path SEGMENT is an ignored directory. Segment-based (not a
64
- * substring) so short names like `env`/`out`/`obj` do not black out
65
- * `environment.ts` or `about/`.
91
+ * True, если ЛЮБОЙ СЕГМЕНТ пути — игнорируемый каталог. Посегментно (не
92
+ * подстрока): короткие имена `env`/`out`/`obj` не должны вычёркивать
93
+ * `environment.ts` или `about/`.
66
94
  */
67
95
  function isIgnoredPath(filename) {
68
- return filename.split(/[\\/]/).some((segment) => isIgnoredDirName(segment));
96
+ const segments = filename.split(/[\\/]/);
97
+ const base = segments[segments.length - 1] ?? "";
98
+ return segments.some((segment) => isIgnoredDirName(segment)) || isIgnoredFileName(base);
69
99
  }
100
+ /** Индексер: обход исходников, вотчер, сигнатура дерева, сводка. */
70
101
  export class Indexer {
71
102
  baseDir;
72
103
  MAX_FILES = 1000;
@@ -74,22 +105,91 @@ export class Indexer {
74
105
  constructor(baseDir) {
75
106
  this.baseDir = baseDir;
76
107
  }
108
+ /** Подписка на изменения дерева (игнорируемые пути не пробрасываются). */
77
109
  watch(callback) {
78
- this.watcher = watch(this.baseDir, { recursive: true }, (event, filename) => {
110
+ const handler = (event, filename) => {
79
111
  if (filename && !isIgnoredPath(filename)) {
80
112
  callback(event, filename);
81
113
  }
82
- });
114
+ };
115
+ try {
116
+ // Рекурсивный вотч работает на macOS/Windows; на Linux опция игнорируется
117
+ // или бросает в зависимости от сборки Node — ниже фолбэк.
118
+ this.watcher = watch(this.baseDir, { recursive: true }, handler);
119
+ }
120
+ catch {
121
+ this.watcher = this.watchNonRecursive(handler);
122
+ }
83
123
  }
124
+ /**
125
+ * Linux-фолбэк: `fs.watch(recursive)` только для macOS/Windows, поэтому
126
+ * смотрим каждый неигнорируемый подкаталог отдельно (нерекурсивно).
127
+ * Best-effort: подкаталоги, созданные после `watch()`, требуют ручного refresh.
128
+ */
129
+ watchNonRecursive(callback) {
130
+ const watchers = [];
131
+ const collectDirs = (dir, out) => {
132
+ let entries;
133
+ try {
134
+ entries = readdirSync(dir);
135
+ }
136
+ catch {
137
+ return;
138
+ }
139
+ out.push(dir);
140
+ for (const entry of entries) {
141
+ const fullPath = join(dir, entry);
142
+ try {
143
+ const lst = lstatSync(fullPath, { throwIfNoEntry: false });
144
+ if (!lst || lst.isSymbolicLink())
145
+ continue;
146
+ if (statSync(fullPath).isDirectory() && !isIgnoredDirName(entry)) {
147
+ collectDirs(fullPath, out);
148
+ }
149
+ }
150
+ catch {
151
+ // Нечитаемая запись — пропускаем, идём по остальным.
152
+ }
153
+ }
154
+ };
155
+ const dirs = [];
156
+ collectDirs(this.baseDir, dirs);
157
+ for (const dir of dirs) {
158
+ try {
159
+ watchers.push(watch(dir, callback));
160
+ }
161
+ catch (e) {
162
+ logger.warn(`watch failed for ${dir}: ${e.message}`);
163
+ }
164
+ }
165
+ if (watchers.length === 0) {
166
+ logger.warn("file watcher unavailable: no directories could be watched");
167
+ }
168
+ return watchers;
169
+ }
170
+ /** Отписывается от вотчера. */
84
171
  unwatch() {
85
172
  if (this.watcher) {
86
- this.watcher.close();
173
+ const all = Array.isArray(this.watcher) ? this.watcher : [this.watcher];
174
+ for (const w of all) {
175
+ try {
176
+ w.close();
177
+ }
178
+ catch {
179
+ // Уже закрыт — нечего делать.
180
+ }
181
+ }
87
182
  this.watcher = null;
88
183
  }
89
184
  }
90
- async walk() {
91
- const files = [];
92
- let totalSize = 0;
185
+ /**
186
+ * Обходит каждый индексируемый исходник под `baseDir` и зовёт `visit` с
187
+ * полным путём, относительным путём, id языка и stat. Уважает `MAX_FILES` и
188
+ * `IGNORE_DIRS`; содержимое НЕ читает — `walk()` читает его для извлечения
189
+ * символов, а `scanSignature()` пользуется только stat. Общий, чтобы оба
190
+ * прохода видели ровно то же множество файлов.
191
+ */
192
+ eachSourceFile(visit) {
93
193
  let count = 0;
94
194
  const walkDir = (dir) => {
95
195
  if (!existsSync(dir))
@@ -106,9 +206,9 @@ export class Indexer {
106
206
  return;
107
207
  const fullPath = join(dir, entry);
108
208
  const relPath = relative(this.baseDir, fullPath);
109
- // Per-entry isolation: a file deleted (or unreadable) between readdir
110
- // and stat must not kill the whole walk. Symlinks are skipped — they
111
- // can point outside the project or create directory cycles.
209
+ // Изоляция на запись: файл, удалённый (или нечитаемый) между readdir
210
+ // и stat, не должен убивать весь обход. Симлинки пропускаем — могут
211
+ // вести наружу или образовывать циклы каталогов.
112
212
  try {
113
213
  const lst = lstatSync(fullPath, { throwIfNoEntry: false });
114
214
  if (!lst || lst.isSymbolicLink())
@@ -120,27 +220,57 @@ export class Indexer {
120
220
  }
121
221
  }
122
222
  else if (stat.isFile()) {
123
- const ext = extname(entry).toLowerCase();
124
- const language = languageIdForExt(ext);
223
+ if (isIgnoredFileName(entry))
224
+ continue;
225
+ const language = languageIdForExt(extname(entry).toLowerCase());
125
226
  if (language) {
126
- const content = readFileSync(fullPath, "utf-8");
127
- const exports = extractSymbols(content, language);
128
- files.push({ path: relPath, language, exports, size: stat.size });
129
- totalSize += stat.size;
227
+ visit(fullPath, relPath, language, stat);
130
228
  count++;
131
229
  }
132
230
  }
133
231
  }
134
232
  catch (e) {
135
- // Entry vanished or is unreadable — skip it, keep indexing the rest.
233
+ // Запись пропала или нечитаема — пропускаем, индексируем остальные.
136
234
  logger.warn(t("env.index_skip_unreadable", { path: relPath }), { error: String(e) });
137
- continue;
138
235
  }
139
236
  }
140
237
  };
141
238
  walkDir(this.baseDir);
239
+ }
240
+ /** Полный обход: читает файлы, извлекает символы. Нечитаемые пропускает. */
241
+ async walk() {
242
+ const files = [];
243
+ let totalSize = 0;
244
+ this.eachSourceFile((fullPath, relPath, language, stat) => {
245
+ try {
246
+ const content = readFileSync(fullPath, "utf-8");
247
+ const exports = extractSymbols(content, language);
248
+ files.push({ path: relPath, language, exports, size: stat.size });
249
+ totalSize += stat.size;
250
+ }
251
+ catch (e) {
252
+ // Чтение упало после успешного stat — пропускаем файл, берём остальные.
253
+ logger.warn(t("env.index_skip_unreadable", { path: relPath }), { error: String(e) });
254
+ }
255
+ });
142
256
  return { files, totalSize };
143
257
  }
258
+ /**
259
+ * Дешёвый бесконтентный отпечаток индексируемого дерева: отсортированные
260
+ * `path\0size\0mtimeMs`, хешированные SHA-1. `IndexCache` хранит его и
261
+ * пересчитывает на load, отбраковывая протухший `index-cache.json` (файл
262
+ * добавлен/удалён/правлен после обхода — сигнатура меняется). Только stat,
263
+ * без чтения файлов — куда дешевле полного обхода.
264
+ */
265
+ scanSignature() {
266
+ const parts = [];
267
+ this.eachSourceFile((_fullPath, relPath, _language, stat) => {
268
+ parts.push(`${relPath}\u0000${stat.size}\u0000${Math.floor(stat.mtimeMs)}`);
269
+ });
270
+ parts.sort();
271
+ return createHash("sha1").update(parts.join("\n")).digest("hex");
272
+ }
273
+ /** Однострочная сводка индекса (файлы, размер, языки). */
144
274
  summarize(result) {
145
275
  const byLang = {};
146
276
  for (const f of result.files) {