@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,322 @@
|
|
|
1
|
+
import { existsSync, watch as fsWatch, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { dispatchExtractionAsync } from "@/ast/extractors/tier-dispatch";
|
|
4
|
+
import { getLanguageForFile } from "@/ast/languages";
|
|
5
|
+
import { createIgnoreMatcher, detectPackagePath, toPosixPath } from "@/ast/path-utils";
|
|
6
|
+
import { computeEdits } from "@/ast/tree-sitter/edit-computer";
|
|
7
|
+
import { TreeSitterService } from "@/ast/tree-sitter/service";
|
|
8
|
+
import { extractTrackA } from "@/capture/track-a-ast";
|
|
9
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
10
|
+
import { getActiveEdges, invalidateEdge } from "@/graph/edges";
|
|
11
|
+
import { insertEntity, invalidateEntity } from "@/graph/entities";
|
|
12
|
+
import type { SiaConfig } from "@/shared/config";
|
|
13
|
+
import { getConfig } from "@/shared/config";
|
|
14
|
+
|
|
15
|
+
let _watcherService: TreeSitterService | null = null;
|
|
16
|
+
function getWatcherService(): TreeSitterService {
|
|
17
|
+
if (!_watcherService) {
|
|
18
|
+
const config = getConfig();
|
|
19
|
+
_watcherService = new TreeSitterService(config.treeSitter!);
|
|
20
|
+
}
|
|
21
|
+
return _watcherService;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface FileWatcher {
|
|
25
|
+
start(): void;
|
|
26
|
+
stop(): Promise<void>;
|
|
27
|
+
ready: Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface TrackedEntity {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
content: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function getEntitiesForPath(db: SiaDb, relPath: string): Promise<TrackedEntity[]> {
|
|
37
|
+
const pattern = `%"${relPath}"%`;
|
|
38
|
+
const result = await db.execute(
|
|
39
|
+
"SELECT id, name, content FROM graph_nodes WHERE t_valid_until IS NULL AND archived_at IS NULL AND file_paths LIKE ?",
|
|
40
|
+
[pattern],
|
|
41
|
+
);
|
|
42
|
+
return result.rows as unknown as TrackedEntity[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function invalidateEdgesForEntity(db: SiaDb, entityId: string, ts: number): Promise<void> {
|
|
46
|
+
const edges = await getActiveEdges(db, entityId);
|
|
47
|
+
for (const edge of edges) {
|
|
48
|
+
await invalidateEdge(db, edge.id, ts);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function handleDeletion(db: SiaDb, relPath: string): Promise<void> {
|
|
53
|
+
const existing = await getEntitiesForPath(db, relPath);
|
|
54
|
+
const ts = Date.now();
|
|
55
|
+
for (const entity of existing) {
|
|
56
|
+
await invalidateEntity(db, entity.id, ts);
|
|
57
|
+
await invalidateEdgesForEntity(db, entity.id, ts);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function handleChange(db: SiaDb, relPath: string, content: string): Promise<void> {
|
|
62
|
+
// --- Tree-sitter incremental re-parse path ---
|
|
63
|
+
let treeSitterFacts: import("@/capture/types").CandidateFact[] | null = null;
|
|
64
|
+
try {
|
|
65
|
+
const langConfig = getLanguageForFile(relPath);
|
|
66
|
+
if (langConfig) {
|
|
67
|
+
const service = getWatcherService();
|
|
68
|
+
const cache = service.cache;
|
|
69
|
+
const cachedEntry = cache.get(relPath);
|
|
70
|
+
|
|
71
|
+
if (cachedEntry) {
|
|
72
|
+
// Incremental re-parse: compute edits, apply to old tree, parse
|
|
73
|
+
const edits = computeEdits(cachedEntry.source, content);
|
|
74
|
+
const treeAny = cachedEntry.tree as any;
|
|
75
|
+
for (const edit of edits) {
|
|
76
|
+
treeAny.edit?.(edit);
|
|
77
|
+
}
|
|
78
|
+
const newTree = await service.parse(content, langConfig.name, cachedEntry.tree);
|
|
79
|
+
if (newTree) {
|
|
80
|
+
cache.set(relPath, newTree, content);
|
|
81
|
+
// Use dispatchExtractionAsync for scoped extraction
|
|
82
|
+
treeSitterFacts = await dispatchExtractionAsync(
|
|
83
|
+
content,
|
|
84
|
+
relPath,
|
|
85
|
+
langConfig.tier,
|
|
86
|
+
langConfig.name,
|
|
87
|
+
langConfig.specialHandling,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
// Full parse (populates cache for next time)
|
|
92
|
+
const tree = await service.parse(content, langConfig.name);
|
|
93
|
+
if (tree) {
|
|
94
|
+
cache.set(relPath, tree, content);
|
|
95
|
+
treeSitterFacts = await dispatchExtractionAsync(
|
|
96
|
+
content,
|
|
97
|
+
relPath,
|
|
98
|
+
langConfig.tier,
|
|
99
|
+
langConfig.name,
|
|
100
|
+
langConfig.specialHandling,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Tree-sitter failed — fall through to regex below
|
|
107
|
+
treeSitterFacts = null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- Regex fallback (existing logic, used when tree-sitter is unavailable) ---
|
|
111
|
+
const facts = treeSitterFacts ?? extractTrackA(content, relPath);
|
|
112
|
+
const existing = await getEntitiesForPath(db, relPath);
|
|
113
|
+
|
|
114
|
+
const existingByName = new Map(existing.map((e) => [e.name, e]));
|
|
115
|
+
const newNames = new Set(facts.map((f) => f.name));
|
|
116
|
+
|
|
117
|
+
for (const fact of facts) {
|
|
118
|
+
const existingEntity = existingByName.get(fact.name);
|
|
119
|
+
if (existingEntity) {
|
|
120
|
+
// SUPERSEDE pattern: invalidate old, insert new
|
|
121
|
+
if (existingEntity.content !== fact.content) {
|
|
122
|
+
const ts = Date.now();
|
|
123
|
+
await invalidateEntity(db, existingEntity.id, ts);
|
|
124
|
+
await invalidateEdgesForEntity(db, existingEntity.id, ts);
|
|
125
|
+
await insertEntity(db, {
|
|
126
|
+
type: fact.type,
|
|
127
|
+
name: fact.name,
|
|
128
|
+
content: fact.content,
|
|
129
|
+
summary: fact.summary,
|
|
130
|
+
tags: JSON.stringify(fact.tags ?? []),
|
|
131
|
+
file_paths: JSON.stringify([relPath]),
|
|
132
|
+
trust_tier: fact.trust_tier,
|
|
133
|
+
confidence: fact.confidence,
|
|
134
|
+
package_path: detectPackagePath(relPath),
|
|
135
|
+
extraction_method: fact.extraction_method ?? null,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await insertEntity(db, {
|
|
142
|
+
type: fact.type,
|
|
143
|
+
name: fact.name,
|
|
144
|
+
content: fact.content,
|
|
145
|
+
summary: fact.summary,
|
|
146
|
+
tags: JSON.stringify(fact.tags ?? []),
|
|
147
|
+
file_paths: JSON.stringify([relPath]),
|
|
148
|
+
trust_tier: fact.trust_tier,
|
|
149
|
+
confidence: fact.confidence,
|
|
150
|
+
package_path: detectPackagePath(relPath),
|
|
151
|
+
extraction_method: fact.extraction_method ?? null,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Invalidate removed entities + edges
|
|
156
|
+
const ts = Date.now();
|
|
157
|
+
for (const entity of existing) {
|
|
158
|
+
if (newNames.has(entity.name)) continue;
|
|
159
|
+
await invalidateEntity(db, entity.id, ts);
|
|
160
|
+
await invalidateEdgesForEntity(db, entity.id, ts);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
type CloseableWatcher = {
|
|
165
|
+
close(): void | Promise<void>;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
async function createUnderlyingWatcher(
|
|
169
|
+
root: string,
|
|
170
|
+
ignoreMatcher: ReturnType<typeof createIgnoreMatcher>,
|
|
171
|
+
onChange: (absPath: string) => void,
|
|
172
|
+
onDelete: (absPath: string) => void,
|
|
173
|
+
onReady: () => void,
|
|
174
|
+
): Promise<CloseableWatcher> {
|
|
175
|
+
try {
|
|
176
|
+
const mod = await import("chokidar");
|
|
177
|
+
const chokidar = mod as unknown as {
|
|
178
|
+
default?: { watch: typeof import("chokidar").watch };
|
|
179
|
+
watch: typeof import("chokidar").watch;
|
|
180
|
+
};
|
|
181
|
+
const watchFn = chokidar.watch ?? chokidar.default?.watch;
|
|
182
|
+
if (!watchFn) {
|
|
183
|
+
throw new Error("chokidar watch not available");
|
|
184
|
+
}
|
|
185
|
+
const watcher = watchFn(root, {
|
|
186
|
+
ignoreInitial: true,
|
|
187
|
+
ignored: (path, stats) =>
|
|
188
|
+
ignoreMatcher.shouldIgnore(path ?? "", Boolean(stats?.isDirectory())),
|
|
189
|
+
});
|
|
190
|
+
watcher.on("change", onChange);
|
|
191
|
+
watcher.on("add", onChange);
|
|
192
|
+
watcher.on("unlink", onDelete);
|
|
193
|
+
(watcher as unknown as { on: (event: string, cb: () => void) => void }).on("ready", onReady);
|
|
194
|
+
return watcher;
|
|
195
|
+
} catch {
|
|
196
|
+
// Fallback: fs.watch (recursive on macOS/Windows)
|
|
197
|
+
const watcher = fsWatch(root, { recursive: true }, (eventType, filename) => {
|
|
198
|
+
if (!filename) return;
|
|
199
|
+
const absPath = join(root, filename.toString());
|
|
200
|
+
if (ignoreMatcher.shouldIgnore(absPath, false)) return;
|
|
201
|
+
if (eventType === "rename") {
|
|
202
|
+
if (existsSync(absPath)) {
|
|
203
|
+
onChange(absPath);
|
|
204
|
+
} else {
|
|
205
|
+
onDelete(absPath);
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
onChange(absPath);
|
|
210
|
+
});
|
|
211
|
+
onReady();
|
|
212
|
+
return watcher;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function createWatcher(repoRoot: string, db: SiaDb, config: SiaConfig): FileWatcher {
|
|
217
|
+
const root = resolve(repoRoot);
|
|
218
|
+
const ignoreMatcher = createIgnoreMatcher(root, config.excludePaths ?? []);
|
|
219
|
+
|
|
220
|
+
let closer: CloseableWatcher | null = null;
|
|
221
|
+
let readyPromise: Promise<void> = Promise.resolve();
|
|
222
|
+
let readyResolve: () => void = () => {};
|
|
223
|
+
// Track file mtimes for initial sync
|
|
224
|
+
const fileMtimes = new Map<string, number>();
|
|
225
|
+
|
|
226
|
+
function normalize(absPath: string): string | null {
|
|
227
|
+
const rel = toPosixPath(relative(root, absPath));
|
|
228
|
+
if (rel.startsWith("..")) return null;
|
|
229
|
+
return rel;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function syncOnce(): Promise<void> {
|
|
233
|
+
const seen = new Set<string>();
|
|
234
|
+
const walk = async (dir: string): Promise<void> => {
|
|
235
|
+
let entries: import("node:fs").Dirent[];
|
|
236
|
+
try {
|
|
237
|
+
entries = readdirSync(dir, { withFileTypes: true }) as import("node:fs").Dirent[];
|
|
238
|
+
} catch {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
for (const entry of entries) {
|
|
242
|
+
const absPath = join(dir, entry.name as string);
|
|
243
|
+
const isDir = entry.isDirectory();
|
|
244
|
+
if (ignoreMatcher.shouldIgnore(absPath, isDir)) continue;
|
|
245
|
+
if (isDir) {
|
|
246
|
+
await walk(absPath);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const relPath = normalize(absPath);
|
|
250
|
+
if (!relPath) continue;
|
|
251
|
+
seen.add(relPath);
|
|
252
|
+
const stat = statSync(absPath);
|
|
253
|
+
const prev = fileMtimes.get(relPath);
|
|
254
|
+
if (prev === undefined || prev !== stat.mtimeMs) {
|
|
255
|
+
fileMtimes.set(relPath, stat.mtimeMs);
|
|
256
|
+
const content = readFileSync(absPath, "utf-8");
|
|
257
|
+
await handleChange(db, relPath, content);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
await walk(root);
|
|
263
|
+
|
|
264
|
+
for (const path of [...fileMtimes.keys()]) {
|
|
265
|
+
if (!seen.has(path)) {
|
|
266
|
+
fileMtimes.delete(path);
|
|
267
|
+
await handleDeletion(db, path);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const start = (): void => {
|
|
273
|
+
readyPromise = new Promise<void>((resolveReady) => {
|
|
274
|
+
readyResolve = resolveReady;
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
void (async () => {
|
|
278
|
+
closer = await createUnderlyingWatcher(
|
|
279
|
+
root,
|
|
280
|
+
ignoreMatcher,
|
|
281
|
+
(absPath) => {
|
|
282
|
+
const relPath = normalize(absPath);
|
|
283
|
+
if (!relPath) return;
|
|
284
|
+
if (ignoreMatcher.shouldIgnore(absPath, false)) return;
|
|
285
|
+
const language = getLanguageForFile(absPath);
|
|
286
|
+
if (!language) return;
|
|
287
|
+
try {
|
|
288
|
+
const content = readFileSync(absPath, "utf-8");
|
|
289
|
+
void handleChange(db, relPath, content);
|
|
290
|
+
} catch {
|
|
291
|
+
void handleDeletion(db, relPath);
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
(absPath) => {
|
|
295
|
+
const relPath = normalize(absPath);
|
|
296
|
+
if (!relPath) return;
|
|
297
|
+
void handleDeletion(db, relPath);
|
|
298
|
+
},
|
|
299
|
+
() => {
|
|
300
|
+
// Initial sync after chokidar is ready
|
|
301
|
+
void syncOnce().then(() => readyResolve());
|
|
302
|
+
},
|
|
303
|
+
);
|
|
304
|
+
})();
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const stop = async (): Promise<void> => {
|
|
308
|
+
await syncOnce();
|
|
309
|
+
if (closer) {
|
|
310
|
+
await closer.close();
|
|
311
|
+
closer = null;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
start,
|
|
317
|
+
stop,
|
|
318
|
+
get ready(): Promise<void> {
|
|
319
|
+
return readyPromise;
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Module: chunker — Convert HookPayload into CandidateFact[], applying
|
|
2
|
+
// filtering, trust-tier assignment, and paranoid-capture quarantining.
|
|
3
|
+
|
|
4
|
+
import { writeAuditEntry } from "@/graph/audit";
|
|
5
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
6
|
+
import type { SiaConfig } from "@/shared/config";
|
|
7
|
+
import type { CandidateFact, HookPayload } from "./types";
|
|
8
|
+
|
|
9
|
+
/** File extensions considered "recognised" source code for Tier 2 classification. */
|
|
10
|
+
const RECOGNISED_EXTENSIONS = new Set([
|
|
11
|
+
".ts",
|
|
12
|
+
".tsx",
|
|
13
|
+
".js",
|
|
14
|
+
".jsx",
|
|
15
|
+
".mjs",
|
|
16
|
+
".cjs",
|
|
17
|
+
".json",
|
|
18
|
+
".md",
|
|
19
|
+
".yaml",
|
|
20
|
+
".yml",
|
|
21
|
+
".toml",
|
|
22
|
+
".py",
|
|
23
|
+
".rs",
|
|
24
|
+
".go",
|
|
25
|
+
".java",
|
|
26
|
+
".c",
|
|
27
|
+
".cpp",
|
|
28
|
+
".h",
|
|
29
|
+
".hpp",
|
|
30
|
+
".cs",
|
|
31
|
+
".rb",
|
|
32
|
+
".php",
|
|
33
|
+
".swift",
|
|
34
|
+
".kt",
|
|
35
|
+
".sh",
|
|
36
|
+
".bash",
|
|
37
|
+
".zsh",
|
|
38
|
+
".sql",
|
|
39
|
+
".html",
|
|
40
|
+
".css",
|
|
41
|
+
".scss",
|
|
42
|
+
".less",
|
|
43
|
+
".vue",
|
|
44
|
+
".svelte",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/** Regex that matches http:// or https:// URLs. */
|
|
48
|
+
const URL_RE = /https?:\/\/[^\s)>"']+/g;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Return true when `url` points outside the project working directory.
|
|
52
|
+
* Localhost / 127.0.0.1 URLs are treated as internal.
|
|
53
|
+
*/
|
|
54
|
+
function isExternalUrl(url: string, cwd: string): boolean {
|
|
55
|
+
// file:// or paths starting with cwd are internal
|
|
56
|
+
if (url.startsWith(`file://${cwd}`)) return false;
|
|
57
|
+
try {
|
|
58
|
+
const parsed = new URL(url);
|
|
59
|
+
const host = parsed.hostname;
|
|
60
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "::1") {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// Malformed URL — treat as external to be safe
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Determine whether a filePath has a recognised source-code extension.
|
|
72
|
+
*/
|
|
73
|
+
function hasRecognisedExtension(filePath: string): boolean {
|
|
74
|
+
const dot = filePath.lastIndexOf(".");
|
|
75
|
+
if (dot === -1) return false;
|
|
76
|
+
return RECOGNISED_EXTENSIONS.has(filePath.slice(dot).toLowerCase());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Convert a single HookPayload into zero or one CandidateFact entries.
|
|
81
|
+
*
|
|
82
|
+
* Filtering rules (returns []):
|
|
83
|
+
* - empty content
|
|
84
|
+
* - content shorter than 20 characters
|
|
85
|
+
* - node_modules reads (toolName contains 'Read' AND filePath contains 'node_modules')
|
|
86
|
+
*
|
|
87
|
+
* Trust tier assignment:
|
|
88
|
+
* - Tier 1: type === 'Stop'
|
|
89
|
+
* - Tier 2: filePath with recognised source extension
|
|
90
|
+
* - Tier 3: no filePath
|
|
91
|
+
* - Tier 4: content contains external URLs (http/https not matching cwd)
|
|
92
|
+
*
|
|
93
|
+
* paranoidCapture + Tier 4: writes QUARANTINE audit entry and discards.
|
|
94
|
+
*/
|
|
95
|
+
export async function chunkPayload(
|
|
96
|
+
payload: HookPayload,
|
|
97
|
+
config: SiaConfig,
|
|
98
|
+
db?: SiaDb,
|
|
99
|
+
): Promise<CandidateFact[]> {
|
|
100
|
+
// --- Filtering -----------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
// Empty content
|
|
103
|
+
if (!payload.content || payload.content.trim().length === 0) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Content shorter than 20 chars
|
|
108
|
+
if (payload.content.length < 20) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// node_modules reads
|
|
113
|
+
if (payload.toolName?.includes("Read") && payload.filePath?.includes("node_modules")) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Trust tier assignment -----------------------------------------------
|
|
118
|
+
|
|
119
|
+
let trust_tier: 1 | 2 | 3 | 4;
|
|
120
|
+
|
|
121
|
+
if (payload.type === "Stop") {
|
|
122
|
+
trust_tier = 1;
|
|
123
|
+
} else if (payload.filePath && hasRecognisedExtension(payload.filePath)) {
|
|
124
|
+
trust_tier = 2;
|
|
125
|
+
} else if (!payload.filePath) {
|
|
126
|
+
trust_tier = 3;
|
|
127
|
+
} else {
|
|
128
|
+
// filePath present but unrecognised extension — default to Tier 2
|
|
129
|
+
trust_tier = 2;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check for external URLs — promotes to Tier 4
|
|
133
|
+
const urls = payload.content.match(URL_RE) ?? [];
|
|
134
|
+
const hasExternal = urls.some((u) => isExternalUrl(u, payload.cwd));
|
|
135
|
+
if (hasExternal) {
|
|
136
|
+
trust_tier = 4;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- Paranoid capture quarantine -----------------------------------------
|
|
140
|
+
|
|
141
|
+
if (config.paranoidCapture && trust_tier === 4) {
|
|
142
|
+
if (db) {
|
|
143
|
+
await writeAuditEntry(db, "QUARANTINE", {
|
|
144
|
+
trust_tier: 4,
|
|
145
|
+
source_episode: payload.sessionId,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// --- Build CandidateFact -------------------------------------------------
|
|
152
|
+
|
|
153
|
+
const name = payload.content.trim().slice(0, 50).trim();
|
|
154
|
+
const summary = payload.content.trim().slice(0, 100).trim();
|
|
155
|
+
|
|
156
|
+
const fact: CandidateFact = {
|
|
157
|
+
type: "Concept",
|
|
158
|
+
name,
|
|
159
|
+
content: payload.content,
|
|
160
|
+
summary,
|
|
161
|
+
tags: [],
|
|
162
|
+
file_paths: payload.filePath ? [payload.filePath] : [],
|
|
163
|
+
trust_tier,
|
|
164
|
+
confidence: trust_tier <= 2 ? 0.8 : 0.5,
|
|
165
|
+
extraction_method: "chunker",
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return [fact];
|
|
169
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Module: consolidate — Two-phase consolidation of candidate facts into the graph
|
|
2
|
+
|
|
3
|
+
import type { CandidateFact, ConsolidationResult } from "@/capture/types";
|
|
4
|
+
import { writeAuditEntry } from "@/graph/audit";
|
|
5
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
6
|
+
import type { Entity } from "@/graph/entities";
|
|
7
|
+
import { insertEntity, invalidateEntity, updateEntity } from "@/graph/entities";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compute the Jaccard similarity between two strings based on their word sets.
|
|
11
|
+
* Splits both strings on whitespace, computes |intersection| / |union|.
|
|
12
|
+
* Returns 0 if either string is empty.
|
|
13
|
+
*/
|
|
14
|
+
export function wordJaccard(a: string, b: string): number {
|
|
15
|
+
const setA = new Set(a.split(/\s+/).filter(Boolean));
|
|
16
|
+
const setB = new Set(b.split(/\s+/).filter(Boolean));
|
|
17
|
+
|
|
18
|
+
if (setA.size === 0 || setB.size === 0) return 0;
|
|
19
|
+
|
|
20
|
+
let intersectionSize = 0;
|
|
21
|
+
for (const word of setA) {
|
|
22
|
+
if (setB.has(word)) intersectionSize++;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const unionSize = setA.size + setB.size - intersectionSize;
|
|
26
|
+
if (unionSize === 0) return 0;
|
|
27
|
+
|
|
28
|
+
return intersectionSize / unionSize;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Consolidate an array of candidate facts into the graph database.
|
|
33
|
+
*
|
|
34
|
+
* For each candidate:
|
|
35
|
+
* 1. Query existing entities matching name + type that are still active.
|
|
36
|
+
* 2. If no match: ADD (insert new entity + audit).
|
|
37
|
+
* 3. If match found, compute wordJaccard on content:
|
|
38
|
+
* - >0.8 => NOOP (skip, audit NOOP)
|
|
39
|
+
* - 0.4-0.8 => UPDATE (update content, audit UPDATE)
|
|
40
|
+
* - <0.4 => INVALIDATE old + ADD new (invalidate old, insert new, audit both)
|
|
41
|
+
* 4. All writes happen inside db.transaction() for atomicity.
|
|
42
|
+
* 5. Returns aggregate counts { added, updated, invalidated, noops }.
|
|
43
|
+
*/
|
|
44
|
+
export async function consolidate(
|
|
45
|
+
db: SiaDb,
|
|
46
|
+
candidates: CandidateFact[],
|
|
47
|
+
): Promise<ConsolidationResult> {
|
|
48
|
+
const result: ConsolidationResult = {
|
|
49
|
+
added: 0,
|
|
50
|
+
updated: 0,
|
|
51
|
+
invalidated: 0,
|
|
52
|
+
noops: 0,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
await db.transaction(async (tx) => {
|
|
56
|
+
for (const candidate of candidates) {
|
|
57
|
+
// 1. Query active entities with same name and type
|
|
58
|
+
const existing = await tx.execute(
|
|
59
|
+
"SELECT * FROM graph_nodes WHERE name = ? AND type = ? AND t_valid_until IS NULL AND archived_at IS NULL",
|
|
60
|
+
[candidate.name, candidate.type],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const match = existing.rows[0] as unknown as Entity | undefined;
|
|
64
|
+
|
|
65
|
+
if (!match) {
|
|
66
|
+
// 2. No match — ADD
|
|
67
|
+
const _inserted = await insertEntity(tx, {
|
|
68
|
+
type: candidate.type,
|
|
69
|
+
name: candidate.name,
|
|
70
|
+
content: candidate.content,
|
|
71
|
+
summary: candidate.summary,
|
|
72
|
+
tags: JSON.stringify(candidate.tags),
|
|
73
|
+
file_paths: JSON.stringify(candidate.file_paths),
|
|
74
|
+
trust_tier: candidate.trust_tier,
|
|
75
|
+
confidence: candidate.confidence,
|
|
76
|
+
extraction_method: candidate.extraction_method ?? null,
|
|
77
|
+
t_valid_from: candidate.t_valid_from ?? null,
|
|
78
|
+
});
|
|
79
|
+
// insertEntity already writes an ADD audit entry
|
|
80
|
+
result.added++;
|
|
81
|
+
} else {
|
|
82
|
+
// 3. Match found — compare content similarity
|
|
83
|
+
const similarity = wordJaccard(match.content, candidate.content);
|
|
84
|
+
|
|
85
|
+
if (similarity > 0.8) {
|
|
86
|
+
// NOOP — content is sufficiently similar
|
|
87
|
+
await writeAuditEntry(tx, "NOOP", { entity_id: match.id });
|
|
88
|
+
result.noops++;
|
|
89
|
+
} else if (similarity >= 0.4) {
|
|
90
|
+
// UPDATE — content is moderately similar
|
|
91
|
+
await updateEntity(tx, match.id, {
|
|
92
|
+
content: candidate.content,
|
|
93
|
+
summary: candidate.summary,
|
|
94
|
+
tags: JSON.stringify(candidate.tags),
|
|
95
|
+
file_paths: JSON.stringify(candidate.file_paths),
|
|
96
|
+
confidence: candidate.confidence,
|
|
97
|
+
extraction_method: candidate.extraction_method ?? null,
|
|
98
|
+
});
|
|
99
|
+
// updateEntity already writes an UPDATE audit entry
|
|
100
|
+
result.updated++;
|
|
101
|
+
} else {
|
|
102
|
+
// INVALIDATE old + ADD new — content is too different
|
|
103
|
+
await invalidateEntity(tx, match.id);
|
|
104
|
+
// invalidateEntity already writes an INVALIDATE audit entry
|
|
105
|
+
|
|
106
|
+
await insertEntity(tx, {
|
|
107
|
+
type: candidate.type,
|
|
108
|
+
name: candidate.name,
|
|
109
|
+
content: candidate.content,
|
|
110
|
+
summary: candidate.summary,
|
|
111
|
+
tags: JSON.stringify(candidate.tags),
|
|
112
|
+
file_paths: JSON.stringify(candidate.file_paths),
|
|
113
|
+
trust_tier: candidate.trust_tier,
|
|
114
|
+
confidence: candidate.confidence,
|
|
115
|
+
extraction_method: candidate.extraction_method ?? null,
|
|
116
|
+
t_valid_from: candidate.t_valid_from ?? null,
|
|
117
|
+
});
|
|
118
|
+
// insertEntity already writes an ADD audit entry
|
|
119
|
+
result.invalidated++;
|
|
120
|
+
result.added++;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return result;
|
|
127
|
+
}
|