@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,266 @@
|
|
|
1
|
+
// Module: middleware — Typed factory methods enforcing co-creation and cardinality constraints
|
|
2
|
+
|
|
3
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
4
|
+
import { insertEdge } from "@/graph/edges";
|
|
5
|
+
import type { Entity } from "@/graph/entities";
|
|
6
|
+
import { insertEntity, invalidateEntity } from "@/graph/entities";
|
|
7
|
+
import { validateEdge } from "@/ontology/constraints";
|
|
8
|
+
import { OntologyError } from "@/ontology/errors";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Shared helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/** Build a tags JSON string from an optional string array. */
|
|
15
|
+
function tagsJson(tags?: string[]): string {
|
|
16
|
+
return JSON.stringify(tags ?? []);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// createBug
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export interface CreateBugOpts {
|
|
24
|
+
name: string;
|
|
25
|
+
content: string;
|
|
26
|
+
causedBy: string;
|
|
27
|
+
tags?: string[];
|
|
28
|
+
sessionId?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a Bug entity together with a required `caused_by` edge.
|
|
33
|
+
*
|
|
34
|
+
* Throws OntologyError if `causedBy` is not provided.
|
|
35
|
+
*/
|
|
36
|
+
export async function createBug(db: SiaDb, opts: CreateBugOpts): Promise<Entity> {
|
|
37
|
+
if (!opts.causedBy) {
|
|
38
|
+
throw new OntologyError("createBug requires a causedBy target entity id");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let created!: Entity;
|
|
42
|
+
await db.transaction(async (tx) => {
|
|
43
|
+
created = await insertEntity(tx, {
|
|
44
|
+
type: "Bug",
|
|
45
|
+
name: opts.name,
|
|
46
|
+
content: opts.content,
|
|
47
|
+
summary: opts.content.slice(0, 120),
|
|
48
|
+
tags: tagsJson(opts.tags),
|
|
49
|
+
source_episode: opts.sessionId ?? null,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const valid = await validateEdge(tx, "Bug", "caused_by", "CodeEntity");
|
|
53
|
+
if (!valid) {
|
|
54
|
+
// Fall back: still allow if the constraint exists for any target type
|
|
55
|
+
const fallback = await validateEdge(tx, "Bug", "caused_by", "FileNode");
|
|
56
|
+
if (!fallback) {
|
|
57
|
+
throw new OntologyError("No edge_constraints entry for Bug→caused_by");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await insertEdge(tx, {
|
|
62
|
+
from_id: created.id,
|
|
63
|
+
to_id: opts.causedBy,
|
|
64
|
+
type: "caused_by",
|
|
65
|
+
source_episode: opts.sessionId ?? null,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return created;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// createConvention
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
export interface CreateConventionOpts {
|
|
77
|
+
name: string;
|
|
78
|
+
content: string;
|
|
79
|
+
pertainsTo: string[];
|
|
80
|
+
tags?: string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create a Convention entity together with one or more `pertains_to` edges.
|
|
85
|
+
*
|
|
86
|
+
* Throws OntologyError if `pertainsTo` is empty.
|
|
87
|
+
*/
|
|
88
|
+
export async function createConvention(db: SiaDb, opts: CreateConventionOpts): Promise<Entity> {
|
|
89
|
+
if (!opts.pertainsTo || opts.pertainsTo.length === 0) {
|
|
90
|
+
throw new OntologyError("createConvention requires at least one pertainsTo target");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let created!: Entity;
|
|
94
|
+
await db.transaction(async (tx) => {
|
|
95
|
+
created = await insertEntity(tx, {
|
|
96
|
+
type: "Convention",
|
|
97
|
+
name: opts.name,
|
|
98
|
+
content: opts.content,
|
|
99
|
+
summary: opts.content.slice(0, 120),
|
|
100
|
+
tags: tagsJson(opts.tags),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
for (const targetId of opts.pertainsTo) {
|
|
104
|
+
await insertEdge(tx, {
|
|
105
|
+
from_id: created.id,
|
|
106
|
+
to_id: targetId,
|
|
107
|
+
type: "pertains_to",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return created;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// createDecision
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
export interface CreateDecisionOpts {
|
|
120
|
+
name: string;
|
|
121
|
+
content: string;
|
|
122
|
+
pertainsTo?: string[];
|
|
123
|
+
supersedes?: string;
|
|
124
|
+
tags?: string[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create a Decision entity with optional `pertains_to` and `supersedes` edges.
|
|
129
|
+
*
|
|
130
|
+
* If `supersedes` is provided the old Decision is invalidated via
|
|
131
|
+
* `invalidateEntity`.
|
|
132
|
+
*/
|
|
133
|
+
export async function createDecision(db: SiaDb, opts: CreateDecisionOpts): Promise<Entity> {
|
|
134
|
+
let created!: Entity;
|
|
135
|
+
await db.transaction(async (tx) => {
|
|
136
|
+
created = await insertEntity(tx, {
|
|
137
|
+
type: "Decision",
|
|
138
|
+
name: opts.name,
|
|
139
|
+
content: opts.content,
|
|
140
|
+
summary: opts.content.slice(0, 120),
|
|
141
|
+
tags: tagsJson(opts.tags),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (opts.pertainsTo) {
|
|
145
|
+
for (const targetId of opts.pertainsTo) {
|
|
146
|
+
await insertEdge(tx, {
|
|
147
|
+
from_id: created.id,
|
|
148
|
+
to_id: targetId,
|
|
149
|
+
type: "pertains_to",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (opts.supersedes) {
|
|
155
|
+
await insertEdge(tx, {
|
|
156
|
+
from_id: created.id,
|
|
157
|
+
to_id: opts.supersedes,
|
|
158
|
+
type: "supersedes",
|
|
159
|
+
});
|
|
160
|
+
await invalidateEntity(tx, opts.supersedes);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return created;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// createSolution
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
export interface CreateSolutionOpts {
|
|
172
|
+
name: string;
|
|
173
|
+
content: string;
|
|
174
|
+
solves: string;
|
|
175
|
+
pertainsTo?: string[];
|
|
176
|
+
tags?: string[];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Create a Solution entity together with a required `solves` edge and
|
|
181
|
+
* optional `pertains_to` edges.
|
|
182
|
+
*
|
|
183
|
+
* Throws OntologyError if `solves` is not provided.
|
|
184
|
+
*/
|
|
185
|
+
export async function createSolution(db: SiaDb, opts: CreateSolutionOpts): Promise<Entity> {
|
|
186
|
+
if (!opts.solves) {
|
|
187
|
+
throw new OntologyError("createSolution requires a solves target entity id");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let created!: Entity;
|
|
191
|
+
await db.transaction(async (tx) => {
|
|
192
|
+
created = await insertEntity(tx, {
|
|
193
|
+
type: "Solution",
|
|
194
|
+
name: opts.name,
|
|
195
|
+
content: opts.content,
|
|
196
|
+
summary: opts.content.slice(0, 120),
|
|
197
|
+
tags: tagsJson(opts.tags),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await insertEdge(tx, {
|
|
201
|
+
from_id: created.id,
|
|
202
|
+
to_id: opts.solves,
|
|
203
|
+
type: "solves",
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (opts.pertainsTo) {
|
|
207
|
+
for (const targetId of opts.pertainsTo) {
|
|
208
|
+
await insertEdge(tx, {
|
|
209
|
+
from_id: created.id,
|
|
210
|
+
to_id: targetId,
|
|
211
|
+
type: "pertains_to",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return created;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// createConcept
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
export interface CreateConceptOpts {
|
|
225
|
+
name: string;
|
|
226
|
+
content: string;
|
|
227
|
+
pertainsTo?: string[];
|
|
228
|
+
elaborates?: string;
|
|
229
|
+
tags?: string[];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Create a Concept entity with optional `pertains_to` and `elaborates` edges.
|
|
234
|
+
*/
|
|
235
|
+
export async function createConcept(db: SiaDb, opts: CreateConceptOpts): Promise<Entity> {
|
|
236
|
+
let created!: Entity;
|
|
237
|
+
await db.transaction(async (tx) => {
|
|
238
|
+
created = await insertEntity(tx, {
|
|
239
|
+
type: "Concept",
|
|
240
|
+
name: opts.name,
|
|
241
|
+
content: opts.content,
|
|
242
|
+
summary: opts.content.slice(0, 120),
|
|
243
|
+
tags: tagsJson(opts.tags),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (opts.pertainsTo) {
|
|
247
|
+
for (const targetId of opts.pertainsTo) {
|
|
248
|
+
await insertEdge(tx, {
|
|
249
|
+
from_id: created.id,
|
|
250
|
+
to_id: targetId,
|
|
251
|
+
type: "pertains_to",
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (opts.elaborates) {
|
|
257
|
+
await insertEdge(tx, {
|
|
258
|
+
from_id: created.id,
|
|
259
|
+
to_id: opts.elaborates,
|
|
260
|
+
type: "elaborates",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return created;
|
|
266
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Module: bm25-search — FTS5 MATCH keyword search with normalized ranking
|
|
2
|
+
|
|
3
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
4
|
+
|
|
5
|
+
/** A single BM25 search result. */
|
|
6
|
+
export interface BM25Result {
|
|
7
|
+
entityId: string;
|
|
8
|
+
score: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Options for bm25Search. */
|
|
12
|
+
export interface BM25SearchOpts {
|
|
13
|
+
/** Maximum number of results to return. Default 20. */
|
|
14
|
+
limit?: number;
|
|
15
|
+
/** When true, exclude entities with trust_tier = 4 (external/untrusted). */
|
|
16
|
+
paranoid?: boolean;
|
|
17
|
+
/** Filter results to a specific monorepo package path. */
|
|
18
|
+
packagePath?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sanitize a query string for FTS5 MATCH syntax.
|
|
23
|
+
*
|
|
24
|
+
* - Preserves double-quoted phrases intact (e.g. `"auth module"`)
|
|
25
|
+
* - Strips FTS5 special characters from unquoted parts
|
|
26
|
+
* - Splits unquoted parts on whitespace so each token is a separate term
|
|
27
|
+
* - Returns the sanitized query string ready for FTS5 MATCH
|
|
28
|
+
*/
|
|
29
|
+
export function sanitizeFts5Query(query: string): string {
|
|
30
|
+
const trimmed = query.trim();
|
|
31
|
+
if (!trimmed) return "";
|
|
32
|
+
|
|
33
|
+
const parts: string[] = [];
|
|
34
|
+
let remaining = trimmed;
|
|
35
|
+
|
|
36
|
+
// Extract quoted phrases and unquoted segments
|
|
37
|
+
while (remaining.length > 0) {
|
|
38
|
+
const quoteStart = remaining.indexOf('"');
|
|
39
|
+
if (quoteStart === -1) {
|
|
40
|
+
// No more quotes — process remaining as unquoted
|
|
41
|
+
const tokens = sanitizeUnquoted(remaining);
|
|
42
|
+
if (tokens) parts.push(tokens);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Process text before the quote
|
|
47
|
+
if (quoteStart > 0) {
|
|
48
|
+
const before = remaining.slice(0, quoteStart);
|
|
49
|
+
const tokens = sanitizeUnquoted(before);
|
|
50
|
+
if (tokens) parts.push(tokens);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Find closing quote
|
|
54
|
+
const quoteEnd = remaining.indexOf('"', quoteStart + 1);
|
|
55
|
+
if (quoteEnd === -1) {
|
|
56
|
+
// Unclosed quote — treat rest as unquoted
|
|
57
|
+
const rest = remaining.slice(quoteStart + 1);
|
|
58
|
+
const tokens = sanitizeUnquoted(rest);
|
|
59
|
+
if (tokens) parts.push(tokens);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Extract the quoted phrase (including quotes)
|
|
64
|
+
const phrase = remaining.slice(quoteStart, quoteEnd + 1);
|
|
65
|
+
// Only add if the phrase has actual content between quotes
|
|
66
|
+
const inner = phrase.slice(1, -1).trim();
|
|
67
|
+
if (inner.length > 0) {
|
|
68
|
+
parts.push(`"${inner}"`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
remaining = remaining.slice(quoteEnd + 1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return parts.join(" ");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Strip FTS5 special characters from an unquoted segment and return
|
|
79
|
+
* space-separated tokens. Returns empty string if no valid tokens remain.
|
|
80
|
+
*/
|
|
81
|
+
function sanitizeUnquoted(text: string): string {
|
|
82
|
+
// Remove FTS5 operators and special characters: * + - ^ ~ : ( ) { } < >
|
|
83
|
+
const cleaned = text.replace(/[*+\-^~:(){}|<>@!.,;'/\\[\]]/g, " ");
|
|
84
|
+
const tokens = cleaned.split(/\s+/).filter((t) => t.length > 0);
|
|
85
|
+
return tokens.join(" ");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Perform a BM25 keyword search against the graph_nodes_fts virtual table.
|
|
90
|
+
*
|
|
91
|
+
* Joins `graph_nodes_fts` with `entities` on rowid, filters for active entities
|
|
92
|
+
* (t_valid_until IS NULL AND archived_at IS NULL), applies optional paranoid
|
|
93
|
+
* and packagePath filters, and normalizes FTS5 rank to a 0–1 range.
|
|
94
|
+
*
|
|
95
|
+
* Returns an empty array for empty or invalid queries.
|
|
96
|
+
*/
|
|
97
|
+
export async function bm25Search(
|
|
98
|
+
db: SiaDb,
|
|
99
|
+
query: string,
|
|
100
|
+
opts?: BM25SearchOpts,
|
|
101
|
+
): Promise<BM25Result[]> {
|
|
102
|
+
const sanitized = sanitizeFts5Query(query);
|
|
103
|
+
if (!sanitized) return [];
|
|
104
|
+
|
|
105
|
+
const limit = opts?.limit ?? 20;
|
|
106
|
+
const paranoid = opts?.paranoid ?? false;
|
|
107
|
+
const packagePath = opts?.packagePath;
|
|
108
|
+
|
|
109
|
+
// Build the query with optional filters
|
|
110
|
+
const conditions: string[] = ["e.t_valid_until IS NULL", "e.archived_at IS NULL"];
|
|
111
|
+
const params: unknown[] = [sanitized];
|
|
112
|
+
|
|
113
|
+
if (paranoid) {
|
|
114
|
+
conditions.push("e.trust_tier != 4");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (packagePath) {
|
|
118
|
+
conditions.push("e.package_path = ?");
|
|
119
|
+
params.push(packagePath);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
params.push(limit);
|
|
123
|
+
|
|
124
|
+
const whereClause = conditions.join(" AND ");
|
|
125
|
+
|
|
126
|
+
const sql = `
|
|
127
|
+
SELECT e.id AS entityId, -fts.rank AS rawRank
|
|
128
|
+
FROM graph_nodes_fts fts
|
|
129
|
+
JOIN graph_nodes e ON e.rowid = fts.rowid
|
|
130
|
+
WHERE graph_nodes_fts MATCH ?
|
|
131
|
+
AND ${whereClause}
|
|
132
|
+
ORDER BY fts.rank
|
|
133
|
+
LIMIT ?
|
|
134
|
+
`;
|
|
135
|
+
|
|
136
|
+
const result = await db.execute(sql, params);
|
|
137
|
+
const rows = result.rows as Array<{ entityId: string; rawRank: number }>;
|
|
138
|
+
|
|
139
|
+
if (rows.length === 0) return [];
|
|
140
|
+
|
|
141
|
+
// Normalize rawRank to 0–1 range using min/max of the result set
|
|
142
|
+
const rawRanks = rows.map((r) => r.rawRank);
|
|
143
|
+
const minRank = Math.min(...rawRanks);
|
|
144
|
+
const maxRank = Math.max(...rawRanks);
|
|
145
|
+
const range = maxRank - minRank;
|
|
146
|
+
|
|
147
|
+
return rows.map((row) => ({
|
|
148
|
+
entityId: row.entityId,
|
|
149
|
+
score: range === 0 ? 1.0 : (row.rawRank - minRank) / range,
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/retrieval/context-assembly.ts
|
|
2
|
+
|
|
3
|
+
import type { SiaSearchResult } from "@/graph/types";
|
|
4
|
+
|
|
5
|
+
export interface AssemblyOptions {
|
|
6
|
+
includeProvenance?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Map a raw entity DB row to a typed SiaSearchResult.
|
|
11
|
+
* - Parses JSON `tags` and `file_paths` (falls back to [] on parse failure)
|
|
12
|
+
* - Includes `extraction_method` only when opts.includeProvenance is true
|
|
13
|
+
* - Always includes `conflict_group_id` and `t_valid_from`
|
|
14
|
+
*/
|
|
15
|
+
export function assembleSearchResult(
|
|
16
|
+
row: Record<string, unknown>,
|
|
17
|
+
opts?: AssemblyOptions,
|
|
18
|
+
): SiaSearchResult {
|
|
19
|
+
const result: SiaSearchResult = {
|
|
20
|
+
entity_id: row.id as string,
|
|
21
|
+
type: row.type as string,
|
|
22
|
+
name: row.name as string,
|
|
23
|
+
summary: row.summary as string,
|
|
24
|
+
content: row.content as string,
|
|
25
|
+
tags: safeParseJsonArray(row.tags as string),
|
|
26
|
+
file_paths: safeParseJsonArray(row.file_paths as string),
|
|
27
|
+
trust_tier: row.trust_tier as SiaSearchResult["trust_tier"],
|
|
28
|
+
confidence: row.confidence as number,
|
|
29
|
+
importance: row.importance as number,
|
|
30
|
+
conflict_group_id: (row.conflict_group_id as string) ?? null,
|
|
31
|
+
t_valid_from: (row.t_valid_from as number) ?? null,
|
|
32
|
+
t_valid_until: (row.t_valid_until as number) ?? null,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (opts?.includeProvenance && row.extraction_method) {
|
|
36
|
+
result.extraction_method = row.extraction_method as string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Enforce maxResponseTokens by estimating ~150 tokens per result.
|
|
44
|
+
* Returns whole results or nothing (no partial results).
|
|
45
|
+
*/
|
|
46
|
+
export function enforceResponseBudget(
|
|
47
|
+
results: SiaSearchResult[],
|
|
48
|
+
maxTokens: number,
|
|
49
|
+
): { results: SiaSearchResult[]; truncated: boolean } {
|
|
50
|
+
const TOKENS_PER_RESULT = 150;
|
|
51
|
+
|
|
52
|
+
if (maxTokens <= 0) {
|
|
53
|
+
return { results: [], truncated: results.length > 0 };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const maxResults = Math.floor(maxTokens / TOKENS_PER_RESULT);
|
|
57
|
+
|
|
58
|
+
if (results.length <= maxResults) {
|
|
59
|
+
return { results, truncated: false };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
results: results.slice(0, maxResults),
|
|
64
|
+
truncated: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function safeParseJsonArray(value: string): string[] {
|
|
69
|
+
if (!value) return [];
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(value);
|
|
72
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Module: graph-traversal — Entity name extraction + 1-hop graph expansion search signal
|
|
2
|
+
|
|
3
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
4
|
+
|
|
5
|
+
/** A single result from the graph traversal search signal. */
|
|
6
|
+
export interface GraphTraversalResult {
|
|
7
|
+
entityId: string;
|
|
8
|
+
score: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Options for graphTraversalSearch. */
|
|
12
|
+
export interface GraphTraversalSearchOpts {
|
|
13
|
+
/** Maximum number of results to return. Default 20. */
|
|
14
|
+
limit?: number;
|
|
15
|
+
/** When true, exclude trust_tier >= 4 entities. */
|
|
16
|
+
paranoid?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extract candidate entity names from a natural-language query.
|
|
21
|
+
*
|
|
22
|
+
* Steps:
|
|
23
|
+
* 1. Split on whitespace, keep tokens >= 2 chars.
|
|
24
|
+
* 2. Try CamelCase splits on each token (e.g. "TokenStore" -> ["Token", "Store"]).
|
|
25
|
+
* 3. Generate two-word combinations from adjacent tokens:
|
|
26
|
+
* - CamelCase joined ("token store" -> "TokenStore")
|
|
27
|
+
* - snake_case joined ("token store" -> "token_store")
|
|
28
|
+
*/
|
|
29
|
+
export function extractQueryTerms(query: string): string[] {
|
|
30
|
+
const raw = query.split(/\s+/).filter((t) => t.length >= 2);
|
|
31
|
+
const terms = new Set<string>();
|
|
32
|
+
|
|
33
|
+
for (const token of raw) {
|
|
34
|
+
terms.add(token);
|
|
35
|
+
|
|
36
|
+
// CamelCase split: "TokenStore" -> ["Token", "Store"]
|
|
37
|
+
const camelParts = token.match(/[A-Z][a-z]+|[a-z]+|[A-Z]+(?=[A-Z][a-z]|\b)/g);
|
|
38
|
+
if (camelParts && camelParts.length > 1) {
|
|
39
|
+
for (const part of camelParts) {
|
|
40
|
+
if (part.length >= 2) {
|
|
41
|
+
terms.add(part);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Two-word combinations from adjacent tokens
|
|
48
|
+
for (let i = 0; i < raw.length - 1; i++) {
|
|
49
|
+
const a = raw[i];
|
|
50
|
+
const b = raw[i + 1];
|
|
51
|
+
// CamelCase: capitalize first letter of each word
|
|
52
|
+
const camel =
|
|
53
|
+
a.charAt(0).toUpperCase() +
|
|
54
|
+
a.slice(1).toLowerCase() +
|
|
55
|
+
b.charAt(0).toUpperCase() +
|
|
56
|
+
b.slice(1).toLowerCase();
|
|
57
|
+
terms.add(camel);
|
|
58
|
+
// snake_case
|
|
59
|
+
terms.add(`${a.toLowerCase()}_${b.toLowerCase()}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return [...terms];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Graph traversal search signal.
|
|
67
|
+
*
|
|
68
|
+
* 1. Extract candidate terms from the query.
|
|
69
|
+
* 2. Direct lookup: exact name match on active entities (score 1.0).
|
|
70
|
+
* 3. LIKE partial match for terms >= 3 chars (score 0.9, limit 5 per term).
|
|
71
|
+
* 4. 1-hop expansion via active edges for each matched entity (score 0.7).
|
|
72
|
+
* 5. Deduplicate (highest score wins), sort by score DESC, cap at limit.
|
|
73
|
+
*/
|
|
74
|
+
export async function graphTraversalSearch(
|
|
75
|
+
db: SiaDb,
|
|
76
|
+
query: string,
|
|
77
|
+
opts?: GraphTraversalSearchOpts,
|
|
78
|
+
): Promise<GraphTraversalResult[]> {
|
|
79
|
+
const limit = opts?.limit ?? 20;
|
|
80
|
+
const paranoid = opts?.paranoid ?? false;
|
|
81
|
+
const terms = extractQueryTerms(query);
|
|
82
|
+
|
|
83
|
+
if (terms.length === 0) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Map<entityId, score> — highest score wins
|
|
88
|
+
const scoreMap = new Map<string, number>();
|
|
89
|
+
|
|
90
|
+
function addScore(entityId: string, score: number): void {
|
|
91
|
+
const existing = scoreMap.get(entityId);
|
|
92
|
+
if (existing === undefined || score > existing) {
|
|
93
|
+
scoreMap.set(entityId, score);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Collect direct-match entity IDs for 1-hop expansion
|
|
98
|
+
const directMatchIds: string[] = [];
|
|
99
|
+
|
|
100
|
+
// Stage 1: Direct exact lookup (score 1.0)
|
|
101
|
+
for (const term of terms) {
|
|
102
|
+
const paranoidClause = paranoid ? " AND trust_tier < 4" : "";
|
|
103
|
+
const result = await db.execute(
|
|
104
|
+
`SELECT id FROM graph_nodes WHERE name = ? AND t_valid_until IS NULL AND archived_at IS NULL${paranoidClause}`,
|
|
105
|
+
[term],
|
|
106
|
+
);
|
|
107
|
+
for (const row of result.rows) {
|
|
108
|
+
const id = row.id as string;
|
|
109
|
+
addScore(id, 1.0);
|
|
110
|
+
directMatchIds.push(id);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Stage 2: LIKE partial match (score 0.9) for terms >= 3 chars
|
|
115
|
+
for (const term of terms) {
|
|
116
|
+
if (term.length < 3) continue;
|
|
117
|
+
const paranoidClause = paranoid ? " AND trust_tier < 4" : "";
|
|
118
|
+
const result = await db.execute(
|
|
119
|
+
`SELECT id FROM graph_nodes WHERE name LIKE ? AND t_valid_until IS NULL AND archived_at IS NULL${paranoidClause} LIMIT 5`,
|
|
120
|
+
[`%${term}%`],
|
|
121
|
+
);
|
|
122
|
+
for (const row of result.rows) {
|
|
123
|
+
const id = row.id as string;
|
|
124
|
+
addScore(id, 0.9);
|
|
125
|
+
if (!directMatchIds.includes(id)) {
|
|
126
|
+
directMatchIds.push(id);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Stage 3: 1-hop expansion via edges (score 0.7)
|
|
132
|
+
for (const entityId of directMatchIds) {
|
|
133
|
+
const edgeResult = await db.execute(
|
|
134
|
+
"SELECT from_id, to_id FROM graph_edges WHERE (from_id = ? OR to_id = ?) AND t_valid_until IS NULL",
|
|
135
|
+
[entityId, entityId],
|
|
136
|
+
);
|
|
137
|
+
for (const row of edgeResult.rows) {
|
|
138
|
+
const fromId = row.from_id as string;
|
|
139
|
+
const toId = row.to_id as string;
|
|
140
|
+
const neighborId = fromId === entityId ? toId : fromId;
|
|
141
|
+
|
|
142
|
+
// Only add neighbor if it is an active, non-archived entity
|
|
143
|
+
if (paranoid) {
|
|
144
|
+
const check = await db.execute(
|
|
145
|
+
"SELECT id FROM graph_nodes WHERE id = ? AND t_valid_until IS NULL AND archived_at IS NULL AND trust_tier < 4",
|
|
146
|
+
[neighborId],
|
|
147
|
+
);
|
|
148
|
+
if (check.rows.length === 0) continue;
|
|
149
|
+
} else {
|
|
150
|
+
const check = await db.execute(
|
|
151
|
+
"SELECT id FROM graph_nodes WHERE id = ? AND t_valid_until IS NULL AND archived_at IS NULL",
|
|
152
|
+
[neighborId],
|
|
153
|
+
);
|
|
154
|
+
if (check.rows.length === 0) continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
addScore(neighborId, 0.7);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Build sorted result list
|
|
162
|
+
const results: GraphTraversalResult[] = [...scoreMap.entries()]
|
|
163
|
+
.map(([entityId, score]) => ({ entityId, score }))
|
|
164
|
+
.sort((a, b) => b.score - a.score)
|
|
165
|
+
.slice(0, limit);
|
|
166
|
+
|
|
167
|
+
return results;
|
|
168
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Module: pagerank — Retrieval-layer PageRank wrapper
|
|
2
|
+
|
|
3
|
+
import { writeAuditEntry } from "@/graph/audit";
|
|
4
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
5
|
+
|
|
6
|
+
export const EDGE_TYPE_WEIGHTS: Record<string, number> = {
|
|
7
|
+
calls: 0.5,
|
|
8
|
+
pertains_to: 0.4,
|
|
9
|
+
solves: 0.4,
|
|
10
|
+
relates_to: 0.3,
|
|
11
|
+
imports: 0.3,
|
|
12
|
+
caused_by: 0.3,
|
|
13
|
+
elaborates: 0.2,
|
|
14
|
+
supersedes: 0.2,
|
|
15
|
+
member_of: 0.1,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export async function getImportanceScore(db: SiaDb, nodeId: string): Promise<number> {
|
|
19
|
+
const { rows } = await db.execute("SELECT importance FROM graph_nodes WHERE id = ?", [nodeId]);
|
|
20
|
+
if (rows.length === 0) return 0.5;
|
|
21
|
+
return (rows[0] as { importance: number }).importance;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function updateImportanceScores(
|
|
25
|
+
db: SiaDb,
|
|
26
|
+
scores: Map<string, number>,
|
|
27
|
+
): Promise<number> {
|
|
28
|
+
if (scores.size === 0) return 0;
|
|
29
|
+
const statements = [...scores.entries()].map(([id, score]) => ({
|
|
30
|
+
sql: "UPDATE graph_nodes SET importance = ? WHERE id = ?",
|
|
31
|
+
params: [score, id] as unknown[],
|
|
32
|
+
}));
|
|
33
|
+
await db.executeMany(statements);
|
|
34
|
+
await writeAuditEntry(db, "PAGERANK_UPDATE", {});
|
|
35
|
+
return scores.size;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getEdgeWeight(edgeType: string): number {
|
|
39
|
+
return EDGE_TYPE_WEIGHTS[edgeType] ?? 0.1;
|
|
40
|
+
}
|