@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,106 @@
|
|
|
1
|
+
// Module: query-classifier — Local vs global query routing + task-type boosts
|
|
2
|
+
|
|
3
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
4
|
+
|
|
5
|
+
export type QueryMode = "local" | "global";
|
|
6
|
+
|
|
7
|
+
export interface ClassificationResult {
|
|
8
|
+
mode: QueryMode;
|
|
9
|
+
globalUnavailable: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const GLOBAL_KEYWORDS: string[] = [
|
|
13
|
+
"architecture",
|
|
14
|
+
"overview",
|
|
15
|
+
"explain",
|
|
16
|
+
"structure",
|
|
17
|
+
"high-level",
|
|
18
|
+
"design",
|
|
19
|
+
"modules",
|
|
20
|
+
"subsystems",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const LOCAL_KEYWORDS: string[] = [
|
|
24
|
+
"function",
|
|
25
|
+
"class",
|
|
26
|
+
"method",
|
|
27
|
+
"variable",
|
|
28
|
+
"import",
|
|
29
|
+
"error",
|
|
30
|
+
"bug",
|
|
31
|
+
"fix",
|
|
32
|
+
"implement",
|
|
33
|
+
"where is",
|
|
34
|
+
"how does",
|
|
35
|
+
"what does",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Classify a query as local (three-stage pipeline) or global (community summaries).
|
|
40
|
+
*
|
|
41
|
+
* Keyword-based classification: count matches against global and local keyword
|
|
42
|
+
* lists, default to local when tied. If the graph is too small for meaningful
|
|
43
|
+
* community summaries (fewer than `config.communityMinGraphSize` active
|
|
44
|
+
* entities), force local and set `globalUnavailable: true`.
|
|
45
|
+
*/
|
|
46
|
+
export async function classifyQuery(
|
|
47
|
+
db: SiaDb,
|
|
48
|
+
query: string,
|
|
49
|
+
config: { communityMinGraphSize: number },
|
|
50
|
+
): Promise<ClassificationResult> {
|
|
51
|
+
const lower = query.toLowerCase();
|
|
52
|
+
|
|
53
|
+
let globalScore = 0;
|
|
54
|
+
for (const kw of GLOBAL_KEYWORDS) {
|
|
55
|
+
if (lower.includes(kw)) {
|
|
56
|
+
globalScore++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let localScore = 0;
|
|
61
|
+
for (const kw of LOCAL_KEYWORDS) {
|
|
62
|
+
if (lower.includes(kw)) {
|
|
63
|
+
localScore++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Default to local when tied (localScore >= globalScore means local wins on tie)
|
|
68
|
+
let mode: QueryMode = globalScore > localScore ? "global" : "local";
|
|
69
|
+
let globalUnavailable = false;
|
|
70
|
+
|
|
71
|
+
// Check graph size — force local if too few entities for community summaries
|
|
72
|
+
if (mode === "global") {
|
|
73
|
+
const result = await db.execute(
|
|
74
|
+
"SELECT COUNT(*) AS cnt FROM graph_nodes WHERE t_valid_until IS NULL AND archived_at IS NULL",
|
|
75
|
+
);
|
|
76
|
+
const count = Number((result.rows[0] as { cnt: number }).cnt);
|
|
77
|
+
if (count < config.communityMinGraphSize) {
|
|
78
|
+
mode = "local";
|
|
79
|
+
globalUnavailable = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { mode, globalUnavailable };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Task-type boost vectors: maps task type strings to sets of entity types
|
|
88
|
+
* that should receive a scoring boost during reranking.
|
|
89
|
+
*/
|
|
90
|
+
export const TASK_TYPE_BOOSTS: Record<string, Set<string>> = {
|
|
91
|
+
"bug-fix": new Set(["Bug", "Solution"]),
|
|
92
|
+
regression: new Set(["Bug", "Solution"]),
|
|
93
|
+
feature: new Set(["Concept", "Decision"]),
|
|
94
|
+
review: new Set(["Convention"]),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Package-path boost: returns 0.15 when the entity's package matches the
|
|
99
|
+
* active package, 0 otherwise.
|
|
100
|
+
*/
|
|
101
|
+
export function packagePathBoost(entityPkg: string | null, activePkg: string | null): number {
|
|
102
|
+
if (entityPkg != null && activePkg != null && entityPkg === activePkg) {
|
|
103
|
+
return 0.15;
|
|
104
|
+
}
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Module: reranker — RRF combination + trust-weighted scoring
|
|
2
|
+
|
|
3
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
4
|
+
import type { Entity } from "@/graph/entities";
|
|
5
|
+
import type { SiaSearchResult } from "@/mcp/tools/sia-search";
|
|
6
|
+
import { packagePathBoost, TASK_TYPE_BOOSTS } from "@/retrieval/query-classifier";
|
|
7
|
+
|
|
8
|
+
/** A candidate with an entity ID and a score from a single retrieval signal. */
|
|
9
|
+
export interface RankedCandidate {
|
|
10
|
+
entityId: string;
|
|
11
|
+
score: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Options for the rerank function. */
|
|
15
|
+
export interface RerankOpts {
|
|
16
|
+
/** Task type for task-type boosting (e.g. "bug-fix", "feature"). */
|
|
17
|
+
taskType?: string;
|
|
18
|
+
/** Active package path for same-package boosting. */
|
|
19
|
+
packagePath?: string;
|
|
20
|
+
/** If true, exclude Tier 4 entities. */
|
|
21
|
+
paranoid?: boolean;
|
|
22
|
+
/** Maximum number of results to return. */
|
|
23
|
+
limit?: number;
|
|
24
|
+
/** If true, include extraction_method in results. */
|
|
25
|
+
includeProvenance?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Trust weights keyed by tier number (1-4). No index-0. */
|
|
29
|
+
const TRUST_WEIGHTS: Record<number, number> = {
|
|
30
|
+
1: 1.0,
|
|
31
|
+
2: 0.9,
|
|
32
|
+
3: 0.7,
|
|
33
|
+
4: 0.5,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Combine multiple ranked candidate lists via Reciprocal Rank Fusion (k=60).
|
|
38
|
+
*
|
|
39
|
+
* Each list is sorted by score DESC. For each entity in each list,
|
|
40
|
+
* the contribution is `1 / (k + rank + 1)` where rank is 0-based.
|
|
41
|
+
* Scores are summed across all lists.
|
|
42
|
+
*/
|
|
43
|
+
export function rrfCombine(...lists: RankedCandidate[][]): Map<string, number> {
|
|
44
|
+
const k = 60;
|
|
45
|
+
const scores = new Map<string, number>();
|
|
46
|
+
|
|
47
|
+
for (const list of lists) {
|
|
48
|
+
// Sort by score descending to establish rank order
|
|
49
|
+
const sorted = [...list].sort((a, b) => b.score - a.score);
|
|
50
|
+
|
|
51
|
+
for (let rank = 0; rank < sorted.length; rank++) {
|
|
52
|
+
const candidate = sorted[rank];
|
|
53
|
+
const rrfScore = 1 / (k + rank + 1);
|
|
54
|
+
const current = scores.get(candidate.entityId) ?? 0;
|
|
55
|
+
scores.set(candidate.entityId, current + rrfScore);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return scores;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Batch size for fetching entities from the database. */
|
|
63
|
+
const ENTITY_BATCH_SIZE = 500;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Rerank entities by combining RRF scores with trust weights, importance,
|
|
67
|
+
* confidence, task-type boosts, and package-path boosts.
|
|
68
|
+
*
|
|
69
|
+
* Formula: rrf_score * importance * confidence * trust_weight[tier] * (1 + task_boost * 0.3) + package_boost
|
|
70
|
+
*/
|
|
71
|
+
export async function rerank(
|
|
72
|
+
db: SiaDb,
|
|
73
|
+
rrfScores: Map<string, number>,
|
|
74
|
+
opts?: RerankOpts,
|
|
75
|
+
): Promise<SiaSearchResult[]> {
|
|
76
|
+
if (rrfScores.size === 0) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const entityIds = Array.from(rrfScores.keys());
|
|
81
|
+
|
|
82
|
+
// Fetch entities in batches of 500
|
|
83
|
+
const entities = new Map<string, Entity>();
|
|
84
|
+
for (let i = 0; i < entityIds.length; i += ENTITY_BATCH_SIZE) {
|
|
85
|
+
const batch = entityIds.slice(i, i + ENTITY_BATCH_SIZE);
|
|
86
|
+
const placeholders = batch.map(() => "?").join(", ");
|
|
87
|
+
const sql = `SELECT * FROM graph_nodes WHERE id IN (${placeholders}) AND t_valid_until IS NULL AND archived_at IS NULL`;
|
|
88
|
+
const result = await db.execute(sql, batch);
|
|
89
|
+
for (const row of result.rows) {
|
|
90
|
+
entities.set(row.id as string, row as unknown as Entity);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Determine task-type boosted entity types
|
|
95
|
+
const boostedTypes: Set<string> | undefined = opts?.taskType
|
|
96
|
+
? TASK_TYPE_BOOSTS[opts.taskType]
|
|
97
|
+
: undefined;
|
|
98
|
+
|
|
99
|
+
// Score and filter
|
|
100
|
+
const scored: Array<{ entity: Entity; finalScore: number }> = [];
|
|
101
|
+
|
|
102
|
+
for (const [entityId, rrfScore] of rrfScores) {
|
|
103
|
+
const entity = entities.get(entityId);
|
|
104
|
+
if (!entity) {
|
|
105
|
+
// Entity was invalidated, archived, or doesn't exist
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Paranoid filter: remove Tier 4
|
|
110
|
+
if (opts?.paranoid && entity.trust_tier === 4) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const trustWeight = TRUST_WEIGHTS[entity.trust_tier] ?? 0.5;
|
|
115
|
+
const taskBoost = boostedTypes?.has(entity.type) ? 1 : 0;
|
|
116
|
+
const pkgBoost = packagePathBoost(entity.package_path, opts?.packagePath ?? null);
|
|
117
|
+
|
|
118
|
+
const finalScore =
|
|
119
|
+
rrfScore * entity.importance * entity.confidence * trustWeight * (1 + taskBoost * 0.3) +
|
|
120
|
+
pkgBoost;
|
|
121
|
+
|
|
122
|
+
scored.push({ entity, finalScore });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Sort by finalScore DESC
|
|
126
|
+
scored.sort((a, b) => b.finalScore - a.finalScore);
|
|
127
|
+
|
|
128
|
+
// Apply limit
|
|
129
|
+
const limit = opts?.limit ?? 15;
|
|
130
|
+
const top = scored.slice(0, limit);
|
|
131
|
+
|
|
132
|
+
// Map to SiaSearchResult
|
|
133
|
+
return top.map(({ entity, finalScore: _finalScore }) => {
|
|
134
|
+
const base: SiaSearchResult = {
|
|
135
|
+
id: entity.id,
|
|
136
|
+
type: entity.type,
|
|
137
|
+
name: entity.name,
|
|
138
|
+
summary: entity.summary,
|
|
139
|
+
content: entity.content,
|
|
140
|
+
trust_tier: entity.trust_tier,
|
|
141
|
+
confidence: entity.confidence,
|
|
142
|
+
importance: entity.importance,
|
|
143
|
+
tags: entity.tags,
|
|
144
|
+
file_paths: entity.file_paths,
|
|
145
|
+
conflict_group_id: entity.conflict_group_id ?? null,
|
|
146
|
+
t_valid_from: entity.t_valid_from ?? null,
|
|
147
|
+
source_repo_name: null,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (opts?.includeProvenance) {
|
|
151
|
+
base.extraction_method = entity.extraction_method ?? null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return base;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// Module: search — Three-stage pipeline orchestration
|
|
2
|
+
//
|
|
3
|
+
// Stage 1: Parallel BM25 + graph traversal + vector search
|
|
4
|
+
// Stage 2: 1-hop neighbor expansion for candidates
|
|
5
|
+
// Stage 3: RRF combination + trust-weighted reranking
|
|
6
|
+
// Global queries bypass the pipeline and return community summaries.
|
|
7
|
+
|
|
8
|
+
import type { Embedder } from "@/capture/embedder";
|
|
9
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
10
|
+
import type { SiaSearchResult } from "@/mcp/tools/sia-search";
|
|
11
|
+
import { bm25Search } from "@/retrieval/bm25-search";
|
|
12
|
+
import { graphTraversalSearch } from "@/retrieval/graph-traversal";
|
|
13
|
+
import { classifyQuery } from "@/retrieval/query-classifier";
|
|
14
|
+
import { type RankedCandidate, rerank, rrfCombine } from "@/retrieval/reranker";
|
|
15
|
+
import { vectorSearch } from "@/retrieval/vector-search";
|
|
16
|
+
|
|
17
|
+
/** Options accepted by hybridSearch. */
|
|
18
|
+
export interface SearchOptions {
|
|
19
|
+
query: string;
|
|
20
|
+
taskType?: string;
|
|
21
|
+
nodeTypes?: string[];
|
|
22
|
+
packagePath?: string;
|
|
23
|
+
paranoid?: boolean;
|
|
24
|
+
limit?: number;
|
|
25
|
+
includeProvenance?: boolean;
|
|
26
|
+
communityMinGraphSize?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Result returned by hybridSearch. */
|
|
30
|
+
export interface SearchResult {
|
|
31
|
+
results: SiaSearchResult[];
|
|
32
|
+
mode: "local" | "global";
|
|
33
|
+
globalUnavailable: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Default minimum graph size before community summaries are available. */
|
|
37
|
+
const DEFAULT_COMMUNITY_MIN_GRAPH_SIZE = 100;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Three-stage hybrid retrieval pipeline.
|
|
41
|
+
*
|
|
42
|
+
* 1. Classify query as local or global.
|
|
43
|
+
* 2. If global, return community summaries from the `communities` table.
|
|
44
|
+
* 3. Stage 1: parallel BM25 + graph traversal + vector search.
|
|
45
|
+
* 4. Stage 2: expand 1-hop neighbors for every candidate.
|
|
46
|
+
* 5. Stage 3: RRF combine + trust-weighted rerank.
|
|
47
|
+
* 6. Post-filter by nodeTypes if specified.
|
|
48
|
+
* 7. Attach extraction_method if includeProvenance is set.
|
|
49
|
+
*
|
|
50
|
+
* The `embedder` parameter is nullable -- when null, vector search is skipped
|
|
51
|
+
* and the pipeline runs on BM25 + graph traversal only.
|
|
52
|
+
*/
|
|
53
|
+
export async function hybridSearch(
|
|
54
|
+
db: SiaDb,
|
|
55
|
+
embedder: Embedder | null,
|
|
56
|
+
opts: SearchOptions,
|
|
57
|
+
): Promise<SearchResult> {
|
|
58
|
+
const limit = opts.limit ?? 15;
|
|
59
|
+
const communityMinGraphSize = opts.communityMinGraphSize ?? DEFAULT_COMMUNITY_MIN_GRAPH_SIZE;
|
|
60
|
+
|
|
61
|
+
// --- Classify query ---------------------------------------------------
|
|
62
|
+
const classification = await classifyQuery(db, opts.query, {
|
|
63
|
+
communityMinGraphSize,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// --- Global mode: return community summaries --------------------------
|
|
67
|
+
if (classification.mode === "global") {
|
|
68
|
+
const communities = await fetchCommunitySummaries(db, limit);
|
|
69
|
+
return {
|
|
70
|
+
results: communities,
|
|
71
|
+
mode: "global",
|
|
72
|
+
globalUnavailable: false,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --- Stage 1: parallel retrieval signals ------------------------------
|
|
77
|
+
const searchOpts = {
|
|
78
|
+
limit: limit * 3, // over-fetch to leave room for reranking
|
|
79
|
+
paranoid: opts.paranoid,
|
|
80
|
+
packagePath: opts.packagePath,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const [bm25Results, graphResults, vecResults] = await Promise.all([
|
|
84
|
+
bm25Search(db, opts.query, searchOpts),
|
|
85
|
+
graphTraversalSearch(db, opts.query, searchOpts),
|
|
86
|
+
embedder ? vectorSearch(db, opts.query, embedder, searchOpts) : Promise.resolve([]),
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
// --- Stage 2: expand 1-hop neighbors ----------------------------------
|
|
90
|
+
const expandedGraphResults = await expandNeighbors(db, graphResults, opts.paranoid);
|
|
91
|
+
|
|
92
|
+
// --- Stage 3: RRF combine + rerank ------------------------------------
|
|
93
|
+
const bm25Candidates: RankedCandidate[] = bm25Results.map((r) => ({
|
|
94
|
+
entityId: r.entityId,
|
|
95
|
+
score: r.score,
|
|
96
|
+
}));
|
|
97
|
+
const graphCandidates: RankedCandidate[] = expandedGraphResults.map((r) => ({
|
|
98
|
+
entityId: r.entityId,
|
|
99
|
+
score: r.score,
|
|
100
|
+
}));
|
|
101
|
+
const vecCandidates: RankedCandidate[] = vecResults.map((r) => ({
|
|
102
|
+
entityId: r.entityId,
|
|
103
|
+
score: r.score,
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
const rrfScores = rrfCombine(bm25Candidates, graphCandidates, vecCandidates);
|
|
107
|
+
|
|
108
|
+
let results = await rerank(db, rrfScores, {
|
|
109
|
+
taskType: opts.taskType,
|
|
110
|
+
packagePath: opts.packagePath,
|
|
111
|
+
paranoid: opts.paranoid,
|
|
112
|
+
limit,
|
|
113
|
+
includeProvenance: opts.includeProvenance,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// --- Post-filter by nodeTypes ------------------------------------------
|
|
117
|
+
if (opts.nodeTypes && opts.nodeTypes.length > 0) {
|
|
118
|
+
const allowed = new Set(opts.nodeTypes);
|
|
119
|
+
results = results.filter((r) => allowed.has(r.type));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --- Provenance --------------------------------------------------------
|
|
123
|
+
if (opts.includeProvenance) {
|
|
124
|
+
await attachProvenance(db, results);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
results,
|
|
129
|
+
mode: "local",
|
|
130
|
+
globalUnavailable: classification.globalUnavailable,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Internal helpers
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Fetch community summaries for global-mode queries.
|
|
140
|
+
* Returns communities that have a non-NULL summary, ordered by member_count DESC.
|
|
141
|
+
*/
|
|
142
|
+
async function fetchCommunitySummaries(db: SiaDb, limit: number): Promise<SiaSearchResult[]> {
|
|
143
|
+
const result = await db.execute(
|
|
144
|
+
"SELECT * FROM communities WHERE summary IS NOT NULL ORDER BY member_count DESC LIMIT ?",
|
|
145
|
+
[limit],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return (result.rows as Record<string, unknown>[]).map((row) => ({
|
|
149
|
+
id: row.id as string,
|
|
150
|
+
type: "Community",
|
|
151
|
+
name: row.id as string,
|
|
152
|
+
summary: (row.summary as string) ?? "",
|
|
153
|
+
content: (row.summary as string) ?? "",
|
|
154
|
+
trust_tier: 1,
|
|
155
|
+
confidence: 1.0,
|
|
156
|
+
importance: 1.0,
|
|
157
|
+
tags: "[]",
|
|
158
|
+
file_paths: "[]",
|
|
159
|
+
conflict_group_id: null,
|
|
160
|
+
t_valid_from: null,
|
|
161
|
+
source_repo_name: null,
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Stage 2: expand 1-hop neighbors for each candidate entity.
|
|
167
|
+
*
|
|
168
|
+
* For each entity in the input list, query the `edges` table for active
|
|
169
|
+
* 1-hop neighbors. Neighbors not already present in the result set are
|
|
170
|
+
* added at score 0.7.
|
|
171
|
+
*/
|
|
172
|
+
async function expandNeighbors(
|
|
173
|
+
db: SiaDb,
|
|
174
|
+
results: Array<{ entityId: string; score: number }>,
|
|
175
|
+
paranoid?: boolean,
|
|
176
|
+
): Promise<Array<{ entityId: string; score: number }>> {
|
|
177
|
+
const scoreMap = new Map<string, number>();
|
|
178
|
+
|
|
179
|
+
// Seed with existing results
|
|
180
|
+
for (const r of results) {
|
|
181
|
+
const existing = scoreMap.get(r.entityId);
|
|
182
|
+
if (existing === undefined || r.score > existing) {
|
|
183
|
+
scoreMap.set(r.entityId, r.score);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Expand each candidate
|
|
188
|
+
const candidateIds = results.map((r) => r.entityId);
|
|
189
|
+
for (const entityId of candidateIds) {
|
|
190
|
+
const edgeResult = await db.execute(
|
|
191
|
+
"SELECT from_id, to_id FROM graph_edges WHERE (from_id = ? OR to_id = ?) AND t_valid_until IS NULL",
|
|
192
|
+
[entityId, entityId],
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
for (const row of edgeResult.rows) {
|
|
196
|
+
const fromId = row.from_id as string;
|
|
197
|
+
const toId = row.to_id as string;
|
|
198
|
+
const neighborId = fromId === entityId ? toId : fromId;
|
|
199
|
+
|
|
200
|
+
// Skip if already in the result set
|
|
201
|
+
if (scoreMap.has(neighborId)) continue;
|
|
202
|
+
|
|
203
|
+
// Validate the neighbor is active (and paranoid-safe)
|
|
204
|
+
const paranoidClause = paranoid ? " AND trust_tier < 4" : "";
|
|
205
|
+
const check = await db.execute(
|
|
206
|
+
`SELECT id FROM graph_nodes WHERE id = ? AND t_valid_until IS NULL AND archived_at IS NULL${paranoidClause}`,
|
|
207
|
+
[neighborId],
|
|
208
|
+
);
|
|
209
|
+
if (check.rows.length === 0) continue;
|
|
210
|
+
|
|
211
|
+
scoreMap.set(neighborId, 0.7);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return [...scoreMap.entries()].map(([entityId, score]) => ({
|
|
216
|
+
entityId,
|
|
217
|
+
score,
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Attach extraction_method to results that don't already have it set.
|
|
223
|
+
* Only queries the DB for results where extraction_method is undefined.
|
|
224
|
+
*/
|
|
225
|
+
async function attachProvenance(db: SiaDb, results: SiaSearchResult[]): Promise<void> {
|
|
226
|
+
for (const result of results) {
|
|
227
|
+
if (result.extraction_method === undefined) {
|
|
228
|
+
const row = await db.execute("SELECT extraction_method FROM graph_nodes WHERE id = ?", [
|
|
229
|
+
result.id,
|
|
230
|
+
]);
|
|
231
|
+
if (row.rows.length > 0) {
|
|
232
|
+
result.extraction_method = (row.rows[0].extraction_method as string | null) ?? null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Module: throttle — Progressive rate limiting for MCP tool calls
|
|
2
|
+
|
|
3
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
4
|
+
|
|
5
|
+
export type ThrottleMode = "normal" | "reduced" | "blocked";
|
|
6
|
+
|
|
7
|
+
export interface ThrottleResult {
|
|
8
|
+
mode: ThrottleMode;
|
|
9
|
+
callCount: number;
|
|
10
|
+
warning?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ThrottleConfig {
|
|
14
|
+
normalMax: number;
|
|
15
|
+
reducedMax: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DEFAULT_THROTTLE_CONFIG: ThrottleConfig = {
|
|
19
|
+
normalMax: 3,
|
|
20
|
+
reducedMax: 8,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const THROTTLED_TOOLS = new Set([
|
|
24
|
+
"sia_search",
|
|
25
|
+
"sia_execute",
|
|
26
|
+
"sia_execute_file",
|
|
27
|
+
"sia_fetch_and_index",
|
|
28
|
+
"sia_by_file",
|
|
29
|
+
"sia_expand",
|
|
30
|
+
"sia_at_time",
|
|
31
|
+
"sia_backlinks",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Progressive throttle backed by the search_throttle table in graph.db.
|
|
36
|
+
*
|
|
37
|
+
* Thresholds (inclusive):
|
|
38
|
+
* - count <= normalMax → "normal"
|
|
39
|
+
* - count <= reducedMax → "reduced" (with warning)
|
|
40
|
+
* - count > reducedMax → "blocked" (with warning mentioning sia_batch_execute)
|
|
41
|
+
*/
|
|
42
|
+
export class ProgressiveThrottle {
|
|
43
|
+
private config: ThrottleConfig;
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
private db: SiaDb,
|
|
47
|
+
config?: Partial<ThrottleConfig>,
|
|
48
|
+
) {
|
|
49
|
+
this.config = { ...DEFAULT_THROTTLE_CONFIG, ...config };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Record a tool call for the given session/tool pair and return the throttle mode.
|
|
54
|
+
*/
|
|
55
|
+
async check(sessionId: string, toolName: string): Promise<ThrottleResult> {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
|
|
58
|
+
// Upsert: insert or increment call_count
|
|
59
|
+
await this.db.execute(
|
|
60
|
+
`INSERT INTO search_throttle (session_id, tool_name, call_count, last_called_at)
|
|
61
|
+
VALUES (?, ?, 1, ?)
|
|
62
|
+
ON CONFLICT(session_id, tool_name) DO UPDATE SET
|
|
63
|
+
call_count = call_count + 1,
|
|
64
|
+
last_called_at = ?`,
|
|
65
|
+
[sessionId, toolName, now, now],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Read back the updated count
|
|
69
|
+
const { rows } = await this.db.execute(
|
|
70
|
+
"SELECT call_count FROM search_throttle WHERE session_id = ? AND tool_name = ?",
|
|
71
|
+
[sessionId, toolName],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const callCount = (rows[0]?.call_count as number) ?? 1;
|
|
75
|
+
const { normalMax, reducedMax } = this.config;
|
|
76
|
+
|
|
77
|
+
if (callCount <= normalMax) {
|
|
78
|
+
return { mode: "normal", callCount };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (callCount <= reducedMax) {
|
|
82
|
+
return {
|
|
83
|
+
mode: "reduced",
|
|
84
|
+
callCount,
|
|
85
|
+
warning: `Reducing results (${callCount} calls). Consider sia_batch_execute for batch operations.`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
mode: "blocked",
|
|
91
|
+
callCount,
|
|
92
|
+
warning: `Tool blocked for this session (${callCount} calls). Use sia_batch_execute instead.`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Clear all throttle entries for the given session.
|
|
98
|
+
*/
|
|
99
|
+
async reset(sessionId: string): Promise<void> {
|
|
100
|
+
await this.db.execute("DELETE FROM search_throttle WHERE session_id = ?", [sessionId]);
|
|
101
|
+
}
|
|
102
|
+
}
|