sneakoscope 7.3.0 → 7.6.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 (283) hide show
  1. package/README.md +129 -10
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/codex-app-command.js +1 -1
  5. package/dist/cli/command-manifest-lite.js +4 -1
  6. package/dist/cli/command-registry.js +2 -0
  7. package/dist/cli/install-helpers-codex-lb-chain.js +42 -0
  8. package/dist/cli/install-helpers-codex-lb-config.js +114 -9
  9. package/dist/cli/install-helpers-install-support.js +6 -16
  10. package/dist/cli/install-helpers.js +788 -600
  11. package/dist/cli/router.js +30 -2
  12. package/dist/commands/codex-app.js +37 -1
  13. package/dist/commands/codex-lb.js +491 -300
  14. package/dist/config/skills-manifest.json +57 -71
  15. package/dist/core/align/align-route.js +127 -0
  16. package/dist/core/codex-app/codex-app-fast-ui-repair.js +8 -4
  17. package/dist/core/codex-app/codex-app-restart.js +83 -0
  18. package/dist/core/codex-app/codex-app-ui-state-snapshot.js +8 -4
  19. package/dist/core/codex-app/desktop-routing-snapshot.js +1 -1
  20. package/dist/core/codex-app/menubar/constants.js +5 -0
  21. package/dist/core/codex-app/menubar/generation-transaction-journal.js +1 -3
  22. package/dist/core/codex-app/menubar/installer.js +18 -0
  23. package/dist/core/codex-app/multi-provider-router.js +22 -10
  24. package/dist/core/codex-app/openrouter-activate.js +26 -1
  25. package/dist/core/codex-app.js +118 -26
  26. package/dist/core/codex-control/codex-app-server-runtime-env.js +86 -0
  27. package/dist/core/codex-control/codex-app-server-v2-client.js +78 -8
  28. package/dist/core/codex-control/codex-task-runner.js +2 -2
  29. package/dist/core/codex-lb/capability-runner.js +204 -0
  30. package/dist/core/codex-lb/capability-types.js +1 -0
  31. package/dist/core/codex-lb/codex-lb-env.js +9 -13
  32. package/dist/core/codex-lb/codex-lb-setup.js +45 -8
  33. package/dist/core/codex-lb/codex-lb-tool-catalog.js +52 -74
  34. package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +1 -1
  35. package/dist/core/codex-lb/desktop-auth-invariant.js +185 -0
  36. package/dist/core/codex-lb/desktop-bridge/client.js +34 -0
  37. package/dist/core/codex-lb/desktop-bridge/header-policy.js +106 -0
  38. package/dist/core/codex-lb/desktop-bridge/http-forward.js +101 -0
  39. package/dist/core/codex-lb/desktop-bridge/index.js +10 -0
  40. package/dist/core/codex-lb/desktop-bridge/launchd.js +81 -0
  41. package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +27 -0
  42. package/dist/core/codex-lb/desktop-bridge/security.js +274 -0
  43. package/dist/core/codex-lb/desktop-bridge/server.js +179 -0
  44. package/dist/core/codex-lb/desktop-bridge/state.js +119 -0
  45. package/dist/core/codex-lb/desktop-bridge/types.js +16 -0
  46. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +151 -0
  47. package/dist/core/codex-lb/desktop-center-credentials.js +189 -0
  48. package/dist/core/codex-lb/desktop-controller.js +1410 -0
  49. package/dist/core/codex-lb/desktop-mode.js +32 -0
  50. package/dist/core/codex-lb/desktop-real-evidence.js +168 -0
  51. package/dist/core/codex-lb/desktop-service.js +613 -0
  52. package/dist/core/codex-lb/legacy-migration.js +575 -0
  53. package/dist/core/codex-lb/migration-receipt.js +246 -0
  54. package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +60 -0
  55. package/dist/core/codex-lb/probes/bridge-probe.js +75 -0
  56. package/dist/core/codex-lb/probes/catalog-probe.js +128 -0
  57. package/dist/core/codex-lb/probes/computer-use-probe.js +50 -0
  58. package/dist/core/codex-lb/probes/image-generation-probe.js +47 -0
  59. package/dist/core/codex-lb/probes/probe-evidence.js +39 -0
  60. package/dist/core/codex-lb/probes/voice-realtime-probe.js +44 -0
  61. package/dist/core/codex-lb/trusted-deep-evidence.js +403 -0
  62. package/dist/core/codex-native/core-skill-manifest.js +22 -6
  63. package/dist/core/codex-runtime/codex-desktop-config-policy.js +2 -1
  64. package/dist/core/commands/align-command.js +220 -0
  65. package/dist/core/commands/image-ux-review-command.js +280 -68
  66. package/dist/core/commands/naruto-command.js +257 -8
  67. package/dist/core/commands/ppt-command.js +2 -2
  68. package/dist/core/commands/telegram-command.js +402 -34
  69. package/dist/core/commands/triwiki-command.js +11 -1
  70. package/dist/core/commands/triwiki-graph-command.js +160 -0
  71. package/dist/core/commands/wiki-command.js +100 -20
  72. package/dist/core/doctor/skill-legacy-surface.js +1 -3
  73. package/dist/core/errors/message.js +3 -0
  74. package/dist/core/evidence/evidence-freshness.js +14 -0
  75. package/dist/core/feature-fixtures.js +9 -1
  76. package/dist/core/feature-registry.js +5 -3
  77. package/dist/core/fsx.js +7 -3
  78. package/dist/core/hooks-runtime/codex-commit-hooks-selftest.js +82 -0
  79. package/dist/core/hooks-runtime/context-graph-freshness-preflight.js +113 -0
  80. package/dist/core/hooks-runtime/hook-context.js +15 -2
  81. package/dist/core/hooks-runtime/managed-skill-generation-heal.js +46 -0
  82. package/dist/core/hooks-runtime/naruto-terminal-finalization.js +1 -0
  83. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +1 -1
  84. package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
  85. package/dist/core/hooks-runtime.js +33 -89
  86. package/dist/core/image-ux-review/callout-extraction.js +6 -0
  87. package/dist/core/image-ux-review/imagegen-adapter.js +64 -23
  88. package/dist/core/image-ux-review/real-callout-extractor.js +1 -0
  89. package/dist/core/image-ux-review.js +230 -12
  90. package/dist/core/imagegen/codex-lb-imagegen-target.js +21 -7
  91. package/dist/core/imagegen/imagegen-capability.js +10 -4
  92. package/dist/core/imagegen/require-imagegen.js +29 -5
  93. package/dist/core/init/skills.js +25 -13
  94. package/dist/core/init.js +1 -1
  95. package/dist/core/install/installed-package-smoke.js +157 -4
  96. package/dist/core/managed-assets/managed-assets-manifest.js +2 -1
  97. package/dist/core/managed-path-safety.js +8 -10
  98. package/dist/core/mcp-config/backup.js +2 -4
  99. package/dist/core/mcp-config/config-reader.js +2 -4
  100. package/dist/core/mcp-config/scope.js +3 -5
  101. package/dist/core/naruto/context-graph-advisor-scope.js +242 -0
  102. package/dist/core/naruto/context-graph-advisor.js +0 -0
  103. package/dist/core/perf/release-latency-slo.js +12 -6
  104. package/dist/core/pipeline-internals/runtime-core.js +78 -0
  105. package/dist/core/pipeline-internals/runtime-gates.js +5 -2
  106. package/dist/core/policy/latest-version-guidance.js +113 -0
  107. package/dist/core/ppt.js +8 -8
  108. package/dist/core/preflight/parallel-preflight-engine.js +1 -1
  109. package/dist/core/proof/auto-finalize.js +98 -6
  110. package/dist/core/proof/route-proof-gate.js +12 -1
  111. package/dist/core/proof/route-proof-policy.js +8 -8
  112. package/dist/core/release/gate-manifest.js +32 -1
  113. package/dist/core/release/npm-stage-contract.js +21 -0
  114. package/dist/core/release/package-size-budget.js +2 -2
  115. package/dist/core/release/release-gate-contract.js +5 -0
  116. package/dist/core/release/release-proof-io.js +1 -3
  117. package/dist/core/release/release-real-contract.js +5 -4
  118. package/dist/core/release/release-stamp-proof.js +1 -3
  119. package/dist/core/release/stage-publish.js +27 -0
  120. package/dist/core/remote/audit-idempotency.js +50 -18
  121. package/dist/core/remote/codex-public-activity.js +283 -0
  122. package/dist/core/remote/index.js +1 -0
  123. package/dist/core/remote/ssh-worker-client.js +4 -1
  124. package/dist/core/remote/worker.js +206 -27
  125. package/dist/core/research/research-adversarial-review.js +1 -3
  126. package/dist/core/research/research-claim-synthesizer.js +1 -3
  127. package/dist/core/research/research-falsification-runner.js +1 -3
  128. package/dist/core/research/research-source-evidence.js +1 -3
  129. package/dist/core/responses-stream.js +65 -19
  130. package/dist/core/routes/constants.js +1 -1
  131. package/dist/core/routes/design-policy.js +7 -2
  132. package/dist/core/routes/dollar-manifest-lite.js +2 -4
  133. package/dist/core/routes/evidence.js +1 -1
  134. package/dist/core/routes.js +30 -10
  135. package/dist/core/safety/command-contract/contracts.js +2 -0
  136. package/dist/core/safety/command-contract/types.js +1 -1
  137. package/dist/core/search/context-graph-seeds.js +205 -0
  138. package/dist/core/search/context.js +0 -0
  139. package/dist/core/search/provider.js +2 -2
  140. package/dist/core/secret-redaction.js +6 -0
  141. package/dist/core/subagents/naruto-help-contract.js +2 -1
  142. package/dist/core/subagents/naruto-host-credentials.js +161 -0
  143. package/dist/core/subagents/official-subagent-preparation.js +4 -1
  144. package/dist/core/subagents/official-subagent-prompt.js +44 -2
  145. package/dist/core/subagents/official-subagent-runner.js +26 -8
  146. package/dist/core/subagents/triwiki-attention.js +135 -123
  147. package/dist/core/telegram/bot-api.js +85 -10
  148. package/dist/core/telegram/center-snapshot.js +718 -0
  149. package/dist/core/telegram/config.js +158 -31
  150. package/dist/core/telegram/hub.js +288 -33
  151. package/dist/core/telegram/index.js +2 -0
  152. package/dist/core/telegram/ingress-spool.js +419 -0
  153. package/dist/core/telegram/ledgers.js +188 -29
  154. package/dist/core/telegram/messages.js +26 -6
  155. package/dist/core/telegram/owner-lock.js +10 -7
  156. package/dist/core/telegram/runtime-projection.js +38 -3
  157. package/dist/core/telegram/runtime.js +298 -63
  158. package/dist/core/telegram/service.js +137 -8
  159. package/dist/core/telegram/setup.js +291 -73
  160. package/dist/core/text/strings.js +3 -0
  161. package/dist/core/triwiki/code-pack.js +12 -120
  162. package/dist/core/triwiki/context-graph/compiler/cache-key.js +277 -0
  163. package/dist/core/triwiki/context-graph/compiler/extract.js +104 -0
  164. package/dist/core/triwiki/context-graph/compiler/freshness.js +108 -0
  165. package/dist/core/triwiki/context-graph/compiler/index.js +237 -0
  166. package/dist/core/triwiki/context-graph/compiler/merge.js +0 -0
  167. package/dist/core/triwiki/context-graph/compiler/serialize.js +115 -0
  168. package/dist/core/triwiki/context-graph/contracts.js +236 -0
  169. package/dist/core/triwiki/context-graph/extractors/code/builders.js +150 -0
  170. package/dist/core/triwiki/context-graph/extractors/code/declarations.js +161 -0
  171. package/dist/core/triwiki/context-graph/extractors/code/index.js +265 -0
  172. package/dist/core/triwiki/context-graph/extractors/code/inventory.js +237 -0
  173. package/dist/core/triwiki/context-graph/extractors/code/module-graph.js +252 -0
  174. package/dist/core/triwiki/context-graph/extractors/code/modules.js +79 -0
  175. package/dist/core/triwiki/context-graph/extractors/code/references.js +184 -0
  176. package/dist/core/triwiki/context-graph/extractors/code/selection.js +100 -0
  177. package/dist/core/triwiki/context-graph/extractors/code/ts-config.js +161 -0
  178. package/dist/core/triwiki/context-graph/extractors/code/types.js +18 -0
  179. package/dist/core/triwiki/context-graph/extractors/evidence/claims.js +289 -0
  180. package/dist/core/triwiki/context-graph/extractors/evidence/index.js +54 -0
  181. package/dist/core/triwiki/context-graph/extractors/evidence/proof-index.js +213 -0
  182. package/dist/core/triwiki/context-graph/extractors/evidence/proofs.js +266 -0
  183. package/dist/core/triwiki/context-graph/extractors/evidence/redaction.js +177 -0
  184. package/dist/core/triwiki/context-graph/extractors/evidence/shared.js +0 -0
  185. package/dist/core/triwiki/context-graph/extractors/evidence/sources.js +154 -0
  186. package/dist/core/triwiki/context-graph/extractors/index.js +7 -0
  187. package/dist/core/triwiki/context-graph/extractors/topology/commands.js +207 -0
  188. package/dist/core/triwiki/context-graph/extractors/topology/gate-edges.js +201 -0
  189. package/dist/core/triwiki/context-graph/extractors/topology/gates.js +182 -0
  190. package/dist/core/triwiki/context-graph/extractors/topology/globs.js +124 -0
  191. package/dist/core/triwiki/context-graph/extractors/topology/index.js +92 -0
  192. package/dist/core/triwiki/context-graph/extractors/topology/routes.js +135 -0
  193. package/dist/core/triwiki/context-graph/extractors/topology/shared.js +215 -0
  194. package/dist/core/triwiki/context-graph/graph-index.js +143 -0
  195. package/dist/core/triwiki/context-graph/ids.js +0 -0
  196. package/dist/core/triwiki/context-graph/lint/index.js +0 -0
  197. package/dist/core/triwiki/context-graph/lint/rules.js +232 -0
  198. package/dist/core/triwiki/context-graph/lint/warnings.js +93 -0
  199. package/dist/core/triwiki/context-graph/paths.js +116 -0
  200. package/dist/core/triwiki/context-graph/profiles.js +78 -0
  201. package/dist/core/triwiki/context-graph/projections/anchors.js +91 -0
  202. package/dist/core/triwiki/context-graph/projections/attention.js +58 -0
  203. package/dist/core/triwiki/context-graph/projections/code-pack-workspace.js +62 -0
  204. package/dist/core/triwiki/context-graph/projections/code-pack.js +144 -0
  205. package/dist/core/triwiki/context-graph/projections/index.js +7 -0
  206. package/dist/core/triwiki/context-graph/projections/module-view.js +140 -0
  207. package/dist/core/triwiki/context-graph/projections/node-summary.js +118 -0
  208. package/dist/core/triwiki/context-graph/projections/pack-contract.js +6 -0
  209. package/dist/core/triwiki/context-graph/query/explain.js +0 -0
  210. package/dist/core/triwiki/context-graph/query/index.js +178 -0
  211. package/dist/core/triwiki/context-graph/query/load.js +116 -0
  212. package/dist/core/triwiki/context-graph/query/pack.js +117 -0
  213. package/dist/core/triwiki/context-graph/query/rank.js +142 -0
  214. package/dist/core/triwiki/context-graph/query/ranking-config.js +64 -0
  215. package/dist/core/triwiki/context-graph/query/seeds.js +241 -0
  216. package/dist/core/triwiki/context-graph/query/snapshot-cache.js +72 -0
  217. package/dist/core/triwiki/context-graph/query/traverse.js +154 -0
  218. package/dist/core/triwiki/context-graph/query-types.js +29 -0
  219. package/dist/core/triwiki/context-graph/store/compile-lock.js +21 -0
  220. package/dist/core/triwiki/context-graph/store/event-log.js +48 -0
  221. package/dist/core/triwiki/context-graph/store/fragment-cache.js +85 -0
  222. package/dist/core/triwiki/context-graph/store/graph-status.js +85 -0
  223. package/dist/core/triwiki/context-graph/store/snapshot-store.js +155 -0
  224. package/dist/core/triwiki/triwiki-proof-bank-index-repair.js +115 -0
  225. package/dist/core/triwiki/triwiki-proof-bank-index-store.js +290 -0
  226. package/dist/core/triwiki/triwiki-proof-bank-index.js +126 -0
  227. package/dist/core/triwiki/triwiki-proof-bank.js +7 -1
  228. package/dist/core/verification/context-graph-affected.js +271 -0
  229. package/dist/core/version.js +1 -1
  230. package/dist/native/sks-menubar/Sources/AlertFactory.swift +11 -3
  231. package/dist/native/sks-menubar/Sources/AppDelegate.swift +9 -0
  232. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +25 -3
  233. package/dist/native/sks-menubar/Sources/ControlKit.swift +19 -8
  234. package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +1 -2
  235. package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +45 -17
  236. package/dist/native/sks-menubar/Sources/OverviewSummary.swift +162 -0
  237. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +136 -190
  238. package/dist/native/sks-menubar/Sources/ProcessClient.swift +107 -11
  239. package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +5 -1
  240. package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +16 -4
  241. package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +4 -1
  242. package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +131 -0
  243. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +288 -341
  244. package/dist/native/sks-menubar/Sources/RemoteTelegramCenterState.swift +447 -0
  245. package/dist/native/sks-menubar/Sources/RemoteTelegramRendering.swift +484 -0
  246. package/dist/native/sks-menubar/Sources/RemoteTelegramViewController.swift +364 -115
  247. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +73 -12
  248. package/dist/native/sks-menubar/Sources/SidebarItem.swift +3 -3
  249. package/dist/native/sks-menubar/Sources/StatusItemController.swift +13 -13
  250. package/dist/native/sks-menubar/Sources/TelegramHubSupervisor.swift +469 -0
  251. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +1 -6
  252. package/dist/native/sks-menubar/Tests/AppRuntimeTestSupport.swift +12 -0
  253. package/dist/native/sks-menubar/Tests/NativeTelegramRegressionRunner.swift +55 -0
  254. package/dist/native/sks-menubar/Tests/ProcessClientTests.swift +33 -0
  255. package/dist/native/sks-menubar/Tests/ProvidersViewControllerTests.swift +142 -0
  256. package/dist/native/sks-menubar/Tests/RemoteTelegramCenterStateTests.swift +56 -0
  257. package/dist/native/sks-menubar/Tests/TelegramHubSupervisorTests.swift +55 -0
  258. package/dist/scripts/codex-lb-catalog-passthrough-check.js +120 -0
  259. package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +169 -0
  260. package/dist/scripts/codex-lb-desktop-bridge-check.js +394 -0
  261. package/dist/scripts/codex-lb-desktop-bridge-latency-check.js +196 -0
  262. package/dist/scripts/codex-lb-desktop-capabilities-check.js +287 -0
  263. package/dist/scripts/codex-lb-desktop-real-evidence-check.js +123 -0
  264. package/dist/scripts/codex-lb-fast-mode-truth-check.js +10 -3
  265. package/dist/scripts/codex-lb-fast-ui-preservation-check.js +81 -40
  266. package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +99 -37
  267. package/dist/scripts/context-graph-check.js +191 -0
  268. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +1 -1
  269. package/dist/scripts/gate-policy-audit-check.js +1 -1
  270. package/dist/scripts/helper-dedup-check.js +0 -2
  271. package/dist/scripts/installed-package-smoke-check.js +4 -1
  272. package/dist/scripts/latest-version-guidance-check.js +74 -0
  273. package/dist/scripts/release-metadata-check.js +21 -4
  274. package/dist/scripts/release-real-check.js +7 -0
  275. package/dist/scripts/ux-review-imagegen-blackbox-check.js +25 -1
  276. package/dist/scripts/ux-review-run-wires-imagegen-check.js +9 -1
  277. package/package.json +8 -3
  278. package/release-gates.v2.json +176 -1
  279. package/runtime-required-scripts.json +30 -0
  280. package/schemas/triwiki/context-graph.schema.json +119 -0
  281. package/dist/cli/install-helpers-codex-lb-selftest-chain.js +0 -112
  282. package/dist/cli/install-helpers-codex-lb-selftest.js +0 -481
  283. package/dist/core/triwiki/code-index-scanner.js +0 -309
