@unerr-ai/unerr 0.2.1 → 0.2.3

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 (363) hide show
  1. package/README.md +36 -45
  2. package/dist/cli.js +37443 -36022
  3. package/package.json +2 -1
  4. package/dist/behaviors/agent-llm-bridge.js +0 -166
  5. package/dist/behaviors/architecture-guard.js +0 -256
  6. package/dist/behaviors/auto-doc.js +0 -247
  7. package/dist/behaviors/cascade-guard.js +0 -289
  8. package/dist/behaviors/change-narrative.js +0 -270
  9. package/dist/behaviors/convention-drift.js +0 -290
  10. package/dist/behaviors/framework.js +0 -235
  11. package/dist/behaviors/guard-formatter.js +0 -44
  12. package/dist/behaviors/incomplete-work.js +0 -270
  13. package/dist/behaviors/loop-breaker.js +0 -300
  14. package/dist/behaviors/session-continuity.js +0 -208
  15. package/dist/commands/branches.js +0 -97
  16. package/dist/commands/check-commit.js +0 -225
  17. package/dist/commands/compress-output.js +0 -64
  18. package/dist/commands/config-verify.js +0 -243
  19. package/dist/commands/daemon.js +0 -905
  20. package/dist/commands/dashboard.js +0 -52
  21. package/dist/commands/debug.js +0 -200
  22. package/dist/commands/enrich.js +0 -184
  23. package/dist/commands/exec.js +0 -233
  24. package/dist/commands/gain.js +0 -156
  25. package/dist/commands/hook.js +0 -88
  26. package/dist/commands/index.js +0 -88
  27. package/dist/commands/init.js +0 -74
  28. package/dist/commands/install.js +0 -505
  29. package/dist/commands/learn.js +0 -116
  30. package/dist/commands/manifest.js +0 -193
  31. package/dist/commands/rewind.js +0 -103
  32. package/dist/commands/serve.js +0 -19
  33. package/dist/commands/setup-wizard.js +0 -414
  34. package/dist/commands/skills.js +0 -64
  35. package/dist/commands/stats.js +0 -20
  36. package/dist/commands/status.js +0 -654
  37. package/dist/commands/timeline.js +0 -139
  38. package/dist/commands/uninstall.js +0 -230
  39. package/dist/components/App.js +0 -109
  40. package/dist/components/Banner.js +0 -12
  41. package/dist/components/ConfirmPrompt.js +0 -25
  42. package/dist/components/DriftSummary.js +0 -23
  43. package/dist/components/GradeBadge.js +0 -15
  44. package/dist/components/HealthCard.js +0 -18
  45. package/dist/components/InkSpinner.js +0 -22
  46. package/dist/components/InputBox.js +0 -17
  47. package/dist/components/KeyValue.js +0 -13
  48. package/dist/components/MessageList.js +0 -14
  49. package/dist/components/ProgressBar.js +0 -26
  50. package/dist/components/Section.js +0 -16
  51. package/dist/components/SessionSummaryCard.js +0 -73
  52. package/dist/components/StartupDisplay.js +0 -24
  53. package/dist/components/StatusDashboard.js +0 -57
  54. package/dist/components/StatusLine.js +0 -8
  55. package/dist/components/StepLine.js +0 -22
  56. package/dist/components/Theme.js +0 -20
  57. package/dist/components/ToolProgress.js +0 -8
  58. package/dist/components/ViolationList.js +0 -21
  59. package/dist/components/render.js +0 -13
  60. package/dist/config/agent-registry.js +0 -237
  61. package/dist/config/claude-settings-hooks.js +0 -304
  62. package/dist/config/hook-installer.js +0 -65
  63. package/dist/config/instruction-writer.js +0 -388
  64. package/dist/config/mcp-config-writer.js +0 -266
  65. package/dist/config/settings.js +0 -174
  66. package/dist/config/tool-detector.js +0 -42
  67. package/dist/config/value-surfacing.js +0 -119
  68. package/dist/core/context-assembly.js +0 -108
  69. package/dist/core/conversation.js +0 -33
  70. package/dist/core/local-chat-provider.js +0 -475
  71. package/dist/core/provider-factory.js +0 -55
  72. package/dist/core/providers.js +0 -90
  73. package/dist/core/query-engine.js +0 -174
  74. package/dist/daemon/api.js +0 -312
  75. package/dist/daemon/autostart.js +0 -119
  76. package/dist/daemon/bootstrap.js +0 -39
  77. package/dist/daemon/client.js +0 -164
  78. package/dist/daemon/detect-ci.js +0 -81
  79. package/dist/daemon/platform-linux.js +0 -146
  80. package/dist/daemon/platform-macos.js +0 -134
  81. package/dist/daemon/platform-windows.js +0 -116
  82. package/dist/daemon/process-manager.js +0 -299
  83. package/dist/daemon/protocol.js +0 -23
  84. package/dist/daemon/registry.js +0 -270
  85. package/dist/daemon/settings-schema.js +0 -72
  86. package/dist/daemon/system-health.js +0 -134
  87. package/dist/daemon/version-checker.js +0 -262
  88. package/dist/daemon/warm-start.js +0 -223
  89. package/dist/entrypoints/cli.js +0 -1043
  90. package/dist/entrypoints/daemon.js +0 -380
  91. package/dist/entrypoints/repl.js +0 -147
  92. package/dist/hooks/adapters/claude-code.js +0 -90
  93. package/dist/hooks/adapters/cline.js +0 -100
  94. package/dist/hooks/adapters/cursor.js +0 -98
  95. package/dist/hooks/hook-dedup.js +0 -79
  96. package/dist/hooks/hook-runner.js +0 -113
  97. package/dist/hooks/navigation-hooks.js +0 -175
  98. package/dist/hooks/prompt-hooks.js +0 -63
  99. package/dist/hooks/shell-hooks.js +0 -47
  100. package/dist/ignore.js +0 -111
  101. package/dist/intelligence/approach-suggester.js +0 -61
  102. package/dist/intelligence/ast-extractor.js +0 -2615
  103. package/dist/intelligence/ast-worker.js +0 -34
  104. package/dist/intelligence/background-indexer.js +0 -121
  105. package/dist/intelligence/blast-radius.js +0 -200
  106. package/dist/intelligence/community-detection.js +0 -691
  107. package/dist/intelligence/community-detector.js +0 -184
  108. package/dist/intelligence/computation-scheduler.js +0 -75
  109. package/dist/intelligence/confidence-propagation.js +0 -47
  110. package/dist/intelligence/convention-detector.js +0 -242
  111. package/dist/intelligence/convention-learner.js +0 -205
  112. package/dist/intelligence/convention-matcher.js +0 -205
  113. package/dist/intelligence/cozo-schema.js +0 -376
  114. package/dist/intelligence/decision-point-detector.js +0 -90
  115. package/dist/intelligence/deep-dive-tools.js +0 -586
  116. package/dist/intelligence/durability-scorer.js +0 -84
  117. package/dist/intelligence/exploration-cost.js +0 -204
  118. package/dist/intelligence/exploration-pattern-tracker.js +0 -61
  119. package/dist/intelligence/fact-generator.js +0 -322
  120. package/dist/intelligence/facts-schema.js +0 -90
  121. package/dist/intelligence/file-intelligence.js +0 -59
  122. package/dist/intelligence/graph-holder.js +0 -220
  123. package/dist/intelligence/graph-temporal-joiner.js +0 -238
  124. package/dist/intelligence/health-grade.js +0 -423
  125. package/dist/intelligence/health-grader.js +0 -200
  126. package/dist/intelligence/health-map-data.js +0 -259
  127. package/dist/intelligence/import-symbols.js +0 -136
  128. package/dist/intelligence/incremental-indexer.js +0 -658
  129. package/dist/intelligence/indexer/centrality.js +0 -62
  130. package/dist/intelligence/indexer/cfg-context.js +0 -95
  131. package/dist/intelligence/indexer/confidence.js +0 -34
  132. package/dist/intelligence/indexer/cross-file-resolver.js +0 -104
  133. package/dist/intelligence/indexer/edge-repair.js +0 -89
  134. package/dist/intelligence/indexer/entity-key.js +0 -17
  135. package/dist/intelligence/indexer/export-map.js +0 -132
  136. package/dist/intelligence/indexer/git-cochange.js +0 -128
  137. package/dist/intelligence/indexer/graph-patch.js +0 -147
  138. package/dist/intelligence/indexer/incremental.js +0 -78
  139. package/dist/intelligence/indexer/ingest.js +0 -160
  140. package/dist/intelligence/indexer/language-detect.js +0 -226
  141. package/dist/intelligence/indexer/metadata.js +0 -63
  142. package/dist/intelligence/indexer/mutation-tracker.js +0 -79
  143. package/dist/intelligence/indexer/orchestrator.js +0 -155
  144. package/dist/intelligence/indexer/plugin-interface.js +0 -31
  145. package/dist/intelligence/indexer/plugins/csharp.js +0 -440
  146. package/dist/intelligence/indexer/plugins/go.js +0 -335
  147. package/dist/intelligence/indexer/plugins/java.js +0 -370
  148. package/dist/intelligence/indexer/plugins/python.js +0 -358
  149. package/dist/intelligence/indexer/plugins/regex-fallback.js +0 -82
  150. package/dist/intelligence/indexer/plugins/ruby.js +0 -290
  151. package/dist/intelligence/indexer/plugins/rust.js +0 -484
  152. package/dist/intelligence/indexer/plugins/tier2-generic.js +0 -310
  153. package/dist/intelligence/indexer/plugins/typescript.js +0 -456
  154. package/dist/intelligence/indexer/resource-monitor.js +0 -93
  155. package/dist/intelligence/indexer/scip/decoder.js +0 -253
  156. package/dist/intelligence/indexer/scip/detector.js +0 -232
  157. package/dist/intelligence/indexer/scip/downloader.js +0 -427
  158. package/dist/intelligence/indexer/scip/fallback.js +0 -34
  159. package/dist/intelligence/indexer/scip/merger.js +0 -109
  160. package/dist/intelligence/indexer/scip/orchestrator.js +0 -433
  161. package/dist/intelligence/indexer/scip/runner.js +0 -98
  162. package/dist/intelligence/indexer/snapshot.js +0 -66
  163. package/dist/intelligence/indexer/test-detector.js +0 -196
  164. package/dist/intelligence/indexer/watch-integration.js +0 -61
  165. package/dist/intelligence/indexer/worker.js +0 -85
  166. package/dist/intelligence/local-convention-detector.js +0 -437
  167. package/dist/intelligence/local-embeddings.js +0 -190
  168. package/dist/intelligence/local-graph.js +0 -1946
  169. package/dist/intelligence/local-indexer.js +0 -1575
  170. package/dist/intelligence/local-llm.js +0 -163
  171. package/dist/intelligence/local-rule-generator.js +0 -154
  172. package/dist/intelligence/local-snapshot.js +0 -213
  173. package/dist/intelligence/negative-knowledge.js +0 -103
  174. package/dist/intelligence/persistent-db.js +0 -85
  175. package/dist/intelligence/query-router.js +0 -2556
  176. package/dist/intelligence/risk-classifier.js +0 -116
  177. package/dist/intelligence/rule-evaluator.js +0 -380
  178. package/dist/intelligence/rule-generator.js +0 -49
  179. package/dist/intelligence/search-index.js +0 -173
  180. package/dist/intelligence/semantic/docstring-extractor.js +0 -67
  181. package/dist/intelligence/semantic/embedding-store.js +0 -52
  182. package/dist/intelligence/semantic/enrichment-orchestrator.js +0 -48
  183. package/dist/intelligence/semantic/git-message-miner.js +0 -114
  184. package/dist/intelligence/semantic/identifier-tokenizer.js +0 -51
  185. package/dist/intelligence/semantic/node2vec-embeddings.js +0 -71
  186. package/dist/intelligence/semantic/node2vec-walks.js +0 -103
  187. package/dist/intelligence/semantic/path-domain-inference.js +0 -112
  188. package/dist/intelligence/semantic/similarity-engine.js +0 -60
  189. package/dist/intelligence/semantic/tfidf-vectors.js +0 -88
  190. package/dist/intelligence/session-brief-builder.js +0 -159
  191. package/dist/intelligence/session-context.js +0 -221
  192. package/dist/intelligence/session-health-monitor.js +0 -211
  193. package/dist/intelligence/session-narrative.js +0 -197
  194. package/dist/intelligence/session-pattern-analyzer.js +0 -218
  195. package/dist/intelligence/signal-scorer.js +0 -390
  196. package/dist/intelligence/signal-show-store.js +0 -182
  197. package/dist/intelligence/smart-truncate.js +0 -158
  198. package/dist/intelligence/subgraph-cache.js +0 -88
  199. package/dist/intelligence/temporal-facts.js +0 -494
  200. package/dist/intelligence/token-estimator.js +0 -100
  201. package/dist/intelligence/tool-injector.js +0 -87
  202. package/dist/intelligence/tree-sitter-loader.js +0 -71
  203. package/dist/intelligence/worker-pool.js +0 -116
  204. package/dist/proxy/arg-validator.js +0 -79
  205. package/dist/proxy/auto-bootstrap.js +0 -167
  206. package/dist/proxy/bridge.js +0 -147
  207. package/dist/proxy/budget-enforcer.js +0 -70
  208. package/dist/proxy/compression-quality-monitor.js +0 -160
  209. package/dist/proxy/compression-stats.js +0 -51
  210. package/dist/proxy/context-rot-detector.js +0 -137
  211. package/dist/proxy/drift-detector.js +0 -139
  212. package/dist/proxy/efficiency-tracker.js +0 -79
  213. package/dist/proxy/fact-ranking.js +0 -154
  214. package/dist/proxy/format-encoder.js +0 -266
  215. package/dist/proxy/http-transport.js +0 -90
  216. package/dist/proxy/lifecycle-actor.js +0 -55
  217. package/dist/proxy/lifecycle-machine.js +0 -187
  218. package/dist/proxy/log-tailer.js +0 -265
  219. package/dist/proxy/model-pricing.js +0 -98
  220. package/dist/proxy/network-firewall.js +0 -141
  221. package/dist/proxy/nudge-state.js +0 -93
  222. package/dist/proxy/output-compressor.js +0 -185
  223. package/dist/proxy/pid-lock.js +0 -291
  224. package/dist/proxy/proxy-context.js +0 -11
  225. package/dist/proxy/proxy.js +0 -2633
  226. package/dist/proxy/response-enrichment.js +0 -32
  227. package/dist/proxy/response-envelope.js +0 -313
  228. package/dist/proxy/session-dedup.js +0 -82
  229. package/dist/proxy/session-legend.js +0 -30
  230. package/dist/proxy/session-persistence.js +0 -210
  231. package/dist/proxy/session-resume.js +0 -94
  232. package/dist/proxy/session-stats.js +0 -513
  233. package/dist/proxy/shell-classifier.js +0 -1346
  234. package/dist/proxy/shell-compression-log.js +0 -93
  235. package/dist/proxy/shell-compressor.js +0 -390
  236. package/dist/proxy/shell-graph-boost.js +0 -202
  237. package/dist/proxy/shell-monitor-map.js +0 -18
  238. package/dist/proxy/shell-stats.js +0 -54
  239. package/dist/proxy/shell-strategies/cloud.js +0 -215
  240. package/dist/proxy/shell-strategies/diff.js +0 -159
  241. package/dist/proxy/shell-strategies/error-diagnostic.js +0 -796
  242. package/dist/proxy/shell-strategies/filter-dsl.js +0 -358
  243. package/dist/proxy/shell-strategies/git-status.js +0 -177
  244. package/dist/proxy/shell-strategies/key-value.js +0 -193
  245. package/dist/proxy/shell-strategies/log-text.js +0 -154
  246. package/dist/proxy/shell-strategies/omni.js +0 -188
  247. package/dist/proxy/shell-strategies/progress.js +0 -55
  248. package/dist/proxy/shell-strategies/redact.js +0 -76
  249. package/dist/proxy/shell-strategies/structured.js +0 -241
  250. package/dist/proxy/shell-strategies/tabular.js +0 -243
  251. package/dist/proxy/shell-strategies/test-results-types.js +0 -13
  252. package/dist/proxy/shell-strategies/test-results.js +0 -784
  253. package/dist/proxy/shell-strategies/tree-paths.js +0 -144
  254. package/dist/proxy/shell-strategies/yaml.js +0 -182
  255. package/dist/proxy/shell-tee.js +0 -111
  256. package/dist/proxy/signal-dedup.js +0 -171
  257. package/dist/proxy/startup-renderer.js +0 -158
  258. package/dist/proxy/task-token-display.js +0 -38
  259. package/dist/proxy/token-counter.js +0 -61
  260. package/dist/proxy/tool-clusters.js +0 -273
  261. package/dist/proxy/tool-definitions.js +0 -525
  262. package/dist/proxy/transport-mux.js +0 -229
  263. package/dist/proxy/wire-cap.js +0 -268
  264. package/dist/rules/developer.mozilla.org.json +0 -9
  265. package/dist/rules/github.com.json +0 -21
  266. package/dist/schemas/api/skills.js +0 -19
  267. package/dist/schemas/common/errors.js +0 -7
  268. package/dist/schemas/common/headers.js +0 -5
  269. package/dist/schemas/entities/edge.js +0 -25
  270. package/dist/schemas/entities/entity.js +0 -22
  271. package/dist/schemas/entities/rule.js +0 -18
  272. package/dist/schemas/index.js +0 -14
  273. package/dist/server/event-bus.js +0 -59
  274. package/dist/server/http.js +0 -156
  275. package/dist/server/middleware.js +0 -70
  276. package/dist/server/routes/drift.js +0 -97
  277. package/dist/server/routes/intelligence.js +0 -1217
  278. package/dist/server/routes/reasoning-quality.js +0 -444
  279. package/dist/server/routes/session.js +0 -86
  280. package/dist/server/routes/stream.js +0 -120
  281. package/dist/server/routes/system.js +0 -73
  282. package/dist/server/routes/temporal.js +0 -170
  283. package/dist/server/routes/timeline.js +0 -232
  284. package/dist/server/routes/token-flow.js +0 -403
  285. package/dist/skills/effectiveness-tracker.js +0 -93
  286. package/dist/skills/local-pack.js +0 -380
  287. package/dist/skills/resolver.js +0 -495
  288. package/dist/state-detector.js +0 -83
  289. package/dist/timeline/intent-detector.js +0 -263
  290. package/dist/timeline/loop-miner.js +0 -140
  291. package/dist/timeline/open-threads.js +0 -49
  292. package/dist/timeline/signal-reinforcer.js +0 -62
  293. package/dist/timeline/timeline-bootstrap.js +0 -151
  294. package/dist/timeline/timeline-store.js +0 -618
  295. package/dist/tools/coding/bash.js +0 -49
  296. package/dist/tools/coding/file-edit.js +0 -72
  297. package/dist/tools/coding/file-outline.js +0 -227
  298. package/dist/tools/coding/file-read-protocol.js +0 -425
  299. package/dist/tools/coding/file-read.js +0 -35
  300. package/dist/tools/coding/file-write.js +0 -43
  301. package/dist/tools/coding/glob-tool.js +0 -109
  302. package/dist/tools/coding/grep.js +0 -162
  303. package/dist/tools/coding/index.js +0 -27
  304. package/dist/tools/intelligence/index.js +0 -269
  305. package/dist/tools/intelligence/record-fact.js +0 -48
  306. package/dist/tools/intelligence/timeline-markers.js +0 -130
  307. package/dist/tools/registry.js +0 -47
  308. package/dist/tools/types.js +0 -8
  309. package/dist/tracking/auto-snapshot-triggers.js +0 -246
  310. package/dist/tracking/branch-context.js +0 -115
  311. package/dist/tracking/branch-snapshot.js +0 -217
  312. package/dist/tracking/causal-bridge.js +0 -317
  313. package/dist/tracking/circuit-breaker.js +0 -147
  314. package/dist/tracking/commit-watcher.js +0 -114
  315. package/dist/tracking/context-ledger.js +0 -119
  316. package/dist/tracking/correction-detector.js +0 -324
  317. package/dist/tracking/drift-tracker.js +0 -874
  318. package/dist/tracking/durability-tracker.js +0 -94
  319. package/dist/tracking/entity-rewind.js +0 -200
  320. package/dist/tracking/file-hash-state.js +0 -114
  321. package/dist/tracking/git-attribution.js +0 -132
  322. package/dist/tracking/git-trailers.js +0 -171
  323. package/dist/tracking/intelligence-counter.js +0 -46
  324. package/dist/tracking/intent-correlator.js +0 -202
  325. package/dist/tracking/intent-encoder.js +0 -52
  326. package/dist/tracking/intent-token-tracker.js +0 -159
  327. package/dist/tracking/ledger-archiver.js +0 -94
  328. package/dist/tracking/ledger-chains.js +0 -245
  329. package/dist/tracking/metrics-store.js +0 -361
  330. package/dist/tracking/native-watcher.js +0 -131
  331. package/dist/tracking/offline-rewind.js +0 -295
  332. package/dist/tracking/pending-violations.js +0 -74
  333. package/dist/tracking/persistence-effectiveness.js +0 -167
  334. package/dist/tracking/prompt-durability.js +0 -202
  335. package/dist/tracking/quality-signals.js +0 -213
  336. package/dist/tracking/redactor.js +0 -73
  337. package/dist/tracking/rewind-engine.js +0 -161
  338. package/dist/tracking/session-history.js +0 -128
  339. package/dist/tracking/session-receipt.js +0 -88
  340. package/dist/tracking/session-summary-writer.js +0 -157
  341. package/dist/tracking/shadow-ledger.js +0 -321
  342. package/dist/tracking/stash-manager.js +0 -258
  343. package/dist/tracking/timeline-fork.js +0 -213
  344. package/dist/tracking/timeline.js +0 -69
  345. package/dist/tracking/token-flow.js +0 -276
  346. package/dist/tracking/turn-segmenter.js +0 -122
  347. package/dist/tracking/weekly-accumulator.js +0 -179
  348. package/dist/tracking/working-snapshots.js +0 -188
  349. package/dist/tracking/workspace-manifest.js +0 -176
  350. package/dist/transport/http.js +0 -102
  351. package/dist/utils/counterfactual.js +0 -65
  352. package/dist/utils/deep-link.js +0 -34
  353. package/dist/utils/detect.js +0 -193
  354. package/dist/utils/exec.js +0 -73
  355. package/dist/utils/file-logger.js +0 -87
  356. package/dist/utils/format-error.js +0 -29
  357. package/dist/utils/git.js +0 -181
  358. package/dist/utils/log.js +0 -57
  359. package/dist/utils/logger.js +0 -35
  360. package/dist/utils/mcp-content-json.js +0 -8
  361. package/dist/utils/session-logger.js +0 -154
  362. package/dist/utils/startup-log.js +0 -512
  363. package/dist/utils/ui.js +0 -56
