@rkarim08/sia 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (355) hide show
  1. package/.claude-plugin/marketplace.json +35 -0
  2. package/.claude-plugin/plugin.json +27 -0
  3. package/.mcp.json +13 -0
  4. package/CLAUDE.md +226 -0
  5. package/LICENSE +202 -0
  6. package/PLUGIN_README.md +253 -0
  7. package/README.md +1013 -0
  8. package/agents/sia-changelog-writer.md +89 -0
  9. package/agents/sia-code-reviewer.md +86 -0
  10. package/agents/sia-conflict-resolver.md +100 -0
  11. package/agents/sia-convention-enforcer.md +69 -0
  12. package/agents/sia-debug.md +106 -0
  13. package/agents/sia-decision-reviewer.md +101 -0
  14. package/agents/sia-dependency-tracker.md +80 -0
  15. package/agents/sia-explain.md +126 -0
  16. package/agents/sia-feature.md +116 -0
  17. package/agents/sia-knowledge-capture.md +117 -0
  18. package/agents/sia-lead-architecture-advisor.md +93 -0
  19. package/agents/sia-lead-team-health.md +107 -0
  20. package/agents/sia-migration.md +100 -0
  21. package/agents/sia-onboarding.md +115 -0
  22. package/agents/sia-orientation.md +99 -0
  23. package/agents/sia-pm-briefing.md +106 -0
  24. package/agents/sia-pm-risk-advisor.md +82 -0
  25. package/agents/sia-qa-analyst.md +116 -0
  26. package/agents/sia-qa-regression-map.md +94 -0
  27. package/agents/sia-refactor.md +115 -0
  28. package/agents/sia-regression.md +112 -0
  29. package/agents/sia-security-audit.md +125 -0
  30. package/agents/sia-test-advisor.md +91 -0
  31. package/hooks/hooks.json +98 -0
  32. package/migrations/bridge/001_initial.sql +34 -0
  33. package/migrations/episodic/001_initial.sql +35 -0
  34. package/migrations/meta/001_initial.sql +68 -0
  35. package/migrations/semantic/001_initial.sql +292 -0
  36. package/migrations/semantic/002_ontology.sql +89 -0
  37. package/migrations/semantic/003_freshness.sql +63 -0
  38. package/migrations/semantic/004_v5_unified_schema.sql +194 -0
  39. package/migrations/semantic/005_backfill_event_kinds.sql +8 -0
  40. package/migrations/semantic/006_tree_sitter.sql +6 -0
  41. package/migrations/semantic/007_branch_snapshots.sql +22 -0
  42. package/package.json +110 -0
  43. package/scripts/branch-switch.sh +13 -0
  44. package/scripts/build-wasm-grammars.sh +81 -0
  45. package/scripts/post-compact.sh +8 -0
  46. package/scripts/post-tool-use.sh +10 -0
  47. package/scripts/pre-compact.sh +8 -0
  48. package/scripts/session-end.sh +8 -0
  49. package/scripts/session-start.sh +8 -0
  50. package/scripts/start-mcp.ts +45 -0
  51. package/scripts/stop-hook.sh +8 -0
  52. package/scripts/user-prompt-submit.sh +8 -0
  53. package/scripts/viz-server.ts +152 -0
  54. package/skills/sia-brainstorm/SKILL.md +156 -0
  55. package/skills/sia-brainstorm/scripts/frame-template.html +214 -0
  56. package/skills/sia-brainstorm/scripts/helper.js +95 -0
  57. package/skills/sia-brainstorm/scripts/server.cjs +338 -0
  58. package/skills/sia-brainstorm/scripts/start-server.sh +153 -0
  59. package/skills/sia-brainstorm/scripts/stop-server.sh +55 -0
  60. package/skills/sia-brainstorm/spec-document-reviewer-prompt.md +49 -0
  61. package/skills/sia-brainstorm/visual-companion.md +286 -0
  62. package/skills/sia-capture/SKILL.md +64 -0
  63. package/skills/sia-compare/SKILL.md +33 -0
  64. package/skills/sia-conflicts/SKILL.md +38 -0
  65. package/skills/sia-debug-workflow/SKILL.md +120 -0
  66. package/skills/sia-debug-workflow/root-cause-tracing.md +70 -0
  67. package/skills/sia-debug-workflow/scripts/find-polluter.sh +64 -0
  68. package/skills/sia-debug-workflow/temporal-investigation.md +72 -0
  69. package/skills/sia-digest/SKILL.md +23 -0
  70. package/skills/sia-dispatch/SKILL.md +69 -0
  71. package/skills/sia-dispatch/agent-task-template.md +99 -0
  72. package/skills/sia-doctor/SKILL.md +39 -0
  73. package/skills/sia-execute/SKILL.md +70 -0
  74. package/skills/sia-execute-plan/SKILL.md +85 -0
  75. package/skills/sia-export-import/SKILL.md +49 -0
  76. package/skills/sia-export-knowledge/SKILL.md +46 -0
  77. package/skills/sia-finish/SKILL.md +100 -0
  78. package/skills/sia-finish/pr-summary-template.md +54 -0
  79. package/skills/sia-freshness/SKILL.md +38 -0
  80. package/skills/sia-history/SKILL.md +42 -0
  81. package/skills/sia-impact/SKILL.md +70 -0
  82. package/skills/sia-index/SKILL.md +54 -0
  83. package/skills/sia-install/SKILL.md +39 -0
  84. package/skills/sia-lead-compliance/SKILL.md +16 -0
  85. package/skills/sia-lead-drift-report/SKILL.md +16 -0
  86. package/skills/sia-lead-knowledge-map/SKILL.md +16 -0
  87. package/skills/sia-learn/SKILL.md +58 -0
  88. package/skills/sia-plan/SKILL.md +68 -0
  89. package/skills/sia-plan/plan-reviewer-prompt.md +63 -0
  90. package/skills/sia-playbooks/SKILL.md +29 -0
  91. package/skills/sia-playbooks/reference-feature.md +100 -0
  92. package/skills/sia-playbooks/reference-flagging.md +50 -0
  93. package/skills/sia-playbooks/reference-orientation.md +92 -0
  94. package/skills/sia-playbooks/reference-regression.md +115 -0
  95. package/skills/sia-playbooks/reference-review.md +64 -0
  96. package/skills/sia-playbooks/reference-tools.md +239 -0
  97. package/skills/sia-pm-decision-log/SKILL.md +28 -0
  98. package/skills/sia-pm-risk-dashboard/SKILL.md +24 -0
  99. package/skills/sia-pm-sprint-summary/SKILL.md +27 -0
  100. package/skills/sia-prune/SKILL.md +45 -0
  101. package/skills/sia-qa-coverage/SKILL.md +28 -0
  102. package/skills/sia-qa-flaky/SKILL.md +20 -0
  103. package/skills/sia-qa-report/SKILL.md +26 -0
  104. package/skills/sia-reindex/SKILL.md +30 -0
  105. package/skills/sia-review-respond/SKILL.md +88 -0
  106. package/skills/sia-review-respond/pushback-patterns.md +90 -0
  107. package/skills/sia-search/SKILL.md +47 -0
  108. package/skills/sia-setup/SKILL.md +82 -0
  109. package/skills/sia-setup/setup-checklist.md +97 -0
  110. package/skills/sia-stats/SKILL.md +36 -0
  111. package/skills/sia-status/SKILL.md +44 -0
  112. package/skills/sia-sync/SKILL.md +46 -0
  113. package/skills/sia-team/SKILL.md +64 -0
  114. package/skills/sia-test/SKILL.md +92 -0
  115. package/skills/sia-test/testing-anti-patterns.md +104 -0
  116. package/skills/sia-tour/SKILL.md +29 -0
  117. package/skills/sia-upgrade/SKILL.md +43 -0
  118. package/skills/sia-verify/SKILL.md +81 -0
  119. package/skills/sia-visualize/SKILL.md +28 -0
  120. package/skills/sia-visualize-live/SKILL.md +55 -0
  121. package/skills/sia-visualize-live/scripts/graph-template.html +389 -0
  122. package/skills/sia-visualize-live/scripts/start-visualizer.sh +161 -0
  123. package/skills/sia-visualize-live/scripts/stop-visualizer.sh +55 -0
  124. package/skills/sia-visualize-live/scripts/visualizer-server.cjs +264 -0
  125. package/skills/sia-workspace/SKILL.md +57 -0
  126. package/src/agent/claude-md-template-flagging.md +219 -0
  127. package/src/agent/claude-md-template.md +213 -0
  128. package/src/agent/modules/sia-feature.md +100 -0
  129. package/src/agent/modules/sia-flagging.md +50 -0
  130. package/src/agent/modules/sia-orientation.md +92 -0
  131. package/src/agent/modules/sia-regression.md +115 -0
  132. package/src/agent/modules/sia-review.md +64 -0
  133. package/src/agent/modules/sia-tools.md +239 -0
  134. package/src/ast/extractors/c-include.ts +189 -0
  135. package/src/ast/extractors/csharp-project.ts +260 -0
  136. package/src/ast/extractors/prisma-schema.ts +44 -0
  137. package/src/ast/extractors/project-manifest.ts +111 -0
  138. package/src/ast/extractors/sql-schema.ts +67 -0
  139. package/src/ast/extractors/tier-a.ts +423 -0
  140. package/src/ast/extractors/tier-b.ts +289 -0
  141. package/src/ast/extractors/tier-dispatch.ts +247 -0
  142. package/src/ast/index-worker.ts +108 -0
  143. package/src/ast/indexer.ts +484 -0
  144. package/src/ast/languages.ts +408 -0
  145. package/src/ast/pagerank-builder.ts +125 -0
  146. package/src/ast/path-utils.ts +137 -0
  147. package/src/ast/tree-sitter/backends/native.ts +57 -0
  148. package/src/ast/tree-sitter/backends/wasm.ts +39 -0
  149. package/src/ast/tree-sitter/call-walker.ts +44 -0
  150. package/src/ast/tree-sitter/edit-computer.ts +55 -0
  151. package/src/ast/tree-sitter/query-runner.ts +46 -0
  152. package/src/ast/tree-sitter/service.ts +174 -0
  153. package/src/ast/tree-sitter/tree-cache.ts +39 -0
  154. package/src/ast/tree-sitter/types.ts +79 -0
  155. package/src/ast/watcher.ts +322 -0
  156. package/src/capture/chunker.ts +169 -0
  157. package/src/capture/consolidate.ts +127 -0
  158. package/src/capture/edge-inferrer.ts +161 -0
  159. package/src/capture/embedder.ts +166 -0
  160. package/src/capture/embedding-cache.ts +73 -0
  161. package/src/capture/flag-processor.ts +64 -0
  162. package/src/capture/hook.ts +67 -0
  163. package/src/capture/pipeline.ts +450 -0
  164. package/src/capture/prompts/consolidate.ts +25 -0
  165. package/src/capture/prompts/edge-infer.ts +29 -0
  166. package/src/capture/prompts/extract-flagged.ts +36 -0
  167. package/src/capture/prompts/extract.ts +42 -0
  168. package/src/capture/tokenizer.ts +147 -0
  169. package/src/capture/track-a-ast.ts +93 -0
  170. package/src/capture/track-b-llm.ts +149 -0
  171. package/src/capture/types.ts +64 -0
  172. package/src/cli/commands/community.ts +137 -0
  173. package/src/cli/commands/compare.ts +123 -0
  174. package/src/cli/commands/conflicts.ts +41 -0
  175. package/src/cli/commands/digest.ts +197 -0
  176. package/src/cli/commands/disable-flagging.ts +34 -0
  177. package/src/cli/commands/doctor.ts +240 -0
  178. package/src/cli/commands/download-model.ts +161 -0
  179. package/src/cli/commands/enable-flagging.ts +34 -0
  180. package/src/cli/commands/export-knowledge.ts +208 -0
  181. package/src/cli/commands/export.ts +85 -0
  182. package/src/cli/commands/freshness.ts +164 -0
  183. package/src/cli/commands/graph.ts +51 -0
  184. package/src/cli/commands/history.ts +139 -0
  185. package/src/cli/commands/import.ts +335 -0
  186. package/src/cli/commands/install.ts +156 -0
  187. package/src/cli/commands/lead-report.ts +241 -0
  188. package/src/cli/commands/learn.ts +321 -0
  189. package/src/cli/commands/pm-report.ts +413 -0
  190. package/src/cli/commands/prune.ts +75 -0
  191. package/src/cli/commands/qa-report.ts +278 -0
  192. package/src/cli/commands/reindex.ts +104 -0
  193. package/src/cli/commands/rollback.ts +70 -0
  194. package/src/cli/commands/search.ts +103 -0
  195. package/src/cli/commands/server.ts +91 -0
  196. package/src/cli/commands/share.ts +33 -0
  197. package/src/cli/commands/stats.ts +79 -0
  198. package/src/cli/commands/status.ts +176 -0
  199. package/src/cli/commands/sync.ts +96 -0
  200. package/src/cli/commands/team.ts +118 -0
  201. package/src/cli/commands/tour.ts +157 -0
  202. package/src/cli/commands/visualize-live.ts +162 -0
  203. package/src/cli/commands/workspace.ts +117 -0
  204. package/src/cli/index.ts +424 -0
  205. package/src/cli/learn-progress.ts +87 -0
  206. package/src/community/detection-bridge.ts +344 -0
  207. package/src/community/leiden.ts +462 -0
  208. package/src/community/raptor.ts +210 -0
  209. package/src/community/scheduler.ts +74 -0
  210. package/src/community/summarize.ts +115 -0
  211. package/src/decay/archiver.ts +73 -0
  212. package/src/decay/bridge-orphan-cleanup.ts +212 -0
  213. package/src/decay/consolidation-sweep.ts +112 -0
  214. package/src/decay/decay.ts +116 -0
  215. package/src/decay/deep-validator.ts +62 -0
  216. package/src/decay/episodic-promoter.ts +132 -0
  217. package/src/decay/maintenance-scheduler.ts +326 -0
  218. package/src/decay/scheduler.ts +6 -0
  219. package/src/decay/session-sweeper.ts +79 -0
  220. package/src/decay/types.ts +17 -0
  221. package/src/freshness/confidence-decay.ts +122 -0
  222. package/src/freshness/cuckoo-filter.ts +176 -0
  223. package/src/freshness/deep-validation.ts +345 -0
  224. package/src/freshness/dirty-tracker.ts +237 -0
  225. package/src/freshness/file-watcher-layer.ts +119 -0
  226. package/src/freshness/firewall.ts +64 -0
  227. package/src/freshness/git-reconcile-layer.ts +161 -0
  228. package/src/freshness/inverted-index.ts +158 -0
  229. package/src/freshness/stale-read-layer.ts +222 -0
  230. package/src/graph/audit.ts +69 -0
  231. package/src/graph/bridge-db.ts +141 -0
  232. package/src/graph/communities.ts +195 -0
  233. package/src/graph/db-interface.ts +259 -0
  234. package/src/graph/edges.ts +163 -0
  235. package/src/graph/entities.ts +327 -0
  236. package/src/graph/episodic-db.ts +113 -0
  237. package/src/graph/flags.ts +31 -0
  238. package/src/graph/meta-db.ts +200 -0
  239. package/src/graph/semantic-db.ts +101 -0
  240. package/src/graph/session-resume.ts +56 -0
  241. package/src/graph/snapshots.ts +342 -0
  242. package/src/graph/staging.ts +151 -0
  243. package/src/graph/types.ts +128 -0
  244. package/src/hooks/adapters/claude-code.ts +21 -0
  245. package/src/hooks/adapters/cline.ts +43 -0
  246. package/src/hooks/adapters/cursor.ts +65 -0
  247. package/src/hooks/adapters/generic.ts +12 -0
  248. package/src/hooks/agent-detect.ts +34 -0
  249. package/src/hooks/claude-md-directives.ts +32 -0
  250. package/src/hooks/event-router.ts +182 -0
  251. package/src/hooks/extractors/pattern-detector.ts +111 -0
  252. package/src/hooks/handlers/post-compact.ts +30 -0
  253. package/src/hooks/handlers/post-tool-use.ts +403 -0
  254. package/src/hooks/handlers/pre-compact.ts +100 -0
  255. package/src/hooks/handlers/session-end.ts +47 -0
  256. package/src/hooks/handlers/session-start.ts +154 -0
  257. package/src/hooks/handlers/stop.ts +128 -0
  258. package/src/hooks/handlers/user-prompt-submit.ts +68 -0
  259. package/src/hooks/plugin-branch-switch.ts +68 -0
  260. package/src/hooks/plugin-common.ts +47 -0
  261. package/src/hooks/plugin-post-compact.ts +28 -0
  262. package/src/hooks/plugin-post-tool-use.ts +38 -0
  263. package/src/hooks/plugin-pre-compact.ts +37 -0
  264. package/src/hooks/plugin-session-end.ts +37 -0
  265. package/src/hooks/plugin-session-start.ts +75 -0
  266. package/src/hooks/plugin-stop.ts +61 -0
  267. package/src/hooks/plugin-user-prompt-submit.ts +47 -0
  268. package/src/hooks/types.ts +43 -0
  269. package/src/knowledge/discovery.ts +238 -0
  270. package/src/knowledge/external-refs.ts +98 -0
  271. package/src/knowledge/freshness.ts +221 -0
  272. package/src/knowledge/ingest.ts +330 -0
  273. package/src/knowledge/markdown-export.ts +229 -0
  274. package/src/knowledge/markdown-import.ts +359 -0
  275. package/src/knowledge/patterns.ts +74 -0
  276. package/src/knowledge/templates.ts +307 -0
  277. package/src/llm/ai-sdk-adapter.ts +46 -0
  278. package/src/llm/config.ts +88 -0
  279. package/src/llm/cost-tracker.ts +110 -0
  280. package/src/llm/prompts/extraction.ts +55 -0
  281. package/src/llm/prompts/summarization.ts +36 -0
  282. package/src/llm/prompts/validation.ts +37 -0
  283. package/src/llm/provider-registry.ts +68 -0
  284. package/src/llm/reliability.ts +179 -0
  285. package/src/llm/schemas.ts +52 -0
  286. package/src/mcp/freshness-annotator.ts +69 -0
  287. package/src/mcp/server.ts +949 -0
  288. package/src/mcp/tools/sia-ast-query.ts +225 -0
  289. package/src/mcp/tools/sia-at-time.ts +151 -0
  290. package/src/mcp/tools/sia-backlinks.ts +87 -0
  291. package/src/mcp/tools/sia-batch-execute.ts +169 -0
  292. package/src/mcp/tools/sia-by-file.ts +89 -0
  293. package/src/mcp/tools/sia-community.ts +113 -0
  294. package/src/mcp/tools/sia-doctor.ts +73 -0
  295. package/src/mcp/tools/sia-execute-file.ts +122 -0
  296. package/src/mcp/tools/sia-execute.ts +104 -0
  297. package/src/mcp/tools/sia-expand.ts +158 -0
  298. package/src/mcp/tools/sia-fetch-and-index.ts +241 -0
  299. package/src/mcp/tools/sia-flag.ts +65 -0
  300. package/src/mcp/tools/sia-index.ts +111 -0
  301. package/src/mcp/tools/sia-note.ts +134 -0
  302. package/src/mcp/tools/sia-search.ts +105 -0
  303. package/src/mcp/tools/sia-stats.ts +63 -0
  304. package/src/mcp/tools/sia-sync-status.ts +44 -0
  305. package/src/mcp/tools/sia-upgrade.ts +247 -0
  306. package/src/mcp/truncate.ts +231 -0
  307. package/src/native/bridge.ts +167 -0
  308. package/src/native/fallback-ast-diff.ts +144 -0
  309. package/src/native/fallback-graph.ts +325 -0
  310. package/src/ontology/constraints.ts +56 -0
  311. package/src/ontology/errors.ts +8 -0
  312. package/src/ontology/middleware.ts +266 -0
  313. package/src/retrieval/bm25-search.ts +151 -0
  314. package/src/retrieval/context-assembly.ts +76 -0
  315. package/src/retrieval/graph-traversal.ts +168 -0
  316. package/src/retrieval/pagerank.ts +40 -0
  317. package/src/retrieval/query-classifier.ts +106 -0
  318. package/src/retrieval/reranker.ts +156 -0
  319. package/src/retrieval/search.ts +236 -0
  320. package/src/retrieval/throttle.ts +102 -0
  321. package/src/retrieval/vector-search.ts +203 -0
  322. package/src/retrieval/workspace-search.ts +130 -0
  323. package/src/sandbox/context-mode.ts +285 -0
  324. package/src/sandbox/credential-pass.ts +55 -0
  325. package/src/sandbox/executor.ts +235 -0
  326. package/src/security/pattern-detector.ts +127 -0
  327. package/src/security/rule-of-two.ts +50 -0
  328. package/src/security/sanitize.ts +46 -0
  329. package/src/security/semantic-consistency.ts +93 -0
  330. package/src/security/staging-promoter.ts +154 -0
  331. package/src/shared/config.ts +302 -0
  332. package/src/shared/diagnostics.ts +210 -0
  333. package/src/shared/errors.ts +48 -0
  334. package/src/shared/git-utils.ts +143 -0
  335. package/src/shared/llm-client.ts +120 -0
  336. package/src/shared/logger.ts +99 -0
  337. package/src/shared/types.ts +79 -0
  338. package/src/sync/client.ts +43 -0
  339. package/src/sync/conflict.ts +106 -0
  340. package/src/sync/dedup.ts +183 -0
  341. package/src/sync/hlc.ts +117 -0
  342. package/src/sync/keychain.ts +144 -0
  343. package/src/sync/pull.ts +232 -0
  344. package/src/sync/push.ts +131 -0
  345. package/src/types/chokidar.d.ts +23 -0
  346. package/src/visualization/graph-renderer.ts +312 -0
  347. package/src/visualization/subgraph-extract.ts +208 -0
  348. package/src/visualization/views/community-clusters.ts +246 -0
  349. package/src/visualization/views/dependency-map.ts +189 -0
  350. package/src/visualization/views/graph-explorer.ts +364 -0
  351. package/src/visualization/views/timeline.ts +247 -0
  352. package/src/workspace/api-contracts.ts +226 -0
  353. package/src/workspace/cross-repo.ts +61 -0
  354. package/src/workspace/detector.ts +190 -0
  355. package/src/workspace/manifest.ts +141 -0
