@unerr-ai/unerr 0.2.1 → 0.2.3
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/README.md +36 -45
- package/dist/cli.js +37443 -36022
- package/package.json +2 -1
- package/dist/behaviors/agent-llm-bridge.js +0 -166
- package/dist/behaviors/architecture-guard.js +0 -256
- package/dist/behaviors/auto-doc.js +0 -247
- package/dist/behaviors/cascade-guard.js +0 -289
- package/dist/behaviors/change-narrative.js +0 -270
- package/dist/behaviors/convention-drift.js +0 -290
- package/dist/behaviors/framework.js +0 -235
- package/dist/behaviors/guard-formatter.js +0 -44
- package/dist/behaviors/incomplete-work.js +0 -270
- package/dist/behaviors/loop-breaker.js +0 -300
- package/dist/behaviors/session-continuity.js +0 -208
- package/dist/commands/branches.js +0 -97
- package/dist/commands/check-commit.js +0 -225
- package/dist/commands/compress-output.js +0 -64
- package/dist/commands/config-verify.js +0 -243
- package/dist/commands/daemon.js +0 -905
- package/dist/commands/dashboard.js +0 -52
- package/dist/commands/debug.js +0 -200
- package/dist/commands/enrich.js +0 -184
- package/dist/commands/exec.js +0 -233
- package/dist/commands/gain.js +0 -156
- package/dist/commands/hook.js +0 -88
- package/dist/commands/index.js +0 -88
- package/dist/commands/init.js +0 -74
- package/dist/commands/install.js +0 -505
- package/dist/commands/learn.js +0 -116
- package/dist/commands/manifest.js +0 -193
- package/dist/commands/rewind.js +0 -103
- package/dist/commands/serve.js +0 -19
- package/dist/commands/setup-wizard.js +0 -414
- package/dist/commands/skills.js +0 -64
- package/dist/commands/stats.js +0 -20
- package/dist/commands/status.js +0 -654
- package/dist/commands/timeline.js +0 -139
- package/dist/commands/uninstall.js +0 -230
- package/dist/components/App.js +0 -109
- package/dist/components/Banner.js +0 -12
- package/dist/components/ConfirmPrompt.js +0 -25
- package/dist/components/DriftSummary.js +0 -23
- package/dist/components/GradeBadge.js +0 -15
- package/dist/components/HealthCard.js +0 -18
- package/dist/components/InkSpinner.js +0 -22
- package/dist/components/InputBox.js +0 -17
- package/dist/components/KeyValue.js +0 -13
- package/dist/components/MessageList.js +0 -14
- package/dist/components/ProgressBar.js +0 -26
- package/dist/components/Section.js +0 -16
- package/dist/components/SessionSummaryCard.js +0 -73
- package/dist/components/StartupDisplay.js +0 -24
- package/dist/components/StatusDashboard.js +0 -57
- package/dist/components/StatusLine.js +0 -8
- package/dist/components/StepLine.js +0 -22
- package/dist/components/Theme.js +0 -20
- package/dist/components/ToolProgress.js +0 -8
- package/dist/components/ViolationList.js +0 -21
- package/dist/components/render.js +0 -13
- package/dist/config/agent-registry.js +0 -237
- package/dist/config/claude-settings-hooks.js +0 -304
- package/dist/config/hook-installer.js +0 -65
- package/dist/config/instruction-writer.js +0 -388
- package/dist/config/mcp-config-writer.js +0 -266
- package/dist/config/settings.js +0 -174
- package/dist/config/tool-detector.js +0 -42
- package/dist/config/value-surfacing.js +0 -119
- package/dist/core/context-assembly.js +0 -108
- package/dist/core/conversation.js +0 -33
- package/dist/core/local-chat-provider.js +0 -475
- package/dist/core/provider-factory.js +0 -55
- package/dist/core/providers.js +0 -90
- package/dist/core/query-engine.js +0 -174
- package/dist/daemon/api.js +0 -312
- package/dist/daemon/autostart.js +0 -119
- package/dist/daemon/bootstrap.js +0 -39
- package/dist/daemon/client.js +0 -164
- package/dist/daemon/detect-ci.js +0 -81
- package/dist/daemon/platform-linux.js +0 -146
- package/dist/daemon/platform-macos.js +0 -134
- package/dist/daemon/platform-windows.js +0 -116
- package/dist/daemon/process-manager.js +0 -299
- package/dist/daemon/protocol.js +0 -23
- package/dist/daemon/registry.js +0 -270
- package/dist/daemon/settings-schema.js +0 -72
- package/dist/daemon/system-health.js +0 -134
- package/dist/daemon/version-checker.js +0 -262
- package/dist/daemon/warm-start.js +0 -223
- package/dist/entrypoints/cli.js +0 -1043
- package/dist/entrypoints/daemon.js +0 -380
- package/dist/entrypoints/repl.js +0 -147
- package/dist/hooks/adapters/claude-code.js +0 -90
- package/dist/hooks/adapters/cline.js +0 -100
- package/dist/hooks/adapters/cursor.js +0 -98
- package/dist/hooks/hook-dedup.js +0 -79
- package/dist/hooks/hook-runner.js +0 -113
- package/dist/hooks/navigation-hooks.js +0 -175
- package/dist/hooks/prompt-hooks.js +0 -63
- package/dist/hooks/shell-hooks.js +0 -47
- package/dist/ignore.js +0 -111
- package/dist/intelligence/approach-suggester.js +0 -61
- package/dist/intelligence/ast-extractor.js +0 -2615
- package/dist/intelligence/ast-worker.js +0 -34
- package/dist/intelligence/background-indexer.js +0 -121
- package/dist/intelligence/blast-radius.js +0 -200
- package/dist/intelligence/community-detection.js +0 -691
- package/dist/intelligence/community-detector.js +0 -184
- package/dist/intelligence/computation-scheduler.js +0 -75
- package/dist/intelligence/confidence-propagation.js +0 -47
- package/dist/intelligence/convention-detector.js +0 -242
- package/dist/intelligence/convention-learner.js +0 -205
- package/dist/intelligence/convention-matcher.js +0 -205
- package/dist/intelligence/cozo-schema.js +0 -376
- package/dist/intelligence/decision-point-detector.js +0 -90
- package/dist/intelligence/deep-dive-tools.js +0 -586
- package/dist/intelligence/durability-scorer.js +0 -84
- package/dist/intelligence/exploration-cost.js +0 -204
- package/dist/intelligence/exploration-pattern-tracker.js +0 -61
- package/dist/intelligence/fact-generator.js +0 -322
- package/dist/intelligence/facts-schema.js +0 -90
- package/dist/intelligence/file-intelligence.js +0 -59
- package/dist/intelligence/graph-holder.js +0 -220
- package/dist/intelligence/graph-temporal-joiner.js +0 -238
- package/dist/intelligence/health-grade.js +0 -423
- package/dist/intelligence/health-grader.js +0 -200
- package/dist/intelligence/health-map-data.js +0 -259
- package/dist/intelligence/import-symbols.js +0 -136
- package/dist/intelligence/incremental-indexer.js +0 -658
- package/dist/intelligence/indexer/centrality.js +0 -62
- package/dist/intelligence/indexer/cfg-context.js +0 -95
- package/dist/intelligence/indexer/confidence.js +0 -34
- package/dist/intelligence/indexer/cross-file-resolver.js +0 -104
- package/dist/intelligence/indexer/edge-repair.js +0 -89
- package/dist/intelligence/indexer/entity-key.js +0 -17
- package/dist/intelligence/indexer/export-map.js +0 -132
- package/dist/intelligence/indexer/git-cochange.js +0 -128
- package/dist/intelligence/indexer/graph-patch.js +0 -147
- package/dist/intelligence/indexer/incremental.js +0 -78
- package/dist/intelligence/indexer/ingest.js +0 -160
- package/dist/intelligence/indexer/language-detect.js +0 -226
- package/dist/intelligence/indexer/metadata.js +0 -63
- package/dist/intelligence/indexer/mutation-tracker.js +0 -79
- package/dist/intelligence/indexer/orchestrator.js +0 -155
- package/dist/intelligence/indexer/plugin-interface.js +0 -31
- package/dist/intelligence/indexer/plugins/csharp.js +0 -440
- package/dist/intelligence/indexer/plugins/go.js +0 -335
- package/dist/intelligence/indexer/plugins/java.js +0 -370
- package/dist/intelligence/indexer/plugins/python.js +0 -358
- package/dist/intelligence/indexer/plugins/regex-fallback.js +0 -82
- package/dist/intelligence/indexer/plugins/ruby.js +0 -290
- package/dist/intelligence/indexer/plugins/rust.js +0 -484
- package/dist/intelligence/indexer/plugins/tier2-generic.js +0 -310
- package/dist/intelligence/indexer/plugins/typescript.js +0 -456
- package/dist/intelligence/indexer/resource-monitor.js +0 -93
- package/dist/intelligence/indexer/scip/decoder.js +0 -253
- package/dist/intelligence/indexer/scip/detector.js +0 -232
- package/dist/intelligence/indexer/scip/downloader.js +0 -427
- package/dist/intelligence/indexer/scip/fallback.js +0 -34
- package/dist/intelligence/indexer/scip/merger.js +0 -109
- package/dist/intelligence/indexer/scip/orchestrator.js +0 -433
- package/dist/intelligence/indexer/scip/runner.js +0 -98
- package/dist/intelligence/indexer/snapshot.js +0 -66
- package/dist/intelligence/indexer/test-detector.js +0 -196
- package/dist/intelligence/indexer/watch-integration.js +0 -61
- package/dist/intelligence/indexer/worker.js +0 -85
- package/dist/intelligence/local-convention-detector.js +0 -437
- package/dist/intelligence/local-embeddings.js +0 -190
- package/dist/intelligence/local-graph.js +0 -1946
- package/dist/intelligence/local-indexer.js +0 -1575
- package/dist/intelligence/local-llm.js +0 -163
- package/dist/intelligence/local-rule-generator.js +0 -154
- package/dist/intelligence/local-snapshot.js +0 -213
- package/dist/intelligence/negative-knowledge.js +0 -103
- package/dist/intelligence/persistent-db.js +0 -85
- package/dist/intelligence/query-router.js +0 -2556
- package/dist/intelligence/risk-classifier.js +0 -116
- package/dist/intelligence/rule-evaluator.js +0 -380
- package/dist/intelligence/rule-generator.js +0 -49
- package/dist/intelligence/search-index.js +0 -173
- package/dist/intelligence/semantic/docstring-extractor.js +0 -67
- package/dist/intelligence/semantic/embedding-store.js +0 -52
- package/dist/intelligence/semantic/enrichment-orchestrator.js +0 -48
- package/dist/intelligence/semantic/git-message-miner.js +0 -114
- package/dist/intelligence/semantic/identifier-tokenizer.js +0 -51
- package/dist/intelligence/semantic/node2vec-embeddings.js +0 -71
- package/dist/intelligence/semantic/node2vec-walks.js +0 -103
- package/dist/intelligence/semantic/path-domain-inference.js +0 -112
- package/dist/intelligence/semantic/similarity-engine.js +0 -60
- package/dist/intelligence/semantic/tfidf-vectors.js +0 -88
- package/dist/intelligence/session-brief-builder.js +0 -159
- package/dist/intelligence/session-context.js +0 -221
- package/dist/intelligence/session-health-monitor.js +0 -211
- package/dist/intelligence/session-narrative.js +0 -197
- package/dist/intelligence/session-pattern-analyzer.js +0 -218
- package/dist/intelligence/signal-scorer.js +0 -390
- package/dist/intelligence/signal-show-store.js +0 -182
- package/dist/intelligence/smart-truncate.js +0 -158
- package/dist/intelligence/subgraph-cache.js +0 -88
- package/dist/intelligence/temporal-facts.js +0 -494
- package/dist/intelligence/token-estimator.js +0 -100
- package/dist/intelligence/tool-injector.js +0 -87
- package/dist/intelligence/tree-sitter-loader.js +0 -71
- package/dist/intelligence/worker-pool.js +0 -116
- package/dist/proxy/arg-validator.js +0 -79
- package/dist/proxy/auto-bootstrap.js +0 -167
- package/dist/proxy/bridge.js +0 -147
- package/dist/proxy/budget-enforcer.js +0 -70
- package/dist/proxy/compression-quality-monitor.js +0 -160
- package/dist/proxy/compression-stats.js +0 -51
- package/dist/proxy/context-rot-detector.js +0 -137
- package/dist/proxy/drift-detector.js +0 -139
- package/dist/proxy/efficiency-tracker.js +0 -79
- package/dist/proxy/fact-ranking.js +0 -154
- package/dist/proxy/format-encoder.js +0 -266
- package/dist/proxy/http-transport.js +0 -90
- package/dist/proxy/lifecycle-actor.js +0 -55
- package/dist/proxy/lifecycle-machine.js +0 -187
- package/dist/proxy/log-tailer.js +0 -265
- package/dist/proxy/model-pricing.js +0 -98
- package/dist/proxy/network-firewall.js +0 -141
- package/dist/proxy/nudge-state.js +0 -93
- package/dist/proxy/output-compressor.js +0 -185
- package/dist/proxy/pid-lock.js +0 -291
- package/dist/proxy/proxy-context.js +0 -11
- package/dist/proxy/proxy.js +0 -2633
- package/dist/proxy/response-enrichment.js +0 -32
- package/dist/proxy/response-envelope.js +0 -313
- package/dist/proxy/session-dedup.js +0 -82
- package/dist/proxy/session-legend.js +0 -30
- package/dist/proxy/session-persistence.js +0 -210
- package/dist/proxy/session-resume.js +0 -94
- package/dist/proxy/session-stats.js +0 -513
- package/dist/proxy/shell-classifier.js +0 -1346
- package/dist/proxy/shell-compression-log.js +0 -93
- package/dist/proxy/shell-compressor.js +0 -390
- package/dist/proxy/shell-graph-boost.js +0 -202
- package/dist/proxy/shell-monitor-map.js +0 -18
- package/dist/proxy/shell-stats.js +0 -54
- package/dist/proxy/shell-strategies/cloud.js +0 -215
- package/dist/proxy/shell-strategies/diff.js +0 -159
- package/dist/proxy/shell-strategies/error-diagnostic.js +0 -796
- package/dist/proxy/shell-strategies/filter-dsl.js +0 -358
- package/dist/proxy/shell-strategies/git-status.js +0 -177
- package/dist/proxy/shell-strategies/key-value.js +0 -193
- package/dist/proxy/shell-strategies/log-text.js +0 -154
- package/dist/proxy/shell-strategies/omni.js +0 -188
- package/dist/proxy/shell-strategies/progress.js +0 -55
- package/dist/proxy/shell-strategies/redact.js +0 -76
- package/dist/proxy/shell-strategies/structured.js +0 -241
- package/dist/proxy/shell-strategies/tabular.js +0 -243
- package/dist/proxy/shell-strategies/test-results-types.js +0 -13
- package/dist/proxy/shell-strategies/test-results.js +0 -784
- package/dist/proxy/shell-strategies/tree-paths.js +0 -144
- package/dist/proxy/shell-strategies/yaml.js +0 -182
- package/dist/proxy/shell-tee.js +0 -111
- package/dist/proxy/signal-dedup.js +0 -171
- package/dist/proxy/startup-renderer.js +0 -158
- package/dist/proxy/task-token-display.js +0 -38
- package/dist/proxy/token-counter.js +0 -61
- package/dist/proxy/tool-clusters.js +0 -273
- package/dist/proxy/tool-definitions.js +0 -525
- package/dist/proxy/transport-mux.js +0 -229
- package/dist/proxy/wire-cap.js +0 -268
- package/dist/rules/developer.mozilla.org.json +0 -9
- package/dist/rules/github.com.json +0 -21
- package/dist/schemas/api/skills.js +0 -19
- package/dist/schemas/common/errors.js +0 -7
- package/dist/schemas/common/headers.js +0 -5
- package/dist/schemas/entities/edge.js +0 -25
- package/dist/schemas/entities/entity.js +0 -22
- package/dist/schemas/entities/rule.js +0 -18
- package/dist/schemas/index.js +0 -14
- package/dist/server/event-bus.js +0 -59
- package/dist/server/http.js +0 -156
- package/dist/server/middleware.js +0 -70
- package/dist/server/routes/drift.js +0 -97
- package/dist/server/routes/intelligence.js +0 -1217
- package/dist/server/routes/reasoning-quality.js +0 -444
- package/dist/server/routes/session.js +0 -86
- package/dist/server/routes/stream.js +0 -120
- package/dist/server/routes/system.js +0 -73
- package/dist/server/routes/temporal.js +0 -170
- package/dist/server/routes/timeline.js +0 -232
- package/dist/server/routes/token-flow.js +0 -403
- package/dist/skills/effectiveness-tracker.js +0 -93
- package/dist/skills/local-pack.js +0 -380
- package/dist/skills/resolver.js +0 -495
- package/dist/state-detector.js +0 -83
- package/dist/timeline/intent-detector.js +0 -263
- package/dist/timeline/loop-miner.js +0 -140
- package/dist/timeline/open-threads.js +0 -49
- package/dist/timeline/signal-reinforcer.js +0 -62
- package/dist/timeline/timeline-bootstrap.js +0 -151
- package/dist/timeline/timeline-store.js +0 -618
- package/dist/tools/coding/bash.js +0 -49
- package/dist/tools/coding/file-edit.js +0 -72
- package/dist/tools/coding/file-outline.js +0 -227
- package/dist/tools/coding/file-read-protocol.js +0 -425
- package/dist/tools/coding/file-read.js +0 -35
- package/dist/tools/coding/file-write.js +0 -43
- package/dist/tools/coding/glob-tool.js +0 -109
- package/dist/tools/coding/grep.js +0 -162
- package/dist/tools/coding/index.js +0 -27
- package/dist/tools/intelligence/index.js +0 -269
- package/dist/tools/intelligence/record-fact.js +0 -48
- package/dist/tools/intelligence/timeline-markers.js +0 -130
- package/dist/tools/registry.js +0 -47
- package/dist/tools/types.js +0 -8
- package/dist/tracking/auto-snapshot-triggers.js +0 -246
- package/dist/tracking/branch-context.js +0 -115
- package/dist/tracking/branch-snapshot.js +0 -217
- package/dist/tracking/causal-bridge.js +0 -317
- package/dist/tracking/circuit-breaker.js +0 -147
- package/dist/tracking/commit-watcher.js +0 -114
- package/dist/tracking/context-ledger.js +0 -119
- package/dist/tracking/correction-detector.js +0 -324
- package/dist/tracking/drift-tracker.js +0 -874
- package/dist/tracking/durability-tracker.js +0 -94
- package/dist/tracking/entity-rewind.js +0 -200
- package/dist/tracking/file-hash-state.js +0 -114
- package/dist/tracking/git-attribution.js +0 -132
- package/dist/tracking/git-trailers.js +0 -171
- package/dist/tracking/intelligence-counter.js +0 -46
- package/dist/tracking/intent-correlator.js +0 -202
- package/dist/tracking/intent-encoder.js +0 -52
- package/dist/tracking/intent-token-tracker.js +0 -159
- package/dist/tracking/ledger-archiver.js +0 -94
- package/dist/tracking/ledger-chains.js +0 -245
- package/dist/tracking/metrics-store.js +0 -361
- package/dist/tracking/native-watcher.js +0 -131
- package/dist/tracking/offline-rewind.js +0 -295
- package/dist/tracking/pending-violations.js +0 -74
- package/dist/tracking/persistence-effectiveness.js +0 -167
- package/dist/tracking/prompt-durability.js +0 -202
- package/dist/tracking/quality-signals.js +0 -213
- package/dist/tracking/redactor.js +0 -73
- package/dist/tracking/rewind-engine.js +0 -161
- package/dist/tracking/session-history.js +0 -128
- package/dist/tracking/session-receipt.js +0 -88
- package/dist/tracking/session-summary-writer.js +0 -157
- package/dist/tracking/shadow-ledger.js +0 -321
- package/dist/tracking/stash-manager.js +0 -258
- package/dist/tracking/timeline-fork.js +0 -213
- package/dist/tracking/timeline.js +0 -69
- package/dist/tracking/token-flow.js +0 -276
- package/dist/tracking/turn-segmenter.js +0 -122
- package/dist/tracking/weekly-accumulator.js +0 -179
- package/dist/tracking/working-snapshots.js +0 -188
- package/dist/tracking/workspace-manifest.js +0 -176
- package/dist/transport/http.js +0 -102
- package/dist/utils/counterfactual.js +0 -65
- package/dist/utils/deep-link.js +0 -34
- package/dist/utils/detect.js +0 -193
- package/dist/utils/exec.js +0 -73
- package/dist/utils/file-logger.js +0 -87
- package/dist/utils/format-error.js +0 -29
- package/dist/utils/git.js +0 -181
- package/dist/utils/log.js +0 -57
- package/dist/utils/logger.js +0 -35
- package/dist/utils/mcp-content-json.js +0 -8
- package/dist/utils/session-logger.js +0 -154
- package/dist/utils/startup-log.js +0 -512
- package/dist/utils/ui.js +0 -56
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File-Level Intelligence Assembly — combines all entity intelligence for a file.
|
|
3
|
-
*
|
|
4
|
-
* S.1: Given a file path, assembles a comprehensive intelligence profile:
|
|
5
|
-
* - All entities in the file ranked by risk
|
|
6
|
-
* - Total blast radius (sum of all entity callers)
|
|
7
|
-
* - Dominant community
|
|
8
|
-
* - Top conventions that apply
|
|
9
|
-
* - Aggregate file risk level
|
|
10
|
-
*/
|
|
11
|
-
/**
|
|
12
|
-
* Assemble file-level intelligence from entity data.
|
|
13
|
-
*/
|
|
14
|
-
export function assembleFileIntelligence(filePath, entities) {
|
|
15
|
-
const fileEntities = entities.filter((e) => e.file_path === filePath);
|
|
16
|
-
const profiles = fileEntities.map((e) => ({
|
|
17
|
-
key: e.key,
|
|
18
|
-
name: e.name,
|
|
19
|
-
kind: e.kind,
|
|
20
|
-
riskLevel: e.risk_level ?? "normal",
|
|
21
|
-
fanIn: e.fan_in ?? 0,
|
|
22
|
-
community: e.community ?? -1,
|
|
23
|
-
}));
|
|
24
|
-
profiles.sort((a, b) => b.fanIn - a.fanIn);
|
|
25
|
-
const topRiskEntities = profiles
|
|
26
|
-
.filter((e) => e.riskLevel !== "normal")
|
|
27
|
-
.slice(0, 3);
|
|
28
|
-
const totalBlastRadius = profiles.reduce((sum, e) => sum + e.fanIn, 0);
|
|
29
|
-
const communityVotes = new Map();
|
|
30
|
-
for (const e of profiles) {
|
|
31
|
-
if (e.community >= 0) {
|
|
32
|
-
communityVotes.set(e.community, (communityVotes.get(e.community) ?? 0) + 1);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
const dominantCommunity = communityVotes.size > 0
|
|
36
|
-
? [...communityVotes.entries()].sort((a, b) => b[1] - a[1])[0][0]
|
|
37
|
-
: -1;
|
|
38
|
-
const maxRisk = profiles.reduce((max, e) => {
|
|
39
|
-
const order = { critical: 4, high: 3, medium: 2, normal: 1 };
|
|
40
|
-
const eLevel = order[e.riskLevel] ?? 1;
|
|
41
|
-
return eLevel > max ? eLevel : max;
|
|
42
|
-
}, 1);
|
|
43
|
-
const fileRiskLevel = maxRisk >= 4
|
|
44
|
-
? "critical"
|
|
45
|
-
: maxRisk >= 3
|
|
46
|
-
? "high"
|
|
47
|
-
: maxRisk >= 2
|
|
48
|
-
? "medium"
|
|
49
|
-
: "normal";
|
|
50
|
-
return {
|
|
51
|
-
filePath,
|
|
52
|
-
entities: profiles,
|
|
53
|
-
topRiskEntities,
|
|
54
|
-
totalBlastRadius,
|
|
55
|
-
dominantCommunity,
|
|
56
|
-
fileRiskLevel,
|
|
57
|
-
entityCount: profiles.length,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Graph Holder — manages the active CozoGraphStore reference and handles
|
|
3
|
-
* swap-on-idle rebuilds for consistent, always-queryable graph access.
|
|
4
|
-
*
|
|
5
|
-
* Design: The old graph remains fully operational during rebuild (~8s).
|
|
6
|
-
* Once the new graph is ready, all consumers are atomically swapped to the
|
|
7
|
-
* new instance. The old graph is then eligible for GC.
|
|
8
|
-
*
|
|
9
|
-
* Trigger: File changes (from file watcher or LLM tool calls) reset an idle
|
|
10
|
-
* timer. When no file change occurs for IDLE_THRESHOLD_MS, a full reindex
|
|
11
|
-
* is triggered into a fresh CozoDB instance. On completion, the reference
|
|
12
|
-
* is swapped across all consumers.
|
|
13
|
-
*/
|
|
14
|
-
const DEFAULT_IDLE_THRESHOLD_MS = 5_000;
|
|
15
|
-
const _log = {
|
|
16
|
-
info: (msg) => process.stderr.write(`▸ [graph-holder] ${msg}\n`),
|
|
17
|
-
warn: (msg) => process.stderr.write(`⚠ [graph-holder] ${msg}\n`),
|
|
18
|
-
};
|
|
19
|
-
export class GraphHolder {
|
|
20
|
-
current;
|
|
21
|
-
rebuildFactory = null;
|
|
22
|
-
incrementalFactory = null;
|
|
23
|
-
swapCallbacks = [];
|
|
24
|
-
idleTimer = null;
|
|
25
|
-
idleThresholdMs;
|
|
26
|
-
incrementalFileLimit;
|
|
27
|
-
fullReindexEveryNCycles;
|
|
28
|
-
rebuilding = false;
|
|
29
|
-
rebuildPending = false;
|
|
30
|
-
fileChangesSinceLastRebuild = 0;
|
|
31
|
-
lastRebuildTimestamp = 0;
|
|
32
|
-
changedFilePaths = new Set();
|
|
33
|
-
incrementalCycleCount = 0;
|
|
34
|
-
constructor(initialGraph, config) {
|
|
35
|
-
this.current = initialGraph;
|
|
36
|
-
this.idleThresholdMs = config?.idleThresholdMs ?? DEFAULT_IDLE_THRESHOLD_MS;
|
|
37
|
-
this.incrementalFileLimit = config?.incrementalFileLimit ?? 20;
|
|
38
|
-
this.fullReindexEveryNCycles = config?.fullReindexEveryNCycles ?? 10;
|
|
39
|
-
}
|
|
40
|
-
/** Get the current active graph instance. Always valid, never null. */
|
|
41
|
-
get graph() {
|
|
42
|
-
return this.current;
|
|
43
|
-
}
|
|
44
|
-
/** Register the factory that creates a fresh graph + indexes into it. */
|
|
45
|
-
setRebuildFactory(factory) {
|
|
46
|
-
this.rebuildFactory = factory;
|
|
47
|
-
}
|
|
48
|
-
/** Register the incremental indexing factory (processes only changed files). */
|
|
49
|
-
setIncrementalFactory(factory) {
|
|
50
|
-
this.incrementalFactory = factory;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Register a callback invoked when the graph is swapped.
|
|
54
|
-
* Used to propagate the new instance to QueryRouter, DriftTracker, behaviors, etc.
|
|
55
|
-
*/
|
|
56
|
-
onSwap(callback) {
|
|
57
|
-
this.swapCallbacks.push(callback);
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Notify the holder that files changed. Resets the idle timer.
|
|
61
|
-
* Called by file watcher (covers both LLM writes and user edits).
|
|
62
|
-
* @param filePaths — absolute paths of changed files (for incremental indexing)
|
|
63
|
-
*/
|
|
64
|
-
notifyFileChange(filePaths) {
|
|
65
|
-
this.fileChangesSinceLastRebuild++;
|
|
66
|
-
if (filePaths) {
|
|
67
|
-
for (const fp of filePaths) {
|
|
68
|
-
this.changedFilePaths.add(fp);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// Reset idle timer
|
|
72
|
-
if (this.idleTimer) {
|
|
73
|
-
clearTimeout(this.idleTimer);
|
|
74
|
-
}
|
|
75
|
-
this.idleTimer = setTimeout(() => {
|
|
76
|
-
this.idleTimer = null;
|
|
77
|
-
this.triggerRebuild();
|
|
78
|
-
}, this.idleThresholdMs);
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Force an immediate rebuild (e.g., on explicit user command).
|
|
82
|
-
* Skips idle timer.
|
|
83
|
-
*/
|
|
84
|
-
forceRebuild() {
|
|
85
|
-
if (this.idleTimer) {
|
|
86
|
-
clearTimeout(this.idleTimer);
|
|
87
|
-
this.idleTimer = null;
|
|
88
|
-
}
|
|
89
|
-
this.triggerRebuild();
|
|
90
|
-
}
|
|
91
|
-
/** Whether a rebuild is currently in progress. */
|
|
92
|
-
get isRebuilding() {
|
|
93
|
-
return this.rebuilding;
|
|
94
|
-
}
|
|
95
|
-
/** Number of file changes since last successful rebuild. */
|
|
96
|
-
get pendingChanges() {
|
|
97
|
-
return this.fileChangesSinceLastRebuild;
|
|
98
|
-
}
|
|
99
|
-
/** Clean up timers on shutdown. */
|
|
100
|
-
dispose() {
|
|
101
|
-
if (this.idleTimer) {
|
|
102
|
-
clearTimeout(this.idleTimer);
|
|
103
|
-
this.idleTimer = null;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// ─── Private ───────────────────────────────────────────────────────────────
|
|
107
|
-
/**
|
|
108
|
-
* Decide whether to use incremental or full reindex, then execute.
|
|
109
|
-
* Incremental when: factory exists, file count ≤ limit, not a periodic full cycle.
|
|
110
|
-
* Full otherwise (or as fallback on incremental failure).
|
|
111
|
-
*/
|
|
112
|
-
triggerRebuild() {
|
|
113
|
-
if (!this.rebuildFactory)
|
|
114
|
-
return;
|
|
115
|
-
if (this.fileChangesSinceLastRebuild === 0)
|
|
116
|
-
return; // No changes since last rebuild
|
|
117
|
-
if (this.rebuilding) {
|
|
118
|
-
this.rebuildPending = true;
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
this.rebuilding = true;
|
|
122
|
-
const changesAtStart = this.fileChangesSinceLastRebuild;
|
|
123
|
-
const changedFiles = [...this.changedFilePaths];
|
|
124
|
-
const startMs = Date.now();
|
|
125
|
-
// Decision: incremental or full?
|
|
126
|
-
const useIncremental = this.incrementalFactory !== null &&
|
|
127
|
-
changedFiles.length > 0 &&
|
|
128
|
-
changedFiles.length <= this.incrementalFileLimit &&
|
|
129
|
-
this.incrementalCycleCount < this.fullReindexEveryNCycles;
|
|
130
|
-
if (useIncremental) {
|
|
131
|
-
this.runIncremental(changedFiles, changesAtStart, startMs);
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
if (this.incrementalCycleCount >= this.fullReindexEveryNCycles) {
|
|
135
|
-
_log.info(`Periodic full reindex (after ${this.incrementalCycleCount} incremental cycles)`);
|
|
136
|
-
this.incrementalCycleCount = 0;
|
|
137
|
-
}
|
|
138
|
-
this.runFullRebuild(changesAtStart, startMs);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
runIncremental(changedFiles, changesAtStart, startMs) {
|
|
142
|
-
_log.info(`Incremental indexing ${changedFiles.length} files...`);
|
|
143
|
-
this.incrementalFactory?.(changedFiles)
|
|
144
|
-
.then((result) => {
|
|
145
|
-
// Clear tracked files that were processed
|
|
146
|
-
for (const fp of changedFiles) {
|
|
147
|
-
this.changedFilePaths.delete(fp);
|
|
148
|
-
}
|
|
149
|
-
this.fileChangesSinceLastRebuild = Math.max(0, this.fileChangesSinceLastRebuild - changesAtStart);
|
|
150
|
-
this.lastRebuildTimestamp = Date.now();
|
|
151
|
-
this.incrementalCycleCount++;
|
|
152
|
-
_log.info(`Incremental done in ${Date.now() - startMs}ms — ` +
|
|
153
|
-
`+${result.entitiesAdded}/-${result.entitiesDeleted}/~${result.entitiesUpdated} entities, ` +
|
|
154
|
-
`+${result.edgesAdded}/-${result.edgesDeleted} edges`);
|
|
155
|
-
// Notify swap callbacks (graph is same instance, but data changed)
|
|
156
|
-
for (const cb of this.swapCallbacks) {
|
|
157
|
-
try {
|
|
158
|
-
cb(this.current);
|
|
159
|
-
}
|
|
160
|
-
catch (err) {
|
|
161
|
-
_log.warn(`Swap callback failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
})
|
|
165
|
-
.catch((err) => {
|
|
166
|
-
_log.warn(`Incremental indexing failed: ${err instanceof Error ? err.message : String(err)}. Falling back to full reindex.`);
|
|
167
|
-
// Fallback to full reindex
|
|
168
|
-
this.runFullRebuild(changesAtStart, startMs);
|
|
169
|
-
return; // runFullRebuild handles its own finally logic via .finally()
|
|
170
|
-
})
|
|
171
|
-
.finally(() => {
|
|
172
|
-
this.rebuilding = false;
|
|
173
|
-
if (this.rebuildPending) {
|
|
174
|
-
this.rebuildPending = false;
|
|
175
|
-
this.triggerRebuild();
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
runFullRebuild(changesAtStart, startMs) {
|
|
180
|
-
_log.info(`Full reindex (${changesAtStart} file changes since last rebuild)...`);
|
|
181
|
-
this.rebuildFactory?.()
|
|
182
|
-
.then(({ graph: newGraph, result }) => {
|
|
183
|
-
const oldGraph = this.current;
|
|
184
|
-
this.current = newGraph;
|
|
185
|
-
// Propagate to all consumers
|
|
186
|
-
for (const cb of this.swapCallbacks) {
|
|
187
|
-
try {
|
|
188
|
-
cb(newGraph);
|
|
189
|
-
}
|
|
190
|
-
catch (err) {
|
|
191
|
-
_log.warn(`Swap callback failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
// Clear all tracked state — full reindex is authoritative
|
|
195
|
-
this.changedFilePaths.clear();
|
|
196
|
-
this.fileChangesSinceLastRebuild = Math.max(0, this.fileChangesSinceLastRebuild - changesAtStart);
|
|
197
|
-
this.lastRebuildTimestamp = Date.now();
|
|
198
|
-
_log.info(`Graph rebuilt in ${Date.now() - startMs}ms — ${result.entityCount} entities, ${result.edgeCount} edges. Swapped.`);
|
|
199
|
-
// Close old graph if different instance
|
|
200
|
-
if (oldGraph !== newGraph) {
|
|
201
|
-
try {
|
|
202
|
-
oldGraph.close?.();
|
|
203
|
-
}
|
|
204
|
-
catch {
|
|
205
|
-
// best-effort
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
})
|
|
209
|
-
.catch((err) => {
|
|
210
|
-
_log.warn(`Graph rebuild failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
211
|
-
})
|
|
212
|
-
.finally(() => {
|
|
213
|
-
this.rebuilding = false;
|
|
214
|
-
if (this.rebuildPending) {
|
|
215
|
-
this.rebuildPending = false;
|
|
216
|
-
this.triggerRebuild();
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
}
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GraphTemporalJoiner — combines graph topology with temporal facts
|
|
3
|
-
* to predict co-change patterns and detect hidden coupling.
|
|
4
|
-
*
|
|
5
|
-
* Part of Layer A of the Three-Layer Experience System.
|
|
6
|
-
*
|
|
7
|
-
* Co-change prediction: "when you change file A, you likely need to change file B"
|
|
8
|
-
* Hidden coupling: files that always co-change but have no structural connection.
|
|
9
|
-
*
|
|
10
|
-
* Combined score formula: 0.4 * graph_coupling + 0.6 * temporal_coupling
|
|
11
|
-
* (temporal is weighted higher — actual editing patterns are more predictive
|
|
12
|
-
* than static structure)
|
|
13
|
-
*/
|
|
14
|
-
export class GraphTemporalJoiner {
|
|
15
|
-
localGraph;
|
|
16
|
-
factStore;
|
|
17
|
-
constructor(localGraph, factStore) {
|
|
18
|
-
this.localGraph = localGraph;
|
|
19
|
-
this.factStore = factStore;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Predict which files are likely to need changes when targetFile changes.
|
|
23
|
-
* Combines graph structure (imports/calls) with temporal facts (co-change history).
|
|
24
|
-
*/
|
|
25
|
-
async predictCoChanges(targetFile) {
|
|
26
|
-
const [graphNeighbors, temporalCouplings] = await Promise.all([
|
|
27
|
-
this.getGraphNeighbors(targetFile),
|
|
28
|
-
this.getTemporalCouplings(targetFile),
|
|
29
|
-
]);
|
|
30
|
-
// Merge graph and temporal signals by file
|
|
31
|
-
const merged = new Map();
|
|
32
|
-
for (const [file, score, evidence] of graphNeighbors) {
|
|
33
|
-
if (file === targetFile)
|
|
34
|
-
continue;
|
|
35
|
-
merged.set(file, {
|
|
36
|
-
graph: score,
|
|
37
|
-
temporal: 0,
|
|
38
|
-
graphEvidence: evidence,
|
|
39
|
-
temporalEvidence: "",
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
for (const [file, score, evidence] of temporalCouplings) {
|
|
43
|
-
if (file === targetFile)
|
|
44
|
-
continue;
|
|
45
|
-
const existing = merged.get(file);
|
|
46
|
-
if (existing) {
|
|
47
|
-
existing.temporal = score;
|
|
48
|
-
existing.temporalEvidence = evidence;
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
merged.set(file, {
|
|
52
|
-
graph: 0,
|
|
53
|
-
temporal: score,
|
|
54
|
-
graphEvidence: "",
|
|
55
|
-
temporalEvidence: evidence,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
// Compute combined scores and sort
|
|
60
|
-
const signals = [];
|
|
61
|
-
for (const [file, data] of merged) {
|
|
62
|
-
const combined = 0.4 * data.graph + 0.6 * data.temporal;
|
|
63
|
-
if (combined < 0.1)
|
|
64
|
-
continue; // Filter noise
|
|
65
|
-
const evidenceParts = [];
|
|
66
|
-
if (data.graphEvidence)
|
|
67
|
-
evidenceParts.push(data.graphEvidence);
|
|
68
|
-
if (data.temporalEvidence)
|
|
69
|
-
evidenceParts.push(data.temporalEvidence);
|
|
70
|
-
signals.push({
|
|
71
|
-
file_a: targetFile,
|
|
72
|
-
file_b: file,
|
|
73
|
-
graph_coupling: data.graph,
|
|
74
|
-
temporal_coupling: data.temporal,
|
|
75
|
-
combined_score: Math.round(combined * 100) / 100,
|
|
76
|
-
evidence: evidenceParts.join(" + "),
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
return signals.sort((a, b) => b.combined_score - a.combined_score);
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Detect hidden couplings — files with high temporal coupling but no graph edges.
|
|
83
|
-
* These are the most dangerous: changes can silently break co-dependent files.
|
|
84
|
-
*/
|
|
85
|
-
async detectHiddenCouplings() {
|
|
86
|
-
if (!this.factStore)
|
|
87
|
-
return [];
|
|
88
|
-
// Get all coupling facts from temporal store
|
|
89
|
-
const couplingFacts = await this.getCouplingFacts();
|
|
90
|
-
const hidden = [];
|
|
91
|
-
for (const fact of couplingFacts) {
|
|
92
|
-
const { fileA, fileB, score, sessions } = this.parseCouplingFact(fact);
|
|
93
|
-
if (!fileA || !fileB || score < 0.5)
|
|
94
|
-
continue;
|
|
95
|
-
// Check if there's a structural connection
|
|
96
|
-
const graphCoupling = await this.getGraphCouplingBetween(fileA, fileB);
|
|
97
|
-
if (graphCoupling < 0.1) {
|
|
98
|
-
hidden.push({
|
|
99
|
-
file_a: fileA,
|
|
100
|
-
file_b: fileB,
|
|
101
|
-
temporal_coupling: score,
|
|
102
|
-
graph_coupling: graphCoupling,
|
|
103
|
-
sessions_observed: sessions,
|
|
104
|
-
evidence: `Co-changed in ${sessions} sessions but no import/call edges`,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return hidden.sort((a, b) => b.temporal_coupling - a.temporal_coupling);
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Get graph-based neighbors for a file (via imports/calls).
|
|
112
|
-
* Returns [filePath, couplingScore, evidence] tuples.
|
|
113
|
-
*/
|
|
114
|
-
async getGraphNeighbors(filePath) {
|
|
115
|
-
const neighbors = [];
|
|
116
|
-
try {
|
|
117
|
-
// Get entities in this file and their callers/callees
|
|
118
|
-
const entities = await this.localGraph.getEntitiesByFile(filePath);
|
|
119
|
-
if (!entities || entities.length === 0)
|
|
120
|
-
return neighbors;
|
|
121
|
-
const fileCounts = new Map();
|
|
122
|
-
for (const entity of entities.slice(0, 10)) {
|
|
123
|
-
const [callers, callees] = await Promise.all([
|
|
124
|
-
this.localGraph.getCallersOf(entity.key),
|
|
125
|
-
this.localGraph.getCalleesOf(entity.key),
|
|
126
|
-
]);
|
|
127
|
-
for (const caller of callers) {
|
|
128
|
-
if (caller.file_path && caller.file_path !== filePath) {
|
|
129
|
-
fileCounts.set(caller.file_path, (fileCounts.get(caller.file_path) ?? 0) + 1);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
for (const callee of callees) {
|
|
133
|
-
if (callee.file_path && callee.file_path !== filePath) {
|
|
134
|
-
fileCounts.set(callee.file_path, (fileCounts.get(callee.file_path) ?? 0) + 1);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
// Normalize: max edges → 1.0
|
|
139
|
-
const maxCount = Math.max(...fileCounts.values(), 1);
|
|
140
|
-
for (const [file, count] of fileCounts) {
|
|
141
|
-
const score = Math.min(1.0, count / maxCount);
|
|
142
|
-
neighbors.push([file, score, `${count} shared edges`]);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
// Graph query failure is non-critical
|
|
147
|
-
}
|
|
148
|
-
return neighbors;
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Get temporal coupling facts for a file.
|
|
152
|
-
* Returns [filePath, couplingScore, evidence] tuples.
|
|
153
|
-
*/
|
|
154
|
-
async getTemporalCouplings(filePath) {
|
|
155
|
-
if (!this.factStore)
|
|
156
|
-
return [];
|
|
157
|
-
try {
|
|
158
|
-
const facts = await this.factStore.recallBySubject(`coupling:${filePath}`, 0.2);
|
|
159
|
-
const couplings = [];
|
|
160
|
-
for (const fact of facts) {
|
|
161
|
-
const parsed = this.parseCouplingFact(fact);
|
|
162
|
-
if (!parsed.fileB)
|
|
163
|
-
continue;
|
|
164
|
-
const otherFile = parsed.fileA === filePath ? parsed.fileB : parsed.fileA;
|
|
165
|
-
if (!otherFile)
|
|
166
|
-
continue;
|
|
167
|
-
couplings.push([
|
|
168
|
-
otherFile,
|
|
169
|
-
parsed.score,
|
|
170
|
-
`co-changed in ${parsed.sessions} sessions`,
|
|
171
|
-
]);
|
|
172
|
-
}
|
|
173
|
-
return couplings;
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
return [];
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Get all coupling facts from the temporal store.
|
|
181
|
-
*/
|
|
182
|
-
async getCouplingFacts() {
|
|
183
|
-
if (!this.factStore)
|
|
184
|
-
return [];
|
|
185
|
-
try {
|
|
186
|
-
const allFacts = await this.factStore.recallByScope("project", 0.2);
|
|
187
|
-
return allFacts.filter((f) => f.fact_type === "semantic" && f.subject.startsWith("coupling:"));
|
|
188
|
-
}
|
|
189
|
-
catch {
|
|
190
|
-
return [];
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Parse a coupling fact's subject and content.
|
|
195
|
-
* Subject format: "coupling:fileA↔fileB"
|
|
196
|
-
* Content format: "Files co-accessed in N sessions"
|
|
197
|
-
*/
|
|
198
|
-
parseCouplingFact(fact) {
|
|
199
|
-
const parts = fact.subject.replace("coupling:", "").split("↔");
|
|
200
|
-
const fileA = parts[0] ?? "";
|
|
201
|
-
const fileB = parts[1] ?? "";
|
|
202
|
-
// Extract session count from content
|
|
203
|
-
const sessionMatch = fact.content.match(/(\d+)\s*session/);
|
|
204
|
-
const sessions = sessionMatch?.[1]
|
|
205
|
-
? Number.parseInt(sessionMatch[1], 10)
|
|
206
|
-
: 1;
|
|
207
|
-
return {
|
|
208
|
-
fileA,
|
|
209
|
-
fileB,
|
|
210
|
-
score: fact.base_confidence,
|
|
211
|
-
sessions,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Check structural coupling between two specific files.
|
|
216
|
-
* Returns 0-1 based on shared edges.
|
|
217
|
-
*/
|
|
218
|
-
async getGraphCouplingBetween(fileA, fileB) {
|
|
219
|
-
try {
|
|
220
|
-
const entitiesA = await this.localGraph.getEntitiesByFile(fileA);
|
|
221
|
-
if (!entitiesA || entitiesA.length === 0)
|
|
222
|
-
return 0;
|
|
223
|
-
for (const entity of entitiesA.slice(0, 5)) {
|
|
224
|
-
const [callers, callees] = await Promise.all([
|
|
225
|
-
this.localGraph.getCallersOf(entity.key),
|
|
226
|
-
this.localGraph.getCalleesOf(entity.key),
|
|
227
|
-
]);
|
|
228
|
-
const hasConnection = [...callers, ...callees].some((e) => e.file_path === fileB);
|
|
229
|
-
if (hasConnection)
|
|
230
|
-
return 0.5; // At least one structural connection
|
|
231
|
-
}
|
|
232
|
-
return 0;
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
return 0;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|