@@ -0,0 +1,241 @@
1
+ import { compareContextGraphIds, contextGraphNodeId } from '../ids.js';
2
+ import { isWorkspaceRelativePosixPath } from '../paths.js';
3
+ import { contextGraphSeedConfidenceScore, isExactContextGraphSeedConfidence } from './ranking-config.js';
4
+ const TOKEN_PATTERN = /[A-Za-z0-9_$@#./:-]+/g;
5
+ const TRIM_PATTERN = /^[.:/-]+|[.:/-]+$/g;
6
+ const PROBE_KINDS = [
7
+ 'file',
8
+ 'module',
9
+ 'command',
10
+ 'route',
11
+ 'pipeline',
12
+ 'gate',
13
+ 'schema',
14
+ 'wiki_claim',
15
+ 'source',
16
+ 'proof',
17
+ 'risk_domain'
18
+ ];
19
+ function probeIdentity(kind, token) {
20
+ switch (kind) {
21
+ case 'file':
22
+ return { kind: 'file', path: token };
23
+ case 'module':
24
+ return { kind: 'module', moduleId: token };
25
+ case 'command':
26
+ return { kind: 'command', name: token };
27
+ case 'route':
28
+ return { kind: 'route', name: token };
29
+ case 'pipeline':
30
+ return { kind: 'pipeline', pipelineId: token };
31
+ case 'gate':
32
+ return { kind: 'gate', gateId: token };
33
+ case 'schema':
34
+ return { kind: 'schema', schemaId: token };
35
+ case 'wiki_claim':
36
+ return { kind: 'wiki_claim', claimHash: token };
37
+ case 'source':
38
+ return { kind: 'source', sourceHash: token };
39
+ case 'proof':
40
+ return { kind: 'proof', proofId: token };
41
+ case 'risk_domain':
42
+ return { kind: 'risk_domain', domain: token };
43
+ default:
44
+ return null;
45
+ }
46
+ }
47
+ const MANIFEST_KINDS = new Set([
48
+ 'command',
49
+ 'gate',
50
+ 'route',
51
+ 'pipeline',
52
+ 'schema'
53
+ ]);
54
+ const PATH_KINDS = new Set(['file', 'test', 'config']);
55
+ export function seedConfidenceFor(kind, match) {
56
+ if (match === 'path')
57
+ return 'file_path';
58
+ if (kind === 'symbol')
59
+ return 'exact_definition';
60
+ if (MANIFEST_KINDS.has(kind))
61
+ return 'manifest';
62
+ if (PATH_KINDS.has(kind))
63
+ return 'file_path';
64
+ return 'exact_reference';
65
+ }
66
+ export function normalizeContextGraphQuery(query, config) {
67
+ const normalized = String(query ?? '').normalize('NFKC').trim();
68
+ const tokens = [];
69
+ const seen = new Set();
70
+ for (const raw of normalized.match(TOKEN_PATTERN) ?? []) {
71
+ const token = raw.replace(TRIM_PATTERN, '');
72
+ if (!token || seen.has(token))
73
+ continue;
74
+ seen.add(token);
75
+ tokens.push(token);
76
+ if (tokens.length >= config.maxQueryTokens)
77
+ break;
78
+ }
79
+ return { normalized, tokens, lowerTokens: tokens.map((token) => token.toLowerCase()) };
80
+ }
81
+ class SeedCollector {
82
+ index;
83
+ config;
84
+ drafts = new Map();
85
+ constructor(index, config) {
86
+ this.index = index;
87
+ this.config = config;
88
+ }
89
+ add(nodeId, confidence, origin, score) {
90
+ const node = this.index.nodesById.get(nodeId);
91
+ if (!node)
92
+ return;
93
+ const resolved = score ?? contextGraphSeedConfidenceScore(this.config, confidence);
94
+ const existing = this.drafts.get(nodeId);
95
+ if (existing && existing.score >= resolved)
96
+ return;
97
+ const draft = {
98
+ nodeId,
99
+ confidence,
100
+ origin,
101
+ score: resolved,
102
+ ...(node.path === undefined ? {} : { path: node.path }),
103
+ ...(node.locator?.line === undefined ? {} : { line: node.locator.line })
104
+ };
105
+ this.drafts.set(nodeId, draft);
106
+ }
107
+ }
108
+ function addExactSeedsForToken(collector, index, token, config) {
109
+ const direct = index.nodesById.get(token);
110
+ if (direct)
111
+ collector.add(direct.id, seedConfidenceFor(direct.kind, 'node_id'), 'exact');
112
+ for (const kind of PROBE_KINDS) {
113
+ const identity = probeIdentity(kind, token);
114
+ if (!identity)
115
+ continue;
116
+ let probed;
117
+ try {
118
+ probed = contextGraphNodeId(identity);
119
+ }
120
+ catch {
121
+ continue;
122
+ }
123
+ const node = index.nodesById.get(probed);
124
+ if (node)
125
+ collector.add(node.id, seedConfidenceFor(node.kind, 'node_id'), 'exact');
126
+ }
127
+ const candidatePath = token.replace(/^\.\//, '');
128
+ if (isWorkspaceRelativePosixPath(candidatePath)) {
129
+ const atPath = index.nodesByPath.get(candidatePath) ?? [];
130
+ let added = 0;
131
+ for (const pass of [true, false]) {
132
+ for (const id of atPath) {
133
+ if (added >= config.maxSeedsPerToken)
134
+ break;
135
+ const node = index.nodesById.get(id);
136
+ if (!node || PATH_KINDS.has(node.kind) !== pass)
137
+ continue;
138
+ collector.add(node.id, 'file_path', 'exact');
139
+ added += 1;
140
+ }
141
+ }
142
+ }
143
+ const byLabel = index.nodesByLabel.get(token.toLowerCase()) ?? [];
144
+ for (const id of byLabel.slice(0, config.maxSeedsPerToken)) {
145
+ const node = index.nodesById.get(id);
146
+ if (node)
147
+ collector.add(node.id, seedConfidenceFor(node.kind, 'label'), 'exact');
148
+ }
149
+ }
150
+ function sweepLexicalSeeds(collector, index, query, config) {
151
+ const needles = query.lowerTokens.filter((token) => token.length >= config.lexicalMinTokenLength);
152
+ const sweep = { scanned: 0, budgetExhausted: false, added: 0 };
153
+ if (needles.length === 0)
154
+ return sweep;
155
+ const hits = [];
156
+ const consider = (keys, idsOf) => {
157
+ for (const key of keys) {
158
+ if (sweep.scanned >= config.lexicalScanBudget) {
159
+ sweep.budgetExhausted = true;
160
+ return;
161
+ }
162
+ sweep.scanned += 1;
163
+ const lower = key.toLowerCase();
164
+ if (!needles.some((needle) => lower.includes(needle)))
165
+ continue;
166
+ for (const id of idsOf(key))
167
+ hits.push(id);
168
+ }
169
+ };
170
+ consider(index.nodesByLabel.keys(), (key) => index.nodesByLabel.get(key) ?? []);
171
+ if (!sweep.budgetExhausted)
172
+ consider(index.nodesByPath.keys(), (key) => index.nodesByPath.get(key) ?? []);
173
+ const ordered = [...new Set(hits)].sort(compareContextGraphIds).slice(0, config.maxLexicalSeeds);
174
+ for (const id of ordered) {
175
+ if (collector.drafts.has(id))
176
+ continue;
177
+ collector.add(id, 'text_candidate', 'lexical', config.lexicalMatchScore);
178
+ sweep.added += 1;
179
+ }
180
+ return sweep;
181
+ }
182
+ function toSeed(draft) {
183
+ return {
184
+ nodeId: draft.nodeId,
185
+ confidence: draft.confidence,
186
+ origin: draft.origin,
187
+ score: draft.score,
188
+ ...(draft.path === undefined ? {} : { path: draft.path }),
189
+ ...(draft.line === undefined ? {} : { line: draft.line })
190
+ };
191
+ }
192
+ export function resolveContextGraphSeeds(input) {
193
+ const { index, request, config, maxSeeds } = input;
194
+ const query = normalizeContextGraphQuery(request.query, config);
195
+ const collector = new SeedCollector(index, config);
196
+ let unknownProvidedSeeds = 0;
197
+ let providedSeedCount = 0;
198
+ for (const seed of request.seeds ?? []) {
199
+ if (!index.nodesById.has(seed.nodeId)) {
200
+ unknownProvidedSeeds += 1;
201
+ continue;
202
+ }
203
+ const score = seed.score === undefined
204
+ ? contextGraphSeedConfidenceScore(config, seed.confidence)
205
+ : Math.min(seed.score, config.providedSeedScoreCeiling);
206
+ collector.add(seed.nodeId, seed.confidence, seed.origin ?? 'provided', score);
207
+ providedSeedCount += 1;
208
+ }
209
+ for (const focus of request.focusPaths ?? []) {
210
+ const normalized = String(focus ?? '').replace(/^\.\//, '');
211
+ if (!isWorkspaceRelativePosixPath(normalized))
212
+ continue;
213
+ for (const id of (index.nodesByPath.get(normalized) ?? []).slice(0, config.maxSeedsPerToken)) {
214
+ collector.add(id, 'file_path', 'exact');
215
+ }
216
+ }
217
+ for (const token of query.tokens)
218
+ addExactSeedsForToken(collector, index, token, config);
219
+ let exactSeedCount = 0;
220
+ for (const draft of collector.drafts.values()) {
221
+ if (isExactContextGraphSeedConfidence(draft.confidence))
222
+ exactSeedCount += 1;
223
+ }
224
+ let sweep = { scanned: 0, budgetExhausted: false, added: 0 };
225
+ if (exactSeedCount < config.minExactSeeds)
226
+ sweep = sweepLexicalSeeds(collector, index, query, config);
227
+ const ranked = [...collector.drafts.values()].sort((left, right) => right.score - left.score || compareContextGraphIds(left.nodeId, right.nodeId));
228
+ const kept = ranked.slice(0, Math.max(0, maxSeeds));
229
+ const seeds = kept.map(toSeed);
230
+ return {
231
+ seeds,
232
+ exactSeedCount: seeds.filter((seed) => isExactContextGraphSeedConfidence(seed.confidence)).length,
233
+ lexicalSeedCount: seeds.filter((seed) => seed.origin === 'lexical').length,
234
+ providedSeedCount,
235
+ unknownProvidedSeeds,
236
+ droppedSeeds: Math.max(0, ranked.length - kept.length),
237
+ scannedKeys: sweep.scanned,
238
+ scanBudgetExhausted: sweep.budgetExhausted,
239
+ query
240
+ };
241
+ }
@@ -0,0 +1,72 @@
1
+ import path from 'node:path';
2
+ import { shortDigest } from '../ids.js';
3
+ export const CONTEXT_GRAPH_SNAPSHOT_CACHE_MAX_ENTRIES = 2;
4
+ const entries = new Map();
5
+ let hits = 0;
6
+ let misses = 0;
7
+ let evictions = 0;
8
+ let clock = 0;
9
+ export function contextGraphWorkspaceKey(root) {
10
+ return shortDigest(path.resolve(String(root ?? '')));
11
+ }
12
+ function cacheKey(workspaceKey, snapshotHash) {
13
+ return `${workspaceKey}:${snapshotHash}`;
14
+ }
15
+ export function getCachedContextGraphIndex(root, snapshotHash) {
16
+ if (!snapshotHash) {
17
+ misses += 1;
18
+ return null;
19
+ }
20
+ const key = cacheKey(contextGraphWorkspaceKey(root), snapshotHash);
21
+ const entry = entries.get(key);
22
+ if (!entry) {
23
+ misses += 1;
24
+ return null;
25
+ }
26
+ clock += 1;
27
+ entry.order = clock;
28
+ hits += 1;
29
+ return entry.index;
30
+ }
31
+ export function cacheContextGraphIndex(root, snapshotHash, index) {
32
+ if (!snapshotHash)
33
+ return;
34
+ const workspaceKey = contextGraphWorkspaceKey(root);
35
+ for (const [key, entry] of [...entries]) {
36
+ if (entry.workspaceKey === workspaceKey)
37
+ entries.delete(key);
38
+ }
39
+ clock += 1;
40
+ entries.set(cacheKey(workspaceKey, snapshotHash), { workspaceKey, snapshotHash, index, order: clock });
41
+ while (entries.size > CONTEXT_GRAPH_SNAPSHOT_CACHE_MAX_ENTRIES) {
42
+ let oldestKey = null;
43
+ let oldestOrder = Number.POSITIVE_INFINITY;
44
+ for (const [key, entry] of entries) {
45
+ if (entry.order < oldestOrder) {
46
+ oldestOrder = entry.order;
47
+ oldestKey = key;
48
+ }
49
+ }
50
+ if (oldestKey === null)
51
+ break;
52
+ entries.delete(oldestKey);
53
+ evictions += 1;
54
+ }
55
+ }
56
+ export function clearContextGraphSnapshotCache() {
57
+ entries.clear();
58
+ hits = 0;
59
+ misses = 0;
60
+ evictions = 0;
61
+ clock = 0;
62
+ }
63
+ export function contextGraphSnapshotCacheStats() {
64
+ const lookups = hits + misses;
65
+ return {
66
+ entries: entries.size,
67
+ hits,
68
+ misses,
69
+ evictions,
70
+ hitRate: lookups === 0 ? 0 : hits / lookups
71
+ };
72
+ }
@@ -0,0 +1,154 @@
1
+ import { incomingEdges, outgoingEdges } from '../graph-index.js';
2
+ import { compareContextGraphIds, contextGraphPathFromId } from '../ids.js';
3
+ import { profileEdgeWeight } from '../profiles.js';
4
+ import { contextGraphSeedConfidenceScore } from './ranking-config.js';
5
+ export function contextGraphNodePath(index, nodeId) {
6
+ const node = index.nodesById.get(nodeId);
7
+ if (node?.path)
8
+ return node.path;
9
+ return contextGraphPathFromId(nodeId);
10
+ }
11
+ export function isUnderFocusPath(nodePath, focusPaths) {
12
+ if (!nodePath)
13
+ return false;
14
+ for (const focus of focusPaths) {
15
+ if (!focus)
16
+ continue;
17
+ if (nodePath === focus || nodePath.startsWith(`${focus}/`))
18
+ return true;
19
+ }
20
+ return false;
21
+ }
22
+ function decayLadder(config, maxDepth) {
23
+ const ladder = [1];
24
+ for (let depth = 1; depth <= maxDepth; depth += 1) {
25
+ ladder.push((ladder[depth - 1] ?? 1) * config.depthDecay);
26
+ }
27
+ return ladder;
28
+ }
29
+ function neighbours(index, nodeId) {
30
+ const out = [];
31
+ for (const edge of outgoingEdges(index, nodeId))
32
+ out.push({ edge, neighbourId: edge.to, reverse: false });
33
+ for (const edge of incomingEdges(index, nodeId))
34
+ out.push({ edge, neighbourId: edge.from, reverse: true });
35
+ return out;
36
+ }
37
+ function improves(existing, depth, weight, edgeId) {
38
+ if (depth !== existing.depth)
39
+ return false;
40
+ if (weight !== existing.weight)
41
+ return weight > existing.weight;
42
+ if (existing.parentEdgeId === null)
43
+ return false;
44
+ return compareContextGraphIds(edgeId, existing.parentEdgeId) < 0;
45
+ }
46
+ export function traverseContextGraph(input) {
47
+ const { index, seeds, profile, maxDepth, maxVisitedNodes, maxVisitedEdges, config, focusPaths, deadline } = input;
48
+ const states = new Map();
49
+ const decay = decayLadder(config, maxDepth);
50
+ const focusActive = focusPaths.length > 0;
51
+ const queue = [];
52
+ for (const seed of seeds) {
53
+ if (!index.nodesById.has(seed.nodeId) || states.has(seed.nodeId))
54
+ continue;
55
+ if (states.size >= maxVisitedNodes)
56
+ break;
57
+ const path = contextGraphNodePath(index, seed.nodeId);
58
+ states.set(seed.nodeId, {
59
+ nodeId: seed.nodeId,
60
+ depth: 0,
61
+ weight: seed.score ?? contextGraphSeedConfidenceScore(config, seed.confidence),
62
+ seedNodeId: seed.nodeId,
63
+ parentNodeId: null,
64
+ parentEdgeId: null,
65
+ parentReverse: false,
66
+ focusMatched: !focusActive || isUnderFocusPath(path, focusPaths)
67
+ });
68
+ queue.push(seed.nodeId);
69
+ }
70
+ let visitedEdges = 0;
71
+ let depthLimited = 0;
72
+ let nodeCapHit = false;
73
+ let edgeCapHit = false;
74
+ let timedOut = false;
75
+ let sinceClockCheck = 0;
76
+ let head = 0;
77
+ while (head < queue.length) {
78
+ const nodeId = queue[head];
79
+ head += 1;
80
+ if (nodeId === undefined)
81
+ continue;
82
+ const state = states.get(nodeId);
83
+ if (!state)
84
+ continue;
85
+ if (state.depth >= maxDepth) {
86
+ depthLimited += 1;
87
+ continue;
88
+ }
89
+ const childDepth = state.depth + 1;
90
+ const childDecay = decay[childDepth] ?? 0;
91
+ for (const candidate of neighbours(index, nodeId)) {
92
+ if (visitedEdges >= maxVisitedEdges) {
93
+ edgeCapHit = true;
94
+ break;
95
+ }
96
+ visitedEdges += 1;
97
+ sinceClockCheck += 1;
98
+ if (sinceClockCheck >= config.timeoutCheckInterval) {
99
+ sinceClockCheck = 0;
100
+ if (deadline !== null && Date.now() > deadline) {
101
+ timedOut = true;
102
+ break;
103
+ }
104
+ }
105
+ const base = profileEdgeWeight(profile, candidate.edge.type);
106
+ if (base <= 0)
107
+ continue;
108
+ const neighbour = index.nodesById.get(candidate.neighbourId);
109
+ if (!neighbour)
110
+ continue;
111
+ const multiplier = config.edgeConfidenceMultiplier[candidate.edge.confidence] ?? 0;
112
+ const direction = candidate.reverse ? config.reverseEdgeMultiplier : 1;
113
+ const weight = state.weight * config.depthDecay + base * multiplier * direction * childDecay;
114
+ const existing = states.get(candidate.neighbourId);
115
+ if (existing) {
116
+ if (!improves(existing, childDepth, weight, candidate.edge.id))
117
+ continue;
118
+ existing.weight = weight;
119
+ existing.seedNodeId = state.seedNodeId;
120
+ existing.parentNodeId = nodeId;
121
+ existing.parentEdgeId = candidate.edge.id;
122
+ existing.parentReverse = candidate.reverse;
123
+ existing.focusMatched = state.focusMatched || isUnderFocusPath(neighbour.path ?? contextGraphPathFromId(neighbour.id), focusPaths);
124
+ continue;
125
+ }
126
+ if (states.size >= maxVisitedNodes) {
127
+ nodeCapHit = true;
128
+ break;
129
+ }
130
+ states.set(candidate.neighbourId, {
131
+ nodeId: candidate.neighbourId,
132
+ depth: childDepth,
133
+ weight,
134
+ seedNodeId: state.seedNodeId,
135
+ parentNodeId: nodeId,
136
+ parentEdgeId: candidate.edge.id,
137
+ parentReverse: candidate.reverse,
138
+ focusMatched: state.focusMatched || isUnderFocusPath(neighbour.path ?? contextGraphPathFromId(neighbour.id), focusPaths)
139
+ });
140
+ queue.push(candidate.neighbourId);
141
+ }
142
+ if (timedOut || edgeCapHit || nodeCapHit)
143
+ break;
144
+ }
145
+ return {
146
+ states,
147
+ visitedNodes: states.size,
148
+ visitedEdges,
149
+ nodeCapHit,
150
+ edgeCapHit,
151
+ depthLimited,
152
+ timedOut
153
+ };
154
+ }
@@ -0,0 +1,29 @@
1
+ export const CONTEXT_GRAPH_QUERY_SCHEMA = 'sks.context-graph-query.v1';
2
+ export function emptyContextGraphQueryResult(snapshotHash, profile, errors = []) {
3
+ return {
4
+ schema: CONTEXT_GRAPH_QUERY_SCHEMA,
5
+ ok: errors.length === 0,
6
+ snapshotHash,
7
+ snapshotFreshness: 'stale',
8
+ profile,
9
+ seeds: [],
10
+ seedCount: 0,
11
+ visitedNodes: 0,
12
+ visitedEdges: 0,
13
+ selected: [],
14
+ selectedNodes: 0,
15
+ explanationPathCount: 0,
16
+ provenanceCoverage: 0,
17
+ staleExcluded: 0,
18
+ invalidatedExcluded: 0,
19
+ tokenCost: 0,
20
+ tokenBudget: 0,
21
+ truncated: false,
22
+ timeout: false,
23
+ omissionReasons: {},
24
+ warnings: [],
25
+ errors,
26
+ durationMs: 0,
27
+ processSpawns: 0
28
+ };
29
+ }
@@ -0,0 +1,21 @@
1
+ import { tryWithFileLock } from '../../../locks/file-lock.js';
2
+ import { contextGraphLockPath } from '../paths.js';
3
+ export const CONTEXT_GRAPH_LOCK_STALE_MS = 120_000;
4
+ function holderOf(owner) {
5
+ if (!owner || typeof owner.pid !== 'number')
6
+ return null;
7
+ return {
8
+ pid: owner.pid,
9
+ acquiredAt: String(owner.acquired_at ?? ''),
10
+ heartbeatAt: String(owner.heartbeat_at ?? '')
11
+ };
12
+ }
13
+ export async function withContextGraphCompileLock(root, fn, options = {}) {
14
+ const result = await tryWithFileLock({
15
+ lockPath: contextGraphLockPath(root),
16
+ staleMs: Math.max(1, options.staleMs ?? CONTEXT_GRAPH_LOCK_STALE_MS)
17
+ }, async () => fn());
18
+ if (!result.acquired)
19
+ return { acquired: false, recovered: false, holder: holderOf(result.owner) };
20
+ return { acquired: true, recovered: result.recovered, value: result.value };
21
+ }
@@ -0,0 +1,48 @@
1
+ import { appendJsonlBounded } from '../../../fsx.js';
2
+ import { contextGraphEventLogPath } from '../paths.js';
3
+ export const CONTEXT_GRAPH_EVENT_SCHEMA = 'sks.context-graph-event.v1';
4
+ export const CONTEXT_GRAPH_EVENT_LOG_MAX_BYTES = 2 * 1024 * 1024;
5
+ export const CONTEXT_GRAPH_EVENT_TYPES = [
6
+ 'compile.started',
7
+ 'compile.committed',
8
+ 'compile.blocked',
9
+ 'compile.lock_contended',
10
+ 'compile.unchanged',
11
+ 'snapshot.corrupt',
12
+ 'snapshot.missing'
13
+ ];
14
+ function code(value) {
15
+ if (value === undefined)
16
+ return undefined;
17
+ const trimmed = value.trim().slice(0, 64);
18
+ return /^[A-Za-z0-9_.:-]+$/.test(trimmed) ? trimmed : 'unspecified';
19
+ }
20
+ export function buildContextGraphEvent(input) {
21
+ const reason = code(input.reason);
22
+ return {
23
+ schema: CONTEXT_GRAPH_EVENT_SCHEMA,
24
+ ts: input.at,
25
+ type: input.type,
26
+ ...(input.cacheKey === undefined ? {} : { cache_key: input.cacheKey }),
27
+ ...(input.snapshotHash === undefined ? {} : { snapshot_hash: input.snapshotHash }),
28
+ ...(input.previousSnapshotHash === undefined
29
+ ? {}
30
+ : { previous_snapshot_hash: input.previousSnapshotHash }),
31
+ ...(input.nodeCount === undefined ? {} : { node_count: input.nodeCount }),
32
+ ...(input.edgeCount === undefined ? {} : { edge_count: input.edgeCount }),
33
+ ...(input.errorCount === undefined ? {} : { error_count: input.errorCount }),
34
+ ...(input.warningCount === undefined ? {} : { warning_count: input.warningCount }),
35
+ ...(input.durationMs === undefined ? {} : { duration_ms: input.durationMs }),
36
+ ...(reason === undefined ? {} : { reason }),
37
+ ...(input.incremental === undefined ? {} : { incremental: input.incremental })
38
+ };
39
+ }
40
+ export async function appendContextGraphEvent(root, input) {
41
+ try {
42
+ await appendJsonlBounded(contextGraphEventLogPath(root), buildContextGraphEvent(input), CONTEXT_GRAPH_EVENT_LOG_MAX_BYTES);
43
+ return true;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
@@ -0,0 +1,85 @@
1
+ import fsp from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { ensureDir, sha256, writeJsonAtomic } from '../../../fsx.js';
4
+ import { CONTEXT_GRAPH_FRAGMENT_SCHEMA } from '../contracts.js';
5
+ import { contextGraphFragmentCacheDir } from '../paths.js';
6
+ export const CONTEXT_GRAPH_FRAGMENT_CACHE_MAX_ENTRIES = 64;
7
+ export function fragmentCacheKey(input) {
8
+ return sha256(JSON.stringify([
9
+ CONTEXT_GRAPH_FRAGMENT_SCHEMA,
10
+ input.extractorId,
11
+ input.extractorRevision,
12
+ input.cacheKey,
13
+ input.changedPaths === null ? null : [...input.changedPaths].sort()
14
+ ]));
15
+ }
16
+ function entryPath(root, key) {
17
+ return path.join(contextGraphFragmentCacheDir(root), `${key}.json`);
18
+ }
19
+ function isFragment(value, extractorId) {
20
+ if (!value || typeof value !== 'object')
21
+ return false;
22
+ const fragment = value;
23
+ return (fragment.schema === CONTEXT_GRAPH_FRAGMENT_SCHEMA
24
+ && fragment.extractor === extractorId
25
+ && Array.isArray(fragment.nodes)
26
+ && Array.isArray(fragment.edges)
27
+ && Array.isArray(fragment.issues)
28
+ && Array.isArray(fragment.skipped)
29
+ && Boolean(fragment.inputHashes)
30
+ && typeof fragment.inputHashes === 'object');
31
+ }
32
+ export async function readCachedFragment(root, key, extractorId) {
33
+ let raw;
34
+ try {
35
+ raw = await fsp.readFile(entryPath(root, key), 'utf8');
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ try {
41
+ const parsed = JSON.parse(raw);
42
+ return isFragment(parsed, extractorId) ? parsed : null;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ export async function writeCachedFragment(root, key, fragment) {
49
+ await ensureDir(contextGraphFragmentCacheDir(root));
50
+ await writeJsonAtomic(entryPath(root, key), fragment);
51
+ }
52
+ export async function pruneFragmentCache(root, maxEntries = CONTEXT_GRAPH_FRAGMENT_CACHE_MAX_ENTRIES) {
53
+ const dir = contextGraphFragmentCacheDir(root);
54
+ let names;
55
+ try {
56
+ names = (await fsp.readdir(dir)).filter((name) => name.endsWith('.json'));
57
+ }
58
+ catch {
59
+ return 0;
60
+ }
61
+ if (names.length <= maxEntries)
62
+ return 0;
63
+ const rows = [];
64
+ for (const name of names) {
65
+ const file = path.join(dir, name);
66
+ try {
67
+ const stat = await fsp.stat(file);
68
+ rows.push({ file, mtimeMs: stat.mtimeMs });
69
+ }
70
+ catch {
71
+ }
72
+ }
73
+ rows.sort((left, right) => right.mtimeMs - left.mtimeMs || (left.file < right.file ? -1 : 1));
74
+ const removable = rows.slice(maxEntries);
75
+ let removed = 0;
76
+ for (const row of removable) {
77
+ try {
78
+ await fsp.rm(row.file, { force: true });
79
+ removed += 1;
80
+ }
81
+ catch {
82
+ }
83
+ }
84
+ return removed;
85
+ }