aether-colony 5.3.2 → 5.4.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/.aether/aether-utils.sh +181 -5
- package/.aether/commands/archaeology.yaml +3 -3
- package/.aether/commands/build.yaml +80 -45
- package/.aether/commands/chaos.yaml +7 -7
- package/.aether/commands/colonize.yaml +17 -17
- package/.aether/commands/continue.yaml +40 -40
- package/.aether/commands/council.yaml +6 -6
- package/.aether/commands/data-clean.yaml +3 -3
- package/.aether/commands/dream.yaml +2 -2
- package/.aether/commands/entomb.yaml +12 -12
- package/.aether/commands/export-signals.yaml +2 -2
- package/.aether/commands/feedback.yaml +6 -6
- package/.aether/commands/flag.yaml +2 -2
- package/.aether/commands/flags.yaml +4 -4
- package/.aether/commands/focus.yaml +6 -6
- package/.aether/commands/help.yaml +1 -1
- package/.aether/commands/history.yaml +1 -1
- package/.aether/commands/import-signals.yaml +2 -2
- package/.aether/commands/init.yaml +44 -27
- package/.aether/commands/insert-phase.yaml +1 -1
- package/.aether/commands/interpret.yaml +2 -2
- package/.aether/commands/lay-eggs.yaml +3 -3
- package/.aether/commands/maturity.yaml +2 -2
- package/.aether/commands/memory-details.yaml +1 -1
- package/.aether/commands/migrate-state.yaml +1 -1
- package/.aether/commands/oracle.yaml +147 -82
- package/.aether/commands/organize.yaml +5 -5
- package/.aether/commands/patrol.yaml +8 -8
- package/.aether/commands/pause-colony.yaml +7 -7
- package/.aether/commands/phase.yaml +1 -1
- package/.aether/commands/pheromones.yaml +1 -1
- package/.aether/commands/plan.yaml +14 -14
- package/.aether/commands/quick.yaml +4 -4
- package/.aether/commands/redirect.yaml +6 -6
- package/.aether/commands/resume-colony.yaml +9 -9
- package/.aether/commands/resume.yaml +5 -38
- package/.aether/commands/run.yaml +10 -10
- package/.aether/commands/seal.yaml +33 -33
- package/.aether/commands/skill-create.yaml +4 -4
- package/.aether/commands/status.yaml +14 -14
- package/.aether/commands/swarm.yaml +14 -14
- package/.aether/commands/tunnels.yaml +7 -7
- package/.aether/commands/update.yaml +1 -1
- package/.aether/commands/verify-castes.yaml +3 -3
- package/.aether/commands/watch.yaml +15 -15
- package/.aether/docs/command-playbooks/build-complete.md +48 -15
- package/.aether/docs/command-playbooks/build-context.md +11 -11
- package/.aether/docs/command-playbooks/build-full.md +76 -76
- package/.aether/docs/command-playbooks/build-prep.md +10 -10
- package/.aether/docs/command-playbooks/build-verify.md +27 -27
- package/.aether/docs/command-playbooks/build-wave.md +38 -38
- package/.aether/docs/command-playbooks/continue-advance.md +60 -27
- package/.aether/docs/command-playbooks/continue-finalize.md +25 -11
- package/.aether/docs/command-playbooks/continue-full.md +60 -46
- package/.aether/docs/command-playbooks/continue-gates.md +18 -18
- package/.aether/docs/command-playbooks/continue-verify.md +10 -10
- package/.aether/docs/source-of-truth-map.md +10 -10
- package/.aether/docs/structural-learning-stack.md +283 -0
- package/.aether/templates/colony-state-template.json +1 -0
- package/.aether/utils/consolidation-seal.sh +196 -0
- package/.aether/utils/consolidation.sh +127 -0
- package/.aether/utils/curation-ants/archivist.sh +97 -0
- package/.aether/utils/curation-ants/critic.sh +214 -0
- package/.aether/utils/curation-ants/herald.sh +102 -0
- package/.aether/utils/curation-ants/janitor.sh +121 -0
- package/.aether/utils/curation-ants/librarian.sh +99 -0
- package/.aether/utils/curation-ants/nurse.sh +153 -0
- package/.aether/utils/curation-ants/orchestrator.sh +181 -0
- package/.aether/utils/curation-ants/scribe.sh +164 -0
- package/.aether/utils/curation-ants/sentinel.sh +119 -0
- package/.aether/utils/event-bus.sh +301 -0
- package/.aether/utils/graph.sh +559 -0
- package/.aether/utils/instinct-store.sh +401 -0
- package/.aether/utils/learning.sh +79 -7
- package/.aether/utils/oracle/oracle-stop-hook.sh +896 -0
- package/.aether/utils/session.sh +13 -0
- package/.aether/utils/state-api.sh +1 -1
- package/.aether/utils/trust-scoring.sh +347 -0
- package/.aether/utils/worktree.sh +97 -0
- package/.claude/commands/ant/archaeology.md +2 -2
- package/.claude/commands/ant/chaos.md +4 -4
- package/.claude/commands/ant/colonize.md +9 -9
- package/.claude/commands/ant/council.md +6 -6
- package/.claude/commands/ant/data-clean.md +3 -3
- package/.claude/commands/ant/dream.md +2 -2
- package/.claude/commands/ant/entomb.md +9 -9
- package/.claude/commands/ant/export-signals.md +2 -2
- package/.claude/commands/ant/feedback.md +4 -4
- package/.claude/commands/ant/flag.md +2 -2
- package/.claude/commands/ant/flags.md +4 -4
- package/.claude/commands/ant/focus.md +4 -4
- package/.claude/commands/ant/help.md +1 -1
- package/.claude/commands/ant/history.md +1 -1
- package/.claude/commands/ant/import-signals.md +2 -2
- package/.claude/commands/ant/init.md +44 -27
- package/.claude/commands/ant/insert-phase.md +1 -1
- package/.claude/commands/ant/interpret.md +2 -2
- package/.claude/commands/ant/lay-eggs.md +2 -2
- package/.claude/commands/ant/maturity.md +2 -2
- package/.claude/commands/ant/memory-details.md +1 -1
- package/.claude/commands/ant/migrate-state.md +1 -1
- package/.claude/commands/ant/oracle.md +78 -42
- package/.claude/commands/ant/organize.md +3 -3
- package/.claude/commands/ant/patrol.md +8 -8
- package/.claude/commands/ant/pause-colony.md +5 -5
- package/.claude/commands/ant/phase.md +1 -1
- package/.claude/commands/ant/pheromones.md +1 -1
- package/.claude/commands/ant/plan.md +8 -8
- package/.claude/commands/ant/quick.md +4 -4
- package/.claude/commands/ant/redirect.md +4 -4
- package/.claude/commands/ant/resume-colony.md +5 -5
- package/.claude/commands/ant/resume.md +17 -29
- package/.claude/commands/ant/run.md +10 -10
- package/.claude/commands/ant/seal.md +25 -25
- package/.claude/commands/ant/skill-create.md +2 -2
- package/.claude/commands/ant/status.md +14 -14
- package/.claude/commands/ant/swarm.md +14 -14
- package/.claude/commands/ant/tunnels.md +4 -4
- package/.claude/commands/ant/update.md +1 -1
- package/.claude/commands/ant/verify-castes.md +2 -2
- package/.claude/commands/ant/watch.md +8 -8
- package/.opencode/commands/ant/archaeology.md +1 -1
- package/.opencode/commands/ant/build.md +80 -45
- package/.opencode/commands/ant/chaos.md +3 -3
- package/.opencode/commands/ant/colonize.md +8 -8
- package/.opencode/commands/ant/continue.md +40 -40
- package/.opencode/commands/ant/council.md +5 -5
- package/.opencode/commands/ant/data-clean.md +2 -2
- package/.opencode/commands/ant/dream.md +1 -1
- package/.opencode/commands/ant/entomb.md +3 -3
- package/.opencode/commands/ant/export-signals.md +1 -1
- package/.opencode/commands/ant/feedback.md +2 -2
- package/.opencode/commands/ant/flag.md +1 -1
- package/.opencode/commands/ant/flags.md +3 -3
- package/.opencode/commands/ant/focus.md +2 -2
- package/.opencode/commands/ant/import-signals.md +1 -1
- package/.opencode/commands/ant/init.md +44 -27
- package/.opencode/commands/ant/insert-phase.md +1 -1
- package/.opencode/commands/ant/interpret.md +1 -1
- package/.opencode/commands/ant/lay-eggs.md +2 -2
- package/.opencode/commands/ant/maturity.md +1 -1
- package/.opencode/commands/ant/memory-details.md +1 -1
- package/.opencode/commands/ant/oracle.md +69 -40
- package/.opencode/commands/ant/organize.md +2 -2
- package/.opencode/commands/ant/patrol.md +8 -8
- package/.opencode/commands/ant/pause-colony.md +2 -2
- package/.opencode/commands/ant/pheromones.md +1 -1
- package/.opencode/commands/ant/plan.md +6 -6
- package/.opencode/commands/ant/quick.md +4 -4
- package/.opencode/commands/ant/redirect.md +2 -2
- package/.opencode/commands/ant/resume-colony.md +4 -4
- package/.opencode/commands/ant/resume.md +5 -17
- package/.opencode/commands/ant/run.md +10 -10
- package/.opencode/commands/ant/seal.md +8 -8
- package/.opencode/commands/ant/skill-create.md +2 -2
- package/.opencode/commands/ant/status.md +10 -10
- package/.opencode/commands/ant/tunnels.md +3 -3
- package/.opencode/commands/ant/verify-castes.md +1 -1
- package/.opencode/commands/ant/watch.md +7 -7
- package/CHANGELOG.md +83 -0
- package/README.md +22 -9
- package/bin/cli.js +118 -3
- package/bin/lib/binary-downloader.js +267 -0
- package/bin/lib/update-transaction.js +27 -3
- package/bin/lib/version-gate.js +179 -0
- package/bin/npx-entry.js +0 -0
- package/package.json +1 -1
- package/.aether/agents/aether-ambassador.md +0 -140
- package/.aether/agents/aether-archaeologist.md +0 -108
- package/.aether/agents/aether-architect.md +0 -133
- package/.aether/agents/aether-auditor.md +0 -144
- package/.aether/agents/aether-builder.md +0 -184
- package/.aether/agents/aether-chaos.md +0 -115
- package/.aether/agents/aether-chronicler.md +0 -122
- package/.aether/agents/aether-gatekeeper.md +0 -116
- package/.aether/agents/aether-includer.md +0 -117
- package/.aether/agents/aether-keeper.md +0 -177
- package/.aether/agents/aether-measurer.md +0 -128
- package/.aether/agents/aether-oracle.md +0 -137
- package/.aether/agents/aether-probe.md +0 -133
- package/.aether/agents/aether-queen.md +0 -286
- package/.aether/agents/aether-route-setter.md +0 -130
- package/.aether/agents/aether-sage.md +0 -106
- package/.aether/agents/aether-scout.md +0 -101
- package/.aether/agents/aether-surveyor-disciplines.md +0 -391
- package/.aether/agents/aether-surveyor-nest.md +0 -329
- package/.aether/agents/aether-surveyor-pathogens.md +0 -264
- package/.aether/agents/aether-surveyor-provisions.md +0 -334
- package/.aether/agents/aether-tracker.md +0 -137
- package/.aether/agents/aether-watcher.md +0 -174
- package/.aether/agents/aether-weaver.md +0 -130
- package/.aether/commands/claude/archaeology.md +0 -334
- package/.aether/commands/claude/build.md +0 -65
- package/.aether/commands/claude/chaos.md +0 -336
- package/.aether/commands/claude/colonize.md +0 -259
- package/.aether/commands/claude/continue.md +0 -60
- package/.aether/commands/claude/council.md +0 -507
- package/.aether/commands/claude/data-clean.md +0 -81
- package/.aether/commands/claude/dream.md +0 -268
- package/.aether/commands/claude/entomb.md +0 -498
- package/.aether/commands/claude/export-signals.md +0 -57
- package/.aether/commands/claude/feedback.md +0 -96
- package/.aether/commands/claude/flag.md +0 -151
- package/.aether/commands/claude/flags.md +0 -169
- package/.aether/commands/claude/focus.md +0 -76
- package/.aether/commands/claude/help.md +0 -154
- package/.aether/commands/claude/history.md +0 -140
- package/.aether/commands/claude/import-signals.md +0 -71
- package/.aether/commands/claude/init.md +0 -505
- package/.aether/commands/claude/insert-phase.md +0 -105
- package/.aether/commands/claude/interpret.md +0 -278
- package/.aether/commands/claude/lay-eggs.md +0 -210
- package/.aether/commands/claude/maturity.md +0 -113
- package/.aether/commands/claude/memory-details.md +0 -77
- package/.aether/commands/claude/migrate-state.md +0 -171
- package/.aether/commands/claude/oracle.md +0 -642
- package/.aether/commands/claude/organize.md +0 -232
- package/.aether/commands/claude/patrol.md +0 -620
- package/.aether/commands/claude/pause-colony.md +0 -233
- package/.aether/commands/claude/phase.md +0 -115
- package/.aether/commands/claude/pheromones.md +0 -156
- package/.aether/commands/claude/plan.md +0 -693
- package/.aether/commands/claude/preferences.md +0 -65
- package/.aether/commands/claude/quick.md +0 -100
- package/.aether/commands/claude/redirect.md +0 -76
- package/.aether/commands/claude/resume-colony.md +0 -197
- package/.aether/commands/claude/resume.md +0 -388
- package/.aether/commands/claude/run.md +0 -231
- package/.aether/commands/claude/seal.md +0 -774
- package/.aether/commands/claude/skill-create.md +0 -286
- package/.aether/commands/claude/status.md +0 -410
- package/.aether/commands/claude/swarm.md +0 -349
- package/.aether/commands/claude/tunnels.md +0 -426
- package/.aether/commands/claude/update.md +0 -132
- package/.aether/commands/claude/verify-castes.md +0 -143
- package/.aether/commands/claude/watch.md +0 -239
- package/.aether/commands/opencode/archaeology.md +0 -331
- package/.aether/commands/opencode/build.md +0 -1168
- package/.aether/commands/opencode/chaos.md +0 -329
- package/.aether/commands/opencode/colonize.md +0 -195
- package/.aether/commands/opencode/continue.md +0 -1436
- package/.aether/commands/opencode/council.md +0 -437
- package/.aether/commands/opencode/data-clean.md +0 -77
- package/.aether/commands/opencode/dream.md +0 -260
- package/.aether/commands/opencode/entomb.md +0 -377
- package/.aether/commands/opencode/export-signals.md +0 -54
- package/.aether/commands/opencode/feedback.md +0 -99
- package/.aether/commands/opencode/flag.md +0 -149
- package/.aether/commands/opencode/flags.md +0 -167
- package/.aether/commands/opencode/focus.md +0 -73
- package/.aether/commands/opencode/help.md +0 -157
- package/.aether/commands/opencode/history.md +0 -136
- package/.aether/commands/opencode/import-signals.md +0 -68
- package/.aether/commands/opencode/init.md +0 -518
- package/.aether/commands/opencode/insert-phase.md +0 -111
- package/.aether/commands/opencode/interpret.md +0 -272
- package/.aether/commands/opencode/lay-eggs.md +0 -213
- package/.aether/commands/opencode/maturity.md +0 -108
- package/.aether/commands/opencode/memory-details.md +0 -83
- package/.aether/commands/opencode/migrate-state.md +0 -165
- package/.aether/commands/opencode/oracle.md +0 -593
- package/.aether/commands/opencode/organize.md +0 -226
- package/.aether/commands/opencode/patrol.md +0 -626
- package/.aether/commands/opencode/pause-colony.md +0 -203
- package/.aether/commands/opencode/phase.md +0 -113
- package/.aether/commands/opencode/pheromones.md +0 -162
- package/.aether/commands/opencode/plan.md +0 -684
- package/.aether/commands/opencode/preferences.md +0 -71
- package/.aether/commands/opencode/quick.md +0 -91
- package/.aether/commands/opencode/redirect.md +0 -84
- package/.aether/commands/opencode/resume-colony.md +0 -190
- package/.aether/commands/opencode/resume.md +0 -394
- package/.aether/commands/opencode/run.md +0 -237
- package/.aether/commands/opencode/seal.md +0 -452
- package/.aether/commands/opencode/skill-create.md +0 -63
- package/.aether/commands/opencode/status.md +0 -307
- package/.aether/commands/opencode/swarm.md +0 -15
- package/.aether/commands/opencode/tunnels.md +0 -400
- package/.aether/commands/opencode/update.md +0 -127
- package/.aether/commands/opencode/verify-castes.md +0 -139
- package/.aether/commands/opencode/watch.md +0 -227
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Graph traversal layer for instinct relationships — Aether Structural Learning Stack
|
|
3
|
+
# Provides: _graph_link, _graph_neighbors, _graph_reach, _graph_cluster
|
|
4
|
+
#
|
|
5
|
+
# These functions are sourced by aether-utils.sh at startup.
|
|
6
|
+
# All shared infrastructure (json_ok, json_err, atomic_write,
|
|
7
|
+
# COLONY_DATA_DIR, SCRIPT_DIR, error constants) is available.
|
|
8
|
+
#
|
|
9
|
+
# Graph is stored as JSON at $COLONY_DATA_DIR/instinct-graph.json:
|
|
10
|
+
# {
|
|
11
|
+
# "version": "1.0",
|
|
12
|
+
# "edges": [
|
|
13
|
+
# { "source": "id", "target": "id", "relationship": "type",
|
|
14
|
+
# "weight": 0.5, "created_at": "ISO8601" }
|
|
15
|
+
# ]
|
|
16
|
+
# }
|
|
17
|
+
#
|
|
18
|
+
# Relationship types: reinforces, contradicts, extends, supersedes, related
|
|
19
|
+
|
|
20
|
+
# ============================================================================
|
|
21
|
+
# _graph_init_file
|
|
22
|
+
# Ensure the graph file exists with an empty structure.
|
|
23
|
+
# Internal helper.
|
|
24
|
+
# ============================================================================
|
|
25
|
+
_graph_init_file() {
|
|
26
|
+
local graph_file="$1"
|
|
27
|
+
if [[ ! -f "$graph_file" ]]; then
|
|
28
|
+
local dir
|
|
29
|
+
dir="$(dirname "$graph_file")"
|
|
30
|
+
mkdir -p "$dir"
|
|
31
|
+
atomic_write "$graph_file" '{"version":"1.0","edges":[]}'
|
|
32
|
+
fi
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# ============================================================================
|
|
36
|
+
# _graph_link
|
|
37
|
+
# Create a directed edge between two instinct IDs. If the same
|
|
38
|
+
# source+target+relationship already exists, update the weight instead.
|
|
39
|
+
#
|
|
40
|
+
# Usage: graph-link --source <id> --target <id> --relationship <type>
|
|
41
|
+
# [--weight <float>]
|
|
42
|
+
#
|
|
43
|
+
# Relationship types: reinforces, contradicts, extends, supersedes, related
|
|
44
|
+
# Default weight: 0.5
|
|
45
|
+
#
|
|
46
|
+
# Output: {edge_id, source, target, relationship, weight, action}
|
|
47
|
+
# ============================================================================
|
|
48
|
+
_graph_link() {
|
|
49
|
+
local source_id=""
|
|
50
|
+
local target_id=""
|
|
51
|
+
local relationship=""
|
|
52
|
+
local weight="0.5"
|
|
53
|
+
|
|
54
|
+
while [[ $# -gt 0 ]]; do
|
|
55
|
+
case "$1" in
|
|
56
|
+
--source)
|
|
57
|
+
source_id="${2:-}"
|
|
58
|
+
shift 2
|
|
59
|
+
;;
|
|
60
|
+
--target)
|
|
61
|
+
target_id="${2:-}"
|
|
62
|
+
shift 2
|
|
63
|
+
;;
|
|
64
|
+
--relationship)
|
|
65
|
+
relationship="${2:-}"
|
|
66
|
+
shift 2
|
|
67
|
+
;;
|
|
68
|
+
--weight)
|
|
69
|
+
weight="${2:-0.5}"
|
|
70
|
+
shift 2
|
|
71
|
+
;;
|
|
72
|
+
*)
|
|
73
|
+
json_err "$E_VALIDATION_FAILED" "Usage: graph-link --source <id> --target <id> --relationship <type> [--weight <float>]"
|
|
74
|
+
return
|
|
75
|
+
;;
|
|
76
|
+
esac
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
[[ -z "$source_id" || -z "$target_id" || -z "$relationship" ]] && \
|
|
80
|
+
json_err "$E_VALIDATION_FAILED" "Usage: graph-link --source <id> --target <id> --relationship <type> [--weight <float>]"
|
|
81
|
+
|
|
82
|
+
# Validate relationship type
|
|
83
|
+
case "$relationship" in
|
|
84
|
+
reinforces|contradicts|extends|supersedes|related) ;;
|
|
85
|
+
*)
|
|
86
|
+
json_err "$E_VALIDATION_FAILED" "Unknown relationship: $relationship. Valid: reinforces, contradicts, extends, supersedes, related"
|
|
87
|
+
return
|
|
88
|
+
;;
|
|
89
|
+
esac
|
|
90
|
+
|
|
91
|
+
# Validate weight is a non-negative number
|
|
92
|
+
if ! [[ "$weight" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
|
|
93
|
+
json_err "$E_VALIDATION_FAILED" "--weight must be a non-negative number, got: $weight"
|
|
94
|
+
return
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
local graph_file="$COLONY_DATA_DIR/instinct-graph.json"
|
|
98
|
+
_graph_init_file "$graph_file"
|
|
99
|
+
|
|
100
|
+
local ts
|
|
101
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
102
|
+
|
|
103
|
+
# Check if a matching edge (same source+target+relationship) exists
|
|
104
|
+
local existing_edge_id
|
|
105
|
+
existing_edge_id=$(jq -r \
|
|
106
|
+
--arg src "$source_id" \
|
|
107
|
+
--arg tgt "$target_id" \
|
|
108
|
+
--arg rel "$relationship" \
|
|
109
|
+
'.edges[] | select(.source == $src and .target == $tgt and .relationship == $rel) | .edge_id' \
|
|
110
|
+
"$graph_file" | head -1)
|
|
111
|
+
|
|
112
|
+
local action
|
|
113
|
+
local updated
|
|
114
|
+
if [[ -n "$existing_edge_id" ]]; then
|
|
115
|
+
# Update existing edge weight
|
|
116
|
+
action="updated"
|
|
117
|
+
updated=$(jq \
|
|
118
|
+
--arg src "$source_id" \
|
|
119
|
+
--arg tgt "$target_id" \
|
|
120
|
+
--arg rel "$relationship" \
|
|
121
|
+
--argjson w "$weight" \
|
|
122
|
+
'.edges = [.edges[] | if (.source == $src and .target == $tgt and .relationship == $rel) then .weight = $w else . end]' \
|
|
123
|
+
"$graph_file")
|
|
124
|
+
local edge_id="$existing_edge_id"
|
|
125
|
+
else
|
|
126
|
+
# Create new edge
|
|
127
|
+
action="created"
|
|
128
|
+
local edge_id
|
|
129
|
+
edge_id="edge_$(date -u +%s)_$(head -c 2 /dev/urandom | od -An -tx1 | tr -d ' \n')"
|
|
130
|
+
updated=$(jq \
|
|
131
|
+
--arg eid "$edge_id" \
|
|
132
|
+
--arg src "$source_id" \
|
|
133
|
+
--arg tgt "$target_id" \
|
|
134
|
+
--arg rel "$relationship" \
|
|
135
|
+
--argjson w "$weight" \
|
|
136
|
+
--arg ts "$ts" \
|
|
137
|
+
'.edges += [{edge_id: $eid, source: $src, target: $tgt, relationship: $rel, weight: $w, created_at: $ts}]' \
|
|
138
|
+
"$graph_file")
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
atomic_write "$graph_file" "$updated"
|
|
142
|
+
|
|
143
|
+
# Re-read the final edge_id for the output (handles both create and update paths)
|
|
144
|
+
local final_edge_id
|
|
145
|
+
final_edge_id=$(jq -r \
|
|
146
|
+
--arg src "$source_id" \
|
|
147
|
+
--arg tgt "$target_id" \
|
|
148
|
+
--arg rel "$relationship" \
|
|
149
|
+
'.edges[] | select(.source == $src and .target == $tgt and .relationship == $rel) | .edge_id' \
|
|
150
|
+
"$graph_file" | head -1)
|
|
151
|
+
|
|
152
|
+
json_ok "$(jq -n \
|
|
153
|
+
--arg edge_id "$final_edge_id" \
|
|
154
|
+
--arg source "$source_id" \
|
|
155
|
+
--arg target "$target_id" \
|
|
156
|
+
--arg relationship "$relationship" \
|
|
157
|
+
--argjson weight "$weight" \
|
|
158
|
+
--arg action "$action" \
|
|
159
|
+
'{
|
|
160
|
+
edge_id: $edge_id,
|
|
161
|
+
source: $source,
|
|
162
|
+
target: $target,
|
|
163
|
+
relationship: $relationship,
|
|
164
|
+
weight: $weight,
|
|
165
|
+
action: $action
|
|
166
|
+
}')"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# ============================================================================
|
|
170
|
+
# _graph_neighbors
|
|
171
|
+
# Find all nodes connected to a given instinct (1-hop).
|
|
172
|
+
#
|
|
173
|
+
# Usage: graph-neighbors --id <instinct_id> [--direction out|in|both]
|
|
174
|
+
# [--relationship <type>]
|
|
175
|
+
#
|
|
176
|
+
# Default direction: both
|
|
177
|
+
#
|
|
178
|
+
# Output: {neighbors: [{id, relationship, weight, direction}], count}
|
|
179
|
+
# ============================================================================
|
|
180
|
+
_graph_neighbors() {
|
|
181
|
+
local instinct_id=""
|
|
182
|
+
local direction="both"
|
|
183
|
+
local filter_rel=""
|
|
184
|
+
|
|
185
|
+
while [[ $# -gt 0 ]]; do
|
|
186
|
+
case "$1" in
|
|
187
|
+
--id)
|
|
188
|
+
instinct_id="${2:-}"
|
|
189
|
+
shift 2
|
|
190
|
+
;;
|
|
191
|
+
--direction)
|
|
192
|
+
direction="${2:-both}"
|
|
193
|
+
shift 2
|
|
194
|
+
;;
|
|
195
|
+
--relationship)
|
|
196
|
+
filter_rel="${2:-}"
|
|
197
|
+
shift 2
|
|
198
|
+
;;
|
|
199
|
+
*)
|
|
200
|
+
json_err "$E_VALIDATION_FAILED" "Usage: graph-neighbors --id <instinct_id> [--direction out|in|both] [--relationship <type>]"
|
|
201
|
+
return
|
|
202
|
+
;;
|
|
203
|
+
esac
|
|
204
|
+
done
|
|
205
|
+
|
|
206
|
+
[[ -z "$instinct_id" ]] && \
|
|
207
|
+
json_err "$E_VALIDATION_FAILED" "Usage: graph-neighbors --id <instinct_id> [--direction out|in|both] [--relationship <type>]"
|
|
208
|
+
|
|
209
|
+
case "$direction" in
|
|
210
|
+
out|in|both) ;;
|
|
211
|
+
*)
|
|
212
|
+
json_err "$E_VALIDATION_FAILED" "Invalid direction: $direction. Valid: out, in, both"
|
|
213
|
+
return
|
|
214
|
+
;;
|
|
215
|
+
esac
|
|
216
|
+
|
|
217
|
+
local graph_file="$COLONY_DATA_DIR/instinct-graph.json"
|
|
218
|
+
|
|
219
|
+
# Return empty if graph file doesn't exist yet
|
|
220
|
+
if [[ ! -f "$graph_file" ]]; then
|
|
221
|
+
json_ok '{"neighbors":[],"count":0}'
|
|
222
|
+
return
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
local neighbors
|
|
226
|
+
neighbors=$(jq -c \
|
|
227
|
+
--arg id "$instinct_id" \
|
|
228
|
+
--arg dir "$direction" \
|
|
229
|
+
--arg rel "$filter_rel" \
|
|
230
|
+
'
|
|
231
|
+
[
|
|
232
|
+
# Outbound edges: id is the source
|
|
233
|
+
if ($dir == "out" or $dir == "both") then
|
|
234
|
+
.edges[]
|
|
235
|
+
| select(.source == $id)
|
|
236
|
+
| select($rel == "" or .relationship == $rel)
|
|
237
|
+
| {id: .target, relationship: .relationship, weight: .weight, direction: "out"}
|
|
238
|
+
else empty end
|
|
239
|
+
,
|
|
240
|
+
# Inbound edges: id is the target
|
|
241
|
+
if ($dir == "in" or $dir == "both") then
|
|
242
|
+
.edges[]
|
|
243
|
+
| select(.target == $id)
|
|
244
|
+
| select($rel == "" or .relationship == $rel)
|
|
245
|
+
| {id: .source, relationship: .relationship, weight: .weight, direction: "in"}
|
|
246
|
+
else empty end
|
|
247
|
+
]
|
|
248
|
+
' \
|
|
249
|
+
"$graph_file")
|
|
250
|
+
|
|
251
|
+
local count
|
|
252
|
+
count=$(echo "$neighbors" | jq 'length')
|
|
253
|
+
|
|
254
|
+
json_ok "$(jq -n \
|
|
255
|
+
--argjson neighbors "$neighbors" \
|
|
256
|
+
--argjson count "$count" \
|
|
257
|
+
'{neighbors: $neighbors, count: $count}')"
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# ============================================================================
|
|
261
|
+
# _graph_reach
|
|
262
|
+
# Find all nodes reachable within N hops using iterative BFS.
|
|
263
|
+
# One jq call per hop level to avoid complex recursive expressions.
|
|
264
|
+
#
|
|
265
|
+
# Usage: graph-reach --id <instinct_id> --hops <N> [--min-weight <float>]
|
|
266
|
+
#
|
|
267
|
+
# Max hops enforced at 3 to prevent expensive traversals.
|
|
268
|
+
# Default min-weight: 0.0
|
|
269
|
+
#
|
|
270
|
+
# Output: {reachable: [{id, hop, path}], count, hops_searched}
|
|
271
|
+
# ============================================================================
|
|
272
|
+
_graph_reach() {
|
|
273
|
+
local instinct_id=""
|
|
274
|
+
local hops=""
|
|
275
|
+
local min_weight="0.0"
|
|
276
|
+
|
|
277
|
+
while [[ $# -gt 0 ]]; do
|
|
278
|
+
case "$1" in
|
|
279
|
+
--id)
|
|
280
|
+
instinct_id="${2:-}"
|
|
281
|
+
shift 2
|
|
282
|
+
;;
|
|
283
|
+
--hops)
|
|
284
|
+
hops="${2:-}"
|
|
285
|
+
shift 2
|
|
286
|
+
;;
|
|
287
|
+
--min-weight)
|
|
288
|
+
min_weight="${2:-0.0}"
|
|
289
|
+
shift 2
|
|
290
|
+
;;
|
|
291
|
+
*)
|
|
292
|
+
json_err "$E_VALIDATION_FAILED" "Usage: graph-reach --id <instinct_id> --hops <N> [--min-weight <float>]"
|
|
293
|
+
return
|
|
294
|
+
;;
|
|
295
|
+
esac
|
|
296
|
+
done
|
|
297
|
+
|
|
298
|
+
[[ -z "$instinct_id" || -z "$hops" ]] && \
|
|
299
|
+
json_err "$E_VALIDATION_FAILED" "Usage: graph-reach --id <instinct_id> --hops <N> [--min-weight <float>]"
|
|
300
|
+
|
|
301
|
+
# Validate hops is a positive integer
|
|
302
|
+
if ! [[ "$hops" =~ ^[0-9]+$ ]] || [[ "$hops" -lt 1 ]]; then
|
|
303
|
+
json_err "$E_VALIDATION_FAILED" "--hops must be a positive integer, got: $hops"
|
|
304
|
+
return
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
# Clamp hops to max 3
|
|
308
|
+
local MAX_HOPS=3
|
|
309
|
+
local hops_searched="$hops"
|
|
310
|
+
if [[ "$hops" -gt "$MAX_HOPS" ]]; then
|
|
311
|
+
hops_searched="$MAX_HOPS"
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
# Validate min_weight
|
|
315
|
+
if ! [[ "$min_weight" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
|
|
316
|
+
json_err "$E_VALIDATION_FAILED" "--min-weight must be a non-negative number, got: $min_weight"
|
|
317
|
+
return
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
local graph_file="$COLONY_DATA_DIR/instinct-graph.json"
|
|
321
|
+
|
|
322
|
+
if [[ ! -f "$graph_file" ]]; then
|
|
323
|
+
json_ok "{\"reachable\":[],\"count\":0,\"hops_searched\":$hops_searched}"
|
|
324
|
+
return
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
# Read all edges once
|
|
328
|
+
local all_edges
|
|
329
|
+
all_edges=$(jq -c '.edges' "$graph_file")
|
|
330
|
+
|
|
331
|
+
# BFS: frontier is the set of IDs at the current hop level
|
|
332
|
+
# reachable accumulates {id, hop, path} for all visited nodes
|
|
333
|
+
# visited tracks IDs seen to avoid cycles
|
|
334
|
+
|
|
335
|
+
# frontier: JSON array of {id, path} objects
|
|
336
|
+
local frontier
|
|
337
|
+
frontier="[{\"id\":\"$instinct_id\",\"path\":[\"$instinct_id\"]}]"
|
|
338
|
+
local visited
|
|
339
|
+
visited="[\"$instinct_id\"]"
|
|
340
|
+
local reachable="[]"
|
|
341
|
+
|
|
342
|
+
local current_hop=0
|
|
343
|
+
while [[ "$current_hop" -lt "$hops_searched" ]]; do
|
|
344
|
+
current_hop=$((current_hop + 1))
|
|
345
|
+
|
|
346
|
+
# Expand frontier: find all outbound neighbors not yet visited
|
|
347
|
+
local new_nodes
|
|
348
|
+
new_nodes=$(jq -c \
|
|
349
|
+
--argjson edges "$all_edges" \
|
|
350
|
+
--argjson frontier "$frontier" \
|
|
351
|
+
--argjson visited "$visited" \
|
|
352
|
+
--argjson hop "$current_hop" \
|
|
353
|
+
--argjson mw "$min_weight" \
|
|
354
|
+
'
|
|
355
|
+
[
|
|
356
|
+
$frontier[] as $f |
|
|
357
|
+
$edges[] |
|
|
358
|
+
select(.source == $f.id) |
|
|
359
|
+
select(.weight >= $mw) |
|
|
360
|
+
select(.target as $t | ($visited | index($t)) == null) |
|
|
361
|
+
{id: .target, hop: $hop, path: ($f.path + [.target])}
|
|
362
|
+
] | unique_by(.id)
|
|
363
|
+
' \
|
|
364
|
+
<<< "null")
|
|
365
|
+
|
|
366
|
+
# If no new nodes found, stop early
|
|
367
|
+
local new_count
|
|
368
|
+
new_count=$(echo "$new_nodes" | jq 'length')
|
|
369
|
+
if [[ "$new_count" -eq 0 ]]; then
|
|
370
|
+
break
|
|
371
|
+
fi
|
|
372
|
+
|
|
373
|
+
# Append new nodes to reachable
|
|
374
|
+
reachable=$(jq -c \
|
|
375
|
+
--argjson existing "$reachable" \
|
|
376
|
+
--argjson new "$new_nodes" \
|
|
377
|
+
'$existing + $new' \
|
|
378
|
+
<<< "null")
|
|
379
|
+
|
|
380
|
+
# Update visited set
|
|
381
|
+
visited=$(jq -c \
|
|
382
|
+
--argjson visited "$visited" \
|
|
383
|
+
--argjson new "$new_nodes" \
|
|
384
|
+
'$visited + [$new[].id]' \
|
|
385
|
+
<<< "null")
|
|
386
|
+
|
|
387
|
+
# Update frontier for next hop
|
|
388
|
+
frontier=$(jq -c \
|
|
389
|
+
--argjson new "$new_nodes" \
|
|
390
|
+
'[.[] | {id: .id, path: .path}]' \
|
|
391
|
+
<<< "$new_nodes")
|
|
392
|
+
done
|
|
393
|
+
|
|
394
|
+
local count
|
|
395
|
+
count=$(echo "$reachable" | jq 'length')
|
|
396
|
+
|
|
397
|
+
json_ok "$(jq -n \
|
|
398
|
+
--argjson reachable "$reachable" \
|
|
399
|
+
--argjson count "$count" \
|
|
400
|
+
--argjson hops_searched "$hops_searched" \
|
|
401
|
+
'{reachable: $reachable, count: $count, hops_searched: $hops_searched}')"
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# ============================================================================
|
|
405
|
+
# _graph_cluster
|
|
406
|
+
# Find clusters of strongly connected instincts.
|
|
407
|
+
# A cluster is a group of nodes that share >= min-edges connections
|
|
408
|
+
# all with weight >= min-weight.
|
|
409
|
+
#
|
|
410
|
+
# Usage: graph-cluster [--min-edges <N>] [--min-weight <float>]
|
|
411
|
+
#
|
|
412
|
+
# Default min-edges: 2
|
|
413
|
+
# Default min-weight: 0.3
|
|
414
|
+
#
|
|
415
|
+
# Output: {clusters: [{nodes, edge_count, avg_weight}], count}
|
|
416
|
+
# ============================================================================
|
|
417
|
+
_graph_cluster() {
|
|
418
|
+
local min_edges=2
|
|
419
|
+
local min_weight="0.3"
|
|
420
|
+
|
|
421
|
+
while [[ $# -gt 0 ]]; do
|
|
422
|
+
case "$1" in
|
|
423
|
+
--min-edges)
|
|
424
|
+
min_edges="${2:-2}"
|
|
425
|
+
shift 2
|
|
426
|
+
;;
|
|
427
|
+
--min-weight)
|
|
428
|
+
min_weight="${2:-0.3}"
|
|
429
|
+
shift 2
|
|
430
|
+
;;
|
|
431
|
+
*)
|
|
432
|
+
json_err "$E_VALIDATION_FAILED" "Usage: graph-cluster [--min-edges <N>] [--min-weight <float>]"
|
|
433
|
+
return
|
|
434
|
+
;;
|
|
435
|
+
esac
|
|
436
|
+
done
|
|
437
|
+
|
|
438
|
+
# Validate min_edges
|
|
439
|
+
if ! [[ "$min_edges" =~ ^[0-9]+$ ]]; then
|
|
440
|
+
json_err "$E_VALIDATION_FAILED" "--min-edges must be a non-negative integer, got: $min_edges"
|
|
441
|
+
return
|
|
442
|
+
fi
|
|
443
|
+
|
|
444
|
+
# Validate min_weight
|
|
445
|
+
if ! [[ "$min_weight" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
|
|
446
|
+
json_err "$E_VALIDATION_FAILED" "--min-weight must be a non-negative number, got: $min_weight"
|
|
447
|
+
return
|
|
448
|
+
fi
|
|
449
|
+
|
|
450
|
+
local graph_file="$COLONY_DATA_DIR/instinct-graph.json"
|
|
451
|
+
|
|
452
|
+
if [[ ! -f "$graph_file" ]]; then
|
|
453
|
+
json_ok '{"clusters":[],"count":0}'
|
|
454
|
+
return
|
|
455
|
+
fi
|
|
456
|
+
|
|
457
|
+
# Strategy:
|
|
458
|
+
# 1. Filter edges by min-weight
|
|
459
|
+
# 2. For each node, count qualifying edges (in + out)
|
|
460
|
+
# 3. Nodes with >= min-edges form candidates
|
|
461
|
+
# 4. Group connected candidate nodes into clusters via union-find in jq
|
|
462
|
+
|
|
463
|
+
local clusters
|
|
464
|
+
clusters=$(jq -c \
|
|
465
|
+
--argjson min_edges "$min_edges" \
|
|
466
|
+
--argjson min_weight "$min_weight" \
|
|
467
|
+
'
|
|
468
|
+
# Step 1: filter qualifying edges
|
|
469
|
+
(.edges | map(select(.weight >= $min_weight))) as $qual_edges |
|
|
470
|
+
|
|
471
|
+
# Step 2: count edges per node (source + target)
|
|
472
|
+
(
|
|
473
|
+
$qual_edges |
|
|
474
|
+
group_by(.source) |
|
|
475
|
+
map({key: .[0].source, value: length}) |
|
|
476
|
+
from_entries
|
|
477
|
+
) as $out_counts |
|
|
478
|
+
(
|
|
479
|
+
$qual_edges |
|
|
480
|
+
group_by(.target) |
|
|
481
|
+
map({key: .[0].target, value: length}) |
|
|
482
|
+
from_entries
|
|
483
|
+
) as $in_counts |
|
|
484
|
+
|
|
485
|
+
# Step 3: build total edge counts per node
|
|
486
|
+
(
|
|
487
|
+
[$qual_edges[] | .source, .target] | unique |
|
|
488
|
+
map({
|
|
489
|
+
id: .,
|
|
490
|
+
edge_count: ((($out_counts[.] // 0) + ($in_counts[.] // 0)))
|
|
491
|
+
}) |
|
|
492
|
+
map(select(.edge_count >= $min_edges))
|
|
493
|
+
) as $candidates |
|
|
494
|
+
|
|
495
|
+
# Step 4: group candidates into clusters via connected components
|
|
496
|
+
# Build adjacency: pairs of candidate nodes that share a qualifying edge
|
|
497
|
+
(
|
|
498
|
+
$candidates | map(.id)
|
|
499
|
+
) as $candidate_ids |
|
|
500
|
+
|
|
501
|
+
(
|
|
502
|
+
$qual_edges |
|
|
503
|
+
map(select(
|
|
504
|
+
(.source as $s | ($candidate_ids | index($s)) != null) and
|
|
505
|
+
(.target as $t | ($candidate_ids | index($t)) != null)
|
|
506
|
+
)) |
|
|
507
|
+
map({a: .source, b: .target, w: .weight})
|
|
508
|
+
) as $adj |
|
|
509
|
+
|
|
510
|
+
# Build clusters: use greedy union approach
|
|
511
|
+
# Start each candidate in its own group, merge groups with shared edges
|
|
512
|
+
reduce $adj[] as $edge (
|
|
513
|
+
($candidate_ids | map({id: ., group: .}));
|
|
514
|
+
. as $groups |
|
|
515
|
+
($groups | map(select(.id == $edge.a)) | first.group) as $ga |
|
|
516
|
+
($groups | map(select(.id == $edge.b)) | first.group) as $gb |
|
|
517
|
+
if $ga == $gb then $groups
|
|
518
|
+
else
|
|
519
|
+
# Merge all nodes with group $gb into group $ga
|
|
520
|
+
[ .[] | if .group == $gb then .group = $ga else . end ]
|
|
521
|
+
end
|
|
522
|
+
) |
|
|
523
|
+
# Group by cluster label
|
|
524
|
+
group_by(.group) |
|
|
525
|
+
map({
|
|
526
|
+
nodes: map(.id),
|
|
527
|
+
edge_count: (
|
|
528
|
+
. as $cluster_nodes |
|
|
529
|
+
($cluster_nodes | map(.id)) as $node_ids |
|
|
530
|
+
$adj | map(select(
|
|
531
|
+
(.a as $a | ($node_ids | index($a)) != null) and
|
|
532
|
+
(.b as $b | ($node_ids | index($b)) != null)
|
|
533
|
+
)) | length
|
|
534
|
+
),
|
|
535
|
+
avg_weight: (
|
|
536
|
+
. as $cluster_nodes |
|
|
537
|
+
($cluster_nodes | map(.id)) as $node_ids |
|
|
538
|
+
($adj | map(select(
|
|
539
|
+
(.a as $a | ($node_ids | index($a)) != null) and
|
|
540
|
+
(.b as $b | ($node_ids | index($b)) != null)
|
|
541
|
+
)) | map(.w)) as $weights |
|
|
542
|
+
if ($weights | length) > 0 then
|
|
543
|
+
($weights | add) / ($weights | length)
|
|
544
|
+
else 0 end
|
|
545
|
+
)
|
|
546
|
+
}) |
|
|
547
|
+
# Only keep clusters with more than 1 node
|
|
548
|
+
map(select(.nodes | length > 1))
|
|
549
|
+
' \
|
|
550
|
+
"$graph_file")
|
|
551
|
+
|
|
552
|
+
local count
|
|
553
|
+
count=$(echo "$clusters" | jq 'length')
|
|
554
|
+
|
|
555
|
+
json_ok "$(jq -n \
|
|
556
|
+
--argjson clusters "$clusters" \
|
|
557
|
+
--argjson count "$count" \
|
|
558
|
+
'{clusters: $clusters, count: $count}')"
|
|
559
|
+
}
|