@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,101 @@
|
|
|
1
|
+
// Module: semantic-db — Migration runner and semantic database openers
|
|
2
|
+
|
|
3
|
+
import { Database } from "bun:sqlite";
|
|
4
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { BunSqliteDb } from "@/graph/db-interface";
|
|
7
|
+
import { SIA_HOME } from "@/shared/config";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Open (or create) a SQLite database at `dbPath`, apply WAL/NORMAL/FK pragmas,
|
|
11
|
+
* then run every unapplied `.sql` file found in `migrationsDir` (sorted
|
|
12
|
+
* alphabetically). Each migration is tracked in a `_migrations` table so it
|
|
13
|
+
* is never applied twice.
|
|
14
|
+
*
|
|
15
|
+
* Returns a `BunSqliteDb` wrapping the open connection.
|
|
16
|
+
*/
|
|
17
|
+
export function runMigrations(dbPath: string, migrationsDir: string): BunSqliteDb {
|
|
18
|
+
// Ensure the parent directory for the database file exists.
|
|
19
|
+
const parentDir = dirname(dbPath);
|
|
20
|
+
mkdirSync(parentDir, { recursive: true });
|
|
21
|
+
|
|
22
|
+
// Open (or create) the database.
|
|
23
|
+
const db = new Database(dbPath);
|
|
24
|
+
|
|
25
|
+
// page_size must come first — only effective on a brand-new file before any
|
|
26
|
+
// tables exist; silently a no-op on existing databases.
|
|
27
|
+
db.exec("PRAGMA page_size = 4096");
|
|
28
|
+
|
|
29
|
+
// WAL for concurrent reads, synchronous NORMAL for safety,
|
|
30
|
+
// foreign keys enforced.
|
|
31
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
32
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
33
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
34
|
+
|
|
35
|
+
// Performance hardening: memory-mapped I/O (1 GB virtual window, demand-paged
|
|
36
|
+
// by the OS), temp tables kept in RAM, and a 64 MB page cache.
|
|
37
|
+
db.exec("PRAGMA mmap_size = 1073741824");
|
|
38
|
+
db.exec("PRAGMA temp_store = MEMORY");
|
|
39
|
+
db.exec("PRAGMA cache_size = -64000");
|
|
40
|
+
|
|
41
|
+
// Ensure the bookkeeping table exists.
|
|
42
|
+
db.exec(`
|
|
43
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
44
|
+
name TEXT PRIMARY KEY,
|
|
45
|
+
applied_at INTEGER NOT NULL
|
|
46
|
+
)
|
|
47
|
+
`);
|
|
48
|
+
|
|
49
|
+
// If the migrations directory does not exist, return early — the database
|
|
50
|
+
// is usable but has no application tables yet.
|
|
51
|
+
if (!existsSync(migrationsDir)) {
|
|
52
|
+
return new BunSqliteDb(db);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Discover which migrations have already been applied.
|
|
56
|
+
const applied = new Set<string>(
|
|
57
|
+
(db.prepare("SELECT name FROM _migrations").all() as Array<{ name: string }>).map(
|
|
58
|
+
(r) => r.name,
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Read .sql files from the migrations directory, sorted alphabetically so
|
|
63
|
+
// that numbered prefixes control execution order (001-foo.sql before 002-bar.sql).
|
|
64
|
+
const files = readdirSync(migrationsDir)
|
|
65
|
+
.filter((f) => f.endsWith(".sql"))
|
|
66
|
+
.sort();
|
|
67
|
+
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
if (applied.has(file)) continue;
|
|
70
|
+
|
|
71
|
+
const sql = readFileSync(`${migrationsDir}/${file}`, "utf-8");
|
|
72
|
+
db.exec(sql);
|
|
73
|
+
db.prepare("INSERT INTO _migrations (name, applied_at) VALUES (?, ?)").run(file, Date.now());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return new BunSqliteDb(db);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Open (or create) the semantic graph database for a given repo.
|
|
81
|
+
* Resolves to `{siaHome}/repos/{repoHash}/graph.db` and applies
|
|
82
|
+
* migrations from the `migrations/semantic` directory.
|
|
83
|
+
*/
|
|
84
|
+
export function openGraphDb(repoHash: string, siaHome?: string): BunSqliteDb {
|
|
85
|
+
const home = siaHome ?? SIA_HOME;
|
|
86
|
+
const dbPath = join(home, "repos", repoHash, "graph.db");
|
|
87
|
+
const migrationsDir = resolve(import.meta.dirname, "../../migrations/semantic");
|
|
88
|
+
return runMigrations(dbPath, migrationsDir);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Open (or create) the episodic database for a given repo.
|
|
93
|
+
* Resolves to `{siaHome}/repos/{repoHash}/episodic.db` and applies
|
|
94
|
+
* migrations from the `migrations/episodic` directory.
|
|
95
|
+
*/
|
|
96
|
+
export function openEpisodicDb(repoHash: string, siaHome?: string): BunSqliteDb {
|
|
97
|
+
const home = siaHome ?? SIA_HOME;
|
|
98
|
+
const dbPath = join(home, "repos", repoHash, "episodic.db");
|
|
99
|
+
const migrationsDir = resolve(import.meta.dirname, "../../migrations/episodic");
|
|
100
|
+
return runMigrations(dbPath, migrationsDir);
|
|
101
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Module: session-resume — Session resume CRUD operations
|
|
2
|
+
|
|
3
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
4
|
+
|
|
5
|
+
/** A session resume row returned by loadSubgraph. */
|
|
6
|
+
export interface SessionResumeRow {
|
|
7
|
+
subgraph_json: string;
|
|
8
|
+
last_prompt: string | null;
|
|
9
|
+
budget_used: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Persist (upsert) a session resume record for the given session.
|
|
14
|
+
* If a row already exists for the session_id, it is updated in place.
|
|
15
|
+
*/
|
|
16
|
+
export async function saveSubgraph(
|
|
17
|
+
db: SiaDb,
|
|
18
|
+
sessionId: string,
|
|
19
|
+
subgraphJson: string,
|
|
20
|
+
lastPrompt: string | null,
|
|
21
|
+
budgetUsed: number,
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
await db.execute(
|
|
25
|
+
`INSERT INTO session_resume (session_id, subgraph_json, last_prompt, budget_used, created_at, updated_at)
|
|
26
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
27
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
28
|
+
subgraph_json = excluded.subgraph_json,
|
|
29
|
+
last_prompt = excluded.last_prompt,
|
|
30
|
+
budget_used = excluded.budget_used,
|
|
31
|
+
updated_at = excluded.updated_at`,
|
|
32
|
+
[sessionId, subgraphJson, lastPrompt, budgetUsed, now, now],
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Load the session resume record for the given session.
|
|
38
|
+
* Returns null if no record exists.
|
|
39
|
+
*/
|
|
40
|
+
export async function loadSubgraph(db: SiaDb, sessionId: string): Promise<SessionResumeRow | null> {
|
|
41
|
+
const result = await db.execute(
|
|
42
|
+
"SELECT subgraph_json, last_prompt, budget_used FROM session_resume WHERE session_id = ?",
|
|
43
|
+
[sessionId],
|
|
44
|
+
);
|
|
45
|
+
if (result.rows.length === 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return result.rows[0] as unknown as SessionResumeRow;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Delete the session resume record for the given session.
|
|
53
|
+
*/
|
|
54
|
+
export async function deleteResume(db: SiaDb, sessionId: string): Promise<void> {
|
|
55
|
+
await db.execute("DELETE FROM session_resume WHERE session_id = ?", [sessionId]);
|
|
56
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// Module: snapshots — Daily snapshot creation and rollback + branch-keyed snapshots
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { basename, join } from "node:path";
|
|
5
|
+
import { writeAuditEntry } from "@/graph/audit";
|
|
6
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
7
|
+
import { SIA_HOME } from "@/shared/config";
|
|
8
|
+
|
|
9
|
+
/** Shape of a serialized snapshot file. */
|
|
10
|
+
export interface SnapshotData {
|
|
11
|
+
version: 1;
|
|
12
|
+
timestamp: number;
|
|
13
|
+
entities: Record<string, unknown>[];
|
|
14
|
+
edges: Record<string, unknown>[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build the snapshot directory path for a given repo.
|
|
19
|
+
*/
|
|
20
|
+
function snapshotDir(repoHash: string, siaHome?: string): string {
|
|
21
|
+
const home = siaHome ?? SIA_HOME;
|
|
22
|
+
return join(home, "snapshots", repoHash);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Format a Date as YYYY-MM-DD.
|
|
27
|
+
*/
|
|
28
|
+
function formatDate(d: Date): string {
|
|
29
|
+
const yyyy = d.getFullYear();
|
|
30
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
31
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
32
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse a YYYY-MM-DD date string into a Unix ms timestamp (midnight UTC).
|
|
37
|
+
*/
|
|
38
|
+
function parseDateFromFilename(filename: string): number {
|
|
39
|
+
// Extract YYYY-MM-DD from "YYYY-MM-DD.snapshot"
|
|
40
|
+
const datePart = basename(filename, ".snapshot");
|
|
41
|
+
const [yearStr, monthStr, dayStr] = datePart.split("-");
|
|
42
|
+
const year = Number(yearStr);
|
|
43
|
+
const month = Number(monthStr) - 1; // JS months are 0-indexed
|
|
44
|
+
const day = Number(dayStr);
|
|
45
|
+
return Date.UTC(year, month, day);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a snapshot of all active entities and edges.
|
|
50
|
+
*
|
|
51
|
+
* Active entities: t_valid_until IS NULL AND archived_at IS NULL
|
|
52
|
+
* Active edges: t_valid_until IS NULL
|
|
53
|
+
*
|
|
54
|
+
* Writes JSON to `{siaHome}/snapshots/{repoHash}/YYYY-MM-DD.snapshot`.
|
|
55
|
+
* Returns the snapshot file path.
|
|
56
|
+
*/
|
|
57
|
+
export async function createSnapshot(
|
|
58
|
+
db: SiaDb,
|
|
59
|
+
repoHash: string,
|
|
60
|
+
siaHome?: string,
|
|
61
|
+
): Promise<string> {
|
|
62
|
+
const dir = snapshotDir(repoHash, siaHome);
|
|
63
|
+
mkdirSync(dir, { recursive: true });
|
|
64
|
+
|
|
65
|
+
// Query active entities
|
|
66
|
+
const entitiesResult = await db.execute(
|
|
67
|
+
"SELECT * FROM graph_nodes WHERE t_valid_until IS NULL AND archived_at IS NULL",
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Query active edges
|
|
71
|
+
const edgesResult = await db.execute("SELECT * FROM graph_edges WHERE t_valid_until IS NULL");
|
|
72
|
+
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
const data: SnapshotData = {
|
|
75
|
+
version: 1,
|
|
76
|
+
timestamp: now,
|
|
77
|
+
entities: entitiesResult.rows,
|
|
78
|
+
edges: edgesResult.rows,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const filename = `${formatDate(new Date(now))}.snapshot`;
|
|
82
|
+
const filepath = join(dir, filename);
|
|
83
|
+
|
|
84
|
+
writeFileSync(filepath, JSON.stringify(data, null, 2), "utf-8");
|
|
85
|
+
|
|
86
|
+
await writeAuditEntry(db, "ADD", { snapshot_id: filepath });
|
|
87
|
+
|
|
88
|
+
return filepath;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* List all snapshot files for a repo, sorted by date (oldest first).
|
|
93
|
+
*/
|
|
94
|
+
export function listSnapshots(repoHash: string, siaHome?: string): string[] {
|
|
95
|
+
const dir = snapshotDir(repoHash, siaHome);
|
|
96
|
+
|
|
97
|
+
if (!existsSync(dir)) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const files = readdirSync(dir)
|
|
102
|
+
.filter((f) => f.endsWith(".snapshot"))
|
|
103
|
+
.sort();
|
|
104
|
+
|
|
105
|
+
return files.map((f) => join(dir, f));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Find the snapshot file closest to and before a target timestamp.
|
|
110
|
+
*
|
|
111
|
+
* Returns the full path, or null if no snapshot exists before the target.
|
|
112
|
+
*/
|
|
113
|
+
export function findNearestSnapshot(
|
|
114
|
+
repoHash: string,
|
|
115
|
+
targetTimestamp: number,
|
|
116
|
+
siaHome?: string,
|
|
117
|
+
): string | null {
|
|
118
|
+
const snapshots = listSnapshots(repoHash, siaHome);
|
|
119
|
+
|
|
120
|
+
let best: string | null = null;
|
|
121
|
+
let bestTs = -1;
|
|
122
|
+
|
|
123
|
+
for (const snap of snapshots) {
|
|
124
|
+
const ts = parseDateFromFilename(basename(snap));
|
|
125
|
+
if (ts <= targetTimestamp && ts > bestTs) {
|
|
126
|
+
best = snap;
|
|
127
|
+
bestTs = ts;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return best;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Restore a snapshot: create a pre-rollback snapshot first (atomicity),
|
|
136
|
+
* then delete all entities and edges, re-insert from the snapshot JSON.
|
|
137
|
+
*/
|
|
138
|
+
export async function restoreSnapshot(
|
|
139
|
+
db: SiaDb,
|
|
140
|
+
snapshotPath: string,
|
|
141
|
+
repoHash: string,
|
|
142
|
+
siaHome?: string,
|
|
143
|
+
): Promise<void> {
|
|
144
|
+
// Step 1: Read the snapshot file BEFORE creating the pre-rollback snapshot
|
|
145
|
+
// to avoid the pre-rollback overwriting the same date-based filename.
|
|
146
|
+
const raw = readFileSync(snapshotPath, "utf-8");
|
|
147
|
+
const data = JSON.parse(raw) as SnapshotData;
|
|
148
|
+
|
|
149
|
+
// Step 2: Create a pre-rollback snapshot for safety
|
|
150
|
+
await createSnapshot(db, repoHash, siaHome);
|
|
151
|
+
|
|
152
|
+
// Step 3: Delete all existing entities and edges, then re-insert from snapshot.
|
|
153
|
+
await db.transaction(async (tx) => {
|
|
154
|
+
// Delete edges first (FK constraint: edges reference entities)
|
|
155
|
+
await tx.execute("DELETE FROM graph_edges");
|
|
156
|
+
// Delete entities
|
|
157
|
+
await tx.execute("DELETE FROM graph_nodes");
|
|
158
|
+
|
|
159
|
+
// Re-insert entities
|
|
160
|
+
for (const entity of data.entities) {
|
|
161
|
+
const columns = Object.keys(entity);
|
|
162
|
+
const safeColumns = columns.map((c) => `"${c.replace(/"/g, '""')}"`);
|
|
163
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
164
|
+
const values = columns.map((col) => entity[col] ?? null);
|
|
165
|
+
const sql = `INSERT INTO graph_nodes (${safeColumns.join(", ")}) VALUES (${placeholders})`;
|
|
166
|
+
await tx.execute(sql, values);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Re-insert edges
|
|
170
|
+
for (const edge of data.edges) {
|
|
171
|
+
const columns = Object.keys(edge);
|
|
172
|
+
const safeColumns = columns.map((c) => `"${c.replace(/"/g, '""')}"`);
|
|
173
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
174
|
+
const values = columns.map((col) => edge[col] ?? null);
|
|
175
|
+
const sql = `INSERT INTO graph_edges (${safeColumns.join(", ")}) VALUES (${placeholders})`;
|
|
176
|
+
await tx.execute(sql, values);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Step 4: Write audit entry for the restore
|
|
181
|
+
await writeAuditEntry(db, "UPDATE", { snapshot_id: snapshotPath });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Branch-keyed snapshots (stored in SQLite, not on disk)
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
/** Shape of a branch snapshot row. */
|
|
189
|
+
export interface BranchSnapshot {
|
|
190
|
+
id: number;
|
|
191
|
+
branch_name: string;
|
|
192
|
+
commit_hash: string;
|
|
193
|
+
node_count: number;
|
|
194
|
+
edge_count: number;
|
|
195
|
+
snapshot_data: string;
|
|
196
|
+
created_at: number;
|
|
197
|
+
updated_at: number;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Create (or update) a branch snapshot.
|
|
202
|
+
* Serializes all active nodes and edges into JSON and UPSERTs into
|
|
203
|
+
* the branch_snapshots table. Each branch gets exactly one snapshot row.
|
|
204
|
+
*/
|
|
205
|
+
export async function createBranchSnapshot(
|
|
206
|
+
db: SiaDb,
|
|
207
|
+
branchName: string,
|
|
208
|
+
commitHash: string,
|
|
209
|
+
): Promise<void> {
|
|
210
|
+
const { rows: nodes } = await db.execute(
|
|
211
|
+
"SELECT * FROM graph_nodes WHERE t_valid_until IS NULL AND archived_at IS NULL",
|
|
212
|
+
);
|
|
213
|
+
const { rows: edges } = await db.execute("SELECT * FROM graph_edges WHERE t_valid_until IS NULL");
|
|
214
|
+
|
|
215
|
+
const snapshotData = JSON.stringify({ nodes, edges });
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
|
|
218
|
+
await db.execute(
|
|
219
|
+
`INSERT INTO branch_snapshots (branch_name, commit_hash, node_count, edge_count, snapshot_data, created_at, updated_at)
|
|
220
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
221
|
+
ON CONFLICT(branch_name) DO UPDATE SET
|
|
222
|
+
commit_hash = excluded.commit_hash,
|
|
223
|
+
node_count = excluded.node_count,
|
|
224
|
+
edge_count = excluded.edge_count,
|
|
225
|
+
snapshot_data = excluded.snapshot_data,
|
|
226
|
+
updated_at = excluded.updated_at`,
|
|
227
|
+
[branchName, commitHash, nodes.length, edges.length, snapshotData, now, now],
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
await writeAuditEntry(db, "ADD", { snapshot_id: `branch:${branchName}:${commitHash}` });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Restore graph state from a branch snapshot.
|
|
235
|
+
* Deletes all current nodes/edges, then re-inserts from the snapshot.
|
|
236
|
+
* Returns true if a snapshot was found and restored, false otherwise.
|
|
237
|
+
*/
|
|
238
|
+
export async function restoreBranchSnapshot(db: SiaDb, branchName: string): Promise<boolean> {
|
|
239
|
+
const { rows } = await db.execute(
|
|
240
|
+
"SELECT snapshot_data FROM branch_snapshots WHERE branch_name = ?",
|
|
241
|
+
[branchName],
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
if (rows.length === 0) return false;
|
|
245
|
+
|
|
246
|
+
let data: { nodes: Record<string, unknown>[]; edges: Record<string, unknown>[] };
|
|
247
|
+
try {
|
|
248
|
+
const parsed = JSON.parse(rows[0].snapshot_data as string);
|
|
249
|
+
if (!parsed || !Array.isArray(parsed.nodes) || !Array.isArray(parsed.edges)) {
|
|
250
|
+
throw new Error("Invalid snapshot structure: missing nodes or edges arrays");
|
|
251
|
+
}
|
|
252
|
+
data = parsed;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`Failed to parse branch snapshot for "${branchName}": ${err instanceof Error ? err.message : err}`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
await db.transaction(async (tx) => {
|
|
260
|
+
await tx.execute("DELETE FROM graph_edges");
|
|
261
|
+
await tx.execute("DELETE FROM graph_nodes");
|
|
262
|
+
|
|
263
|
+
for (const node of data.nodes) {
|
|
264
|
+
const columns = Object.keys(node);
|
|
265
|
+
const safeColumns = columns.map((c) => `"${c.replace(/"/g, '""')}"`);
|
|
266
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
267
|
+
const values = columns.map((col) => node[col] ?? null);
|
|
268
|
+
await tx.execute(
|
|
269
|
+
`INSERT INTO graph_nodes (${safeColumns.join(", ")}) VALUES (${placeholders})`,
|
|
270
|
+
values,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const edge of data.edges) {
|
|
275
|
+
const columns = Object.keys(edge);
|
|
276
|
+
const safeColumns = columns.map((c) => `"${c.replace(/"/g, '""')}"`);
|
|
277
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
278
|
+
const values = columns.map((col) => edge[col] ?? null);
|
|
279
|
+
await tx.execute(
|
|
280
|
+
`INSERT INTO graph_edges (${safeColumns.join(", ")}) VALUES (${placeholders})`,
|
|
281
|
+
values,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
await writeAuditEntry(db, "UPDATE", { snapshot_id: `branch-restore:${branchName}` });
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* List all branch snapshots, ordered by updated_at descending.
|
|
292
|
+
*/
|
|
293
|
+
export async function listBranchSnapshots(db: SiaDb): Promise<BranchSnapshot[]> {
|
|
294
|
+
const { rows } = await db.execute(
|
|
295
|
+
"SELECT id, branch_name, commit_hash, node_count, edge_count, snapshot_data, created_at, updated_at FROM branch_snapshots ORDER BY updated_at DESC",
|
|
296
|
+
);
|
|
297
|
+
return rows as unknown as BranchSnapshot[];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Prune snapshots for specific branches (e.g., deleted branches).
|
|
302
|
+
* Returns the number of snapshots deleted.
|
|
303
|
+
*/
|
|
304
|
+
export async function pruneBranchSnapshots(db: SiaDb, branchNames: string[]): Promise<number> {
|
|
305
|
+
if (branchNames.length === 0) return 0;
|
|
306
|
+
|
|
307
|
+
const { rows: before } = await db.execute("SELECT COUNT(*) as cnt FROM branch_snapshots");
|
|
308
|
+
|
|
309
|
+
const placeholders = branchNames.map(() => "?").join(", ");
|
|
310
|
+
await db.execute(
|
|
311
|
+
`DELETE FROM branch_snapshots WHERE branch_name IN (${placeholders})`,
|
|
312
|
+
branchNames,
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const { rows: after } = await db.execute("SELECT COUNT(*) as cnt FROM branch_snapshots");
|
|
316
|
+
|
|
317
|
+
const deleted = Number(before[0].cnt) - Number(after[0].cnt);
|
|
318
|
+
if (deleted > 0) {
|
|
319
|
+
await writeAuditEntry(db, "ARCHIVE", { snapshot_id: `branch-prune:${branchNames.join(",")}` });
|
|
320
|
+
}
|
|
321
|
+
return deleted;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Garbage-collect branch snapshots older than ttlDays.
|
|
326
|
+
* Returns the number of snapshots deleted.
|
|
327
|
+
*/
|
|
328
|
+
export async function gcBranchSnapshots(db: SiaDb, ttlDays: number): Promise<number> {
|
|
329
|
+
const cutoff = Date.now() - ttlDays * 24 * 60 * 60 * 1000;
|
|
330
|
+
|
|
331
|
+
const { rows: before } = await db.execute("SELECT COUNT(*) as cnt FROM branch_snapshots");
|
|
332
|
+
|
|
333
|
+
await db.execute("DELETE FROM branch_snapshots WHERE updated_at < ?", [cutoff]);
|
|
334
|
+
|
|
335
|
+
const { rows: after } = await db.execute("SELECT COUNT(*) as cnt FROM branch_snapshots");
|
|
336
|
+
|
|
337
|
+
const deleted = Number(before[0].cnt) - Number(after[0].cnt);
|
|
338
|
+
if (deleted > 0) {
|
|
339
|
+
await writeAuditEntry(db, "ARCHIVE", { snapshot_id: `branch-gc:${deleted}:ttl=${ttlDays}d` });
|
|
340
|
+
}
|
|
341
|
+
return deleted;
|
|
342
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Module: staging — CRUD for the memory_staging table (Tier 4 security staging area)
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { writeAuditEntry } from "@/graph/audit";
|
|
5
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
6
|
+
|
|
7
|
+
/** Row shape matching all columns of the `memory_staging` table. */
|
|
8
|
+
export interface StagedFact {
|
|
9
|
+
id: string;
|
|
10
|
+
source_episode: string | null;
|
|
11
|
+
proposed_type: string;
|
|
12
|
+
proposed_name: string;
|
|
13
|
+
proposed_content: string;
|
|
14
|
+
proposed_tags: string;
|
|
15
|
+
proposed_file_paths: string;
|
|
16
|
+
trust_tier: number;
|
|
17
|
+
raw_confidence: number;
|
|
18
|
+
validation_status: string;
|
|
19
|
+
rejection_reason: string | null;
|
|
20
|
+
created_at: number;
|
|
21
|
+
expires_at: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Fields the caller provides when inserting a staged fact. */
|
|
25
|
+
export interface InsertStagedFactInput {
|
|
26
|
+
source_episode?: string;
|
|
27
|
+
proposed_type: string;
|
|
28
|
+
proposed_name: string;
|
|
29
|
+
proposed_content: string;
|
|
30
|
+
proposed_tags?: string;
|
|
31
|
+
proposed_file_paths?: string;
|
|
32
|
+
trust_tier?: number;
|
|
33
|
+
raw_confidence: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 7-day TTL in milliseconds. */
|
|
37
|
+
const SEVEN_DAYS_MS = 7 * 86_400_000;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Insert a new staged fact into `memory_staging`.
|
|
41
|
+
*
|
|
42
|
+
* Generates a UUID, sets `created_at = Date.now()`, computes
|
|
43
|
+
* `expires_at = created_at + 7 days`, defaults `validation_status = 'pending'`.
|
|
44
|
+
* Writes a STAGE audit entry. Returns the generated id.
|
|
45
|
+
*/
|
|
46
|
+
export async function insertStagedFact(db: SiaDb, input: InsertStagedFactInput): Promise<string> {
|
|
47
|
+
const id = randomUUID();
|
|
48
|
+
const createdAt = Date.now();
|
|
49
|
+
const expiresAt = createdAt + SEVEN_DAYS_MS;
|
|
50
|
+
|
|
51
|
+
await db.execute(
|
|
52
|
+
`INSERT INTO memory_staging (
|
|
53
|
+
id, source_episode, proposed_type, proposed_name, proposed_content,
|
|
54
|
+
proposed_tags, proposed_file_paths, trust_tier, raw_confidence,
|
|
55
|
+
validation_status, created_at, expires_at
|
|
56
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
57
|
+
[
|
|
58
|
+
id,
|
|
59
|
+
input.source_episode ?? null,
|
|
60
|
+
input.proposed_type,
|
|
61
|
+
input.proposed_name,
|
|
62
|
+
input.proposed_content,
|
|
63
|
+
input.proposed_tags ?? "[]",
|
|
64
|
+
input.proposed_file_paths ?? "[]",
|
|
65
|
+
input.trust_tier ?? 4,
|
|
66
|
+
input.raw_confidence,
|
|
67
|
+
"pending",
|
|
68
|
+
createdAt,
|
|
69
|
+
expiresAt,
|
|
70
|
+
],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
await writeAuditEntry(db, "STAGE", {
|
|
74
|
+
entity_id: id,
|
|
75
|
+
trust_tier: input.trust_tier ?? 4,
|
|
76
|
+
source_episode: input.source_episode,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return id;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Retrieve pending staged facts that have not yet expired.
|
|
84
|
+
*
|
|
85
|
+
* Returns rows WHERE `validation_status = 'pending' AND expires_at > now`,
|
|
86
|
+
* ordered by `created_at ASC`, limited to `limit` rows (default 100).
|
|
87
|
+
*/
|
|
88
|
+
export async function getPendingStagedFacts(db: SiaDb, limit = 100): Promise<StagedFact[]> {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const result = await db.execute(
|
|
91
|
+
`SELECT * FROM memory_staging
|
|
92
|
+
WHERE validation_status = 'pending' AND expires_at > ?
|
|
93
|
+
ORDER BY created_at ASC
|
|
94
|
+
LIMIT ?`,
|
|
95
|
+
[now, limit],
|
|
96
|
+
);
|
|
97
|
+
return result.rows as unknown as StagedFact[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Update the validation status of a staged fact.
|
|
102
|
+
*
|
|
103
|
+
* Optionally sets `rejection_reason`. Writes a QUARANTINE audit entry
|
|
104
|
+
* when the status is 'rejected' or 'quarantined'.
|
|
105
|
+
*/
|
|
106
|
+
export async function updateStagingStatus(
|
|
107
|
+
db: SiaDb,
|
|
108
|
+
id: string,
|
|
109
|
+
status: string,
|
|
110
|
+
rejectionReason?: string,
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
await db.execute(
|
|
113
|
+
`UPDATE memory_staging
|
|
114
|
+
SET validation_status = ?, rejection_reason = ?
|
|
115
|
+
WHERE id = ?`,
|
|
116
|
+
[status, rejectionReason ?? null, id],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (status === "rejected" || status === "quarantined") {
|
|
120
|
+
await writeAuditEntry(db, "QUARANTINE", { entity_id: id });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Expire stale staged facts whose `expires_at` has passed while still pending.
|
|
126
|
+
*
|
|
127
|
+
* Sets `validation_status = 'expired'` for all matching rows.
|
|
128
|
+
* Returns the number of rows affected.
|
|
129
|
+
*/
|
|
130
|
+
export async function expireStaleStagedFacts(db: SiaDb): Promise<number> {
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
|
|
133
|
+
// Get count of rows that will be affected before updating
|
|
134
|
+
const countResult = await db.execute(
|
|
135
|
+
`SELECT COUNT(*) as cnt FROM memory_staging
|
|
136
|
+
WHERE expires_at <= ? AND validation_status = 'pending'`,
|
|
137
|
+
[now],
|
|
138
|
+
);
|
|
139
|
+
const count = (countResult.rows[0]?.cnt as number) ?? 0;
|
|
140
|
+
|
|
141
|
+
if (count > 0) {
|
|
142
|
+
await db.execute(
|
|
143
|
+
`UPDATE memory_staging
|
|
144
|
+
SET validation_status = 'expired'
|
|
145
|
+
WHERE expires_at <= ? AND validation_status = 'pending'`,
|
|
146
|
+
[now],
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return count;
|
|
151
|
+
}
|