@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,87 @@
|
|
|
1
|
+
// Module: learn-progress — Cross-crash recovery for sia-learn.
|
|
2
|
+
//
|
|
3
|
+
// Writes a .sia-learn-progress.json file to the project root tracking
|
|
4
|
+
// which phases completed and how many files were indexed. On re-run
|
|
5
|
+
// after a crash, the orchestrator reads this file and resumes.
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
const PROGRESS_FILE = ".sia-learn-progress.json";
|
|
11
|
+
|
|
12
|
+
export interface LearnProgress {
|
|
13
|
+
started_at: number;
|
|
14
|
+
repo_hash: string;
|
|
15
|
+
branch: string;
|
|
16
|
+
phases_completed: number[];
|
|
17
|
+
files_indexed: number;
|
|
18
|
+
total_files: number;
|
|
19
|
+
last_checkpoint_at: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read the progress file from a directory. Returns null if not found or invalid.
|
|
24
|
+
*/
|
|
25
|
+
export function readProgress(dir: string): LearnProgress | null {
|
|
26
|
+
const path = join(dir, PROGRESS_FILE);
|
|
27
|
+
if (!existsSync(path)) return null;
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(readFileSync(path, "utf-8")) as LearnProgress;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Write (or update) the progress file.
|
|
37
|
+
*/
|
|
38
|
+
export function writeProgress(dir: string, progress: LearnProgress): void {
|
|
39
|
+
const path = join(dir, PROGRESS_FILE);
|
|
40
|
+
writeFileSync(path, JSON.stringify(progress, null, 2), "utf-8");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Delete the progress file (called on successful completion).
|
|
45
|
+
*/
|
|
46
|
+
export function deleteProgress(dir: string): void {
|
|
47
|
+
const path = join(dir, PROGRESS_FILE);
|
|
48
|
+
try {
|
|
49
|
+
unlinkSync(path);
|
|
50
|
+
} catch {
|
|
51
|
+
// File doesn't exist — that's fine
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Run a function with retry and exponential backoff.
|
|
57
|
+
* Returns the result on success, or null if all retries are exhausted.
|
|
58
|
+
*
|
|
59
|
+
* @param phaseName - Name for logging
|
|
60
|
+
* @param fn - The async function to run
|
|
61
|
+
* @param maxRetries - Maximum attempts (default 3)
|
|
62
|
+
* @param baseDelayMs - Base delay for backoff (default 1000ms)
|
|
63
|
+
*/
|
|
64
|
+
export async function runWithRetry<T>(
|
|
65
|
+
phaseName: string,
|
|
66
|
+
fn: () => Promise<T>,
|
|
67
|
+
maxRetries: number = 3,
|
|
68
|
+
baseDelayMs: number = 1000,
|
|
69
|
+
): Promise<T | null> {
|
|
70
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
71
|
+
try {
|
|
72
|
+
return await fn();
|
|
73
|
+
} catch (err) {
|
|
74
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
75
|
+
process.stderr.write(
|
|
76
|
+
`[sia-learn] ${phaseName} failed (attempt ${attempt}/${maxRetries}): ${msg}\n`,
|
|
77
|
+
);
|
|
78
|
+
if (attempt === maxRetries) {
|
|
79
|
+
process.stderr.write(`[sia-learn] ${phaseName} failed permanently — skipping\n`);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
// Exponential backoff: base * 4^(attempt-1)
|
|
83
|
+
await new Promise((r) => setTimeout(r, baseDelayMs * 4 ** (attempt - 1)));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// Module: detection-bridge — Community detection with native/JS fallback
|
|
2
|
+
//
|
|
3
|
+
// Routes to the Rust Leiden implementation when @sia/native is available,
|
|
4
|
+
// otherwise runs a simplified JavaScript Louvain algorithm.
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Public types
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export interface CommunityLevelResult {
|
|
11
|
+
/** community label for each node (index = node index) */
|
|
12
|
+
membership: number[];
|
|
13
|
+
/** modularity Q score for this level */
|
|
14
|
+
modularity: number;
|
|
15
|
+
/** number of distinct communities at this level */
|
|
16
|
+
n_communities: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CommunityResult {
|
|
20
|
+
levels: CommunityLevelResult[];
|
|
21
|
+
backend: "rust-leiden" | "js-louvain";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Native module probe (same pattern as bridge.ts)
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
interface NativeLeidenModule {
|
|
29
|
+
detectCommunities(
|
|
30
|
+
edges: Array<[number, number, number]>,
|
|
31
|
+
nodeCount: number,
|
|
32
|
+
resolutions?: number[],
|
|
33
|
+
): CommunityResult;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function loadNativeLeiden(): NativeLeidenModule | null {
|
|
37
|
+
try {
|
|
38
|
+
return require("@sia/native") as NativeLeidenModule;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Simplified Louvain — pure TypeScript fallback
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compute modularity Q for a given partition.
|
|
50
|
+
*
|
|
51
|
+
* Q = (1/2m) * sum_{ij}[ A_ij - k_i * k_j / (2m) ] * delta(c_i, c_j)
|
|
52
|
+
*
|
|
53
|
+
* where m = total edge weight, k_i = weighted degree of node i,
|
|
54
|
+
* and A_ij = weight of edge (i,j).
|
|
55
|
+
*/
|
|
56
|
+
function computeModularity(
|
|
57
|
+
adjacency: Map<number, Map<number, number>>,
|
|
58
|
+
membership: number[],
|
|
59
|
+
totalWeight: number,
|
|
60
|
+
degrees: Float64Array,
|
|
61
|
+
): number {
|
|
62
|
+
if (totalWeight === 0) return 0;
|
|
63
|
+
const m2 = 2 * totalWeight;
|
|
64
|
+
let q = 0;
|
|
65
|
+
|
|
66
|
+
for (const [i, nbrs] of adjacency) {
|
|
67
|
+
for (const [j, w] of nbrs) {
|
|
68
|
+
if (membership[i] === membership[j]) {
|
|
69
|
+
q += w - (degrees[i] * degrees[j]) / m2;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return q / m2;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Compute the modularity gain from moving node `v` into community `C`.
|
|
79
|
+
*
|
|
80
|
+
* Standard Louvain formula:
|
|
81
|
+
* ΔQ = k_v_in / m - resolution * k_v * sigma_tot / (2m^2)
|
|
82
|
+
* = 2*k_v_in / m2 - 2*resolution * k_v * sigma_tot / m2^2
|
|
83
|
+
*
|
|
84
|
+
* where m2 = 2m, k_v_in = edge weight sum between v and C,
|
|
85
|
+
* sigma_tot = sum of degrees in C.
|
|
86
|
+
*/
|
|
87
|
+
function modularityGain(
|
|
88
|
+
kVIn: number,
|
|
89
|
+
kV: number,
|
|
90
|
+
sigmaTot: number,
|
|
91
|
+
m2: number,
|
|
92
|
+
resolution: number,
|
|
93
|
+
): number {
|
|
94
|
+
return (2 * kVIn) / m2 - (2 * resolution * kV * sigmaTot) / (m2 * m2);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildAdjacency(
|
|
98
|
+
edges: Array<[number, number, number]>,
|
|
99
|
+
nodeCount: number,
|
|
100
|
+
): {
|
|
101
|
+
adj: Map<number, Map<number, number>>;
|
|
102
|
+
degrees: Float64Array;
|
|
103
|
+
totalWeight: number;
|
|
104
|
+
} {
|
|
105
|
+
const adj = new Map<number, Map<number, number>>();
|
|
106
|
+
for (let i = 0; i < nodeCount; i++) adj.set(i, new Map());
|
|
107
|
+
|
|
108
|
+
let totalWeight = 0;
|
|
109
|
+
for (const [from, to, weight] of edges) {
|
|
110
|
+
if (from < 0 || to < 0 || from >= nodeCount || to >= nodeCount) continue;
|
|
111
|
+
const w = weight ?? 1;
|
|
112
|
+
|
|
113
|
+
// Undirected: add both directions
|
|
114
|
+
const fwd = adj.get(from);
|
|
115
|
+
if (fwd) fwd.set(to, (fwd.get(to) ?? 0) + w);
|
|
116
|
+
|
|
117
|
+
if (from !== to) {
|
|
118
|
+
const bwd = adj.get(to);
|
|
119
|
+
if (bwd) bwd.set(from, (bwd.get(from) ?? 0) + w);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
totalWeight += w;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const degrees = new Float64Array(nodeCount);
|
|
126
|
+
for (const [node, nbrs] of adj) {
|
|
127
|
+
let deg = 0;
|
|
128
|
+
for (const w of nbrs.values()) deg += w;
|
|
129
|
+
degrees[node] = deg;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { adj, degrees, totalWeight };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Run one pass of the Louvain algorithm at a given resolution.
|
|
137
|
+
* Returns the membership array.
|
|
138
|
+
*/
|
|
139
|
+
function louvainPass(
|
|
140
|
+
adj: Map<number, Map<number, number>>,
|
|
141
|
+
degrees: Float64Array,
|
|
142
|
+
totalWeight: number,
|
|
143
|
+
initialMembership: number[],
|
|
144
|
+
resolution: number,
|
|
145
|
+
maxIterations = 100,
|
|
146
|
+
): number[] {
|
|
147
|
+
const n = degrees.length;
|
|
148
|
+
if (n === 0) return [];
|
|
149
|
+
|
|
150
|
+
const m2 = 2 * totalWeight;
|
|
151
|
+
const membership = [...initialMembership];
|
|
152
|
+
|
|
153
|
+
// sigma_tot[c] = sum of degrees in community c
|
|
154
|
+
const sigmaTot = new Map<number, number>();
|
|
155
|
+
for (let i = 0; i < n; i++) {
|
|
156
|
+
const c = membership[i];
|
|
157
|
+
sigmaTot.set(c, (sigmaTot.get(c) ?? 0) + degrees[i]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let improved = true;
|
|
161
|
+
let iterations = 0;
|
|
162
|
+
|
|
163
|
+
while (improved && iterations < maxIterations) {
|
|
164
|
+
improved = false;
|
|
165
|
+
iterations++;
|
|
166
|
+
|
|
167
|
+
for (let v = 0; v < n; v++) {
|
|
168
|
+
const currentComm = membership[v];
|
|
169
|
+
const kV = degrees[v];
|
|
170
|
+
|
|
171
|
+
// Weight from v to each neighboring community
|
|
172
|
+
const commWeights = new Map<number, number>();
|
|
173
|
+
for (const [nbr, w] of adj.get(v) ?? new Map()) {
|
|
174
|
+
const c = membership[nbr];
|
|
175
|
+
commWeights.set(c, (commWeights.get(c) ?? 0) + w);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Gain from removing v from current community
|
|
179
|
+
const kVInCurrent = commWeights.get(currentComm) ?? 0;
|
|
180
|
+
const sigmaCurrent = sigmaTot.get(currentComm) ?? 0;
|
|
181
|
+
|
|
182
|
+
// Try moving v to each neighboring community
|
|
183
|
+
let bestGain = 0;
|
|
184
|
+
let bestComm = currentComm;
|
|
185
|
+
|
|
186
|
+
for (const [targetComm, kVIn] of commWeights) {
|
|
187
|
+
if (targetComm === currentComm) continue;
|
|
188
|
+
const sigmaTarget = sigmaTot.get(targetComm) ?? 0;
|
|
189
|
+
|
|
190
|
+
// Gain from adding v to targetComm minus cost of removing from currentComm
|
|
191
|
+
const gain =
|
|
192
|
+
modularityGain(kVIn, kV, sigmaTarget, m2, resolution) -
|
|
193
|
+
modularityGain(kVInCurrent, kV, sigmaCurrent - kV, m2, resolution);
|
|
194
|
+
|
|
195
|
+
if (gain > bestGain) {
|
|
196
|
+
bestGain = gain;
|
|
197
|
+
bestComm = targetComm;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (bestComm !== currentComm) {
|
|
202
|
+
// Move v to bestComm
|
|
203
|
+
sigmaTot.set(currentComm, (sigmaTot.get(currentComm) ?? 0) - kV);
|
|
204
|
+
sigmaTot.set(bestComm, (sigmaTot.get(bestComm) ?? 0) + kV);
|
|
205
|
+
membership[v] = bestComm;
|
|
206
|
+
improved = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Compact community IDs to [0, k)
|
|
212
|
+
const remap = new Map<number, number>();
|
|
213
|
+
let nextId = 0;
|
|
214
|
+
for (let i = 0; i < n; i++) {
|
|
215
|
+
if (!remap.has(membership[i])) {
|
|
216
|
+
remap.set(membership[i], nextId++);
|
|
217
|
+
}
|
|
218
|
+
membership[i] = remap.get(membership[i]) ?? 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return membership;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Post-process: split disconnected nodes within a community into separate
|
|
226
|
+
* communities using BFS. The largest (first-found) component keeps the
|
|
227
|
+
* original community ID; additional components get new IDs.
|
|
228
|
+
*/
|
|
229
|
+
function splitDisconnected(
|
|
230
|
+
adj: Map<number, Map<number, number>>,
|
|
231
|
+
membership: number[],
|
|
232
|
+
n: number,
|
|
233
|
+
): number[] {
|
|
234
|
+
const result = [...membership];
|
|
235
|
+
let nextComm = Math.max(0, ...membership) + 1;
|
|
236
|
+
|
|
237
|
+
// Group nodes by community
|
|
238
|
+
const communityNodes = new Map<number, number[]>();
|
|
239
|
+
for (let i = 0; i < n; i++) {
|
|
240
|
+
const c = membership[i];
|
|
241
|
+
if (!communityNodes.has(c)) communityNodes.set(c, []);
|
|
242
|
+
communityNodes.get(c)?.push(i);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
for (const [, nodes] of communityNodes) {
|
|
246
|
+
if (nodes.length <= 1) continue;
|
|
247
|
+
const nodeSet = new Set(nodes);
|
|
248
|
+
const visited = new Set<number>();
|
|
249
|
+
let isFirstComponent = true;
|
|
250
|
+
|
|
251
|
+
for (const start of nodes) {
|
|
252
|
+
if (visited.has(start)) continue;
|
|
253
|
+
|
|
254
|
+
// BFS within community
|
|
255
|
+
const queue = [start];
|
|
256
|
+
const component: number[] = [];
|
|
257
|
+
visited.add(start);
|
|
258
|
+
while (queue.length > 0) {
|
|
259
|
+
const cur = queue.shift();
|
|
260
|
+
if (cur === undefined) break;
|
|
261
|
+
component.push(cur);
|
|
262
|
+
for (const nbr of adj.get(cur)?.keys() ?? []) {
|
|
263
|
+
if (nodeSet.has(nbr) && !visited.has(nbr)) {
|
|
264
|
+
visited.add(nbr);
|
|
265
|
+
queue.push(nbr);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (isFirstComponent) {
|
|
271
|
+
// First component keeps the original community ID
|
|
272
|
+
isFirstComponent = false;
|
|
273
|
+
} else {
|
|
274
|
+
// Subsequent disconnected components get new community IDs
|
|
275
|
+
for (const node of component) {
|
|
276
|
+
result[node] = nextComm;
|
|
277
|
+
}
|
|
278
|
+
nextComm++;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function jsLouvain(
|
|
287
|
+
edges: Array<[number, number, number]>,
|
|
288
|
+
nodeCount: number,
|
|
289
|
+
resolutions: number[],
|
|
290
|
+
): CommunityResult {
|
|
291
|
+
if (nodeCount === 0) {
|
|
292
|
+
return {
|
|
293
|
+
levels: [],
|
|
294
|
+
backend: "js-louvain",
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const { adj, degrees, totalWeight } = buildAdjacency(edges, nodeCount);
|
|
299
|
+
const levels: CommunityLevelResult[] = [];
|
|
300
|
+
|
|
301
|
+
for (const resolution of resolutions) {
|
|
302
|
+
// Each node starts in its own community
|
|
303
|
+
const initial = Array.from({ length: nodeCount }, (_, i) => i);
|
|
304
|
+
let membership = louvainPass(adj, degrees, totalWeight, initial, resolution);
|
|
305
|
+
membership = splitDisconnected(adj, membership, nodeCount);
|
|
306
|
+
|
|
307
|
+
const n_communities = new Set(membership).size;
|
|
308
|
+
const modularity = computeModularity(adj, membership, totalWeight, degrees);
|
|
309
|
+
|
|
310
|
+
levels.push({ membership, modularity, n_communities });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return { levels, backend: "js-louvain" };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Public API
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Detect communities in a weighted graph.
|
|
322
|
+
*
|
|
323
|
+
* @param edges Array of [from, to, weight] triples (node indices).
|
|
324
|
+
* @param nodeCount Total number of nodes.
|
|
325
|
+
* @param resolutions Resolution parameters per level. Defaults to [1.0].
|
|
326
|
+
* Higher resolution → more, smaller communities.
|
|
327
|
+
*/
|
|
328
|
+
export function detectCommunities(
|
|
329
|
+
edges: Array<[number, number, number]>,
|
|
330
|
+
nodeCount: number,
|
|
331
|
+
resolutions: number[] = [1.0],
|
|
332
|
+
): CommunityResult {
|
|
333
|
+
// Try native Rust Leiden implementation first
|
|
334
|
+
const nativeMod = loadNativeLeiden();
|
|
335
|
+
if (nativeMod) {
|
|
336
|
+
try {
|
|
337
|
+
return nativeMod.detectCommunities(edges, nodeCount, resolutions);
|
|
338
|
+
} catch {
|
|
339
|
+
// Fall through to JS implementation
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return jsLouvain(edges, nodeCount, resolutions);
|
|
344
|
+
}
|