@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.
- package/.claude-plugin/marketplace.json +35 -0
- package/.claude-plugin/plugin.json +27 -0
- package/.mcp.json +13 -0
- package/CLAUDE.md +226 -0
- package/LICENSE +202 -0
- package/PLUGIN_README.md +253 -0
- package/README.md +1013 -0
- package/agents/sia-changelog-writer.md +89 -0
- package/agents/sia-code-reviewer.md +86 -0
- package/agents/sia-conflict-resolver.md +100 -0
- package/agents/sia-convention-enforcer.md +69 -0
- package/agents/sia-debug.md +106 -0
- package/agents/sia-decision-reviewer.md +101 -0
- package/agents/sia-dependency-tracker.md +80 -0
- package/agents/sia-explain.md +126 -0
- package/agents/sia-feature.md +116 -0
- package/agents/sia-knowledge-capture.md +117 -0
- package/agents/sia-lead-architecture-advisor.md +93 -0
- package/agents/sia-lead-team-health.md +107 -0
- package/agents/sia-migration.md +100 -0
- package/agents/sia-onboarding.md +115 -0
- package/agents/sia-orientation.md +99 -0
- package/agents/sia-pm-briefing.md +106 -0
- package/agents/sia-pm-risk-advisor.md +82 -0
- package/agents/sia-qa-analyst.md +116 -0
- package/agents/sia-qa-regression-map.md +94 -0
- package/agents/sia-refactor.md +115 -0
- package/agents/sia-regression.md +112 -0
- package/agents/sia-security-audit.md +125 -0
- package/agents/sia-test-advisor.md +91 -0
- package/hooks/hooks.json +98 -0
- package/migrations/bridge/001_initial.sql +34 -0
- package/migrations/episodic/001_initial.sql +35 -0
- package/migrations/meta/001_initial.sql +68 -0
- package/migrations/semantic/001_initial.sql +292 -0
- package/migrations/semantic/002_ontology.sql +89 -0
- package/migrations/semantic/003_freshness.sql +63 -0
- package/migrations/semantic/004_v5_unified_schema.sql +194 -0
- package/migrations/semantic/005_backfill_event_kinds.sql +8 -0
- package/migrations/semantic/006_tree_sitter.sql +6 -0
- package/migrations/semantic/007_branch_snapshots.sql +22 -0
- package/package.json +110 -0
- package/scripts/branch-switch.sh +13 -0
- package/scripts/build-wasm-grammars.sh +81 -0
- package/scripts/post-compact.sh +8 -0
- package/scripts/post-tool-use.sh +10 -0
- package/scripts/pre-compact.sh +8 -0
- package/scripts/session-end.sh +8 -0
- package/scripts/session-start.sh +8 -0
- package/scripts/start-mcp.ts +45 -0
- package/scripts/stop-hook.sh +8 -0
- package/scripts/user-prompt-submit.sh +8 -0
- package/scripts/viz-server.ts +152 -0
- package/skills/sia-brainstorm/SKILL.md +156 -0
- package/skills/sia-brainstorm/scripts/frame-template.html +214 -0
- package/skills/sia-brainstorm/scripts/helper.js +95 -0
- package/skills/sia-brainstorm/scripts/server.cjs +338 -0
- package/skills/sia-brainstorm/scripts/start-server.sh +153 -0
- package/skills/sia-brainstorm/scripts/stop-server.sh +55 -0
- package/skills/sia-brainstorm/spec-document-reviewer-prompt.md +49 -0
- package/skills/sia-brainstorm/visual-companion.md +286 -0
- package/skills/sia-capture/SKILL.md +64 -0
- package/skills/sia-compare/SKILL.md +33 -0
- package/skills/sia-conflicts/SKILL.md +38 -0
- package/skills/sia-debug-workflow/SKILL.md +120 -0
- package/skills/sia-debug-workflow/root-cause-tracing.md +70 -0
- package/skills/sia-debug-workflow/scripts/find-polluter.sh +64 -0
- package/skills/sia-debug-workflow/temporal-investigation.md +72 -0
- package/skills/sia-digest/SKILL.md +23 -0
- package/skills/sia-dispatch/SKILL.md +69 -0
- package/skills/sia-dispatch/agent-task-template.md +99 -0
- package/skills/sia-doctor/SKILL.md +39 -0
- package/skills/sia-execute/SKILL.md +70 -0
- package/skills/sia-execute-plan/SKILL.md +85 -0
- package/skills/sia-export-import/SKILL.md +49 -0
- package/skills/sia-export-knowledge/SKILL.md +46 -0
- package/skills/sia-finish/SKILL.md +100 -0
- package/skills/sia-finish/pr-summary-template.md +54 -0
- package/skills/sia-freshness/SKILL.md +38 -0
- package/skills/sia-history/SKILL.md +42 -0
- package/skills/sia-impact/SKILL.md +70 -0
- package/skills/sia-index/SKILL.md +54 -0
- package/skills/sia-install/SKILL.md +39 -0
- package/skills/sia-lead-compliance/SKILL.md +16 -0
- package/skills/sia-lead-drift-report/SKILL.md +16 -0
- package/skills/sia-lead-knowledge-map/SKILL.md +16 -0
- package/skills/sia-learn/SKILL.md +58 -0
- package/skills/sia-plan/SKILL.md +68 -0
- package/skills/sia-plan/plan-reviewer-prompt.md +63 -0
- package/skills/sia-playbooks/SKILL.md +29 -0
- package/skills/sia-playbooks/reference-feature.md +100 -0
- package/skills/sia-playbooks/reference-flagging.md +50 -0
- package/skills/sia-playbooks/reference-orientation.md +92 -0
- package/skills/sia-playbooks/reference-regression.md +115 -0
- package/skills/sia-playbooks/reference-review.md +64 -0
- package/skills/sia-playbooks/reference-tools.md +239 -0
- package/skills/sia-pm-decision-log/SKILL.md +28 -0
- package/skills/sia-pm-risk-dashboard/SKILL.md +24 -0
- package/skills/sia-pm-sprint-summary/SKILL.md +27 -0
- package/skills/sia-prune/SKILL.md +45 -0
- package/skills/sia-qa-coverage/SKILL.md +28 -0
- package/skills/sia-qa-flaky/SKILL.md +20 -0
- package/skills/sia-qa-report/SKILL.md +26 -0
- package/skills/sia-reindex/SKILL.md +30 -0
- package/skills/sia-review-respond/SKILL.md +88 -0
- package/skills/sia-review-respond/pushback-patterns.md +90 -0
- package/skills/sia-search/SKILL.md +47 -0
- package/skills/sia-setup/SKILL.md +82 -0
- package/skills/sia-setup/setup-checklist.md +97 -0
- package/skills/sia-stats/SKILL.md +36 -0
- package/skills/sia-status/SKILL.md +44 -0
- package/skills/sia-sync/SKILL.md +46 -0
- package/skills/sia-team/SKILL.md +64 -0
- package/skills/sia-test/SKILL.md +92 -0
- package/skills/sia-test/testing-anti-patterns.md +104 -0
- package/skills/sia-tour/SKILL.md +29 -0
- package/skills/sia-upgrade/SKILL.md +43 -0
- package/skills/sia-verify/SKILL.md +81 -0
- package/skills/sia-visualize/SKILL.md +28 -0
- package/skills/sia-visualize-live/SKILL.md +55 -0
- package/skills/sia-visualize-live/scripts/graph-template.html +389 -0
- package/skills/sia-visualize-live/scripts/start-visualizer.sh +161 -0
- package/skills/sia-visualize-live/scripts/stop-visualizer.sh +55 -0
- package/skills/sia-visualize-live/scripts/visualizer-server.cjs +264 -0
- package/skills/sia-workspace/SKILL.md +57 -0
- package/src/agent/claude-md-template-flagging.md +219 -0
- package/src/agent/claude-md-template.md +213 -0
- package/src/agent/modules/sia-feature.md +100 -0
- package/src/agent/modules/sia-flagging.md +50 -0
- package/src/agent/modules/sia-orientation.md +92 -0
- package/src/agent/modules/sia-regression.md +115 -0
- package/src/agent/modules/sia-review.md +64 -0
- package/src/agent/modules/sia-tools.md +239 -0
- package/src/ast/extractors/c-include.ts +189 -0
- package/src/ast/extractors/csharp-project.ts +260 -0
- package/src/ast/extractors/prisma-schema.ts +44 -0
- package/src/ast/extractors/project-manifest.ts +111 -0
- package/src/ast/extractors/sql-schema.ts +67 -0
- package/src/ast/extractors/tier-a.ts +423 -0
- package/src/ast/extractors/tier-b.ts +289 -0
- package/src/ast/extractors/tier-dispatch.ts +247 -0
- package/src/ast/index-worker.ts +108 -0
- package/src/ast/indexer.ts +484 -0
- package/src/ast/languages.ts +408 -0
- package/src/ast/pagerank-builder.ts +125 -0
- package/src/ast/path-utils.ts +137 -0
- package/src/ast/tree-sitter/backends/native.ts +57 -0
- package/src/ast/tree-sitter/backends/wasm.ts +39 -0
- package/src/ast/tree-sitter/call-walker.ts +44 -0
- package/src/ast/tree-sitter/edit-computer.ts +55 -0
- package/src/ast/tree-sitter/query-runner.ts +46 -0
- package/src/ast/tree-sitter/service.ts +174 -0
- package/src/ast/tree-sitter/tree-cache.ts +39 -0
- package/src/ast/tree-sitter/types.ts +79 -0
- package/src/ast/watcher.ts +322 -0
- package/src/capture/chunker.ts +169 -0
- package/src/capture/consolidate.ts +127 -0
- package/src/capture/edge-inferrer.ts +161 -0
- package/src/capture/embedder.ts +166 -0
- package/src/capture/embedding-cache.ts +73 -0
- package/src/capture/flag-processor.ts +64 -0
- package/src/capture/hook.ts +67 -0
- package/src/capture/pipeline.ts +450 -0
- package/src/capture/prompts/consolidate.ts +25 -0
- package/src/capture/prompts/edge-infer.ts +29 -0
- package/src/capture/prompts/extract-flagged.ts +36 -0
- package/src/capture/prompts/extract.ts +42 -0
- package/src/capture/tokenizer.ts +147 -0
- package/src/capture/track-a-ast.ts +93 -0
- package/src/capture/track-b-llm.ts +149 -0
- package/src/capture/types.ts +64 -0
- package/src/cli/commands/community.ts +137 -0
- package/src/cli/commands/compare.ts +123 -0
- package/src/cli/commands/conflicts.ts +41 -0
- package/src/cli/commands/digest.ts +197 -0
- package/src/cli/commands/disable-flagging.ts +34 -0
- package/src/cli/commands/doctor.ts +240 -0
- package/src/cli/commands/download-model.ts +161 -0
- package/src/cli/commands/enable-flagging.ts +34 -0
- package/src/cli/commands/export-knowledge.ts +208 -0
- package/src/cli/commands/export.ts +85 -0
- package/src/cli/commands/freshness.ts +164 -0
- package/src/cli/commands/graph.ts +51 -0
- package/src/cli/commands/history.ts +139 -0
- package/src/cli/commands/import.ts +335 -0
- package/src/cli/commands/install.ts +156 -0
- package/src/cli/commands/lead-report.ts +241 -0
- package/src/cli/commands/learn.ts +321 -0
- package/src/cli/commands/pm-report.ts +413 -0
- package/src/cli/commands/prune.ts +75 -0
- package/src/cli/commands/qa-report.ts +278 -0
- package/src/cli/commands/reindex.ts +104 -0
- package/src/cli/commands/rollback.ts +70 -0
- package/src/cli/commands/search.ts +103 -0
- package/src/cli/commands/server.ts +91 -0
- package/src/cli/commands/share.ts +33 -0
- package/src/cli/commands/stats.ts +79 -0
- package/src/cli/commands/status.ts +176 -0
- package/src/cli/commands/sync.ts +96 -0
- package/src/cli/commands/team.ts +118 -0
- package/src/cli/commands/tour.ts +157 -0
- package/src/cli/commands/visualize-live.ts +162 -0
- package/src/cli/commands/workspace.ts +117 -0
- package/src/cli/index.ts +424 -0
- package/src/cli/learn-progress.ts +87 -0
- package/src/community/detection-bridge.ts +344 -0
- package/src/community/leiden.ts +462 -0
- package/src/community/raptor.ts +210 -0
- package/src/community/scheduler.ts +74 -0
- package/src/community/summarize.ts +115 -0
- package/src/decay/archiver.ts +73 -0
- package/src/decay/bridge-orphan-cleanup.ts +212 -0
- package/src/decay/consolidation-sweep.ts +112 -0
- package/src/decay/decay.ts +116 -0
- package/src/decay/deep-validator.ts +62 -0
- package/src/decay/episodic-promoter.ts +132 -0
- package/src/decay/maintenance-scheduler.ts +326 -0
- package/src/decay/scheduler.ts +6 -0
- package/src/decay/session-sweeper.ts +79 -0
- package/src/decay/types.ts +17 -0
- package/src/freshness/confidence-decay.ts +122 -0
- package/src/freshness/cuckoo-filter.ts +176 -0
- package/src/freshness/deep-validation.ts +345 -0
- package/src/freshness/dirty-tracker.ts +237 -0
- package/src/freshness/file-watcher-layer.ts +119 -0
- package/src/freshness/firewall.ts +64 -0
- package/src/freshness/git-reconcile-layer.ts +161 -0
- package/src/freshness/inverted-index.ts +158 -0
- package/src/freshness/stale-read-layer.ts +222 -0
- package/src/graph/audit.ts +69 -0
- package/src/graph/bridge-db.ts +141 -0
- package/src/graph/communities.ts +195 -0
- package/src/graph/db-interface.ts +259 -0
- package/src/graph/edges.ts +163 -0
- package/src/graph/entities.ts +327 -0
- package/src/graph/episodic-db.ts +113 -0
- package/src/graph/flags.ts +31 -0
- package/src/graph/meta-db.ts +200 -0
- package/src/graph/semantic-db.ts +101 -0
- package/src/graph/session-resume.ts +56 -0
- package/src/graph/snapshots.ts +342 -0
- package/src/graph/staging.ts +151 -0
- package/src/graph/types.ts +128 -0
- package/src/hooks/adapters/claude-code.ts +21 -0
- package/src/hooks/adapters/cline.ts +43 -0
- package/src/hooks/adapters/cursor.ts +65 -0
- package/src/hooks/adapters/generic.ts +12 -0
- package/src/hooks/agent-detect.ts +34 -0
- package/src/hooks/claude-md-directives.ts +32 -0
- package/src/hooks/event-router.ts +182 -0
- package/src/hooks/extractors/pattern-detector.ts +111 -0
- package/src/hooks/handlers/post-compact.ts +30 -0
- package/src/hooks/handlers/post-tool-use.ts +403 -0
- package/src/hooks/handlers/pre-compact.ts +100 -0
- package/src/hooks/handlers/session-end.ts +47 -0
- package/src/hooks/handlers/session-start.ts +154 -0
- package/src/hooks/handlers/stop.ts +128 -0
- package/src/hooks/handlers/user-prompt-submit.ts +68 -0
- package/src/hooks/plugin-branch-switch.ts +68 -0
- package/src/hooks/plugin-common.ts +47 -0
- package/src/hooks/plugin-post-compact.ts +28 -0
- package/src/hooks/plugin-post-tool-use.ts +38 -0
- package/src/hooks/plugin-pre-compact.ts +37 -0
- package/src/hooks/plugin-session-end.ts +37 -0
- package/src/hooks/plugin-session-start.ts +75 -0
- package/src/hooks/plugin-stop.ts +61 -0
- package/src/hooks/plugin-user-prompt-submit.ts +47 -0
- package/src/hooks/types.ts +43 -0
- package/src/knowledge/discovery.ts +238 -0
- package/src/knowledge/external-refs.ts +98 -0
- package/src/knowledge/freshness.ts +221 -0
- package/src/knowledge/ingest.ts +330 -0
- package/src/knowledge/markdown-export.ts +229 -0
- package/src/knowledge/markdown-import.ts +359 -0
- package/src/knowledge/patterns.ts +74 -0
- package/src/knowledge/templates.ts +307 -0
- package/src/llm/ai-sdk-adapter.ts +46 -0
- package/src/llm/config.ts +88 -0
- package/src/llm/cost-tracker.ts +110 -0
- package/src/llm/prompts/extraction.ts +55 -0
- package/src/llm/prompts/summarization.ts +36 -0
- package/src/llm/prompts/validation.ts +37 -0
- package/src/llm/provider-registry.ts +68 -0
- package/src/llm/reliability.ts +179 -0
- package/src/llm/schemas.ts +52 -0
- package/src/mcp/freshness-annotator.ts +69 -0
- package/src/mcp/server.ts +949 -0
- package/src/mcp/tools/sia-ast-query.ts +225 -0
- package/src/mcp/tools/sia-at-time.ts +151 -0
- package/src/mcp/tools/sia-backlinks.ts +87 -0
- package/src/mcp/tools/sia-batch-execute.ts +169 -0
- package/src/mcp/tools/sia-by-file.ts +89 -0
- package/src/mcp/tools/sia-community.ts +113 -0
- package/src/mcp/tools/sia-doctor.ts +73 -0
- package/src/mcp/tools/sia-execute-file.ts +122 -0
- package/src/mcp/tools/sia-execute.ts +104 -0
- package/src/mcp/tools/sia-expand.ts +158 -0
- package/src/mcp/tools/sia-fetch-and-index.ts +241 -0
- package/src/mcp/tools/sia-flag.ts +65 -0
- package/src/mcp/tools/sia-index.ts +111 -0
- package/src/mcp/tools/sia-note.ts +134 -0
- package/src/mcp/tools/sia-search.ts +105 -0
- package/src/mcp/tools/sia-stats.ts +63 -0
- package/src/mcp/tools/sia-sync-status.ts +44 -0
- package/src/mcp/tools/sia-upgrade.ts +247 -0
- package/src/mcp/truncate.ts +231 -0
- package/src/native/bridge.ts +167 -0
- package/src/native/fallback-ast-diff.ts +144 -0
- package/src/native/fallback-graph.ts +325 -0
- package/src/ontology/constraints.ts +56 -0
- package/src/ontology/errors.ts +8 -0
- package/src/ontology/middleware.ts +266 -0
- package/src/retrieval/bm25-search.ts +151 -0
- package/src/retrieval/context-assembly.ts +76 -0
- package/src/retrieval/graph-traversal.ts +168 -0
- package/src/retrieval/pagerank.ts +40 -0
- package/src/retrieval/query-classifier.ts +106 -0
- package/src/retrieval/reranker.ts +156 -0
- package/src/retrieval/search.ts +236 -0
- package/src/retrieval/throttle.ts +102 -0
- package/src/retrieval/vector-search.ts +203 -0
- package/src/retrieval/workspace-search.ts +130 -0
- package/src/sandbox/context-mode.ts +285 -0
- package/src/sandbox/credential-pass.ts +55 -0
- package/src/sandbox/executor.ts +235 -0
- package/src/security/pattern-detector.ts +127 -0
- package/src/security/rule-of-two.ts +50 -0
- package/src/security/sanitize.ts +46 -0
- package/src/security/semantic-consistency.ts +93 -0
- package/src/security/staging-promoter.ts +154 -0
- package/src/shared/config.ts +302 -0
- package/src/shared/diagnostics.ts +210 -0
- package/src/shared/errors.ts +48 -0
- package/src/shared/git-utils.ts +143 -0
- package/src/shared/llm-client.ts +120 -0
- package/src/shared/logger.ts +99 -0
- package/src/shared/types.ts +79 -0
- package/src/sync/client.ts +43 -0
- package/src/sync/conflict.ts +106 -0
- package/src/sync/dedup.ts +183 -0
- package/src/sync/hlc.ts +117 -0
- package/src/sync/keychain.ts +144 -0
- package/src/sync/pull.ts +232 -0
- package/src/sync/push.ts +131 -0
- package/src/types/chokidar.d.ts +23 -0
- package/src/visualization/graph-renderer.ts +312 -0
- package/src/visualization/subgraph-extract.ts +208 -0
- package/src/visualization/views/community-clusters.ts +246 -0
- package/src/visualization/views/dependency-map.ts +189 -0
- package/src/visualization/views/graph-explorer.ts +364 -0
- package/src/visualization/views/timeline.ts +247 -0
- package/src/workspace/api-contracts.ts +226 -0
- package/src/workspace/cross-repo.ts +61 -0
- package/src/workspace/detector.ts +190 -0
- 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
|
+
}
|