@@ -1,263 +0,0 @@
1
- /**
2
- * Intent Detector (ST-4) — cross-session task stitching.
3
- *
4
- * Walks closed turns + recorded session_files + markers in timeline.db and
5
- * groups sessions into `intents`. A session attaches to an intent if EITHER:
6
- *
7
- * a) it emitted a `mark_intent` whose text matches an existing intent's
8
- * title (anchored stitch, source="agent_marker"); OR
9
- * b) its file set has Jaccard overlap > 0.4 with an intent's file_set AND
10
- * the intent was last active within the freshness window (default 14 d).
11
- *
12
- * Otherwise a new intent is created with source="file_jaccard". After each
13
- * run, intents inactive for > 21 d move to "dormant".
14
- *
15
- * Pure functions are exported for testing; the orchestrator `runIntentStitch`
16
- * does the I/O against CozoTimelineStore.
17
- */
18
- import { randomUUID } from "node:crypto";
19
- const DEFAULT_JACCARD = 0.4;
20
- const DEFAULT_FRESHNESS_MS = 14 * 24 * 60 * 60_000;
21
- const DEFAULT_DORMANT_MS = 21 * 24 * 60 * 60_000;
22
- export function jaccard(a, b) {
23
- if (a.size === 0 && b.size === 0)
24
- return 0;
25
- let intersection = 0;
26
- const [small, big] = a.size <= b.size ? [a, b] : [b, a];
27
- for (const v of small)
28
- if (big.has(v))
29
- intersection += 1;
30
- const union = a.size + b.size - intersection;
31
- return union === 0 ? 0 : intersection / union;
32
- }
33
- export function hashFileSet(files) {
34
- const sorted = [...new Set(files)].sort();
35
- // Tiny deterministic hash — enough to dedupe identical file sets without
36
- // hauling in a crypto dep. Stable across processes since it's data-only.
37
- let h = 0;
38
- for (const f of sorted) {
39
- for (let i = 0; i < f.length; i++) {
40
- h = (h * 31 + f.charCodeAt(i)) | 0;
41
- }
42
- h = (h * 31 + 0x5f) | 0;
43
- }
44
- return `${sorted.length}-${(h >>> 0).toString(36)}`;
45
- }
46
- /**
47
- * Pure stitch — given session summaries + existing intents (with their session
48
- * lists), return updated intents + new attachments. Does NOT call the DB.
49
- */
50
- export function stitchIntents(sessions, existingIntents, existingAttachments, opts = {}) {
51
- const jaccardThreshold = opts.jaccardThreshold ?? DEFAULT_JACCARD;
52
- const freshnessMs = opts.freshnessMs ?? DEFAULT_FRESHNESS_MS;
53
- const dormantAfterMs = opts.dormantAfterMs ?? DEFAULT_DORMANT_MS;
54
- const now = opts.nowMs ?? Date.now();
55
- const attachedSessions = new Set(existingAttachments.map((a) => a.session_id));
56
- const titleIndex = new Map();
57
- for (const i of existingIntents) {
58
- if (i.title.length > 0)
59
- titleIndex.set(i.title.toLowerCase(), i);
60
- }
61
- // Working copy of intents; we mutate file_set + last_active_at.
62
- const intents = new Map();
63
- for (const i of existingIntents) {
64
- let files = [];
65
- try {
66
- const parsed = JSON.parse(i.file_set || "[]");
67
- if (Array.isArray(parsed))
68
- files = parsed.map((x) => String(x));
69
- }
70
- catch {
71
- files = [];
72
- }
73
- intents.set(i.intent_id, { ...i, _filesSet: new Set(files) });
74
- }
75
- const newAttachments = [];
76
- // Stitch each session, oldest-first so deterministic merge order.
77
- const ordered = [...sessions].sort((a, b) => a.started_at - b.started_at);
78
- for (const s of ordered) {
79
- if (attachedSessions.has(s.session_id))
80
- continue;
81
- // 1) marker anchor
82
- if (s.intent_text && s.intent_text.length > 0) {
83
- const existing = titleIndex.get(s.intent_text.toLowerCase());
84
- if (existing) {
85
- attachSession(intents.get(existing.intent_id), s);
86
- newAttachments.push({
87
- intent_id: existing.intent_id,
88
- session_id: s.session_id,
89
- });
90
- attachedSessions.add(s.session_id);
91
- continue;
92
- }
93
- const created = createIntent(s, s.intent_text, "agent_marker");
94
- intents.set(created.intent_id, created);
95
- titleIndex.set(s.intent_text.toLowerCase(), created);
96
- newAttachments.push({
97
- intent_id: created.intent_id,
98
- session_id: s.session_id,
99
- });
100
- attachedSessions.add(s.session_id);
101
- continue;
102
- }
103
- // 2) Jaccard
104
- let best = null;
105
- for (const i of intents.values()) {
106
- if (s.started_at - i.last_active_at > freshnessMs)
107
- continue;
108
- const score = jaccard(s.files, i._filesSet);
109
- if (score >= jaccardThreshold && (best === null || score > best.score)) {
110
- best = { intent: i, score };
111
- }
112
- }
113
- if (best) {
114
- attachSession(best.intent, s);
115
- newAttachments.push({
116
- intent_id: best.intent.intent_id,
117
- session_id: s.session_id,
118
- });
119
- attachedSessions.add(s.session_id);
120
- continue;
121
- }
122
- // 3) Create fresh
123
- const created = createIntent(s, deriveTitle(s.files), "file_jaccard");
124
- intents.set(created.intent_id, created);
125
- if (created.title.length > 0)
126
- titleIndex.set(created.title.toLowerCase(), created);
127
- newAttachments.push({
128
- intent_id: created.intent_id,
129
- session_id: s.session_id,
130
- });
131
- attachedSessions.add(s.session_id);
132
- }
133
- // Pass — dormant transitions
134
- const dormantTransitions = [];
135
- for (const i of intents.values()) {
136
- if (i.status === "active" && now - i.last_active_at > dormantAfterMs) {
137
- i.status = "dormant";
138
- dormantTransitions.push(i.intent_id);
139
- }
140
- }
141
- return {
142
- intents: [...intents.values()].map((i) => stripWorkingFields(i)),
143
- attachments: newAttachments,
144
- dormantTransitions,
145
- };
146
- }
147
- function attachSession(intent, session) {
148
- for (const f of session.files)
149
- intent._filesSet.add(f);
150
- intent.last_active_at = Math.max(intent.last_active_at, session.last_active_at);
151
- intent.confidence = Math.min(1, intent.confidence + 0.05);
152
- intent.file_set = JSON.stringify([...intent._filesSet].sort());
153
- intent.file_set_hash = hashFileSet(intent._filesSet);
154
- }
155
- function createIntent(session, title, source) {
156
- const filesSorted = [...session.files].sort();
157
- return {
158
- intent_id: randomUUID(),
159
- title,
160
- started_at: session.started_at,
161
- last_active_at: session.last_active_at,
162
- file_set: JSON.stringify(filesSorted),
163
- file_set_hash: hashFileSet(filesSorted),
164
- status: "active",
165
- confidence: source === "agent_marker" ? 0.8 : 0.5,
166
- source,
167
- _filesSet: new Set(session.files),
168
- };
169
- }
170
- function stripWorkingFields(i) {
171
- const { _filesSet, ...row } = i;
172
- void _filesSet;
173
- return row;
174
- }
175
- function deriveTitle(files) {
176
- if (files.size === 0)
177
- return "Misc";
178
- // Pick the most common directory prefix as a title hint.
179
- const dirCounts = new Map();
180
- for (const f of files) {
181
- const dir = f.split("/").slice(0, -1).join("/") || "/";
182
- dirCounts.set(dir, (dirCounts.get(dir) ?? 0) + 1);
183
- }
184
- let bestDir = "";
185
- let bestCount = -1;
186
- for (const [d, c] of dirCounts) {
187
- if (c > bestCount) {
188
- bestDir = d;
189
- bestCount = c;
190
- }
191
- }
192
- return bestDir ? `Work in ${bestDir}` : "Misc";
193
- }
194
- /**
195
- * Build session summaries from turns + markers fetched from the store.
196
- */
197
- export function buildSessionSummaries(turns, markers, filesBySession) {
198
- const intentBySession = new Map();
199
- for (const m of markers) {
200
- if (m.type === "mark_intent" && !intentBySession.has(m.session_id)) {
201
- intentBySession.set(m.session_id, m.text);
202
- }
203
- }
204
- const byId = new Map();
205
- for (const t of turns) {
206
- let s = byId.get(t.session_id);
207
- if (!s) {
208
- s = {
209
- session_id: t.session_id,
210
- started_at: t.started_at,
211
- last_active_at: t.ended_at,
212
- files: filesBySession.get(t.session_id) ?? new Set(),
213
- intent_text: intentBySession.get(t.session_id),
214
- };
215
- byId.set(t.session_id, s);
216
- }
217
- else {
218
- s.started_at = Math.min(s.started_at, t.started_at);
219
- s.last_active_at = Math.max(s.last_active_at, t.ended_at);
220
- }
221
- }
222
- return [...byId.values()];
223
- }
224
- /**
225
- * IO orchestrator — fetches state from the store, runs stitchIntents, writes
226
- * back. Safe to call repeatedly (idempotent: previously-attached sessions are
227
- * skipped).
228
- */
229
- export async function runIntentStitch(store, opts = {}) {
230
- const turns = await store.listTurns({ limit: 500 });
231
- const markers = await store.listMarkers({ limit: 1000 });
232
- const sessionIds = new Set(turns.map((t) => t.session_id));
233
- const filesBySession = new Map();
234
- for (const sid of sessionIds) {
235
- const files = await store.getSessionFiles(sid);
236
- filesBySession.set(sid, new Set(files));
237
- }
238
- const summaries = buildSessionSummaries(turns, markers, filesBySession);
239
- const existingIntents = await store.listIntents({ limit: 500 });
240
- const existingAttachments = [];
241
- for (const i of existingIntents) {
242
- const sessions = await store.listIntentSessions(i.intent_id);
243
- for (const sid of sessions) {
244
- existingAttachments.push({ intent_id: i.intent_id, session_id: sid });
245
- }
246
- }
247
- const result = stitchIntents(summaries, existingIntents, existingAttachments, opts);
248
- let created = 0;
249
- const existingIds = new Set(existingIntents.map((i) => i.intent_id));
250
- for (const i of result.intents) {
251
- await store.upsertIntent(i);
252
- if (!existingIds.has(i.intent_id))
253
- created += 1;
254
- }
255
- for (const a of result.attachments) {
256
- await store.attachSession(a.intent_id, a.session_id);
257
- }
258
- return {
259
- created,
260
- attached: result.attachments.length,
261
- dormant: result.dormantTransitions.length,
262
- };
263
- }
@@ -1,140 +0,0 @@
1
- /**
2
- * Loop Miner (ST-3a).
3
- *
4
- * Pure functions over shadow-ledger entries. Surfaces two confusion shapes:
5
- * 1. File-read loops — same file read ≥5 times within a 10-minute window
6
- * with no edit in between. Signal that the agent is rereading instead of
7
- * progressing.
8
- * 2. Query-search loops — same `search_code` query issued ≥3 times within
9
- * a 20-minute window. Signal of a gap in the agent's mental model (the
10
- * function was renamed, the symbol doesn't exist, etc.).
11
- *
12
- * Read-only: never writes anything. Callers (the dashboard route, the future
13
- * insights panel) pass in entries from `ShadowLedger.getRecentEntries()`.
14
- */
15
- const READ_TOOLS = new Set(["file_read", "file_outline", "get_file", "Read"]);
16
- const EDIT_TOOLS = new Set([
17
- "Edit",
18
- "Write",
19
- "MultiEdit",
20
- "NotebookEdit",
21
- "edit_file",
22
- "write_file",
23
- ]);
24
- const DEFAULT_READ_WINDOW_MS = 10 * 60_000;
25
- const DEFAULT_QUERY_WINDOW_MS = 20 * 60_000;
26
- const DEFAULT_READ_THRESHOLD = 5;
27
- const DEFAULT_QUERY_THRESHOLD = 3;
28
- function entryFilePath(entry) {
29
- const fp = entry.args_summary?.file_path ??
30
- entry.args_summary?.path;
31
- return typeof fp === "string" && fp.length > 0 ? fp : null;
32
- }
33
- function entryTsMs(entry) {
34
- return Date.parse(entry.ts);
35
- }
36
- export function detectFileReadLoops(entries, opts = {}) {
37
- const window = opts.readWindowMs ?? DEFAULT_READ_WINDOW_MS;
38
- const threshold = opts.readThreshold ?? DEFAULT_READ_THRESHOLD;
39
- const now = opts.nowMs ?? maxTs(entries) ?? Date.now();
40
- const cutoff = now - window;
41
- const grouped = new Map();
42
- for (const e of entries) {
43
- const ts = entryTsMs(e);
44
- if (!Number.isFinite(ts) || ts < cutoff)
45
- continue;
46
- const fp = entryFilePath(e);
47
- if (!fp)
48
- continue;
49
- const key = `${e.session_id}::${fp}`;
50
- let bucket = grouped.get(key);
51
- if (!bucket) {
52
- bucket = { reads: [], lastEditTs: 0, session_id: e.session_id };
53
- grouped.set(key, bucket);
54
- }
55
- if (EDIT_TOOLS.has(e.tool)) {
56
- // Edit clears the read run — only reads AFTER the last edit count.
57
- bucket.reads = [];
58
- bucket.lastEditTs = ts;
59
- continue;
60
- }
61
- if (READ_TOOLS.has(e.tool)) {
62
- bucket.reads.push(e);
63
- }
64
- }
65
- const out = [];
66
- for (const [key, bucket] of grouped) {
67
- if (bucket.reads.length < threshold)
68
- continue;
69
- const fp = key.split("::").slice(1).join("::");
70
- const first = bucket.reads[0];
71
- const last = bucket.reads[bucket.reads.length - 1];
72
- out.push({
73
- kind: "file_reread",
74
- file_path: fp,
75
- count: bucket.reads.length,
76
- first_ts: first.ts,
77
- last_ts: last.ts,
78
- session_id: bucket.session_id,
79
- });
80
- }
81
- return out.sort((a, b) => b.count - a.count);
82
- }
83
- export function detectQueryLoops(entries, opts = {}) {
84
- const window = opts.queryWindowMs ?? DEFAULT_QUERY_WINDOW_MS;
85
- const threshold = opts.queryThreshold ?? DEFAULT_QUERY_THRESHOLD;
86
- const now = opts.nowMs ?? maxTs(entries) ?? Date.now();
87
- const cutoff = now - window;
88
- const grouped = new Map();
89
- for (const e of entries) {
90
- if (e.tool !== "search_code")
91
- continue;
92
- const ts = entryTsMs(e);
93
- if (!Number.isFinite(ts) || ts < cutoff)
94
- continue;
95
- const q = e.args_summary?.query;
96
- if (typeof q !== "string" || q.length === 0)
97
- continue;
98
- const key = `${e.session_id}::${q}`;
99
- const bucket = grouped.get(key) ?? [];
100
- bucket.push(e);
101
- grouped.set(key, bucket);
102
- }
103
- const out = [];
104
- for (const [key, bucket] of grouped) {
105
- if (bucket.length < threshold)
106
- continue;
107
- const query = key.split("::").slice(1).join("::");
108
- const first = bucket[0];
109
- const last = bucket[bucket.length - 1];
110
- out.push({
111
- kind: "search_repeat",
112
- query,
113
- count: bucket.length,
114
- first_ts: first.ts,
115
- last_ts: last.ts,
116
- session_id: first.session_id,
117
- });
118
- }
119
- return out.sort((a, b) => b.count - a.count);
120
- }
121
- /**
122
- * Convenience aggregator — runs both detectors and returns the union sorted by
123
- * recency.
124
- */
125
- export function detectLoops(entries, opts = {}) {
126
- const all = [
127
- ...detectFileReadLoops(entries, opts),
128
- ...detectQueryLoops(entries, opts),
129
- ];
130
- return all.sort((a, b) => Date.parse(b.last_ts) - Date.parse(a.last_ts));
131
- }
132
- function maxTs(entries) {
133
- let max = Number.NEGATIVE_INFINITY;
134
- for (const e of entries) {
135
- const t = entryTsMs(e);
136
- if (Number.isFinite(t) && t > max)
137
- max = t;
138
- }
139
- return Number.isFinite(max) ? max : null;
140
- }
@@ -1,49 +0,0 @@
1
- /**
2
- * Open Threads (ST-3a).
3
- *
4
- * Reads markers from timeline.db and returns mark_blocker rows that do not yet
5
- * have a matching mark_resolution (matched by blocker_ref → marker_id).
6
- * Resume strip + insights panel use this to surface unfinished work.
7
- *
8
- * Read-only: never writes anything.
9
- */
10
- /**
11
- * Compute open threads from a pre-fetched list of markers. Pure — testable
12
- * without a database. Resolutions whose blocker_ref points at a blocker outside
13
- * this list are still counted (lookup by id, not membership).
14
- */
15
- export function computeOpenThreads(markers) {
16
- const resolvedRefs = new Set();
17
- for (const m of markers) {
18
- if (m.type === "mark_resolution" && m.blocker_ref.length > 0) {
19
- resolvedRefs.add(m.blocker_ref);
20
- }
21
- }
22
- const out = [];
23
- for (const m of markers) {
24
- if (m.type !== "mark_blocker")
25
- continue;
26
- if (resolvedRefs.has(m.marker_id))
27
- continue;
28
- out.push({
29
- marker_id: m.marker_id,
30
- text: m.text,
31
- session_id: m.session_id,
32
- turn_id: m.turn_id,
33
- ts: m.ts,
34
- file_path: m.file_path,
35
- });
36
- }
37
- return out.sort((a, b) => b.ts - a.ts);
38
- }
39
- /**
40
- * Async convenience wrapper: pulls markers from the store, then runs
41
- * computeOpenThreads. Limit caps the underlying marker fetch (default 500).
42
- */
43
- export async function getOpenThreads(store, opts = {}) {
44
- const markers = await store.listMarkers({
45
- sessionId: opts.sessionId,
46
- limit: opts.limit ?? 500,
47
- });
48
- return computeOpenThreads(markers);
49
- }
@@ -1,62 +0,0 @@
1
- /**
2
- * Signal Reinforcer (ST-5).
3
- *
4
- * Reinforce / contradict derived timeline signals (hot files, loops, co-changes,
5
- * conventions) in `timeline.db`. Never touches `facts.db` — Layer 9 reinforcement
6
- * stays on its own path.
7
- *
8
- * Confidence clamps to [0, 1]. Reinforcement history is unbounded in storage but
9
- * the API + UI surface only the most recent 10 events. Stale signals (no
10
- * reinforcement in `staleAfterMs`) are evicted by `pruneStaleSignals`.
11
- */
12
- import { randomUUID } from "node:crypto";
13
- const DEFAULT_STALE_MS = 14 * 24 * 60 * 60_000;
14
- /**
15
- * Reinforce (or contradict) a signal. Identity is either an explicit
16
- * `signal_id` or a `(type, scope)` natural key — the function upserts a row
17
- * if it doesn't exist yet, then appends a reinforcement event.
18
- */
19
- export async function reinforceSignal(store, identity, delta, source, opts = {}) {
20
- const now = opts.nowMs ?? Date.now();
21
- const signal = await resolveSignal(store, identity);
22
- if (!signal) {
23
- const created = {
24
- signal_id: "signal_id" in identity ? identity.signal_id : randomUUID(),
25
- type: "type" in identity ? identity.type : "unknown",
26
- scope: "scope" in identity ? identity.scope : "",
27
- content: "type" in identity ? (identity.content ?? "") : "",
28
- confidence: clamp01(0.5 + delta),
29
- first_seen_at: now,
30
- last_seen_at: now,
31
- };
32
- await store.upsertSignal(created);
33
- await store.appendReinforcement(created.signal_id, now, delta, source);
34
- return created;
35
- }
36
- signal.confidence = clamp01(signal.confidence + delta);
37
- signal.last_seen_at = now;
38
- await store.upsertSignal(signal);
39
- await store.appendReinforcement(signal.signal_id, now, delta, source);
40
- return signal;
41
- }
42
- async function resolveSignal(store, identity) {
43
- if ("signal_id" in identity) {
44
- return store.getSignal(identity.signal_id);
45
- }
46
- const all = await store.listSignals({ type: identity.type, limit: 500 });
47
- return all.find((s) => s.scope === identity.scope) ?? null;
48
- }
49
- export async function pruneStaleSignals(store, opts = {}) {
50
- const now = opts.nowMs ?? Date.now();
51
- const cutoff = now - (opts.staleAfterMs ?? DEFAULT_STALE_MS);
52
- return store.deleteSignalsBefore(cutoff);
53
- }
54
- function clamp01(v) {
55
- if (!Number.isFinite(v))
56
- return 0;
57
- if (v < 0)
58
- return 0;
59
- if (v > 1)
60
- return 1;
61
- return v;
62
- }
@@ -1,151 +0,0 @@
1
- /**
2
- * Timeline subsystem bootstrap.
3
- *
4
- * Wires together the three pieces of the Timeline Layer:
5
- * 1. CozoTimelineStore (timeline.db)
6
- * 2. The TurnSegmenter embedded in the active ShadowLedger
7
- * 3. A turn-close listener that computes a rollup and upserts it into turns
8
- *
9
- * Kill-switch: `UNERR_TIMELINE_V2=0` short-circuits everything — no db is
10
- * opened, no listener attaches, the existing proxy/mcp-server code paths are
11
- * unaffected. Same is true if startup throws — the bootstrap swallows errors
12
- * after logging so the host process never crashes on timeline issues.
13
- *
14
- * No reads or writes to graph.db / facts.db. No edits to the shadow ledger.
15
- */
16
- import { CozoTimelineStore } from "./timeline-store.js";
17
- const EDIT_TOOLS = new Set([
18
- "Edit",
19
- "Write",
20
- "MultiEdit",
21
- "NotebookEdit",
22
- "edit_file",
23
- "write_file",
24
- ]);
25
- function defaultLog(level, msg) {
26
- process.stderr.write(`[unerr:timeline] ${level.toUpperCase()}: ${msg}\n`);
27
- }
28
- /**
29
- * Start the timeline subsystem. Returns null when disabled via env, or when
30
- * initialisation fails (after logging the error). Callers should treat null
31
- * as "timeline is off" and continue normally.
32
- */
33
- export async function startTimelineBootstrap(opts) {
34
- if (process.env.UNERR_TIMELINE_V2 === "0")
35
- return null;
36
- const log = opts.log ?? defaultLog;
37
- let store;
38
- try {
39
- store = await CozoTimelineStore.create(opts.projectRoot);
40
- }
41
- catch (err) {
42
- log("warn", `init failed: ${err instanceof Error ? err.message : String(err)}`);
43
- return null;
44
- }
45
- const segmenter = opts.ledger.getTurnSegmenter();
46
- const unsubscribe = segmenter.onTurnClose((event) => {
47
- const entries = opts.ledger
48
- .getRecentEntries(100)
49
- .filter((e) => e.turn_id === event.turn_id);
50
- const rollup = computeTurnRollup(event, entries);
51
- store.upsertTurn(rollup).catch((err) => {
52
- log("warn", `upsertTurn failed for ${rollup.turn_id}: ${err instanceof Error ? err.message : String(err)}`);
53
- });
54
- // ST-4: persist distinct files touched in this turn so the intent stitcher
55
- // has a file set to Jaccard against.
56
- const files = collectFilePaths(entries);
57
- if (files.length > 0) {
58
- store
59
- .recordSessionFiles(event.session_id, files)
60
- .catch((err) => {
61
- log("warn", `recordSessionFiles failed for ${event.session_id}: ${err instanceof Error ? err.message : String(err)}`);
62
- });
63
- }
64
- // UX-2: best-effort agent name capture. Lazy resolver so late identity
65
- // (after MCP `initialize`) still gets recorded.
66
- const agentName = opts.getAgentName?.();
67
- if (agentName) {
68
- store
69
- .setSessionAgent(event.session_id, agentName)
70
- .catch((err) => {
71
- log("warn", `setSessionAgent failed for ${event.session_id}: ${err instanceof Error ? err.message : String(err)}`);
72
- });
73
- }
74
- });
75
- log("info", `active (db ${store.dbPath})`);
76
- return {
77
- store,
78
- stop: () => {
79
- unsubscribe();
80
- store.close();
81
- },
82
- };
83
- }
84
- /**
85
- * Build a turn rollup from a close event + the entries that belong to it.
86
- * Exported for testing.
87
- */
88
- export function computeTurnRollup(event, entries) {
89
- const tsValues = entries
90
- .map((e) => Date.parse(e.ts))
91
- .filter((n) => Number.isFinite(n));
92
- const started_at = tsValues.length > 0 ? Math.min(...tsValues) : event.closed_at;
93
- const filePaths = new Set();
94
- let edit_count = 0;
95
- let intentText;
96
- const opened_by = entries[0]?.turn_confidence ?? "first_call";
97
- for (const e of entries) {
98
- const fp = e.args_summary?.file_path ??
99
- e.args_summary?.path;
100
- if (typeof fp === "string" && fp.length > 0)
101
- filePaths.add(fp);
102
- if (EDIT_TOOLS.has(e.tool))
103
- edit_count += 1;
104
- if (!intentText &&
105
- e.tool === "mark_intent" &&
106
- typeof e.args_summary?.text === "string") {
107
- intentText = e.args_summary.text;
108
- }
109
- }
110
- const title = intentText ?? deriveTitleFromFiles(filePaths);
111
- return {
112
- turn_id: event.turn_id,
113
- session_id: event.session_id,
114
- started_at,
115
- ended_at: event.closed_at,
116
- opened_by,
117
- closed_reason: event.reason,
118
- tool_count: entries.length,
119
- file_count: filePaths.size,
120
- edit_count,
121
- title,
122
- outcome: "unknown",
123
- };
124
- }
125
- /** Extract distinct file paths touched by these entries. */
126
- export function collectFilePaths(entries) {
127
- const set = new Set();
128
- for (const e of entries) {
129
- const fp = e.args_summary?.file_path ??
130
- e.args_summary?.path;
131
- if (typeof fp === "string" && fp.length > 0)
132
- set.add(fp);
133
- }
134
- return [...set];
135
- }
136
- function deriveTitleFromFiles(paths) {
137
- if (paths.size === 0)
138
- return "";
139
- // Pick the shortest path as a rough proxy for "most important file" (entry
140
- // points, index files, top-level modules tend to be shorter). Cheap heuristic
141
- // until ST-3 adds a smarter title miner.
142
- let shortest = null;
143
- for (const p of paths) {
144
- if (shortest === null || p.length < shortest.length)
145
- shortest = p;
146
- }
147
- if (!shortest)
148
- return "";
149
- const base = shortest.split("/").pop() ?? shortest;
150
- return base;
151
- }