aether-colony 3.1.15 → 3.1.16

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.
@@ -68,6 +68,15 @@ SYSTEM_FILES=(
68
68
  "utils/xml-core.sh"
69
69
  "utils/xml-compose.sh"
70
70
  "utils/queen-to-md.xsl"
71
+ "exchange/pheromone-xml.sh"
72
+ "exchange/wisdom-xml.sh"
73
+ "exchange/registry-xml.sh"
74
+ "schemas/aether-types.xsd"
75
+ "schemas/pheromone.xsd"
76
+ "schemas/queen-wisdom.xsd"
77
+ "schemas/colony-registry.xsd"
78
+ "schemas/worker-priming.xsd"
79
+ "schemas/prompt.xsd"
71
80
  "templates/QUEEN.md.template"
72
81
  )
73
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aether-colony",
3
- "version": "3.1.15",
3
+ "version": "3.1.16",
4
4
  "description": "Multi-agent system using ant colony intelligence for Claude Code and OpenCode — workers self-organize via pheromone signals",
5
5
  "bin": {
6
6
  "aether": "bin/cli.js"
@@ -0,0 +1,574 @@
1
+ #!/bin/bash
2
+ # Pheromone Exchange Module
3
+ # JSON/XML bidirectional conversion for pheromone signals
4
+ #
5
+ # Usage: source .aether/exchange/pheromone-xml.sh
6
+ # xml-pheromone-export <pheromone_json> [output_xml]
7
+ # xml-pheromone-import <pheromone_xml> [output_json]
8
+ # xml-pheromone-validate <pheromone_xml>
9
+ # xml-pheromone-merge <colony_prefix> <xml_files...> [output_xml]
10
+
11
+ # Don't use set -e for library-style scripts - let callers handle errors
12
+ # set -euo pipefail
13
+
14
+ # Source dependencies - handle being sourced vs executed
15
+ if [[ -n "${BASH_SOURCE[0]:-}" ]]; then
16
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
17
+ else
18
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
19
+ fi
20
+ source "$SCRIPT_DIR/utils/xml-core.sh"
21
+
22
+ # Ensure tool availability variables are available
23
+ if [[ -z "${XMLLINT_AVAILABLE:-}" ]]; then
24
+ XMLLINT_AVAILABLE=false
25
+ command -v xmllint >/dev/null 2>&1 && XMLLINT_AVAILABLE=true
26
+ fi
27
+ if [[ -z "${XMLSTARLET_AVAILABLE:-}" ]]; then
28
+ XMLSTARLET_AVAILABLE=false
29
+ command -v xmlstarlet >/dev/null 2>&1 && XMLSTARLET_AVAILABLE=true
30
+ fi
31
+
32
+ # ============================================================================
33
+ # Pheromone Export (JSON to XML)
34
+ # ============================================================================
35
+
36
+ # xml-pheromone-export: Convert pheromone JSON to XML format
37
+ # Usage: xml-pheromone-export <pheromone_json_file> [output_xml_file]
38
+ # Returns: {"ok":true,"result":{"xml":"...","path":"..."}}
39
+ xml-pheromone-export() {
40
+ local json_file="${1:-}"
41
+ local output_xml="${2:-}"
42
+ local xsd_file="${3:-.aether/schemas/pheromone.xsd}"
43
+
44
+ [[ -z "$json_file" ]] && { xml_json_err "MISSING_ARG" "Missing JSON file argument"; return 1; }
45
+ [[ -f "$json_file" ]] || { xml_json_err "FILE_NOT_FOUND" "JSON file not found: $json_file"; return 1; }
46
+
47
+ # Validate JSON
48
+ if ! jq empty "$json_file" 2>/dev/null; then
49
+ xml_json_err "PARSE_ERROR" "Invalid JSON file: $json_file"
50
+ return 1
51
+ fi
52
+
53
+ # Generate ISO timestamp
54
+ local generated_at
55
+ generated_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
56
+
57
+ # Extract metadata
58
+ local version colony_id
59
+ version=$(jq -r '.version // "1.0.0"' "$json_file")
60
+ colony_id=$(jq -r '.colony_id // "unknown"' "$json_file")
61
+
62
+ # Build XML header
63
+ local xml_output
64
+ xml_output="<?xml version=\"1.0\" encoding=\"UTF-8\"?>
65
+ <pheromones xmlns=\"http://aether.colony/schemas/pheromones\"
66
+ xmlns:ph=\"http://aether.colony/schemas/pheromones\"
67
+ version=\"$version\"
68
+ generated_at=\"$generated_at\"
69
+ colony_id=\"$colony_id\">"
70
+
71
+ # Add metadata
72
+ local source_type context
73
+ source_type=$(jq -r '.metadata.source.type // "system"' "$json_file" 2>/dev/null || echo "system")
74
+ context=$(jq -r '.metadata.context // "Colony pheromone signals"' "$json_file" 2>/dev/null || echo "Colony pheromone signals")
75
+
76
+ xml_output="$xml_output
77
+ <metadata>
78
+ <source type=\"$source_type\">aether-pheromone-converter</source>
79
+ <context>$(echo "$context" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')</context>
80
+ </metadata>"
81
+
82
+ # Process signals
83
+ local sig_array_length
84
+ sig_array_length=$(jq '.signals | length' "$json_file" 2>/dev/null || echo "0")
85
+
86
+ local sig_idx=0
87
+ while [[ $sig_idx -lt $sig_array_length ]]; do
88
+ local signal
89
+ signal=$(jq -c ".signals[$sig_idx]" "$json_file" 2>/dev/null)
90
+ [[ -n "$signal" ]] || { ((sig_idx++)); continue; }
91
+
92
+ # Extract signal fields
93
+ local sig_id sig_type priority source created_at expires_at active
94
+ sig_id=$(echo "$signal" | jq -r '.id // "sig_'"$(date +%s)"'_'"$sig_idx"'"')
95
+ sig_type=$(echo "$signal" | jq -r '.type // "FOCUS"' | tr '[:lower:]' '[:upper:]')
96
+ priority=$(echo "$signal" | jq -r '.priority // "normal"' | tr '[:upper:]' '[:lower:]')
97
+ source=$(echo "$signal" | jq -r '.source // "system"')
98
+ created_at=$(echo "$signal" | jq -r '.created_at // "'"$generated_at"'"')
99
+ expires_at=$(echo "$signal" | jq -r '.expires_at // empty')
100
+ active=$(echo "$signal" | jq -r '.active // true')
101
+
102
+ # Validate signal type
103
+ case "$sig_type" in
104
+ FOCUS|REDIRECT|FEEDBACK) ;;
105
+ *) sig_type="FOCUS" ;;
106
+ esac
107
+
108
+ # Validate priority
109
+ case "$priority" in
110
+ critical|high|normal|low) ;;
111
+ *) priority="normal" ;;
112
+ esac
113
+
114
+ # Build signal element
115
+ xml_output="$xml_output
116
+ <signal id=\"$(echo "$sig_id" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')\"
117
+ type=\"$sig_type\"
118
+ priority=\"$priority\"
119
+ source=\"$(echo "$source" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')\"
120
+ created_at=\"$created_at\""
121
+
122
+ [[ -n "$expires_at" && "$expires_at" != "null" ]] && xml_output="$xml_output
123
+ expires_at=\"$expires_at\""
124
+
125
+ xml_output="$xml_output
126
+ active=\"$active\">"
127
+
128
+ # Content section
129
+ local content_text
130
+ content_text=$(echo "$signal" | jq -r '.content.text // .message // ""')
131
+ if [[ -n "$content_text" ]]; then
132
+ xml_output="$xml_output
133
+ <content>
134
+ <text>$(echo "$content_text" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')</text>
135
+ </content>"
136
+ fi
137
+
138
+ xml_output="$xml_output
139
+ </signal>"
140
+
141
+ ((sig_idx++))
142
+ done
143
+
144
+ xml_output="$xml_output
145
+ </pheromones>"
146
+
147
+ # Validate against schema if available
148
+ local validated=false
149
+ if [[ -f "$xsd_file" && "$XMLLINT_AVAILABLE" == "true" ]]; then
150
+ local temp_xml
151
+ temp_xml=$(mktemp)
152
+ echo "$xml_output" > "$temp_xml"
153
+ if xmllint --nonet --noent --noout --schema "$xsd_file" "$temp_xml" 2>/dev/null; then
154
+ validated=true
155
+ fi
156
+ rm -f "$temp_xml"
157
+ fi
158
+
159
+ # Output result
160
+ if [[ -n "$output_xml" ]]; then
161
+ echo "$xml_output" > "$output_xml"
162
+ xml_json_ok "{\"path\":\"$output_xml\",\"validated\":$validated}"
163
+ else
164
+ local escaped_xml
165
+ escaped_xml=$(echo "$xml_output" | jq -Rs '.')
166
+ xml_json_ok "{\"xml\":$escaped_xml,\"validated\":$validated}"
167
+ fi
168
+ }
169
+
170
+ # ============================================================================
171
+ # Pheromone Import (XML to JSON)
172
+ # ============================================================================
173
+
174
+ # xml-pheromone-import: Convert pheromone XML back to JSON
175
+ # Usage: xml-pheromone-import <pheromone_xml_file> [output_json_file]
176
+ # Returns: {"ok":true,"result":{"json":"...","signals":N,"path":"..."}}
177
+ xml-pheromone-import() {
178
+ local xml_file="${1:-}"
179
+ local output_json="${2:-}"
180
+ local preserve_prefixes="${3:-false}"
181
+
182
+ [[ -z "$xml_file" ]] && { xml_json_err "MISSING_ARG" "Missing XML file argument"; return 1; }
183
+ [[ -f "$xml_file" ]] || { xml_json_err "FILE_NOT_FOUND" "XML file not found: $xml_file"; return 1; }
184
+
185
+ # Check well-formedness
186
+ xmllint --nonet --noent --noout "$xml_file" 2>/dev/null || {
187
+ xml_json_err "PARSE_ERROR" "XML is not well-formed"
188
+ return 1
189
+ }
190
+
191
+ # Extract metadata using XPath
192
+ local version colony_id generated_at
193
+ if [[ "$XMLLINT_AVAILABLE" == "true" ]]; then
194
+ version=$(xmllint --nonet --noent --xpath "string(/*/@version)" "$xml_file" 2>/dev/null || echo "1.0.0")
195
+ colony_id=$(xmllint --nonet --noent --xpath "string(/*/@colony_id)" "$xml_file" 2>/dev/null || echo "unknown")
196
+ generated_at=$(xmllint --nonet --noent --xpath "string(/*/@generated_at)" "$xml_file" 2>/dev/null || echo "$(date -u +"%Y-%m-%dT%H:%M:%SZ")")
197
+ else
198
+ xml_json_err "TOOL_NOT_AVAILABLE" "xmllint required for XML import"
199
+ return 1
200
+ fi
201
+
202
+ # Build JSON structure
203
+ local json_output
204
+ json_output=$(jq -n \
205
+ --arg version "$version" \
206
+ --arg colony_id "$colony_id" \
207
+ --arg generated_at "$generated_at" \
208
+ '{
209
+ version: $version,
210
+ colony_id: $colony_id,
211
+ generated_at: $generated_at,
212
+ signals: []
213
+ }')
214
+
215
+ # Extract signals - use xmlstarlet if available, fallback to grep/awk
216
+ if [[ "$XMLSTARLET_AVAILABLE" == "true" ]]; then
217
+ # Use xmlstarlet for proper namespace handling
218
+ local signals_json
219
+ signals_json=$(xmlstarlet sel -N ph="http://aether.colony/schemas/pheromones" \
220
+ -t -m "//ph:signal" \
221
+ -o '{"id":"' -v "@id" -o '","type":"' -v "@type" -o '","priority":"' -v "@priority" -o '","source":"' -v "@source" -o '","created_at":"' -v "@created_at" -o '","active":' -v "@active" -o '}' \
222
+ -n "$xml_file" 2>/dev/null | jq -s '.')
223
+
224
+ # Extract content text for each signal
225
+ local idx=0
226
+ local enriched_signals="[]"
227
+ while true; do
228
+ local content_text
229
+ content_text=$(xmlstarlet sel -N ph="http://aether.colony/schemas/pheromones" \
230
+ -t -v "//ph:signal[$idx + 1]/ph:content/ph:text" "$xml_file" 2>/dev/null || echo "")
231
+
232
+ if [[ -z "$content_text" && $idx -gt 0 ]]; then
233
+ break
234
+ fi
235
+
236
+ # Add content to signal
237
+ enriched_signals=$(echo "$signals_json" | jq --arg idx "$idx" --arg text "$content_text" '
238
+ .[$idx | tonumber] |= . + {content: {text: $text}}'
239
+ )
240
+
241
+ ((idx++))
242
+ [[ $idx -gt 100 ]] && break # Safety limit
243
+ done
244
+
245
+ # Merge signals into output
246
+ if [[ "$enriched_signals" != "[]" ]]; then
247
+ json_output=$(echo "$json_output" | jq --argjson signals "$enriched_signals" '.signals = $signals')
248
+ else
249
+ json_output=$(echo "$json_output" | jq --argjson signals "$signals_json" '.signals = $signals')
250
+ fi
251
+ else
252
+ # Fallback: basic extraction with grep/sed
253
+ local fallback_signals="[]"
254
+ while IFS= read -r line; do
255
+ if [[ "$line" =~ id=\"([^\"]+)\" ]]; then
256
+ local sid="${BASH_REMATCH[1]}"
257
+ local stype="FOCUS"
258
+ local spriority="normal"
259
+
260
+ # Try to extract type and priority
261
+ if [[ "$line" =~ type=\"([^\"]+)\" ]]; then
262
+ stype="${BASH_REMATCH[1]}"
263
+ fi
264
+ if [[ "$line" =~ priority=\"([^\"]+)\" ]]; then
265
+ spriority="${BASH_REMATCH[1]}"
266
+ fi
267
+
268
+ # Remove namespace prefix if not preserving
269
+ if [[ "$preserve_prefixes" != "true" ]]; then
270
+ sid=$(echo "$sid" | sed 's/^[^:]*://')
271
+ fi
272
+
273
+ fallback_signals=$(echo "$fallback_signals" | jq \
274
+ --arg id "$sid" \
275
+ --arg type "$stype" \
276
+ --arg priority "$spriority" \
277
+ '. + [{id: $id, type: $type, priority: $priority, source: "xml-import", created_at: "'"$generated_at"'", active: true}]')
278
+ fi
279
+ done < <(grep '<signal' "$xml_file")
280
+
281
+ json_output=$(echo "$json_output" | jq --argjson signals "$fallback_signals" '.signals = $signals')
282
+ fi
283
+
284
+ local signal_count
285
+ signal_count=$(echo "$json_output" | jq '.signals | length')
286
+
287
+ # Output result
288
+ if [[ -n "$output_json" ]]; then
289
+ echo "$json_output" > "$output_json"
290
+ xml_json_ok "{\"path\":\"$output_json\",\"signals\":$signal_count}"
291
+ else
292
+ local escaped_json
293
+ escaped_json=$(echo "$json_output" | jq -Rs '.')
294
+ xml_json_ok "{\"json\":$escaped_json,\"signals\":$signal_count}"
295
+ fi
296
+ }
297
+
298
+ # ============================================================================
299
+ # Pheromone Merge (Multiple Colonies)
300
+ # ============================================================================
301
+
302
+ # xml-pheromone-merge: Merge pheromone XML from multiple colonies
303
+ # Usage: xml-pheromone-merge <output_xml> <input_xml_files...>
304
+ # Options:
305
+ # --namespace <prefix> - Add colony prefix to signal IDs (default: auto-generate from colony_id)
306
+ # --deduplicate - Remove duplicate signals by ID (default: true)
307
+ # --target <path> - Target output file (default: ~/.aether/eternal/pheromones.xml)
308
+ # Returns: {"ok":true,"result":{"path":"...","signals":N,"colonies":M}}
309
+ xml-pheromone-merge() {
310
+ local output_file=""
311
+ local namespace_prefix=""
312
+ local deduplicate=true
313
+ local input_files=()
314
+ local arg_idx=0
315
+
316
+ # Parse arguments
317
+ while [[ $arg_idx -lt $# ]]; do
318
+ local arg="${*:$((arg_idx + 1)):1}"
319
+ case "$arg" in
320
+ --namespace)
321
+ namespace_prefix="${*:$((arg_idx + 2)):1}"
322
+ ((arg_idx += 2))
323
+ ;;
324
+ --no-deduplicate)
325
+ deduplicate=false
326
+ ((arg_idx++))
327
+ ;;
328
+ --deduplicate)
329
+ deduplicate=true
330
+ ((arg_idx++))
331
+ ;;
332
+ --target)
333
+ output_file="${*:$((arg_idx + 2)):1}"
334
+ ((arg_idx += 2))
335
+ ;;
336
+ -*)
337
+ xml_json_err "INVALID_ARG" "Unknown option: $arg"
338
+ return 1
339
+ ;;
340
+ *)
341
+ if [[ -z "$output_file" ]]; then
342
+ output_file="$arg"
343
+ else
344
+ input_files+=("$arg")
345
+ fi
346
+ ((arg_idx++))
347
+ ;;
348
+ esac
349
+ done
350
+
351
+ # Default output path
352
+ [[ -z "$output_file" ]] && output_file="${HOME}/.aether/eternal/pheromones.xml"
353
+
354
+ # Validate input files
355
+ if [[ ${#input_files[@]} -eq 0 ]]; then
356
+ xml_json_err "MISSING_ARG" "No input XML files specified"
357
+ return 1
358
+ fi
359
+
360
+ for file in "${input_files[@]}"; do
361
+ [[ -f "$file" ]] || { xml_json_err "FILE_NOT_FOUND" "Input file not found: $file"; return 1; }
362
+ done
363
+
364
+ # Ensure output directory exists
365
+ mkdir -p "$(dirname "$output_file")"
366
+
367
+ # Generate merged XML
368
+ local generated_at
369
+ generated_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
370
+
371
+ local merged_xml="<?xml version=\"1.0\" encoding=\"UTF-8\"?>
372
+ <pheromones xmlns=\"http://aether.colony/schemas/pheromones\"
373
+ xmlns:ph=\"http://aether.colony/schemas/pheromones\"
374
+ version=\"1.0.0\"
375
+ generated_at=\"$generated_at\"
376
+ colony_id=\"merged\">"
377
+
378
+ merged_xml="$merged_xml
379
+ <metadata>
380
+ <source type=\"system\">aether-pheromone-merge</source>
381
+ <context>Merged pheromones from ${#input_files[@]} colonies</context>
382
+ </metadata>"
383
+
384
+ # Track unique signal IDs for deduplication
385
+ declare -A seen_ids
386
+ local total_signals=0
387
+ local unique_signals=0
388
+ local colonies_merged=0
389
+
390
+ for input_file in "${input_files[@]}"; do
391
+ # Get colony ID from source file
392
+ local source_colony_id
393
+ if [[ "$XMLLINT_AVAILABLE" == "true" ]]; then
394
+ source_colony_id=$(xmllint --nonet --noent --xpath "string(/*/@colony_id)" "$input_file" 2>/dev/null || echo "colony_$colonies_merged")
395
+ else
396
+ source_colony_id=$(grep -o 'colony_id="[^"]*"' "$input_file" | head -1 | cut -d'"' -f2 || echo "colony_$colonies_merged")
397
+ fi
398
+
399
+ # Determine prefix for this colony
400
+ local colony_prefix
401
+ if [[ -n "$namespace_prefix" ]]; then
402
+ colony_prefix="$namespace_prefix"
403
+ else
404
+ colony_prefix="$source_colony_id"
405
+ fi
406
+
407
+ # Extract signals from this file using xmlstarlet
408
+ if [[ "$XMLSTARLET_AVAILABLE" == "true" ]]; then
409
+ # Get signal count
410
+ local sig_count
411
+ sig_count=$(xmlstarlet sel -N ph="http://aether.colony/schemas/pheromones" \
412
+ -t -v "count(//ph:signal)" "$input_file" 2>/dev/null || echo "0")
413
+
414
+ local idx=1
415
+ while [[ $idx -le $sig_count ]]; do
416
+ # Extract full signal element
417
+ local sig_xml
418
+ sig_xml=$(xmlstarlet sel -N ph="http://aether.colony/schemas/pheromones" \
419
+ -t -c "//ph:signal[$idx]" "$input_file" 2>/dev/null)
420
+
421
+ if [[ -n "$sig_xml" ]]; then
422
+ # Extract original ID
423
+ local orig_id
424
+ orig_id=$(echo "$sig_xml" | grep -oE 'id="[^"]+"' | head -1 | cut -d'"' -f2)
425
+ local new_id="${colony_prefix}:${orig_id}"
426
+
427
+ ((total_signals++))
428
+
429
+ # Check for duplicates
430
+ if [[ "$deduplicate" == "true" ]]; then
431
+ if [[ -n "${seen_ids[$new_id]:-}" ]]; then
432
+ ((idx++))
433
+ continue
434
+ fi
435
+ seen_ids[$new_id]=1
436
+ fi
437
+
438
+ # Replace ID with prefixed version
439
+ sig_xml=$(echo "$sig_xml" | sed "s/id=\"$orig_id\"/id=\"$new_id\"/")
440
+
441
+ # Add signal to merged XML
442
+ merged_xml="$merged_xml
443
+ $sig_xml"
444
+ ((unique_signals++))
445
+ fi
446
+ ((idx++))
447
+ done
448
+ else
449
+ # Fallback: extract with awk and sed
450
+ local sig_block=""
451
+ local in_signal=false
452
+ while IFS= read -r line; do
453
+ if echo "$line" | grep -qE '^[[:space:]]*<signal[[:space:]]'; then
454
+ in_signal=true
455
+ sig_block="$line"
456
+ # Extract and prefix ID
457
+ if [[ "$line" =~ id=\"([^\"]+)\" ]]; then
458
+ local orig_id="${BASH_REMATCH[1]}"
459
+ local new_id="${colony_prefix}:${orig_id}"
460
+ line=$(echo "$line" | sed "s/id=\"$orig_id\"/id=\"$new_id\"/")
461
+ fi
462
+ elif [[ "$in_signal" == true ]]; then
463
+ sig_block="$sig_block
464
+ $line"
465
+ if echo "$line" | grep -qE '</signal>'; then
466
+ # Process complete signal
467
+ local signal_id
468
+ if [[ "$sig_block" =~ id=\"([^\"]+)\" ]]; then
469
+ signal_id="${BASH_REMATCH[1]}"
470
+ ((total_signals++))
471
+
472
+ # Check for duplicates
473
+ if [[ "$deduplicate" == "true" ]]; then
474
+ if [[ -n "${seen_ids[$signal_id]:-}" ]]; then
475
+ in_signal=false
476
+ sig_block=""
477
+ continue
478
+ fi
479
+ seen_ids[$signal_id]=1
480
+ fi
481
+
482
+ # Add signal to merged XML
483
+ merged_xml="$merged_xml
484
+ $sig_block"
485
+ ((unique_signals++))
486
+ fi
487
+ in_signal=false
488
+ sig_block=""
489
+ fi
490
+ fi
491
+ done < "$input_file"
492
+ fi
493
+
494
+ ((colonies_merged++))
495
+ done
496
+
497
+ # Close root element
498
+ merged_xml="$merged_xml
499
+ </pheromones>"
500
+
501
+ # Write output
502
+ echo "$merged_xml" > "$output_file"
503
+
504
+ xml_json_ok "{\"path\":\"$output_file\",\"signals\":$unique_signals,\"colonies\":$colonies_merged,\"duplicates_removed\":$((total_signals - unique_signals))}"
505
+ }
506
+
507
+ # ============================================================================
508
+ # Pheromone Validation
509
+ # ============================================================================
510
+
511
+ # xml-pheromone-validate: Validate pheromone XML against schema
512
+ # Usage: xml-pheromone-validate <pheromone_xml> [xsd_schema]
513
+ # Returns: {"ok":true,"result":{"valid":true,"errors":[]}}
514
+ xml-pheromone-validate() {
515
+ local xml_file="${1:-}"
516
+ local xsd_file="${2:-.aether/schemas/pheromone.xsd}"
517
+
518
+ [[ -z "$xml_file" ]] && { xml_json_err "MISSING_ARG" "Missing XML file argument"; return 1; }
519
+ [[ -f "$xml_file" ]] || { xml_json_err "FILE_NOT_FOUND" "XML file not found: $xml_file"; return 1; }
520
+
521
+ if [[ "$XMLLINT_AVAILABLE" != "true" ]]; then
522
+ xml_json_err "TOOL_NOT_AVAILABLE" "xmllint required for validation"
523
+ return 1
524
+ fi
525
+
526
+ if [[ ! -f "$xsd_file" ]]; then
527
+ xml_json_err "SCHEMA_NOT_FOUND" "XSD schema not found: $xsd_file"
528
+ return 1
529
+ fi
530
+
531
+ local errors
532
+ errors=$(xmllint --nonet --noent --noout --schema "$xsd_file" "$xml_file" 2>&1) && {
533
+ xml_json_ok '{"valid":true,"errors":[]}'
534
+ return 0
535
+ } || {
536
+ local escaped_errors
537
+ escaped_errors=$(echo "$errors" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' ')
538
+ xml_json_ok "{\"valid\":false,\"errors\":[\"$escaped_errors\"]}"
539
+ return 0
540
+ }
541
+ }
542
+
543
+ # ============================================================================
544
+ # Namespace Utilities
545
+ # ============================================================================
546
+
547
+ # xml-pheromone-prefix-id: Add namespace prefix to signal ID
548
+ # Usage: xml-pheromone-prefix-id <signal_id> <colony_prefix>
549
+ # Returns: Prefixed ID (direct output, not JSON)
550
+ xml-pheromone-prefix-id() {
551
+ local signal_id="${1:-}"
552
+ local colony_prefix="${2:-}"
553
+
554
+ [[ -z "$signal_id" ]] && { echo ""; return 1; }
555
+ [[ -z "$colony_prefix" ]] && { echo "$signal_id"; return 0; }
556
+
557
+ echo "${colony_prefix}:${signal_id}"
558
+ }
559
+
560
+ # xml-pheromone-deprefix-id: Remove namespace prefix from signal ID
561
+ # Usage: xml-pheromone-deprefix-id <prefixed_id>
562
+ # Returns: Original ID (direct output, not JSON)
563
+ xml-pheromone-deprefix-id() {
564
+ local prefixed_id="${1:-}"
565
+
566
+ [[ -z "$prefixed_id" ]] && { echo ""; return 1; }
567
+
568
+ # Extract ID after colon
569
+ echo "$prefixed_id" | sed 's/^[^:]*://'
570
+ }
571
+
572
+ # Export functions
573
+ export -f xml-pheromone-export xml-pheromone-import xml-pheromone-validate xml-pheromone-merge
574
+ export -f xml-pheromone-prefix-id xml-pheromone-deprefix-id