@@ -0,0 +1,237 @@
1
+ // Module: dirty-tracker — Salsa-inspired dirty propagation engine
2
+ //
3
+ // Coordination layer for the freshness system. Maintains an in-memory map
4
+ // (NOT persisted to SQLite) of node dirty states. The push phase marks
5
+ // nodes dirty when source files change; the pull phase checks dirty state
6
+ // before serving query results.
7
+ //
8
+ // Key design decisions:
9
+ // - In-memory only — rebuilt from source_deps at startup
10
+ // - BFS propagation with firewall cutoff at high-fan-out nodes
11
+ // - Early cutoff: markClean does NOT propagate (content hash unchanged)
12
+ // - Durable nodes skip dirty when only volatile sources change
13
+
14
+ import { getOutgoingNeighbors } from "@/freshness/firewall";
15
+ import { getDependentsForFile } from "@/freshness/inverted-index";
16
+ import type { SiaDb } from "@/graph/db-interface";
17
+
18
+ export type DirtyState = "clean" | "dirty" | "maybe_dirty";
19
+ export type Durability = "volatile" | "durable";
20
+
21
+ /** Default BFS depth limit for dirty propagation. */
22
+ const DEFAULT_MAX_DEPTH = 2;
23
+
24
+ /** Default edge_count threshold above which a node is a firewall. */
25
+ const DEFAULT_FIREWALL_THRESHOLD = 50;
26
+
27
+ export class DirtyTracker {
28
+ /** In-memory only — rebuilt from source_deps at startup. */
29
+ private dirtyMap = new Map<string, DirtyState>();
30
+ private durabilityMap = new Map<string, Durability>();
31
+
32
+ /**
33
+ * Get the dirty state of a node. Nodes not in the map are assumed clean.
34
+ */
35
+ getState(nodeId: string): DirtyState {
36
+ return this.dirtyMap.get(nodeId) ?? "clean";
37
+ }
38
+
39
+ /**
40
+ * Phase 1 — Push: Mark nodes as dirty when their source files change.
41
+ * Called by Layer 1 (file-watcher) and Layer 2 (git-reconcile).
42
+ *
43
+ * Algorithm:
44
+ * 1. Look up source_deps[changedFile] -> affected_node_ids
45
+ * 2. For each affected node:
46
+ * a. Skip if node is durable and only volatile sources changed
47
+ * b. Set dirtyMap[nodeId] = 'dirty'
48
+ * c. BFS outgoing dependency edges up to maxDepth (default 2)
49
+ * d. For each neighbor:
50
+ * - If edge_count > firewallThreshold (50): set 'maybe_dirty', STOP
51
+ * - Else: set 'dirty', continue
52
+ */
53
+ async markDirty(
54
+ db: SiaDb,
55
+ changedFile: string,
56
+ opts?: { maxDepth?: number; firewallThreshold?: number },
57
+ ): Promise<string[]> {
58
+ const maxDepth = opts?.maxDepth ?? DEFAULT_MAX_DEPTH;
59
+ const firewallThreshold = opts?.firewallThreshold ?? DEFAULT_FIREWALL_THRESHOLD;
60
+
61
+ // Step 1: Look up affected nodes from inverted index
62
+ const deps = await getDependentsForFile(db, changedFile);
63
+ if (deps.length === 0) return [];
64
+
65
+ const newlyDirtied: string[] = [];
66
+
67
+ // Step 2: Mark each affected node
68
+ for (const dep of deps) {
69
+ const nodeId = dep.node_id;
70
+
71
+ // 2a: Skip durable nodes for volatile source changes
72
+ const durability = this.durabilityMap.get(nodeId);
73
+ if (durability === "durable") {
74
+ continue;
75
+ }
76
+
77
+ // 2b: Set dirty
78
+ this.dirtyMap.set(nodeId, "dirty");
79
+ newlyDirtied.push(nodeId);
80
+
81
+ // 2c-d: BFS outgoing edges
82
+ const bfsDirtied = await this.bfsPropagation(db, nodeId, maxDepth, firewallThreshold);
83
+ for (const id of bfsDirtied) {
84
+ newlyDirtied.push(id);
85
+ }
86
+ }
87
+
88
+ return newlyDirtied;
89
+ }
90
+
91
+ /**
92
+ * Phase 2 — Pull: Check and resolve dirty state for a node.
93
+ * Called by Layer 3 (stale-while-revalidate) before serving a query result.
94
+ *
95
+ * Returns:
96
+ * - 'clean': serve immediately
97
+ * - 'dirty': needs re-verification (caller must re-extract)
98
+ * - 'maybe_dirty': needs mtime check (caller does stat())
99
+ */
100
+ checkNode(nodeId: string): DirtyState {
101
+ return this.dirtyMap.get(nodeId) ?? "clean";
102
+ }
103
+
104
+ /**
105
+ * Mark a node as clean after successful re-verification.
106
+ * This is the early cutoff: if content hash unchanged, clear dirty
107
+ * WITHOUT propagating to dependents.
108
+ */
109
+ markClean(nodeId: string): void {
110
+ this.dirtyMap.delete(nodeId);
111
+ }
112
+
113
+ /**
114
+ * Mark a node as clean and propagate dirty to its dependents.
115
+ * Called when re-verification found the content actually changed.
116
+ */
117
+ async markCleanAndPropagate(
118
+ db: SiaDb,
119
+ nodeId: string,
120
+ opts?: { maxDepth?: number; firewallThreshold?: number },
121
+ ): Promise<string[]> {
122
+ const maxDepth = opts?.maxDepth ?? DEFAULT_MAX_DEPTH;
123
+ const firewallThreshold = opts?.firewallThreshold ?? DEFAULT_FIREWALL_THRESHOLD;
124
+
125
+ // Clear this node
126
+ this.dirtyMap.delete(nodeId);
127
+
128
+ // Propagate to dependents
129
+ return this.bfsPropagation(db, nodeId, maxDepth, firewallThreshold);
130
+ }
131
+
132
+ /**
133
+ * Set durability for a node. Durable nodes skip dirty-checking
134
+ * when only volatile sources change.
135
+ */
136
+ setDurability(nodeId: string, durability: Durability): void {
137
+ this.durabilityMap.set(nodeId, durability);
138
+ }
139
+
140
+ /**
141
+ * Clear all dirty state (used on restart or full reindex).
142
+ */
143
+ reset(): void {
144
+ this.dirtyMap.clear();
145
+ this.durabilityMap.clear();
146
+ }
147
+
148
+ /**
149
+ * Get counts for diagnostics.
150
+ */
151
+ getStats(): {
152
+ clean: number;
153
+ dirty: number;
154
+ maybeDirty: number;
155
+ total: number;
156
+ } {
157
+ let dirty = 0;
158
+ let maybeDirty = 0;
159
+ let clean = 0;
160
+
161
+ for (const state of this.dirtyMap.values()) {
162
+ switch (state) {
163
+ case "dirty":
164
+ dirty++;
165
+ break;
166
+ case "maybe_dirty":
167
+ maybeDirty++;
168
+ break;
169
+ case "clean":
170
+ clean++;
171
+ break;
172
+ }
173
+ }
174
+
175
+ return {
176
+ clean,
177
+ dirty,
178
+ maybeDirty,
179
+ total: this.dirtyMap.size,
180
+ };
181
+ }
182
+
183
+ // -----------------------------------------------------------------
184
+ // Internal BFS propagation
185
+ // -----------------------------------------------------------------
186
+
187
+ /**
188
+ * BFS propagation from a starting node through outgoing dependency edges.
189
+ *
190
+ * For each neighbor:
191
+ * - If edge_count > firewallThreshold: set 'maybe_dirty', STOP (don't enqueue)
192
+ * - Else: set 'dirty', continue BFS
193
+ *
194
+ * Returns list of newly-dirtied node IDs (excluding the start node).
195
+ */
196
+ private async bfsPropagation(
197
+ db: SiaDb,
198
+ startNodeId: string,
199
+ maxDepth: number,
200
+ firewallThreshold: number,
201
+ ): Promise<string[]> {
202
+ const newlyDirtied: string[] = [];
203
+ const visited = new Set<string>([startNodeId]);
204
+
205
+ // BFS queue: [nodeId, currentDepth]
206
+ const queue: Array<[string, number]> = [[startNodeId, 0]];
207
+
208
+ while (queue.length > 0) {
209
+ const entry = queue.shift();
210
+ if (!entry) break;
211
+ const [currentId, depth] = entry;
212
+
213
+ if (depth >= maxDepth) continue;
214
+
215
+ const neighbors = await getOutgoingNeighbors(db, currentId);
216
+
217
+ for (const neighbor of neighbors) {
218
+ if (visited.has(neighbor.nodeId)) continue;
219
+ visited.add(neighbor.nodeId);
220
+
221
+ if (neighbor.edgeCount > firewallThreshold) {
222
+ // Firewall node: mark maybe_dirty and STOP propagation
223
+ this.dirtyMap.set(neighbor.nodeId, "maybe_dirty");
224
+ newlyDirtied.push(neighbor.nodeId);
225
+ // Do NOT enqueue — BFS stops here
226
+ } else {
227
+ // Normal node: mark dirty and continue BFS
228
+ this.dirtyMap.set(neighbor.nodeId, "dirty");
229
+ newlyDirtied.push(neighbor.nodeId);
230
+ queue.push([neighbor.nodeId, depth + 1]);
231
+ }
232
+ }
233
+ }
234
+
235
+ return newlyDirtied;
236
+ }
237
+ }
@@ -0,0 +1,119 @@
1
+ // Module: file-watcher-layer — Layer 1 freshness invalidation
2
+ //
3
+ // Handles >90% of invalidation cases: code edits during active development.
4
+ // Pipeline per file-save event:
5
+ // 1. Cuckoo filter fast-reject (O(1) — skip files with no derived nodes)
6
+ // 2. Inverted index lookup for affected node IDs
7
+ // 3. DirtyTracker.markDirty with bounded BFS propagation
8
+ // 4. For 'delete' events: invalidate nodes derived solely from that file
9
+ // 5. For 'modify' events: trigger re-extraction for structural nodes
10
+ //
11
+ // Must complete in < 200ms per file save.
12
+
13
+ import type { SiaDb } from "@/graph/db-interface";
14
+ import type { CuckooFilter } from "./cuckoo-filter";
15
+ import type { DirtyTracker } from "./dirty-tracker";
16
+ import { getDependenciesForNode, getDependentsForFile } from "./inverted-index";
17
+
18
+ export interface FileChangeEvent {
19
+ filePath: string; // relative path from repo root
20
+ type: "create" | "modify" | "delete";
21
+ mtime?: number; // file modification time (Unix ms)
22
+ }
23
+
24
+ /**
25
+ * Layer 1: Process a file change event from the file watcher.
26
+ *
27
+ * Pipeline:
28
+ * 1. Check Cuckoo filter — if file has no derived nodes, skip entirely
29
+ * 2. Look up inverted index for affected nodes
30
+ * 3. Mark affected nodes dirty via DirtyTracker
31
+ * 4. For 'delete' events, invalidate all nodes derived solely from this file
32
+ * 5. For 'modify' events, trigger re-extraction for structural nodes
33
+ *
34
+ * Returns the list of newly-dirtied node IDs.
35
+ */
36
+ export async function handleFileChange(
37
+ db: SiaDb,
38
+ event: FileChangeEvent,
39
+ tracker: DirtyTracker,
40
+ filter: CuckooFilter,
41
+ _opts?: { debounceMs?: number },
42
+ ): Promise<string[]> {
43
+ // Step 1: Cuckoo filter fast-reject
44
+ if (!filter.contains(event.filePath)) {
45
+ return [];
46
+ }
47
+
48
+ // Step 2-3: Look up inverted index and mark dirty via DirtyTracker
49
+ // DirtyTracker.markDirty already does: inverted index lookup -> mark dirty -> BFS propagation
50
+ const dirtied = await tracker.markDirty(db, event.filePath);
51
+
52
+ // Step 4: For 'delete' events, invalidate nodes derived solely from this file
53
+ if (event.type === "delete") {
54
+ const deps = await getDependentsForFile(db, event.filePath);
55
+ const now = Date.now();
56
+
57
+ for (const dep of deps) {
58
+ // Check if the node has any OTHER source dependencies besides the deleted file
59
+ const allDeps = await getDependenciesForNode(db, dep.node_id);
60
+ const otherDeps = allDeps.filter((d) => d.source_path !== event.filePath);
61
+
62
+ if (otherDeps.length === 0) {
63
+ // Node derived solely from the deleted file — invalidate it
64
+ await db.execute(
65
+ "UPDATE graph_nodes SET t_valid_until = ?, t_expired = ? WHERE id = ? AND t_valid_until IS NULL",
66
+ [now, now, dep.node_id],
67
+ );
68
+ }
69
+ }
70
+
71
+ // Remove the deleted file from the Cuckoo filter
72
+ filter.remove(event.filePath);
73
+ }
74
+
75
+ return dirtied;
76
+ }
77
+
78
+ /**
79
+ * Create a debounced file change handler that coalesces rapid saves.
80
+ * Default debounce: 50ms. Per-file debouncing so that changes to
81
+ * different files are not blocked by each other.
82
+ */
83
+ export function createDebouncedHandler(
84
+ db: SiaDb,
85
+ tracker: DirtyTracker,
86
+ filter: CuckooFilter,
87
+ debounceMs?: number,
88
+ ): (event: FileChangeEvent) => void {
89
+ const delay = debounceMs ?? 50;
90
+ const timers = new Map<string, ReturnType<typeof setTimeout>>();
91
+ const latestEvents = new Map<string, FileChangeEvent>();
92
+
93
+ return (event: FileChangeEvent) => {
94
+ // Store the latest event for this file path
95
+ latestEvents.set(event.filePath, event);
96
+
97
+ // Clear any existing timer for this file
98
+ const existing = timers.get(event.filePath);
99
+ if (existing != null) {
100
+ clearTimeout(existing);
101
+ }
102
+
103
+ // Set a new timer — when it fires, process the latest event
104
+ const timer = setTimeout(() => {
105
+ timers.delete(event.filePath);
106
+ const latest = latestEvents.get(event.filePath);
107
+ latestEvents.delete(event.filePath);
108
+
109
+ if (latest) {
110
+ // Fire and forget — errors are logged but not propagated
111
+ handleFileChange(db, latest, tracker, filter).catch(() => {
112
+ // Swallow errors in debounced handler
113
+ });
114
+ }
115
+ }, delay);
116
+
117
+ timers.set(event.filePath, timer);
118
+ };
119
+ }
@@ -0,0 +1,64 @@
1
+ // Module: firewall — Detects high-fan-out nodes that stop dirty propagation
2
+ //
3
+ // Firewall nodes have more than `threshold` incoming edges. When BFS dirty
4
+ // propagation reaches a firewall node, it marks the node as 'maybe_dirty'
5
+ // and stops — preventing cascading invalidation through hub nodes like
6
+ // utils/helpers.ts that are imported by hundreds of files.
7
+
8
+ import type { SiaDb } from "@/graph/db-interface";
9
+
10
+ /** Default edge_count threshold above which a node is a firewall. */
11
+ const DEFAULT_FIREWALL_THRESHOLD = 50;
12
+
13
+ /**
14
+ * Check if a node is a firewall — has more than threshold incoming edges.
15
+ * Firewall nodes stop dirty propagation to prevent cascading invalidation
16
+ * (e.g., utils/helpers.ts imported by 200 files shouldn't cascade to all 200).
17
+ *
18
+ * Uses the denormalized `edge_count` column on the entities table for O(1) lookup.
19
+ * Returns false for unknown nodes (not in the database).
20
+ */
21
+ export async function isFirewallNode(
22
+ db: SiaDb,
23
+ nodeId: string,
24
+ threshold?: number,
25
+ ): Promise<boolean> {
26
+ const limit = threshold ?? DEFAULT_FIREWALL_THRESHOLD;
27
+
28
+ const { rows } = await db.execute("SELECT edge_count FROM graph_nodes WHERE id = ?", [nodeId]);
29
+
30
+ if (rows.length === 0) return false;
31
+
32
+ const edgeCount = rows[0].edge_count as number;
33
+ return edgeCount > limit;
34
+ }
35
+
36
+ /**
37
+ * Get the outgoing neighbors of a node for BFS propagation.
38
+ * Only follows active edges (t_valid_until IS NULL).
39
+ * Returns the neighbor node ID along with its edge_count (for firewall checks).
40
+ *
41
+ * "Outgoing" here means both directions — edges where this node is the source
42
+ * (from_id) or the target (to_id) — because dependency relationships are
43
+ * bidirectional for invalidation purposes.
44
+ */
45
+ export async function getOutgoingNeighbors(
46
+ db: SiaDb,
47
+ nodeId: string,
48
+ ): Promise<Array<{ nodeId: string; edgeCount: number }>> {
49
+ const { rows } = await db.execute(
50
+ `SELECT
51
+ CASE WHEN e.from_id = ? THEN e.to_id ELSE e.from_id END AS neighbor_id,
52
+ ent.edge_count
53
+ FROM graph_edges e
54
+ JOIN graph_nodes ent ON ent.id = CASE WHEN e.from_id = ? THEN e.to_id ELSE e.from_id END
55
+ WHERE (e.from_id = ? OR e.to_id = ?)
56
+ AND e.t_valid_until IS NULL`,
57
+ [nodeId, nodeId, nodeId, nodeId],
58
+ );
59
+
60
+ return rows.map((r) => ({
61
+ nodeId: r.neighbor_id as string,
62
+ edgeCount: r.edge_count as number,
63
+ }));
64
+ }
@@ -0,0 +1,161 @@
1
+ // Module: git-reconcile-layer — Layer 2 freshness invalidation
2
+ //
3
+ // Handles changes made outside the file watcher's scope: merges, rebases,
4
+ // checkouts, stash pops, and pulls. Processes 5-50 files at once but still
5
+ // targets O(affected nodes) not O(all nodes).
6
+ //
7
+ // Pipeline per git operation:
8
+ // 1. Parse the list of changed files from the git operation
9
+ // 2. For each file, call tracker.markDirty() with bounded BFS
10
+ // 3. Firewall nodes (edge_count > 50) stop propagation
11
+
12
+ import type { SiaDb } from "@/graph/db-interface";
13
+ import type { DirtyTracker } from "./dirty-tracker";
14
+
15
+ export interface GitOperation {
16
+ type: "commit" | "merge" | "rebase" | "checkout" | "stash_pop" | "pull";
17
+ changedFiles: string[]; // relative paths
18
+ }
19
+
20
+ /**
21
+ * Layer 2: Process a git operation by invalidating all affected files' nodes.
22
+ *
23
+ * Pipeline:
24
+ * 1. Parse the list of changed files from the git operation
25
+ * 2. For each file, call tracker.markDirty() with bounded BFS
26
+ * 3. Firewall nodes (edge_count > 50) stop propagation
27
+ *
28
+ * This is heavier than Layer 1 (may process 5-50 files at once for a merge)
29
+ * but still targets O(affected nodes), not O(all nodes).
30
+ */
31
+ export async function handleGitOperation(
32
+ db: SiaDb,
33
+ op: GitOperation,
34
+ tracker: DirtyTracker,
35
+ ): Promise<{ filesProcessed: number; nodesDirtied: number }> {
36
+ const allDirtied = new Set<string>();
37
+
38
+ for (const filePath of op.changedFiles) {
39
+ const dirtied = await tracker.markDirty(db, filePath);
40
+ for (const nodeId of dirtied) {
41
+ allDirtied.add(nodeId);
42
+ }
43
+ }
44
+
45
+ return {
46
+ filesProcessed: op.changedFiles.length,
47
+ nodesDirtied: allDirtied.size,
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Parse git diff output to extract list of changed files.
53
+ * Handles various diff formats from git commands:
54
+ * - --name-only: bare file paths, one per line
55
+ * - --name-status: "M\tpath" / "A\tpath" / "D\tpath" / "R100\told\tnew"
56
+ * - --stat: " path | N ++--" with a summary line at the end
57
+ */
58
+ export function parseGitDiff(diffOutput: string): string[] {
59
+ const lines = diffOutput.split("\n").filter((l) => l.trim().length > 0);
60
+ if (lines.length === 0) return [];
61
+
62
+ const files: string[] = [];
63
+
64
+ for (const line of lines) {
65
+ const trimmed = line.trim();
66
+
67
+ // Skip git --stat summary lines like "3 files changed, 12 insertions(+), 6 deletions(-)"
68
+ if (/^\d+\s+files?\s+changed/.test(trimmed)) {
69
+ continue;
70
+ }
71
+
72
+ // --name-status format: "M\tpath" or "R100\told\tnew"
73
+ if (/^[ACDMRTUX]\d*\t/.test(trimmed)) {
74
+ const parts = trimmed.split("\t");
75
+ // Skip the status prefix (parts[0])
76
+ for (let i = 1; i < parts.length; i++) {
77
+ const p = parts[i].trim();
78
+ if (p.length > 0) {
79
+ files.push(p);
80
+ }
81
+ }
82
+ continue;
83
+ }
84
+
85
+ // --stat format: " path | N ++--"
86
+ if (trimmed.includes("|")) {
87
+ const pipeIdx = trimmed.indexOf("|");
88
+ const path = trimmed.slice(0, pipeIdx).trim();
89
+ if (path.length > 0 && !path.includes(" changed")) {
90
+ files.push(path);
91
+ }
92
+ continue;
93
+ }
94
+
95
+ // --name-only format: bare file path (contains / or . but no spaces that look like a summary)
96
+ if ((trimmed.includes("/") || trimmed.includes(".")) && !trimmed.includes(" files changed")) {
97
+ files.push(trimmed);
98
+ }
99
+ }
100
+
101
+ return files;
102
+ }
103
+
104
+ /** Git subcommands that modify the working tree and should trigger Layer 2. */
105
+ const GIT_OP_PATTERNS: Array<{
106
+ pattern: RegExp;
107
+ type: GitOperation["type"];
108
+ }> = [
109
+ { pattern: /^git\s+commit\b/, type: "commit" },
110
+ { pattern: /^git\s+merge\b/, type: "merge" },
111
+ { pattern: /^git\s+rebase\b/, type: "rebase" },
112
+ { pattern: /^git\s+checkout\b/, type: "checkout" },
113
+ { pattern: /^git\s+switch\b/, type: "checkout" },
114
+ { pattern: /^git\s+stash\s+pop\b/, type: "stash_pop" },
115
+ { pattern: /^git\s+stash\s+apply\b/, type: "stash_pop" },
116
+ { pattern: /^git\s+pull\b/, type: "pull" },
117
+ ];
118
+
119
+ /** Git subcommands that are read-only and should NOT trigger Layer 2. */
120
+ const GIT_READONLY_PATTERNS: RegExp[] = [
121
+ /^git\s+status\b/,
122
+ /^git\s+log\b/,
123
+ /^git\s+diff\b/,
124
+ /^git\s+branch\b/,
125
+ /^git\s+show\b/,
126
+ /^git\s+remote\b/,
127
+ /^git\s+fetch\b/,
128
+ /^git\s+tag\b/,
129
+ /^git\s+blame\b/,
130
+ /^git\s+stash\s+list\b/,
131
+ ];
132
+
133
+ /**
134
+ * Detect if a bash command is a git operation that should trigger Layer 2.
135
+ * Returns a GitOperation stub (with empty changedFiles — caller must populate
136
+ * from `git diff` output) or null if the command is not a relevant git op.
137
+ */
138
+ export function isGitOperation(command: string): GitOperation | null {
139
+ const trimmed = command.trim();
140
+
141
+ // Not a git command at all
142
+ if (!trimmed.startsWith("git ")) {
143
+ return null;
144
+ }
145
+
146
+ // Explicitly skip read-only git commands
147
+ for (const ro of GIT_READONLY_PATTERNS) {
148
+ if (ro.test(trimmed)) {
149
+ return null;
150
+ }
151
+ }
152
+
153
+ // Match against known write operations
154
+ for (const { pattern, type } of GIT_OP_PATTERNS) {
155
+ if (pattern.test(trimmed)) {
156
+ return { type, changedFiles: [] };
157
+ }
158
+ }
159
+
160
+ return null;
161
+ }