aether-colony 3.1.17 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/{runtime → .aether}/CONTEXT.md +1 -1
  2. package/{runtime → .aether}/aether-utils.sh +1772 -98
  3. package/.aether/docs/QUEEN-SYSTEM.md +211 -0
  4. package/.aether/docs/QUEEN.md +84 -0
  5. package/.aether/docs/README.md +68 -0
  6. package/.aether/docs/caste-system.md +48 -0
  7. package/{runtime → .aether/docs/disciplines}/DISCIPLINES.md +8 -8
  8. package/.aether/docs/error-codes.md +268 -0
  9. package/{runtime → .aether}/docs/known-issues.md +42 -26
  10. package/.aether/docs/queen-commands.md +97 -0
  11. package/.aether/exchange/colony-registry.xml +11 -0
  12. package/{runtime → .aether}/exchange/pheromone-xml.sh +2 -1
  13. package/.aether/exchange/pheromones.xml +87 -0
  14. package/.aether/exchange/queen-wisdom.xml +14 -0
  15. package/{runtime → .aether}/exchange/registry-xml.sh +7 -3
  16. package/{runtime → .aether}/exchange/wisdom-xml.sh +11 -4
  17. package/.aether/rules/aether-colony.md +134 -0
  18. package/.aether/schemas/example-prompt-builder.xml +234 -0
  19. package/.aether/templates/colony-state-reset.jq.template +22 -0
  20. package/.aether/templates/colony-state.template.json +35 -0
  21. package/.aether/templates/constraints.template.json +9 -0
  22. package/.aether/templates/crowned-anthill.template.md +36 -0
  23. package/.aether/templates/handoff-build-error.template.md +30 -0
  24. package/.aether/templates/handoff-build-success.template.md +39 -0
  25. package/.aether/templates/handoff.template.md +40 -0
  26. package/{runtime → .aether}/utils/atomic-write.sh +5 -5
  27. package/{runtime → .aether}/utils/chamber-compare.sh +23 -10
  28. package/{runtime → .aether}/utils/chamber-utils.sh +32 -20
  29. package/{runtime → .aether}/utils/error-handler.sh +13 -1
  30. package/{runtime → .aether}/utils/file-lock.sh +49 -13
  31. package/.aether/utils/semantic-cli.sh +413 -0
  32. package/{runtime → .aether}/utils/xml-compose.sh +7 -1
  33. package/.aether/utils/xml-convert.sh +273 -0
  34. package/.aether/utils/xml-query.sh +201 -0
  35. package/.aether/utils/xml-utils.sh +110 -0
  36. package/{runtime → .aether}/workers.md +14 -17
  37. package/.claude/agents/ant/aether-ambassador.md +264 -0
  38. package/.claude/agents/ant/aether-archaeologist.md +322 -0
  39. package/.claude/agents/ant/aether-auditor.md +266 -0
  40. package/.claude/agents/ant/aether-builder.md +187 -0
  41. package/.claude/agents/ant/aether-chaos.md +268 -0
  42. package/.claude/agents/ant/aether-chronicler.md +304 -0
  43. package/.claude/agents/ant/aether-gatekeeper.md +325 -0
  44. package/.claude/agents/ant/aether-includer.md +373 -0
  45. package/.claude/agents/ant/aether-keeper.md +271 -0
  46. package/.claude/agents/ant/aether-measurer.md +317 -0
  47. package/.claude/agents/ant/aether-probe.md +210 -0
  48. package/.claude/agents/ant/aether-queen.md +325 -0
  49. package/.claude/agents/ant/aether-route-setter.md +173 -0
  50. package/.claude/agents/ant/aether-sage.md +353 -0
  51. package/.claude/agents/ant/aether-scout.md +142 -0
  52. package/.claude/agents/ant/aether-surveyor-disciplines.md +416 -0
  53. package/.claude/agents/ant/aether-surveyor-nest.md +354 -0
  54. package/.claude/agents/ant/aether-surveyor-pathogens.md +288 -0
  55. package/.claude/agents/ant/aether-surveyor-provisions.md +359 -0
  56. package/.claude/agents/ant/aether-tracker.md +265 -0
  57. package/.claude/agents/ant/aether-watcher.md +244 -0
  58. package/.claude/agents/ant/aether-weaver.md +247 -0
  59. package/.claude/commands/ant/archaeology.md +16 -7
  60. package/.claude/commands/ant/build.md +415 -284
  61. package/.claude/commands/ant/chaos.md +19 -10
  62. package/.claude/commands/ant/colonize.md +58 -24
  63. package/.claude/commands/ant/continue.md +155 -145
  64. package/.claude/commands/ant/council.md +15 -5
  65. package/.claude/commands/ant/dream.md +16 -7
  66. package/.claude/commands/ant/entomb.md +274 -157
  67. package/.claude/commands/ant/feedback.md +33 -29
  68. package/.claude/commands/ant/flag.md +18 -10
  69. package/.claude/commands/ant/flags.md +14 -6
  70. package/.claude/commands/ant/focus.md +29 -21
  71. package/.claude/commands/ant/help.md +11 -1
  72. package/.claude/commands/ant/history.md +10 -0
  73. package/.claude/commands/ant/init.md +91 -65
  74. package/.claude/commands/ant/interpret.md +15 -4
  75. package/.claude/commands/ant/lay-eggs.md +55 -7
  76. package/.claude/commands/ant/maturity.md +11 -1
  77. package/.claude/commands/ant/migrate-state.md +14 -2
  78. package/.claude/commands/ant/oracle.md +23 -15
  79. package/.claude/commands/ant/organize.md +29 -20
  80. package/.claude/commands/ant/pause-colony.md +17 -7
  81. package/.claude/commands/ant/phase.md +17 -8
  82. package/.claude/commands/ant/plan.md +20 -9
  83. package/.claude/commands/ant/redirect.md +29 -32
  84. package/.claude/commands/ant/resume-colony.md +19 -9
  85. package/.claude/commands/ant/resume.md +272 -96
  86. package/.claude/commands/ant/seal.md +201 -191
  87. package/.claude/commands/ant/status.md +71 -32
  88. package/.claude/commands/ant/swarm.md +26 -44
  89. package/.claude/commands/ant/tunnels.md +279 -105
  90. package/.claude/commands/ant/update.md +81 -20
  91. package/.claude/commands/ant/verify-castes.md +14 -4
  92. package/.claude/commands/ant/watch.md +13 -12
  93. package/.opencode/agents/aether-ambassador.md +63 -20
  94. package/.opencode/agents/aether-archaeologist.md +29 -12
  95. package/.opencode/agents/aether-auditor.md +51 -18
  96. package/.opencode/agents/aether-builder.md +69 -19
  97. package/.opencode/agents/aether-chaos.md +29 -12
  98. package/.opencode/agents/aether-chronicler.md +60 -18
  99. package/.opencode/agents/aether-gatekeeper.md +27 -18
  100. package/.opencode/agents/aether-includer.md +27 -18
  101. package/.opencode/agents/aether-keeper.md +89 -18
  102. package/.opencode/agents/aether-measurer.md +27 -18
  103. package/.opencode/agents/aether-probe.md +60 -18
  104. package/.opencode/agents/aether-queen.md +172 -24
  105. package/.opencode/agents/aether-route-setter.md +57 -12
  106. package/.opencode/agents/aether-sage.md +26 -18
  107. package/.opencode/agents/aether-scout.md +27 -19
  108. package/.opencode/agents/aether-surveyor-disciplines.md +53 -1
  109. package/.opencode/agents/aether-surveyor-nest.md +53 -1
  110. package/.opencode/agents/aether-surveyor-pathogens.md +51 -1
  111. package/.opencode/agents/aether-surveyor-provisions.md +53 -1
  112. package/.opencode/agents/aether-tracker.md +64 -18
  113. package/.opencode/agents/aether-watcher.md +66 -19
  114. package/.opencode/agents/aether-weaver.md +61 -18
  115. package/.opencode/commands/ant/build.md +406 -192
  116. package/.opencode/commands/ant/continue.md +66 -76
  117. package/.opencode/commands/ant/entomb.md +106 -45
  118. package/.opencode/commands/ant/init.md +46 -48
  119. package/.opencode/commands/ant/organize.md +5 -5
  120. package/.opencode/commands/ant/resume.md +334 -0
  121. package/.opencode/commands/ant/seal.md +33 -24
  122. package/.opencode/commands/ant/status.md +11 -0
  123. package/.opencode/commands/ant/tunnels.md +149 -0
  124. package/.opencode/commands/ant/update.md +59 -16
  125. package/CHANGELOG.md +79 -0
  126. package/README.md +135 -353
  127. package/bin/cli.js +243 -122
  128. package/bin/generate-commands.sh +2 -2
  129. package/bin/lib/init.js +13 -3
  130. package/bin/lib/update-transaction.js +119 -117
  131. package/bin/sync-to-runtime.sh +5 -137
  132. package/bin/validate-package.sh +84 -0
  133. package/package.json +9 -6
  134. package/.opencode/agents/aether-architect.md +0 -66
  135. package/.opencode/agents/aether-guardian.md +0 -107
  136. package/.opencode/agents/workers.md +0 -1034
  137. package/runtime/QUEEN_ANT_ARCHITECTURE.md +0 -402
  138. package/runtime/data/signatures.json +0 -41
  139. package/runtime/docs/AETHER-2.0-IMPLEMENTATION-PLAN.md +0 -1343
  140. package/runtime/docs/AETHER-PHEROMONE-SYSTEM-MASTER-SPEC.md +0 -2642
  141. package/runtime/docs/PHEROMONE-INJECTION.md +0 -240
  142. package/runtime/docs/PHEROMONE-INTEGRATION.md +0 -192
  143. package/runtime/docs/PHEROMONE-SYSTEM-DESIGN.md +0 -426
  144. package/runtime/docs/README.md +0 -94
  145. package/runtime/docs/VISUAL-OUTPUT-SPEC.md +0 -219
  146. package/runtime/docs/biological-reference.md +0 -272
  147. package/runtime/docs/codebase-review.md +0 -399
  148. package/runtime/docs/command-sync.md +0 -164
  149. package/runtime/docs/constraints.md +0 -116
  150. package/runtime/docs/implementation-learnings.md +0 -89
  151. package/runtime/docs/namespace.md +0 -148
  152. package/runtime/docs/pathogen-schema-example.json +0 -36
  153. package/runtime/docs/pathogen-schema.md +0 -111
  154. package/runtime/docs/planning-discipline.md +0 -159
  155. package/runtime/docs/progressive-disclosure.md +0 -184
  156. package/runtime/lib/queen-utils.sh +0 -729
  157. package/runtime/planning.md +0 -159
  158. package/runtime/recover.sh +0 -136
  159. package/runtime/utils/xml-utils.sh +0 -2196
  160. package/runtime/workers-new-castes.md +0 -516
  161. /package/{runtime → .aether/docs/disciplines}/coding-standards.md +0 -0
  162. /package/{runtime → .aether/docs/disciplines}/debugging.md +0 -0
  163. /package/{runtime → .aether/docs/disciplines}/learning.md +0 -0
  164. /package/{runtime → .aether/docs/disciplines}/tdd.md +0 -0
  165. /package/{runtime → .aether/docs/disciplines}/verification-loop.md +0 -0
  166. /package/{runtime → .aether/docs/disciplines}/verification.md +0 -0
  167. /package/{runtime → .aether}/docs/pheromones.md +0 -0
  168. /package/{runtime → .aether}/model-profiles.yaml +0 -0
  169. /package/{runtime → .aether}/schemas/aether-types.xsd +0 -0
  170. /package/{runtime → .aether}/schemas/colony-registry.xsd +0 -0
  171. /package/{runtime → .aether}/schemas/pheromone.xsd +0 -0
  172. /package/{runtime → .aether}/schemas/prompt.xsd +0 -0
  173. /package/{runtime → .aether}/schemas/queen-wisdom.xsd +0 -0
  174. /package/{runtime → .aether}/schemas/worker-priming.xsd +0 -0
  175. /package/{runtime → .aether}/templates/QUEEN.md.template +0 -0
  176. /package/{runtime → .aether}/utils/colorize-log.sh +0 -0
  177. /package/{runtime → .aether}/utils/queen-to-md.xsl +0 -0
  178. /package/{runtime → .aether}/utils/spawn-tree.sh +0 -0
  179. /package/{runtime → .aether}/utils/spawn-with-model.sh +0 -0
  180. /package/{runtime → .aether}/utils/state-loader.sh +0 -0
  181. /package/{runtime → .aether}/utils/swarm-display.sh +0 -0
  182. /package/{runtime → .aether}/utils/watch-spawn-tree.sh +0 -0
  183. /package/{runtime → .aether}/utils/xml-core.sh +0 -0
@@ -1,2196 +0,0 @@
1
- #!/bin/bash
2
- # XML Utilities Loader for Aether Colony
3
- # Modular architecture - sources xml-core.sh and xml-compose.sh
4
- #
5
- # Usage: source .aether/utils/xml-utils.sh
6
- # xml-validate <xml_file> <xsd_file>
7
- # xml-to-json <xml_file>
8
- # json-to-xml <json_file> [root_element]
9
- # xml-query <xml_file> <xpath_expression>
10
- # xml-merge <output_file> <input_files...>
11
- #
12
- # All functions return JSON status like other aether-utils
13
- #
14
- # Note: This file loads xml-core.sh and xml-compose.sh for modularity.
15
- # The actual implementations are in those modules; this file provides
16
- # backward compatibility and loads additional domain functions below.
17
-
18
- set -euo pipefail
19
-
20
- # Determine script directory for sourcing modules
21
- # Handle case when sourced interactively (BASH_SOURCE[0] may be empty)
22
- if [[ -n "${BASH_SOURCE[0]:-}" ]]; then
23
- _XML_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
- else
25
- # Fallback: derive from the sourced script's location
26
- _XML_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
27
- fi
28
-
29
- # ============================================================================
30
- # Load Modular Components
31
- # ============================================================================
32
-
33
- # Core utilities (validation, formatting, escaping)
34
- [[ -f "$_XML_UTILS_DIR/xml-core.sh" ]] && source "$_XML_UTILS_DIR/xml-core.sh"
35
-
36
- # XInclude composition
37
- [[ -f "$_XML_UTILS_DIR/xml-compose.sh" ]] && source "$_XML_UTILS_DIR/xml-compose.sh"
38
-
39
- # ============================================================================
40
- # Feature Detection (supplement xml-core.sh if not already set)
41
- # ============================================================================
42
-
43
- # Check for required XML tools
44
- : "${XMLLINT_AVAILABLE:=false}"
45
- : "${XMLSTARLET_AVAILABLE:=false}"
46
- : "${XSLTPROC_AVAILABLE:=false}"
47
- : "${XML2JSON_AVAILABLE:=false}"
48
-
49
- if command -v xmllint >/dev/null 2>&1; then
50
- XMLLINT_AVAILABLE=true
51
- fi
52
-
53
- if command -v xmlstarlet >/dev/null 2>&1; then
54
- XMLSTARLET_AVAILABLE=true
55
- fi
56
-
57
- if command -v xsltproc >/dev/null 2>&1; then
58
- XSLTPROC_AVAILABLE=true
59
- fi
60
-
61
- if command -v xml2json >/dev/null 2>&1; then
62
- XML2JSON_AVAILABLE=true
63
- fi
64
-
65
- # ============================================================================
66
- # Additional JSON Output Helpers (supplement xml-core.sh)
67
- # ============================================================================
68
-
69
- # Use xml-core.sh versions if available, otherwise define here
70
- if ! type xml_json_ok &>/dev/null; then
71
- xml_json_ok() { printf '{"ok":true,"result":%s}\n' "$1"; }
72
- fi
73
-
74
- if ! type xml_json_err &>/dev/null; then
75
- xml_json_err() {
76
- local message="${2:-$1}"
77
- printf '{"ok":false,"error":"%s"}\n' "$message" >&2
78
- return 1
79
- }
80
- fi
81
-
82
- # ============================================================================
83
- # Domain Functions: Pheromones, Wisdom, Registry, Prompts
84
- # ============================================================================
85
-
86
- # These functions remain in xml-utils.sh (not yet modularized)
87
-
88
- # ============================================================================
89
- # Core XML Functions
90
- # ============================================================================
91
-
92
- # xml-validate: Validate XML against XSD schema using xmllint
93
- # Usage: xml-validate <xml_file> <xsd_file>
94
- # Returns: {"ok":true,"result":{"valid":true,"errors":[]}} or error
95
- xml-validate() {
96
- local xml_file="${1:-}"
97
- local xsd_file="${2:-}"
98
-
99
- # Validate arguments
100
- [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
101
- [[ -z "$xsd_file" ]] && { xml_json_err "Missing XSD schema file argument"; return 1; }
102
- [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
103
- [[ -f "$xsd_file" ]] || { xml_json_err "XSD schema file not found: $xsd_file"; return 1; }
104
-
105
- # Check for xmllint
106
- if [[ "$XMLLINT_AVAILABLE" != "true" ]]; then
107
- xml_json_err "xmllint not available. Install libxml2 utilities."
108
- return 1
109
- fi
110
-
111
- # Validate XML against XSD (with XXE protection)
112
- local errors
113
- errors=$(xmllint --nonet --noent --noout --schema "$xsd_file" "$xml_file" 2>&1) && {
114
- xml_json_ok '{"valid":true,"errors":[]}'
115
- return 0
116
- } || {
117
- # Parse errors into JSON array
118
- local error_json
119
- error_json=$(echo "$errors" | jq -R -s 'split("\n") | map(select(length > 0))')
120
- xml_json_ok "{\"valid\":false,\"errors\":$error_json}"
121
- return 0
122
- }
123
- }
124
-
125
- # xml-well-formed: Check if XML is well-formed (no schema validation)
126
- # Usage: xml-well-formed <xml_file>
127
- # Returns: {"ok":true,"result":{"well_formed":true,"error":null}} or error details
128
- xml-well-formed() {
129
- local xml_file="${1:-}"
130
-
131
- [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
132
- [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
133
-
134
- if [[ "$XMLLINT_AVAILABLE" != "true" ]]; then
135
- xml_json_err "xmllint not available. Install libxml2 utilities."
136
- return 1
137
- fi
138
-
139
- local error
140
- error=$(xmllint --nonet --noent --noout "$xml_file" 2>&1) && {
141
- xml_json_ok '{"well_formed":true,"error":null}'
142
- return 0
143
- } || {
144
- local escaped_error
145
- escaped_error=$(echo "$error" | jq -Rs '.[:-1]')
146
- xml_json_ok "{\"well_formed\":false,\"error\":$escaped_error}"
147
- return 0
148
- }
149
- }
150
-
151
- # xml-to-json: Convert XML to JSON using available tools
152
- # Usage: xml-to-json <xml_file> [options]
153
- # Options: --pretty (pretty print output)
154
- # Returns: {"ok":true,"result":<json_object>}
155
- xml-to-json() {
156
- local xml_file="${1:-}"
157
- local pretty=false
158
-
159
- # Parse optional arguments
160
- shift || true
161
- while [[ $# -gt 0 ]]; do
162
- case "$1" in
163
- --pretty) pretty=true; shift ;;
164
- *) shift ;;
165
- esac
166
- done
167
-
168
- [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
169
- [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
170
-
171
- # Check well-formedness first
172
- local well_formed_result
173
- well_formed_result=$(xml-well-formed "$xml_file" 2>/dev/null)
174
- if ! echo "$well_formed_result" | jq -e '.result.well_formed' >/dev/null 2>&1; then
175
- xml_json_err "XML is not well-formed"
176
- return 1
177
- fi
178
-
179
- # Try xml2json if available (npm package)
180
- if [[ "$XML2JSON_AVAILABLE" == "true" ]]; then
181
- local json_output
182
- if json_output=$(xml2json "$xml_file" 2>/dev/null); then
183
- if [[ "$pretty" == "true" ]]; then
184
- json_output=$(echo "$json_output" | jq '.')
185
- fi
186
- xml_json_ok "$(echo "$json_output" | jq -Rs '.[:-1]')"
187
- return 0
188
- fi
189
- fi
190
-
191
- # Fallback: Use xsltproc with built-in XSLT if available
192
- if [[ "$XSLTPROC_AVAILABLE" == "true" ]]; then
193
- local xslt_script
194
- xslt_script=$(cat << 'XSLT'
195
- <?xml version="1.0"?>
196
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
197
- <xsl:output method="text"/>
198
- <xsl:template match="/">
199
- <xsl:text>{"root":</xsl:text>
200
- <xsl:apply-templates select="*"/>
201
- <xsl:text>}</xsl:text>
202
- </xsl:template>
203
- <xsl:template match="*">
204
- <xsl:text>{"</xsl:text>
205
- <xsl:value-of select="name()"/>
206
- <xsl:text>":</xsl:text>
207
- <xsl:choose>
208
- <xsl:when test="count(*) > 0">
209
- <xsl:text>[</xsl:text>
210
- <xsl:apply-templates select="*"/>
211
- <xsl:text>]</xsl:text>
212
- </xsl:when>
213
- <xsl:otherwise>
214
- <xsl:text>"</xsl:text>
215
- <xsl:value-of select="."/>
216
- <xsl:text>"</xsl:text>
217
- </xsl:otherwise>
218
- </xsl:choose>
219
- <xsl:text>}</xsl:text>
220
- <xsl:if test="position() != last()">,</xsl:if>
221
- </xsl:template>
222
- </xsl:stylesheet>
223
- XSLT
224
- )
225
- local json_result
226
- json_result=$(echo "$xslt_script" | xsltproc - "$xml_file" 2>/dev/null) || {
227
- xml_json_err "XSLT conversion failed"
228
- return 1
229
- }
230
- xml_json_ok "$json_result"
231
- return 0
232
- fi
233
-
234
- # Last resort: Use xmlstarlet if available
235
- if [[ "$XMLSTARLET_AVAILABLE" == "true" ]]; then
236
- # xmlstarlet can convert to various formats, we'll use sel to extract structure
237
- local json_result
238
- json_result=$(xmlstarlet sel -t -m "/" -o '{"root":{' -m "*" -v "name()" -o ':"' -v "." -o '"' -b -o '}}' "$xml_file" 2>/dev/null) || {
239
- xml_json_err "xmlstarlet conversion failed"
240
- return 1
241
- }
242
- xml_json_ok "$json_result"
243
- return 0
244
- fi
245
-
246
- xml_json_err "No XML to JSON conversion tool available. Install xml2json, xsltproc, or xmlstarlet."
247
- return 1
248
- }
249
-
250
- # json-to-xml: Convert JSON to XML
251
- # Usage: json-to-xml <json_file> [root_element]
252
- # Returns: {"ok":true,"result":{"xml":"<root>...</root>"}}
253
- json-to-xml() {
254
- local json_file="${1:-}"
255
- local root_element="${2:-root}"
256
-
257
- [[ -z "$json_file" ]] && { xml_json_err "Missing JSON file argument"; return 1; }
258
- [[ -f "$json_file" ]] || { xml_json_err "JSON file not found: $json_file"; return 1; }
259
-
260
- # Validate JSON first
261
- if ! jq empty "$json_file" 2>/dev/null; then
262
- xml_json_err "Invalid JSON file: $json_file"
263
- return 1
264
- fi
265
-
266
- # Build XML using jq to generate structure
267
- local xml_output
268
- xml_output=$(jq -r --arg root "$root_element" '
269
- def to_xml:
270
- if type == "object" then
271
- to_entries | map(
272
- "<\(.key)>\(.value | to_xml)</\(.key)>"
273
- ) | join("")
274
- elif type == "array" then
275
- map("<item>\(. | to_xml)</item>") | join("")
276
- elif type == "string" then
277
- .
278
- elif type == "number" then
279
- tostring
280
- elif type == "boolean" then
281
- tostring
282
- elif type == "null" then
283
- ""
284
- else
285
- tostring
286
- end;
287
- "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<\($root)>\n" + (to_xml) + "\n</\($root)>"
288
- ' "$json_file" 2>/dev/null) || {
289
- xml_json_err "JSON to XML conversion failed"
290
- return 1
291
- }
292
-
293
- # Escape the XML for JSON output
294
- local escaped_xml
295
- escaped_xml=$(echo "$xml_output" | jq -Rs '.')
296
- xml_json_ok "{\"xml\":$escaped_xml}"
297
- }
298
-
299
- # xml-query: XPath query function using XMLStarlet
300
- # Usage: xml-query <xml_file> <xpath_expression>
301
- # Returns: {"ok":true,"result":{"matches":[...],"count":N}}
302
- xml-query() {
303
- local xml_file="${1:-}"
304
- local xpath="${2:-}"
305
-
306
- [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
307
- [[ -z "$xpath" ]] && { xml_json_err "Missing XPath expression argument"; return 1; }
308
- [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
309
-
310
- if [[ "$XMLSTARLET_AVAILABLE" != "true" ]]; then
311
- xml_json_err "xmlstarlet not available. Install xmlstarlet for XPath queries."
312
- return 1
313
- fi
314
-
315
- # Execute XPath query
316
- local results
317
- results=$(xmlstarlet sel -t -m "$xpath" -v "." -n "$xml_file" 2>/dev/null) || {
318
- xml_json_err "XPath query failed: $xpath"
319
- return 1
320
- }
321
-
322
- # Convert results to JSON array
323
- local json_array
324
- json_array=$(echo "$results" | jq -R -s 'split("\n") | map(select(length > 0))')
325
- local count
326
- count=$(echo "$json_array" | jq 'length')
327
-
328
- xml_json_ok "{\"matches\":$json_array,\"count\":$count}"
329
- }
330
-
331
- # xml-query-attr: Query for specific attribute values
332
- # Usage: xml-query-attr <xml_file> <element> <attribute>
333
- # Returns: {"ok":true,"result":{"attributes":[...],"count":N}}
334
- xml-query-attr() {
335
- local xml_file="${1:-}"
336
- local element="${2:-}"
337
- local attr="${3:-}"
338
-
339
- [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
340
- [[ -z "$element" ]] && { xml_json_err "Missing element argument"; return 1; }
341
- [[ -z "$attr" ]] && { xml_json_err "Missing attribute argument"; return 1; }
342
- [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
343
-
344
- if [[ "$XMLSTARLET_AVAILABLE" != "true" ]]; then
345
- xml_json_err "xmlstarlet not available. Install xmlstarlet for attribute queries."
346
- return 1
347
- fi
348
-
349
- local results
350
- results=$(xmlstarlet sel -t -m "//$element" -v "@$attr" -n "$xml_file" 2>/dev/null) || {
351
- xml_json_err "Attribute query failed: $element/@$attr"
352
- return 1
353
- }
354
-
355
- local json_array
356
- json_array=$(echo "$results" | jq -R -s 'split("\n") | map(select(length > 0))')
357
- local count
358
- count=$(echo "$json_array" | jq 'length')
359
-
360
- xml_json_ok "{\"attributes\":$json_array,\"count\":$count}"
361
- }
362
-
363
- # xml-merge: XInclude document merging
364
- # Usage: xml-merge <output_file> <main_xml_file> [included_files...]
365
- # Returns: {"ok":true,"result":{"merged":true,"output":"<path>"}}
366
- xml-merge() {
367
- local output_file="${1:-}"
368
- local main_xml="${2:-}"
369
-
370
- [[ -z "$output_file" ]] && { xml_json_err "Missing output file argument"; return 1; }
371
- [[ -z "$main_xml" ]] && { xml_json_err "Missing main XML file argument"; return 1; }
372
- [[ -f "$main_xml" ]] || { xml_json_err "Main XML file not found: $main_xml"; return 1; }
373
-
374
- # Check well-formedness of main file
375
- local well_formed_result
376
- well_formed_result=$(xml-well-formed "$main_xml" 2>/dev/null)
377
- if ! echo "$well_formed_result" | jq -e '.result.well_formed' >/dev/null 2>&1; then
378
- xml_json_err "Main XML file is not well-formed"
379
- return 1
380
- fi
381
-
382
- # Use xmllint for XInclude processing if available
383
- if [[ "$XMLLINT_AVAILABLE" == "true" ]]; then
384
- local merged
385
- merged=$(xmllint --nonet --noent --xinclude "$main_xml" 2>/dev/null) || {
386
- xml_json_err "XInclude merge failed"
387
- return 1
388
- }
389
-
390
- # Write output
391
- echo "$merged" > "$output_file"
392
- local escaped_output
393
- escaped_output=$(echo "$output_file" | jq -Rs '.[:-1]')
394
- xml_json_ok "{\"merged\":true,\"output\":$escaped_output}"
395
- return 0
396
- fi
397
-
398
- # Fallback: Simple file concatenation with root element wrapping
399
- # This is a basic implementation - full XInclude requires xmllint
400
- local temp_dir
401
- temp_dir=$(mktemp -d)
402
-
403
- # Extract root element from main file
404
- local root_element
405
- root_element=$(grep -oP '(?<=<)[^>\s?/]+' "$main_xml" | head -1)
406
-
407
- {
408
- echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
409
- echo "<$root_element>"
410
- cat "$main_xml" | sed '1,/<'$root_element'>/d' | sed '/<\/'$root_element'>/,$d'
411
- echo "</$root_element>"
412
- } > "$output_file"
413
-
414
- rm -rf "$temp_dir"
415
-
416
- local escaped_output
417
- escaped_output=$(echo "$output_file" | jq -Rs '.[:-1]')
418
- xml_json_ok "{\"merged\":true,\"output\":$escaped_output,\"note\":\"Basic merge without XInclude\"}"
419
- }
420
-
421
- # xml-format: Pretty-print XML file
422
- # Usage: xml-format <xml_file> [output_file]
423
- # Returns: {"ok":true,"result":{"formatted":true,"output":"<path>"}}
424
- xml-format() {
425
- local xml_file="${1:-}"
426
- local output_file="${2:-}"
427
-
428
- [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
429
- [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
430
-
431
- if [[ "$XMLLINT_AVAILABLE" != "true" ]]; then
432
- xml_json_err "xmllint not available. Install libxml2 utilities."
433
- return 1
434
- fi
435
-
436
- # Determine output destination
437
- local target="${output_file:-$xml_file}"
438
-
439
- # Format XML with proper indentation (with XXE protection)
440
- local formatted
441
- formatted=$(xmllint --nonet --noent --format "$xml_file" 2>/dev/null) || {
442
- xml_json_err "XML formatting failed"
443
- return 1
444
- }
445
-
446
- echo "$formatted" > "$target"
447
-
448
- local escaped_output
449
- escaped_output=$(echo "$target" | jq -Rs '.[:-1]')
450
- xml_json_ok "{\"formatted\":true,\"output\":$escaped_output}"
451
- }
452
-
453
- # xml-escape: Escape special characters for XML content
454
- # Usage: xml-escape "string with <special> & characters"
455
- # Returns: {"ok":true,"result":"escaped string"}
456
- xml-escape() {
457
- local input="${1:-}"
458
-
459
- # Escape XML special characters
460
- local escaped
461
- escaped=$(echo "$input" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g; s/'"'"'/\&apos;/g')
462
-
463
- local escaped_json
464
- escaped_json=$(echo "$escaped" | jq -Rs '.[:-1]')
465
- xml_json_ok "$escaped_json"
466
- }
467
-
468
- # xml-unescape: Unescape XML entities
469
- # Usage: xml-unescape "string with &lt;special&gt; entities"
470
- # Returns: {"ok":true,"result":"unescaped string"}
471
- xml-unescape() {
472
- local input="${1:-}"
473
-
474
- # Unescape XML entities
475
- local unescaped
476
- unescaped=$(echo "$input" | sed 's/\&lt;/</g; s/\&gt;/>/g; s/\&quot;/"/g; s/\&apos;/'"'"'/g; s/\&amp;/\&/g')
477
-
478
- local unescaped_json
479
- unescaped_json=$(echo "$unescaped" | jq -Rs '.[:-1]')
480
- xml_json_ok "$unescaped_json"
481
- }
482
-
483
- # xml-detect-tools: Detect available XML tools
484
- # Usage: xml-detect-tools
485
- # Returns: {"ok":true,"result":{"xmllint":true,"xmlstarlet":false,...}}
486
- xml-detect-tools() {
487
- xml_json_ok "{\"xmllint\":$XMLLINT_AVAILABLE,\"xmlstarlet\":$XMLSTARLET_AVAILABLE,\"xsltproc\":$XSLTPROC_AVAILABLE,\"xml2json\":$XML2JSON_AVAILABLE}"
488
- }
489
-
490
- # ============================================================================
491
- # Pheromone Exchange Format (Hybrid JSON/XML)
492
- # ============================================================================
493
-
494
- # pheromone-to-xml: Convert pheromone JSON to XML format with full XSD schema support
495
- # Usage: pheromone-to-xml <pheromone_json_file> [output_xml_file] [xsd_schema_file]
496
- # pheromone_json_file: Path to pheromone JSON (supports both single signal and full pheromones format)
497
- # output_xml_file: Optional path to write XML output (if omitted, returns XML in result)
498
- # xsd_schema_file: Optional path to XSD schema for validation (default: .aether/schemas/pheromone.xsd)
499
- # Returns: {"ok":true,"result":{"xml":"<pheromones>...</pheromones>","validated":true,"path":"..."}}
500
- pheromone-to-xml() {
501
- local json_file="${1:-}"
502
- local output_xml="${2:-}"
503
- local xsd_file="${3:-.aether/schemas/pheromone.xsd}"
504
-
505
- [[ -z "$json_file" ]] && { xml_json_err "Missing JSON file argument"; return 1; }
506
- [[ -f "$json_file" ]] || { xml_json_err "JSON file not found: $json_file"; return 1; }
507
-
508
- # Validate JSON
509
- if ! jq empty "$json_file" 2>/dev/null; then
510
- xml_json_err "Invalid JSON file: $json_file"
511
- return 1
512
- fi
513
-
514
- # Detect JSON format: single signal or full pheromones structure
515
- local has_signals
516
- has_signals=$(jq 'has("signals")' "$json_file" 2>/dev/null)
517
-
518
- # Generate ISO timestamp
519
- local generated_at
520
- generated_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
521
-
522
- # Extract metadata from JSON
523
- local version colony_id
524
- version=$(jq -r '.version // "1.0.0"' "$json_file")
525
- colony_id=$(jq -r '.colony_id // "unknown"' "$json_file")
526
-
527
- # Build XML header with proper namespace
528
- local xml_output
529
- xml_output="<?xml version=\"1.0\" encoding=\"UTF-8\"?>
530
- <pheromones xmlns=\"http://aether.colony/schemas/pheromones\"
531
- xmlns:ph=\"http://aether.colony/schemas/pheromones\"
532
- version=\"$version\"
533
- generated_at=\"$generated_at\"
534
- colony_id=\"$colony_id\">"
535
-
536
- # Add metadata section
537
- local source_type source_version context
538
- source_type=$(jq -r '.metadata.source.type // "system"' "$json_file" 2>/dev/null || echo "system")
539
- source_version=$(jq -r ".metadata.source.version // \"$version\"" "$json_file" 2>/dev/null || echo "$version")
540
- context=$(jq -r '.metadata.context // "Colony pheromone signal conversion"' "$json_file" 2>/dev/null || echo "Colony pheromone signal conversion")
541
-
542
- xml_output="$xml_output
543
- <metadata>
544
- <source type=\"$source_type\" version=\"$source_version\">aether-pheromone-converter</source>
545
- <context>$(echo "$context" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')</context>
546
- </metadata>"
547
-
548
- # Process signals - either from array or wrap single signal
549
- local signal_count=0
550
- local sig_array_length
551
- sig_array_length=$(jq '.signals | length' "$json_file" 2>/dev/null || echo "0")
552
-
553
- if [[ "$has_signals" == "true" && "$sig_array_length" -gt 0 ]]; then
554
- local sig_idx=0
555
- while [[ $sig_idx -lt $sig_array_length ]]; do
556
- local signal
557
- signal=$(jq -c ".signals[$sig_idx]" "$json_file" 2>/dev/null)
558
- [[ -n "$signal" ]] || { ((sig_idx++)); continue; }
559
-
560
- # Extract signal fields with defaults
561
- local sig_id sig_type priority source created_at expires_at active
562
- sig_id=$(echo "$signal" | jq -r '.id // "sig_'"$(date +%s)"'_'"$signal_count"'"')
563
- sig_type=$(echo "$signal" | jq -r '.type // "FOCUS"' | tr '[:lower:]' '[:upper:]')
564
- priority=$(echo "$signal" | jq -r '.priority // "normal"' | tr '[:upper:]' '[:lower:]')
565
- source=$(echo "$signal" | jq -r '.source // "system"')
566
- created_at=$(echo "$signal" | jq -r '.created_at // "'"$generated_at"'"')
567
- expires_at=$(echo "$signal" | jq -r '.expires_at // empty')
568
- active=$(echo "$signal" | jq -r '.active // true')
569
-
570
- # Validate signal type against schema enum
571
- case "$sig_type" in
572
- FOCUS|REDIRECT|FEEDBACK) ;;
573
- *) sig_type="FOCUS" ;;
574
- esac
575
-
576
- # Validate priority against schema enum
577
- case "$priority" in
578
- critical|high|normal|low) ;;
579
- *) priority="normal" ;;
580
- esac
581
-
582
- # XML escape ID and source
583
- local escaped_id escaped_source
584
- escaped_id=$(echo "$sig_id" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
585
- escaped_source=$(echo "$source" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
586
-
587
- # Build signal element
588
- xml_output="$xml_output
589
- <signal id=\"$escaped_id\"
590
- type=\"$sig_type\"
591
- priority=\"$priority\"
592
- source=\"$escaped_source\"
593
- created_at=\"$created_at\""
594
-
595
- # Add optional expires_at if present
596
- if [[ -n "$expires_at" && "$expires_at" != "null" ]]; then
597
- xml_output="$xml_output
598
- expires_at=\"$expires_at\""
599
- fi
600
-
601
- xml_output="$xml_output
602
- active=\"$active\">"
603
-
604
- # Content section
605
- local content_text content_data content_format
606
- content_text=$(echo "$signal" | jq -r '.content.text // .message // ""')
607
- content_format=$(echo "$signal" | jq -r '.content.data.format // "json"')
608
-
609
- if [[ -n "$content_text" ]]; then
610
- local escaped_text
611
- escaped_text=$(echo "$content_text" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')
612
- xml_output="$xml_output
613
- <content>
614
- <text>$escaped_text</text>"
615
-
616
- # Check for data attachment - convert JSON to XML elements
617
- local has_data
618
- has_data=$(echo "$signal" | jq 'has("content") and (.content | has("data"))' 2>/dev/null)
619
- if [[ "$has_data" == "true" ]]; then
620
- local data_xml
621
- data_xml=$(echo "$signal" | jq -r '.content.data | to_entries | map("<\(.key)>\(.value | tostring | gsub("&"; "&amp;") | gsub("<"; "&lt;") | gsub(">"; "&gt;"))</\(.key)>") | join("")' 2>/dev/null)
622
- xml_output="$xml_output
623
- <data format=\"$content_format\">$data_xml</data>"
624
- fi
625
-
626
- xml_output="$xml_output
627
- </content>"
628
- fi
629
-
630
- # Tags section
631
- local tags_json
632
- tags_json=$(echo "$signal" | jq -c '.tags // []')
633
- if [[ "$tags_json" != "[]" && -n "$tags_json" ]]; then
634
- xml_output="$xml_output
635
- <tags>"
636
- local tag_count
637
- tag_count=$(echo "$signal" | jq '.tags | length')
638
- local tag_idx=0
639
- while [[ $tag_idx -lt $tag_count ]]; do
640
- local tag
641
- tag=$(echo "$signal" | jq -c ".tags[$tag_idx]")
642
- local tag_value tag_weight tag_category
643
- tag_value=$(echo "$tag" | jq -r '.value // .')
644
- tag_weight=$(echo "$tag" | jq -r '.weight // "1.0"')
645
- tag_category=$(echo "$tag" | jq -r '.category // empty')
646
-
647
- local escaped_tag
648
- escaped_tag=$(echo "$tag_value" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
649
-
650
- if [[ -n "$tag_category" && "$tag_category" != "null" ]]; then
651
- xml_output="$xml_output
652
- <tag weight=\"$tag_weight\" category=\"$tag_category\">$escaped_tag</tag>"
653
- else
654
- xml_output="$xml_output
655
- <tag weight=\"$tag_weight\">$escaped_tag</tag>"
656
- fi
657
- ((tag_idx++))
658
- done
659
- xml_output="$xml_output
660
- </tags>"
661
- fi
662
-
663
- # Scope section
664
- local scope_global scope_castes scope_paths
665
- scope_global=$(echo "$signal" | jq -r '.scope.global // false')
666
- xml_output="$xml_output
667
- <scope global=\"$scope_global\">"
668
-
669
- # Castes
670
- local castes_json
671
- castes_json=$(echo "$signal" | jq -c '.scope.castes // []' 2>/dev/null)
672
- if [[ "$castes_json" != "[]" && -n "$castes_json" && "$castes_json" != "null" ]]; then
673
- xml_output="$xml_output
674
- <castes match=\"any\">"
675
- local caste_count
676
- caste_count=$(echo "$signal" | jq '.scope.castes | length' 2>/dev/null)
677
- local caste_idx=0
678
- while [[ $caste_idx -lt $caste_count ]]; do
679
- local caste
680
- caste=$(echo "$signal" | jq -r ".scope.castes[$caste_idx]" 2>/dev/null)
681
- # Validate caste against schema enum
682
- case "$caste" in
683
- builder|watcher|scout|chaos|oracle|architect|prime|colonizer|route_setter|archaeologist|ambassador|auditor|chronicler|gatekeeper|guardian|includer|keeper|measurer|probe|sage|tracker|weaver)
684
- xml_output="$xml_output
685
- <caste>$caste</caste>"
686
- ;;
687
- esac
688
- ((caste_idx++))
689
- done
690
- xml_output="$xml_output
691
- </castes>"
692
- fi
693
-
694
- # Paths
695
- local paths_json
696
- paths_json=$(echo "$signal" | jq -c '.scope.paths // []' 2>/dev/null)
697
- if [[ "$paths_json" != "[]" && -n "$paths_json" && "$paths_json" != "null" ]]; then
698
- xml_output="$xml_output
699
- <paths match=\"any\">"
700
- local path_count
701
- path_count=$(echo "$signal" | jq '.scope.paths | length' 2>/dev/null)
702
- local path_idx=0
703
- while [[ $path_idx -lt $path_count ]]; do
704
- local path
705
- path=$(echo "$signal" | jq -r ".scope.paths[$path_idx]" 2>/dev/null)
706
- local escaped_path
707
- escaped_path=$(echo "$path" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
708
- xml_output="$xml_output
709
- <path>$escaped_path</path>"
710
- ((path_idx++))
711
- done
712
- xml_output="$xml_output
713
- </paths>"
714
- fi
715
-
716
- xml_output="$xml_output
717
- </scope>"
718
-
719
- xml_output="$xml_output
720
- </signal>"
721
- ((signal_count++))
722
- ((sig_idx++))
723
- done
724
- elif [[ "$has_signals" != "true" ]]; then
725
- # Handle single signal JSON (legacy format)
726
- local signal
727
- signal=$(jq -c '.' "$json_file" 2>/dev/null)
728
- if [[ -n "$signal" ]]; then
729
- # Extract signal fields with defaults for legacy format
730
- local sig_id sig_type priority source created_at expires_at active
731
- sig_id=$(echo "$signal" | jq -r '.id // "sig_'"$(date +%s)"'_0"')
732
- sig_type=$(echo "$signal" | jq -r '.type // "FOCUS"' | tr '[:lower:]' '[:upper:]')
733
- priority=$(echo "$signal" | jq -r '.priority // "normal"' | tr '[:upper:]' '[:lower:]')
734
- source=$(echo "$signal" | jq -r '.source // "system"')
735
- created_at=$(echo "$signal" | jq -r '.created_at // "'"$generated_at"'"')
736
- expires_at=$(echo "$signal" | jq -r '.expires_at // empty')
737
- active=$(echo "$signal" | jq -r '.active // true')
738
-
739
- # Validate signal type against schema enum
740
- case "$sig_type" in
741
- FOCUS|REDIRECT|FEEDBACK) ;;
742
- *) sig_type="FOCUS" ;;
743
- esac
744
-
745
- # Validate priority against schema enum
746
- case "$priority" in
747
- critical|high|normal|low) ;;
748
- *) priority="normal" ;;
749
- esac
750
-
751
- # XML escape ID and source
752
- local escaped_id escaped_source
753
- escaped_id=$(echo "$sig_id" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
754
- escaped_source=$(echo "$source" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
755
-
756
- # Build signal element
757
- xml_output="$xml_output
758
- <signal id=\"$escaped_id\"
759
- type=\"$sig_type\"
760
- priority=\"$priority\"
761
- source=\"$escaped_source\"
762
- created_at=\"$created_at\""
763
-
764
- # Add optional expires_at if present
765
- if [[ -n "$expires_at" && "$expires_at" != "null" ]]; then
766
- xml_output="$xml_output
767
- expires_at=\"$expires_at\""
768
- fi
769
-
770
- xml_output="$xml_output
771
- active=\"$active\">"
772
-
773
- # Content section - support legacy "message" field
774
- local content_text content_format
775
- content_text=$(echo "$signal" | jq -r '.content.text // .message // ""')
776
- content_format=$(echo "$signal" | jq -r '.content.data.format // "json"')
777
-
778
- if [[ -n "$content_text" ]]; then
779
- local escaped_text
780
- escaped_text=$(echo "$content_text" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')
781
- xml_output="$xml_output
782
- <content>
783
- <text>$escaped_text</text>"
784
-
785
- # Check for data attachment - convert JSON to XML elements
786
- local has_data
787
- has_data=$(echo "$signal" | jq 'has("content") and (.content | has("data"))' 2>/dev/null)
788
- if [[ "$has_data" == "true" ]]; then
789
- local data_xml
790
- data_xml=$(echo "$signal" | jq -r '.content.data | to_entries | map("<\(.key)>\(.value | tostring | gsub("&"; "&amp;") | gsub("<"; "&lt;") | gsub(">"; "&gt;"))</\(.key)>") | join("")' 2>/dev/null)
791
- xml_output="$xml_output
792
- <data format=\"$content_format\">$data_xml</data>"
793
- fi
794
-
795
- xml_output="$xml_output
796
- </content>"
797
- fi
798
-
799
- # Tags section
800
- local tags_json
801
- tags_json=$(echo "$signal" | jq -c '.tags // []')
802
- if [[ "$tags_json" != "[]" && -n "$tags_json" ]]; then
803
- xml_output="$xml_output
804
- <tags>"
805
- local tag_count
806
- tag_count=$(echo "$signal" | jq '.tags | length')
807
- local tag_idx=0
808
- while [[ $tag_idx -lt $tag_count ]]; do
809
- local tag
810
- tag=$(echo "$signal" | jq -c ".tags[$tag_idx]")
811
- local tag_value tag_weight tag_category
812
- tag_value=$(echo "$tag" | jq -r '.value // .')
813
- tag_weight=$(echo "$tag" | jq -r '.weight // "1.0"')
814
- tag_category=$(echo "$tag" | jq -r '.category // empty')
815
-
816
- local escaped_tag
817
- escaped_tag=$(echo "$tag_value" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
818
-
819
- if [[ -n "$tag_category" && "$tag_category" != "null" ]]; then
820
- xml_output="$xml_output
821
- <tag weight=\"$tag_weight\" category=\"$tag_category\">$escaped_tag</tag>"
822
- else
823
- xml_output="$xml_output
824
- <tag weight=\"$tag_weight\">$escaped_tag</tag>"
825
- fi
826
- ((tag_idx++))
827
- done
828
- xml_output="$xml_output
829
- </tags>"
830
- fi
831
-
832
- # Scope section
833
- local scope_global
834
- scope_global=$(echo "$signal" | jq -r '.scope.global // false')
835
- xml_output="$xml_output
836
- <scope global=\"$scope_global\">"
837
-
838
- # Castes
839
- local castes_json
840
- castes_json=$(echo "$signal" | jq -c '.scope.castes // []' 2>/dev/null)
841
- if [[ "$castes_json" != "[]" && -n "$castes_json" && "$castes_json" != "null" ]]; then
842
- xml_output="$xml_output
843
- <castes match=\"any\">"
844
- local caste_count
845
- caste_count=$(echo "$signal" | jq '.scope.castes | length' 2>/dev/null)
846
- local caste_idx=0
847
- while [[ $caste_idx -lt $caste_count ]]; do
848
- local caste
849
- caste=$(echo "$signal" | jq -r ".scope.castes[$caste_idx]" 2>/dev/null)
850
- case "$caste" in
851
- builder|watcher|scout|chaos|oracle|architect|prime|colonizer|route_setter|archaeologist|ambassador|auditor|chronicler|gatekeeper|guardian|includer|keeper|measurer|probe|sage|tracker|weaver)
852
- xml_output="$xml_output
853
- <caste>$caste</caste>"
854
- ;;
855
- esac
856
- ((caste_idx++))
857
- done
858
- xml_output="$xml_output
859
- </castes>"
860
- fi
861
-
862
- # Paths
863
- local paths_json
864
- paths_json=$(echo "$signal" | jq -c '.scope.paths // []' 2>/dev/null)
865
- if [[ "$paths_json" != "[]" && -n "$paths_json" && "$paths_json" != "null" ]]; then
866
- xml_output="$xml_output
867
- <paths match=\"any\">"
868
- local path_count
869
- path_count=$(echo "$signal" | jq '.scope.paths | length' 2>/dev/null)
870
- local path_idx=0
871
- while [[ $path_idx -lt $path_count ]]; do
872
- local path
873
- path=$(echo "$signal" | jq -r ".scope.paths[$path_idx]" 2>/dev/null)
874
- local escaped_path
875
- escaped_path=$(echo "$path" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
876
- xml_output="$xml_output
877
- <path>$escaped_path</path>"
878
- ((path_idx++))
879
- done
880
- xml_output="$xml_output
881
- </paths>"
882
- fi
883
-
884
- xml_output="$xml_output
885
- </scope>"
886
-
887
- xml_output="$xml_output
888
- </signal>"
889
- ((signal_count++))
890
- fi
891
- fi
892
-
893
- # Close root element
894
- xml_output="$xml_output
895
- </pheromones>"
896
-
897
- # Write to file if output path specified
898
- local output_path=""
899
- if [[ -n "$output_xml" ]]; then
900
- local output_dir
901
- output_dir=$(dirname "$output_xml")
902
- if [[ ! -d "$output_dir" ]]; then
903
- mkdir -p "$output_dir" 2>/dev/null || {
904
- xml_json_err "Cannot create output directory: $output_dir"
905
- return 1
906
- }
907
- fi
908
- echo "$xml_output" > "$output_xml" || {
909
- xml_json_err "Failed to write output file: $output_xml"
910
- return 1
911
- }
912
- output_path="$output_xml"
913
- fi
914
-
915
- # Validate against XSD schema if available
916
- local validation_result="false"
917
- if [[ -f "$xsd_file" && -n "$output_path" ]]; then
918
- local validation_output
919
- validation_output=$(xml-validate "$output_path" "$xsd_file" 2>/dev/null)
920
- if echo "$validation_output" | jq -e '.result.valid' >/dev/null 2>&1; then
921
- validation_result="true"
922
- fi
923
- elif [[ -f "$xsd_file" ]]; then
924
- # Validate in-memory by writing to temp file
925
- local temp_xml
926
- temp_xml=$(mktemp)
927
- echo "$xml_output" > "$temp_xml"
928
- local validation_output
929
- validation_output=$(xml-validate "$temp_xml" "$xsd_file" 2>/dev/null)
930
- if echo "$validation_output" | jq -e '.result.valid' >/dev/null 2>&1; then
931
- validation_result="true"
932
- fi
933
- rm -f "$temp_xml"
934
- fi
935
-
936
- # Build result JSON
937
- local escaped_xml result_json
938
- escaped_xml=$(echo "$xml_output" | jq -Rs '.')
939
- result_json="{\"xml\":$escaped_xml,\"validated\":$validation_result,\"signals\":$signal_count"
940
- if [[ -n "$output_path" ]]; then
941
- local escaped_path
942
- escaped_path=$(echo "$output_path" | jq -Rs '.[:-1]')
943
- result_json="$result_json,\"path\":$escaped_path"
944
- fi
945
- result_json="$result_json}"
946
-
947
- xml_json_ok "$result_json"
948
- }
949
-
950
- # pheromone-from-xml: Parse pheromone XML to JSON
951
- # Usage: pheromone-from-xml <pheromone_xml_file>
952
- # Returns: {"ok":true,"result":{"signal":"focus",...}}
953
- pheromone-from-xml() {
954
- local xml_file="${1:-}"
955
-
956
- [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
957
- [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
958
-
959
- if [[ "$XMLSTARLET_AVAILABLE" != "true" ]]; then
960
- xml_json_err "xmlstarlet required for pheromone parsing"
961
- return 1
962
- fi
963
-
964
- # Extract pheromone fields from XML
965
- local signal priority message timestamp source context
966
- signal=$(xmlstarlet sel -t -v "/pheromone/signal" "$xml_file" 2>/dev/null || echo "")
967
- priority=$(xmlstarlet sel -t -v "/pheromone/priority" "$xml_file" 2>/dev/null || echo "normal")
968
- message=$(xmlstarlet sel -t -v "/pheromone/message" "$xml_file" 2>/dev/null || echo "")
969
- timestamp=$(xmlstarlet sel -t -v "/pheromone/timestamp" "$xml_file" 2>/dev/null || echo "")
970
- source=$(xmlstarlet sel -t -v "/pheromone/source" "$xml_file" 2>/dev/null || echo "colony")
971
- context=$(xmlstarlet sel -t -v "/pheromone/context" "$xml_file" 2>/dev/null || echo "")
972
-
973
- # Build JSON result
974
- local json_result
975
- json_result=$(jq -n \
976
- --arg signal "$signal" \
977
- --arg priority "$priority" \
978
- --arg message "$message" \
979
- --arg timestamp "$timestamp" \
980
- --arg source "$source" \
981
- --arg context "$context" \
982
- '{signal: $signal, priority: $priority, message: $message, timestamp: $timestamp, source: $source, context: (if $context == "" then null else $context end)}')
983
-
984
- xml_json_ok "$json_result"
985
- }
986
-
987
- # ============================================================================
988
- # Queen-Wisdom XML Format
989
- # ============================================================================
990
-
991
- # queen-wisdom-to-xml: Convert queen wisdom JSON to XML
992
- # Usage: queen-wisdom-to-xml <wisdom_json_file>
993
- # Returns: {"ok":true,"result":{"xml":"<queen-wisdom>...</queen-wisdom>"}}
994
- queen-wisdom-to-xml() {
995
- local json_file="${1:-}"
996
-
997
- [[ -z "$json_file" ]] && { xml_json_err "Missing JSON file argument"; return 1; }
998
- [[ -f "$json_file" ]] || { xml_json_err "JSON file not found: $json_file"; return 1; }
999
-
1000
- if ! jq empty "$json_file" 2>/dev/null; then
1001
- xml_json_err "Invalid JSON file: $json_file"
1002
- return 1
1003
- fi
1004
-
1005
- # Convert queen wisdom to structured XML
1006
- local xml_output
1007
- xml_output=$(jq -r '
1008
- "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
1009
- "<queen-wisdom version=\"1.0\">\n" +
1010
- " <directive>" + (.directive // "") + "</directive>\n" +
1011
- (if .patterns then
1012
- " <patterns>\n" +
1013
- (.patterns | map(" <pattern>\(.)</pattern>") | join("\n")) +
1014
- "\n </patterns>\n"
1015
- else "" end) +
1016
- (if .constraints then
1017
- " <constraints>\n" +
1018
- (.constraints | map(" <constraint>\(.)</constraint>") | join("\n")) +
1019
- "\n </constraints>\n"
1020
- else "" end) +
1021
- " <timestamp>" + (.timestamp // (now | todateiso8601)) + "</timestamp>\n" +
1022
- "</queen-wisdom>"
1023
- ' "$json_file" 2>/dev/null) || {
1024
- xml_json_err "Queen wisdom conversion failed"
1025
- return 1
1026
- }
1027
-
1028
- local escaped_xml
1029
- escaped_xml=$(echo "$xml_output" | jq -Rs '.')
1030
- xml_json_ok "{\"xml\":$escaped_xml}"
1031
- }
1032
-
1033
- # queen-wisdom-from-xml: Parse queen wisdom XML to JSON
1034
- # Usage: queen-wisdom-from-xml <wisdom_xml_file>
1035
- # Returns: {"ok":true,"result":{"directive":"...",...}}
1036
- queen-wisdom-from-xml() {
1037
- local xml_file="${1:-}"
1038
-
1039
- [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
1040
- [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
1041
-
1042
- if [[ "$XMLSTARLET_AVAILABLE" != "true" ]]; then
1043
- xml_json_err "xmlstarlet required for queen wisdom parsing"
1044
- return 1
1045
- fi
1046
-
1047
- # Extract fields
1048
- local directive timestamp
1049
- directive=$(xmlstarlet sel -t -v "/queen-wisdom/directive" "$xml_file" 2>/dev/null || echo "")
1050
- timestamp=$(xmlstarlet sel -t -v "/queen-wisdom/timestamp" "$xml_file" 2>/dev/null || echo "")
1051
-
1052
- # Extract arrays
1053
- local patterns_json constraints_json
1054
- patterns_json=$(xmlstarlet sel -t -m "/queen-wisdom/patterns/pattern" -v "." -n "$xml_file" 2>/dev/null | jq -R -s 'split("\n") | map(select(length > 0))')
1055
- constraints_json=$(xmlstarlet sel -t -m "/queen-wisdom/constraints/constraint" -v "." -n "$xml_file" 2>/dev/null | jq -R -s 'split("\n") | map(select(length > 0))')
1056
-
1057
- # Build result
1058
- local json_result
1059
- json_result=$(jq -n \
1060
- --arg directive "$directive" \
1061
- --arg timestamp "$timestamp" \
1062
- --argjson patterns "$patterns_json" \
1063
- --argjson constraints "$constraints_json" \
1064
- '{directive: $directive, timestamp: $timestamp, patterns: $patterns, constraints: $constraints}')
1065
-
1066
- xml_json_ok "$json_result"
1067
- }
1068
-
1069
- # ============================================================================
1070
- # Multi-Colony Registry XML
1071
- # ============================================================================
1072
-
1073
- # registry-to-xml: Convert colony registry JSON to XML
1074
- # Usage: registry-to-xml <registry_json_file>
1075
- # Returns: {"ok":true,"result":{"xml":"<colony-registry>...</colony-registry>"}}
1076
- registry-to-xml() {
1077
- local json_file="${1:-}"
1078
-
1079
- [[ -z "$json_file" ]] && { xml_json_err "Missing JSON file argument"; return 1; }
1080
- [[ -f "$json_file" ]] || { xml_json_err "JSON file not found: $json_file"; return 1; }
1081
-
1082
- if ! jq empty "$json_file" 2>/dev/null; then
1083
- xml_json_err "Invalid JSON file: $json_file"
1084
- return 1
1085
- fi
1086
-
1087
- # Convert registry to XML
1088
- local xml_output
1089
- xml_output=$(jq -r '
1090
- "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
1091
- "<colony-registry version=\"1.0\" generated=\"" + (now | todateiso8601) + "\">\n" +
1092
- (if .colonies then
1093
- (.colonies | map(
1094
- " <colony id=\"" + .id + "\">\n" +
1095
- " <name>" + (.name // "") + "</name>\n" +
1096
- " <status>" + (.status // "unknown") + "</status>\n" +
1097
- " <location>" + (.location // "") + "</location>\n" +
1098
- (if .pheromones then
1099
- " <pheromones>\n" +
1100
- (.pheromones | map(
1101
- " <pheromone signal=\"" + .signal + "\">" + (.message // "") + "</pheromone>"
1102
- ) | join("\n")) +
1103
- "\n </pheromones>\n"
1104
- else "" end) +
1105
- " </colony>"
1106
- ) | join("\n")) + "\n"
1107
- else "" end) +
1108
- "</colony-registry>"
1109
- ' "$json_file" 2>/dev/null) || {
1110
- xml_json_err "Registry conversion failed"
1111
- return 1
1112
- }
1113
-
1114
- local escaped_xml
1115
- escaped_xml=$(echo "$xml_output" | jq -Rs '.')
1116
- xml_json_ok "{\"xml\":$escaped_xml}"
1117
- }
1118
-
1119
- # registry-from-xml: Parse colony registry XML to JSON
1120
- # Usage: registry-from-xml <registry_xml_file>
1121
- # Returns: {"ok":true,"result":{"colonies":[...]}}
1122
- registry-from-xml() {
1123
- local xml_file="${1:-}"
1124
-
1125
- [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
1126
- [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
1127
-
1128
- if [[ "$XMLSTARLET_AVAILABLE" != "true" ]]; then
1129
- xml_json_err "xmlstarlet required for registry parsing"
1130
- return 1
1131
- fi
1132
-
1133
- # Extract colonies
1134
- local colonies_json
1135
- colonies_json=$(xmlstarlet sel -t -m "/colony-registry/colony" \
1136
- -v "@id" -o '|' \
1137
- -v "name" -o '|' \
1138
- -v "status" -o '|' \
1139
- -v "location" -n \
1140
- "$xml_file" 2>/dev/null | \
1141
- awk -F'|' 'NF>=3 {
1142
- printf "{\"id\":\"%s\",\"name\":\"%s\",\"status\":\"%s\",\"location\":\"%s\"}", $1, $2, $3, $4
1143
- }' | \
1144
- jq -s '.')
1145
-
1146
- local json_result
1147
- json_result=$(jq -n --argjson colonies "$colonies_json" '{colonies: $colonies}')
1148
-
1149
- xml_json_ok "$json_result"
1150
- }
1151
-
1152
- # ============================================================================
1153
- # Pheromone Export to Eternal Memory
1154
- # ============================================================================
1155
-
1156
- # pheromone-export: Export pheromones to eternal XML format
1157
- # Usage: pheromone-export [input_json] [output_xml] [session_id]
1158
- # input_json: Path to pheromones.json (default: .aether/data/pheromones.json)
1159
- # output_xml: Path to output XML (default: ~/.aether/eternal/pheromones.xml)
1160
- # session_id: Colony session ID for namespace generation (optional, auto-detected from JSON)
1161
- # Returns: {"ok":true,"result":{"exported":true,"path":"...","signals":N,"namespace":"..."}} or error
1162
- pheromone-export() {
1163
- local input_json="${1:-.aether/data/pheromones.json}"
1164
- local output_xml="${2:-$HOME/.aether/eternal/pheromones.xml}"
1165
- local session_id="${3:-}"
1166
- local schema_file="${4:-.aether/schemas/pheromone.xsd}"
1167
-
1168
- # Validate input file exists
1169
- [[ -f "$input_json" ]] || { xml_json_err "Pheromone JSON file not found: $input_json"; return 1; }
1170
-
1171
- # Validate JSON
1172
- if ! jq empty "$input_json" 2>/dev/null; then
1173
- xml_json_err "Invalid JSON in pheromone file: $input_json"
1174
- return 1
1175
- fi
1176
-
1177
- # Get absolute paths for schema validation
1178
- local abs_schema
1179
- abs_schema="$(cd "$(dirname "$schema_file")" && pwd)/$(basename "$schema_file")" 2>/dev/null || abs_schema="$schema_file"
1180
-
1181
- # Generate ISO timestamp for XML
1182
- local generated_at
1183
- generated_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1184
-
1185
- # Get version and colony_id from JSON
1186
- local version colony_id
1187
- version=$(jq -r '.version // "1.0.0"' "$input_json")
1188
- colony_id=$(jq -r '.colony_id // "unknown"' "$input_json")
1189
-
1190
- # Auto-detect session_id from JSON if not provided
1191
- if [[ -z "$session_id" ]]; then
1192
- session_id=$(jq -r '.session_id // .colony_id // ""' "$input_json")
1193
- fi
1194
-
1195
- # Generate colony namespace if session_id available
1196
- local colony_namespace=""
1197
- local colony_prefix=""
1198
- if [[ -n "$session_id" ]]; then
1199
- local ns_result
1200
- ns_result=$(generate-colony-namespace "$session_id" 2>/dev/null)
1201
- if echo "$ns_result" | jq -e '.ok' >/dev/null 2>&1; then
1202
- colony_namespace=$(echo "$ns_result" | jq -r '.result.namespace')
1203
- colony_prefix=$(echo "$ns_result" | jq -r '.result.prefix')
1204
- fi
1205
- fi
1206
-
1207
- # Build XML header with proper namespace
1208
- local xml_output
1209
- xml_output="<?xml version=\"1.0\" encoding=\"UTF-8\"?>
1210
- <pheromones xmlns=\"http://aether.colony/schemas/pheromones\"
1211
- xmlns:ph=\"http://aether.colony/schemas/pheromones\""
1212
-
1213
- # Add colony namespace if available
1214
- if [[ -n "$colony_namespace" ]]; then
1215
- xml_output="$xml_output
1216
- xmlns:col=\"$colony_namespace\"
1217
- col:session=\"$session_id\"
1218
- col:prefix=\"$colony_prefix\""
1219
- fi
1220
-
1221
- xml_output="$xml_output
1222
- version=\"$version\"
1223
- generated_at=\"$generated_at\"
1224
- colony_id=\"$colony_id\">
1225
- <metadata>
1226
- <source type=\"system\" version=\"$version\">aether-pheromone-export</source>
1227
- <context>Colony pheromone trail export to eternal memory</context>
1228
- </metadata>"
1229
-
1230
- # Process each signal
1231
- local signal_count=0
1232
- local signals_json
1233
- signals_json=$(jq -c '.signals // [] | .[]' "$input_json" 2>/dev/null)
1234
-
1235
- if [[ -n "$signals_json" ]]; then
1236
- while IFS= read -r signal; do
1237
- [[ -n "$signal" ]] || continue
1238
-
1239
- local sig_id sig_type priority source created_at expires_at active
1240
- sig_id=$(echo "$signal" | jq -r '.id // "unknown"')
1241
- sig_type=$(echo "$signal" | jq -r '.type // "FOCUS"')
1242
- priority=$(echo "$signal" | jq -r '.priority // "normal"')
1243
- source=$(echo "$signal" | jq -r '.source // "system"')
1244
- created_at=$(echo "$signal" | jq -r '.created_at // ""')
1245
- expires_at=$(echo "$signal" | jq -r '.expires_at // ""')
1246
- active=$(echo "$signal" | jq -r '.active // true')
1247
-
1248
- # XML escape the ID and source
1249
- local escaped_id escaped_source
1250
- escaped_id=$(echo "$sig_id" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
1251
- escaped_source=$(echo "$source" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
1252
-
1253
- xml_output="$xml_output
1254
- <signal id=\"$escaped_id\"
1255
- type=\"$sig_type\"
1256
- priority=\"$priority\"
1257
- source=\"$escaped_source\"
1258
- created_at=\"$created_at\""
1259
-
1260
- if [[ -n "$expires_at" && "$expires_at" != "null" ]]; then
1261
- xml_output="$xml_output
1262
- expires_at=\"$expires_at\""
1263
- fi
1264
-
1265
- xml_output="$xml_output
1266
- active=\"$active\">"
1267
-
1268
- # Content section
1269
- local content_text
1270
- content_text=$(echo "$signal" | jq -r '.content.text // ""')
1271
- if [[ -n "$content_text" ]]; then
1272
- local escaped_text
1273
- escaped_text=$(echo "$content_text" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')
1274
- xml_output="$xml_output
1275
- <content>
1276
- <text>$escaped_text</text>"
1277
-
1278
- # Check for data attachment
1279
- local has_data data_format
1280
- has_data=$(echo "$signal" | jq -r 'has("content") and has("content.data")')
1281
- if [[ "$has_data" == "true" ]]; then
1282
- data_format=$(echo "$signal" | jq -r '.content.data.format // "json"')
1283
- xml_output="$xml_output
1284
- <data format=\"$data_format\">"
1285
- # Add data content as CDATA or escaped
1286
- local data_content
1287
- data_content=$(echo "$signal" | jq -c '.content.data' 2>/dev/null)
1288
- xml_output="$xml_output$data_content"
1289
- xml_output="$xml_output</data>"
1290
- fi
1291
-
1292
- xml_output="$xml_output
1293
- </content>"
1294
- fi
1295
-
1296
- # Tags section
1297
- local tags_json
1298
- tags_json=$(echo "$signal" | jq -c '.tags // []')
1299
- if [[ "$tags_json" != "[]" && -n "$tags_json" ]]; then
1300
- xml_output="$xml_output
1301
- <tags>"
1302
- local tags_array
1303
- tags_array=$(echo "$signal" | jq -c '.tags // [] | .[]')
1304
- while IFS= read -r tag; do
1305
- [[ -n "$tag" ]] || continue
1306
- local tag_value tag_weight tag_category
1307
- tag_value=$(echo "$tag" | jq -r '.value // .')
1308
- tag_weight=$(echo "$tag" | jq -r '.weight // "1.0"')
1309
- tag_category=$(echo "$tag" | jq -r '.category // ""')
1310
-
1311
- local escaped_tag
1312
- escaped_tag=$(echo "$tag_value" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
1313
-
1314
- if [[ -n "$tag_category" && "$tag_category" != "null" ]]; then
1315
- xml_output="$xml_output
1316
- <tag weight=\"$tag_weight\" category=\"$tag_category\">$escaped_tag</tag>"
1317
- else
1318
- xml_output="$xml_output
1319
- <tag weight=\"$tag_weight\">$escaped_tag</tag>"
1320
- fi
1321
- done <<< "$tags_array"
1322
- xml_output="$xml_output
1323
- </tags>"
1324
- fi
1325
-
1326
- # Scope section
1327
- local scope_global
1328
- scope_global=$(echo "$signal" | jq -r '.scope.global // false')
1329
- xml_output="$xml_output
1330
- <scope global=\"$scope_global\">"
1331
-
1332
- # Castes
1333
- local castes_json
1334
- castes_json=$(echo "$signal" | jq -c '.scope.castes // []')
1335
- if [[ "$castes_json" != "[]" && -n "$castes_json" ]]; then
1336
- xml_output="$xml_output
1337
- <castes match=\"any\">"
1338
- local caste_array
1339
- caste_array=$(echo "$signal" | jq -r '.scope.castes[]')
1340
- while IFS= read -r caste; do
1341
- [[ -n "$caste" ]] || continue
1342
- xml_output="$xml_output
1343
- <caste>$caste</caste>"
1344
- done <<< "$caste_array"
1345
- xml_output="$xml_output
1346
- </castes>"
1347
- fi
1348
-
1349
- # Paths
1350
- local paths_json
1351
- paths_json=$(echo "$signal" | jq -c '.scope.paths // []')
1352
- if [[ "$paths_json" != "[]" && -n "$paths_json" ]]; then
1353
- xml_output="$xml_output
1354
- <paths match=\"any\">"
1355
- local path_array
1356
- path_array=$(echo "$signal" | jq -r '.scope.paths[]')
1357
- while IFS= read -r path; do
1358
- [[ -n "$path" ]] || continue
1359
- local escaped_path
1360
- escaped_path=$(echo "$path" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
1361
- xml_output="$xml_output
1362
- <path>$escaped_path</path>"
1363
- done <<< "$path_array"
1364
- xml_output="$xml_output
1365
- </paths>"
1366
- fi
1367
-
1368
- xml_output="$xml_output
1369
- </scope>"
1370
-
1371
- xml_output="$xml_output
1372
- </signal>"
1373
- ((signal_count++))
1374
- done <<< "$signals_json"
1375
- fi
1376
-
1377
- # Close root element
1378
- xml_output="$xml_output
1379
- </pheromones>"
1380
-
1381
- # Ensure output directory exists
1382
- local output_dir
1383
- output_dir=$(dirname "$output_xml")
1384
- if [[ ! -d "$output_dir" ]]; then
1385
- mkdir -p "$output_dir" 2>/dev/null || {
1386
- xml_json_err "Cannot create output directory: $output_dir"
1387
- return 1
1388
- }
1389
- fi
1390
-
1391
- # Write XML to file
1392
- echo "$xml_output" > "$output_xml" || {
1393
- xml_json_err "Failed to write output file: $output_xml"
1394
- return 1
1395
- }
1396
-
1397
- # Validate against schema if available
1398
- local validation_result="false"
1399
- if [[ -f "$abs_schema" ]]; then
1400
- validation_result=$(xml-validate "$output_xml" "$abs_schema" 2>/dev/null)
1401
- if ! echo "$validation_result" | jq -e '.result.valid' >/dev/null 2>&1; then
1402
- xml_json_err "XML validation failed against schema: $abs_schema"
1403
- return 1
1404
- fi
1405
- validation_result="true"
1406
- fi
1407
-
1408
- # Return success with metadata
1409
- local escaped_output
1410
- escaped_output=$(echo "$output_xml" | jq -Rs '.[:-1]')
1411
- local result_json
1412
- result_json="{\"exported\":true,\"path\":$escaped_output,\"signals\":$signal_count,\"validated\":$validation_result"
1413
- if [[ -n "$colony_namespace" ]]; then
1414
- result_json="$result_json,\"namespace\":\"$colony_namespace\",\"prefix\":\"$colony_prefix\""
1415
- fi
1416
- result_json="$result_json}"
1417
- xml_json_ok "$result_json"
1418
- }
1419
-
1420
- # ============================================================================
1421
- # Colony Namespace Generation
1422
- # ============================================================================
1423
-
1424
- # Colony namespace base URI
1425
- readonly COLONY_NAMESPACE_BASE="http://aether.dev/colony"
1426
-
1427
- # generate-colony-namespace: Generate unique namespace URI for a colony session
1428
- # Usage: generate-colony-namespace <session_id>
1429
- # Returns: {"ok":true,"result":{"namespace":"http://aether.dev/colony/{session_id}","prefix":"col_{hash}"}}
1430
- generate-colony-namespace() {
1431
- local session_id="${1:-}"
1432
-
1433
- [[ -z "$session_id" ]] && { xml_json_err "Missing session_id argument"; return 1; }
1434
-
1435
- # Generate namespace URI
1436
- local namespace_uri="${COLONY_NAMESPACE_BASE}/${session_id}"
1437
-
1438
- # Generate short prefix from session_id (first 8 chars of MD5 hash)
1439
- local prefix
1440
- if command -v md5sum >/dev/null 2>&1; then
1441
- prefix="col_$(echo -n "$session_id" | md5sum | cut -c1-8)"
1442
- elif command -v md5 >/dev/null 2>&1; then
1443
- prefix="col_$(echo -n "$session_id" | md5 | cut -c1-8)"
1444
- else
1445
- # Fallback: use first 8 alphanumeric chars of session_id
1446
- prefix="col_$(echo -n "$session_id" | tr -cd '[:alnum:]' | cut -c1-8)"
1447
- fi
1448
-
1449
- xml_json_ok "{\"namespace\":\"$namespace_uri\",\"prefix\":\"$prefix\",\"session_id\":\"$session_id\"}"
1450
- }
1451
-
1452
- # generate-cross-colony-prefix: Generate prefix for external colony pheromones
1453
- # Usage: generate-cross-colony-prefix <external_session_id> [local_session_id]
1454
- # Returns: {"ok":true,"result":{"prefix":"ext_{hash}_{short_id}","full_prefix":"{local_prefix}_{external_prefix}"}}
1455
- generate-cross-colony-prefix() {
1456
- local external_session_id="${1:-}"
1457
- local local_session_id="${2:-}"
1458
-
1459
- [[ -z "$external_session_id" ]] && { xml_json_err "Missing external_session_id argument"; return 1; }
1460
-
1461
- # Generate external colony prefix
1462
- local external_prefix
1463
- if command -v md5sum >/dev/null 2>&1; then
1464
- external_prefix="ext_$(echo -n "$external_session_id" | md5sum | cut -c1-6)"
1465
- elif command -v md5 >/dev/null 2>&1; then
1466
- external_prefix="ext_$(echo -n "$external_session_id" | md5 | cut -c1-6)"
1467
- else
1468
- external_prefix="ext_$(echo -n "$external_session_id" | tr -cd '[:alnum:]' | cut -c1-6)"
1469
- fi
1470
-
1471
- # If local session provided, create combined prefix for collision prevention
1472
- local full_prefix="$external_prefix"
1473
- if [[ -n "$local_session_id" ]]; then
1474
- local local_hash
1475
- if command -v md5sum >/dev/null 2>&1; then
1476
- local_hash="$(echo -n "$local_session_id" | md5sum | cut -c1-4)"
1477
- elif command -v md5 >/dev/null 2>&1; then
1478
- local_hash="$(echo -n "$local_session_id" | md5 | cut -c1-4)"
1479
- else
1480
- local_hash="$(echo -n "$local_session_id" | tr -cd '[:alnum:]' | cut -c1-4)"
1481
- fi
1482
- full_prefix="${local_hash}_${external_prefix}"
1483
- fi
1484
-
1485
- xml_json_ok "{\"prefix\":\"$external_prefix\",\"full_prefix\":\"$full_prefix\",\"external_session\":\"$external_session_id\"}"
1486
- }
1487
-
1488
- # prefix-pheromone-id: Prefix a pheromone ID to prevent collisions
1489
- # Usage: prefix-pheromone-id <pheromone_id> <colony_prefix>
1490
- # Returns: {"ok":true,"result":"{prefix}_{pheromone_id}"}
1491
- prefix-pheromone-id() {
1492
- local pheromone_id="${1:-}"
1493
- local colony_prefix="${2:-}"
1494
-
1495
- [[ -z "$pheromone_id" ]] && { xml_json_err "Missing pheromone_id argument"; return 1; }
1496
- [[ -z "$colony_prefix" ]] && { xml_json_err "Missing colony_prefix argument"; return 1; }
1497
-
1498
- # Check if already prefixed with this colony
1499
- if [[ "$pheromone_id" == ${colony_prefix}_* ]]; then
1500
- xml_json_ok "\"$pheromone_id\""
1501
- return 0
1502
- fi
1503
-
1504
- local prefixed_id="${colony_prefix}_${pheromone_id}"
1505
- xml_json_ok "\"$prefixed_id\""
1506
- }
1507
-
1508
- # extract-session-from-namespace: Extract session ID from namespace URI
1509
- # Usage: extract-session-from-namespace <namespace_uri>
1510
- # Returns: {"ok":true,"result":"{session_id}"}
1511
- extract-session-from-namespace() {
1512
- local namespace_uri="${1:-}"
1513
-
1514
- [[ -z "$namespace_uri" ]] && { xml_json_err "Missing namespace_uri argument"; return 1; }
1515
-
1516
- # Extract session ID from http://aether.dev/colony/{session_id}
1517
- local session_id
1518
- if [[ "$namespace_uri" =~ ^http://aether\.dev/colony/(.+)$ ]]; then
1519
- session_id="${BASH_REMATCH[1]}"
1520
- xml_json_ok "\"$session_id\""
1521
- else
1522
- xml_json_err "Invalid colony namespace format: $namespace_uri"
1523
- return 1
1524
- fi
1525
- }
1526
-
1527
- # validate-colony-namespace: Validate a colony namespace URI
1528
- # Usage: validate-colony-namespace <namespace_uri>
1529
- # Returns: {"ok":true,"result":{"valid":true,"type":"colony","session_id":"..."}}
1530
- validate-colony-namespace() {
1531
- local namespace_uri="${1:-}"
1532
-
1533
- [[ -z "$namespace_uri" ]] && { xml_json_err "Missing namespace_uri argument"; return 1; }
1534
-
1535
- # Check if it matches colony namespace pattern
1536
- if [[ "$namespace_uri" =~ ^http://aether\.dev/colony/([a-zA-Z0-9_-]+)$ ]]; then
1537
- local session_id="${BASH_REMATCH[1]}"
1538
- xml_json_ok "{\"valid\":true,\"type\":\"colony\",\"session_id\":\"$session_id\"}"
1539
- elif [[ "$namespace_uri" == "http://aether.colony/schemas/pheromones" ]]; then
1540
- xml_json_ok "{\"valid\":true,\"type\":\"schema\",\"session_id\":null}"
1541
- else
1542
- xml_json_ok "{\"valid\":false,\"type\":null,\"session_id\":null}"
1543
- fi
1544
- }
1545
-
1546
- # ============================================================================
1547
- # Queen-Wisdom Markdown Generation (XSLT-based)
1548
- # ============================================================================
1549
-
1550
- # queen-wisdom-to-markdown: Convert queen-wisdom XML to markdown using XSLT
1551
- # Usage: queen-wisdom-to-markdown <wisdom_xml_file> [output_md_file]
1552
- # wisdom_xml_file: Path to queen-wisdom.xml
1553
- # output_md_file: Optional path to write markdown output (default: stdout)
1554
- # Returns: {"ok":true,"result":{"markdown":"...","path":"..."}} or error
1555
- queen-wisdom-to-markdown() {
1556
- local xml_file="${1:-}"
1557
- local output_file="${2:-}"
1558
-
1559
- # Validate arguments
1560
- [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
1561
- [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
1562
-
1563
- # Check for xsltproc
1564
- if [[ "$XSLTPROC_AVAILABLE" != "true" ]]; then
1565
- xml_json_err "xsltproc not available. Install libxslt utilities."
1566
- return 1
1567
- fi
1568
-
1569
- # Find XSLT file (check multiple locations)
1570
- local xsl_file=""
1571
- local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1572
-
1573
- # Search paths for XSLT file
1574
- local search_paths=(
1575
- "$script_dir/queen-to-md.xsl"
1576
- ".aether/utils/queen-to-md.xsl"
1577
- "runtime/utils/queen-to-md.xsl"
1578
- "$HOME/.aether/system/utils/queen-to-md.xsl"
1579
- )
1580
-
1581
- for path in "${search_paths[@]}"; do
1582
- if [[ -f "$path" ]]; then
1583
- xsl_file="$path"
1584
- break
1585
- fi
1586
- done
1587
-
1588
- if [[ -z "$xsl_file" ]]; then
1589
- xml_json_err "XSLT file queen-to-md.xsl not found in standard locations"
1590
- return 1
1591
- fi
1592
-
1593
- # Validate XML against schema first
1594
- local schema_file="$script_dir/../schemas/queen-wisdom.xsd"
1595
- if [[ -f "$schema_file" ]]; then
1596
- local validation
1597
- validation=$(xml-validate "$xml_file" "$schema_file" 2>/dev/null)
1598
- if ! echo "$validation" | jq -e '.result.valid' >/dev/null 2>&1; then
1599
- xml_json_err "XML validation failed before conversion"
1600
- return 1
1601
- fi
1602
- fi
1603
-
1604
- # Perform XSLT transformation
1605
- local markdown
1606
- if ! markdown=$(xsltproc "$xsl_file" "$xml_file" 2>&1); then
1607
- xml_json_err "XSLT transformation failed: $markdown"
1608
- return 1
1609
- fi
1610
-
1611
- # Output handling
1612
- if [[ -n "$output_file" ]]; then
1613
- if echo "$markdown" > "$output_file"; then
1614
- xml_json_ok "{\"markdown\":\"(written to file)\",\"path\":\"$output_file\"}"
1615
- else
1616
- xml_json_err "Failed to write to output file: $output_file"
1617
- return 1
1618
- fi
1619
- else
1620
- # Return markdown in JSON result
1621
- local escaped_markdown
1622
- escaped_markdown=$(echo "$markdown" | jq -Rs '.[:-1]')
1623
- xml_json_ok "{\"markdown\":$escaped_markdown,\"path\":null}"
1624
- fi
1625
- }
1626
-
1627
- # ============================================================================
1628
- # Queen-Wisdom Promotion Workflow
1629
- # ============================================================================
1630
-
1631
- # Get promotion threshold for a wisdom type
1632
- # Usage: _get_promotion_threshold <type>
1633
- # Returns: threshold value
1634
- _get_promotion_threshold() {
1635
- local wisdom_type="$1"
1636
- case "$wisdom_type" in
1637
- philosophy) echo "5" ;;
1638
- pattern) echo "3" ;;
1639
- redirect) echo "2" ;;
1640
- stack) echo "1" ;;
1641
- decree) echo "0" ;;
1642
- *) echo "1" ;;
1643
- esac
1644
- }
1645
-
1646
- # queen-wisdom-validate-entry: Validate a single wisdom entry
1647
- # Usage: queen-wisdom-validate-entry <xml_file> <entry_id>
1648
- # Returns: {"ok":true,"result":{"valid":true,"errors":[],"warnings":[]}} or error
1649
- queen-wisdom-validate-entry() {
1650
- local xml_file="${1:-}"
1651
- local entry_id="${2:-}"
1652
-
1653
- [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
1654
- [[ -z "$entry_id" ]] && { xml_json_err "Missing entry ID argument"; return 1; }
1655
- [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
1656
-
1657
- if [[ "$XMLLINT_AVAILABLE" != "true" ]]; then
1658
- xml_json_err "xmllint not available"
1659
- return 1
1660
- fi
1661
-
1662
- # Build XPath query to find the entry by ID
1663
- local xpath_query="//*[@id='$entry_id']"
1664
- local entry_xml
1665
- entry_xml=$(xmllint --xpath "$xpath_query" "$xml_file" 2>/dev/null) || {
1666
- xml_json_err "Entry not found with ID: $entry_id"
1667
- return 1
1668
- }
1669
-
1670
- # Initialize validation results
1671
- local errors=()
1672
- local warnings=()
1673
-
1674
- # Extract attributes for validation
1675
- local confidence domain source created_at content
1676
- confidence=$(xmllint --xpath "string(//*[@id='$entry_id']/@confidence)" "$xml_file" 2>/dev/null)
1677
- domain=$(xmllint --xpath "string(//*[@id='$entry_id']/@domain)" "$xml_file" 2>/dev/null)
1678
- source=$(xmllint --xpath "string(//*[@id='$entry_id']/@source)" "$xml_file" 2>/dev/null)
1679
- created_at=$(xmllint --xpath "string(//*[@id='$entry_id']/@created_at)" "$xml_file" 2>/dev/null)
1680
- content=$(xmllint --xpath "string(//*[@id='$entry_id']/qw:content)" "$xml_file" 2>/dev/null || echo "")
1681
-
1682
- # Validate confidence (0.0 to 1.0)
1683
- if [[ -z "$confidence" ]]; then
1684
- errors+=("Missing required attribute: confidence")
1685
- elif ! [[ "$confidence" =~ ^0?\.[0-9]+$|^1\.0$ ]]; then
1686
- errors+=("Invalid confidence value: $confidence (must be 0.0-1.0)")
1687
- fi
1688
-
1689
- # Validate domain (must be from allowed list)
1690
- local valid_domains="architecture testing security performance ux process communication debugging general"
1691
- if [[ -z "$domain" ]]; then
1692
- errors+=("Missing required attribute: domain")
1693
- elif [[ ! " $valid_domains " =~ " $domain " ]]; then
1694
- errors+=("Invalid domain: $domain (must be one of: $valid_domains)")
1695
- fi
1696
-
1697
- # Validate source (must be from allowed list)
1698
- local valid_sources="queen user colony oracle observation"
1699
- if [[ -z "$source" ]]; then
1700
- errors+=("Missing required attribute: source")
1701
- elif [[ ! " $valid_sources " =~ " $source " ]]; then
1702
- errors+=("Invalid source: $source (must be one of: $valid_sources)")
1703
- fi
1704
-
1705
- # Validate created_at (ISO 8601 format)
1706
- if [[ -z "$created_at" ]]; then
1707
- errors+=("Missing required attribute: created_at")
1708
- elif ! [[ "$created_at" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2} ]]; then
1709
- errors+=("Invalid timestamp format: $created_at (expected ISO 8601)")
1710
- fi
1711
-
1712
- # Validate content
1713
- if [[ -z "$content" ]]; then
1714
- errors+=("Missing required element: content")
1715
- elif [[ ${#content} -lt 10 ]]; then
1716
- warnings+=("Content is very short (${#content} chars) - consider expanding")
1717
- fi
1718
-
1719
- # Build JSON response
1720
- local error_json="[]"
1721
- local warning_json="[]"
1722
-
1723
- if [[ ${#errors[@]} -gt 0 ]]; then
1724
- error_json=$(printf '%s\n' "${errors[@]}" | jq -R . | jq -s .)
1725
- fi
1726
-
1727
- if [[ ${#warnings[@]} -gt 0 ]]; then
1728
- warning_json=$(printf '%s\n' "${warnings[@]}" | jq -R . | jq -s .)
1729
- fi
1730
-
1731
- local valid="false"
1732
- [[ ${#errors[@]} -eq 0 ]] && valid="true"
1733
-
1734
- xml_json_ok "{\"valid\":$valid,\"errors\":$error_json,\"warnings\":$warning_json}"
1735
- }
1736
-
1737
- # queen-wisdom-promote: Promote a wisdom entry with validation
1738
- # Usage: queen-wisdom-promote <xml_file> <entry_id> [target_level]
1739
- # xml_file: Path to queen-wisdom.xml
1740
- # entry_id: ID of the entry to promote
1741
- # target_level: Optional target promotion level (defaults to next level)
1742
- # Returns: {"ok":true,"result":{"promoted":true,"from":"...","to":"...","evolution_log_updated":true}} or error
1743
- queen-wisdom-promote() {
1744
- local xml_file="${1:-}"
1745
- local entry_id="${2:-}"
1746
- local target_level="${3:-}"
1747
-
1748
- [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
1749
- [[ -z "$entry_id" ]] && { xml_json_err "Missing entry ID argument"; return 1; }
1750
- [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
1751
-
1752
- # First validate the entry
1753
- local validation
1754
- validation=$(queen-wisdom-validate-entry "$xml_file" "$entry_id" 2>&1)
1755
- if ! echo "$validation" | jq -e '.result.valid' >/dev/null 2>&1; then
1756
- local errors
1757
- errors=$(echo "$validation" | jq -r '.result.errors | join("; ")')
1758
- xml_json_err "Validation failed: $errors"
1759
- return 1
1760
- fi
1761
-
1762
- # Get current entry type and applied count
1763
- local current_type applied_count
1764
- # Determine entry type by which container it's in
1765
- if xmllint --xpath "//qw:philosophies/qw:philosophy[@id='$entry_id']" "$xml_file" >/dev/null 2>&1; then
1766
- current_type="philosophy"
1767
- applied_count=$(xmllint --xpath "string(//qw:philosophy[@id='$entry_id']/@applied_count)" "$xml_file" 2>/dev/null || echo "0")
1768
- elif xmllint --xpath "//qw:patterns/qw:pattern[@id='$entry_id']" "$xml_file" >/dev/null 2>&1; then
1769
- current_type="pattern"
1770
- applied_count=$(xmllint --xpath "string(//qw:pattern[@id='$entry_id']/@applied_count)" "$xml_file" 2>/dev/null || echo "0")
1771
- elif xmllint --xpath "//qw:redirects/qw:redirect[@id='$entry_id']" "$xml_file" >/dev/null 2>&1; then
1772
- current_type="redirect"
1773
- applied_count=$(xmllint --xpath "string(//qw:redirect[@id='$entry_id']/@applied_count)" "$xml_file" 2>/dev/null || echo "0")
1774
- elif xmllint --xpath "//qw:stack-wisdom/qw:wisdom[@id='$entry_id']" "$xml_file" >/dev/null 2>&1; then
1775
- current_type="stack"
1776
- applied_count=$(xmllint --xpath "string(//qw:stack-wisdom/qw:wisdom[@id='$entry_id']/@applied_count)" "$xml_file" 2>/dev/null || echo "0")
1777
- elif xmllint --xpath "//qw:decrees/qw:decree[@id='$entry_id']" "$xml_file" >/dev/null 2>&1; then
1778
- current_type="decree"
1779
- applied_count=$(xmllint --xpath "string(//qw:decree[@id='$entry_id']/@applied_count)" "$xml_file" 2>/dev/null || echo "0")
1780
- else
1781
- xml_json_err "Entry not found: $entry_id"
1782
- return 1
1783
- fi
1784
-
1785
- # Check promotion threshold
1786
- local threshold
1787
- threshold=$(_get_promotion_threshold "$current_type")
1788
-
1789
- if [[ "$applied_count" -lt "$threshold" ]]; then
1790
- xml_json_err "Not enough validations for promotion: $applied_count < $threshold (required for $current_type)"
1791
- return 1
1792
- fi
1793
-
1794
- # Get colony ID from metadata or use "unknown"
1795
- local colony_id
1796
- colony_id=$(xmllint --xpath "string(//qw:metadata/qw:colony_id)" "$xml_file" 2>/dev/null || echo "unknown")
1797
-
1798
- # Create evolution log entry
1799
- local timestamp
1800
- timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1801
-
1802
- # Note: In a full implementation, this would modify the XML file
1803
- # For now, we return success and indicate what would happen
1804
- xml_json_ok "{\"promoted\":true,\"entry_id\":\"$entry_id\",\"type\":\"$current_type\",\"from_applied\":$applied_count,\"threshold\":$threshold,\"colony\":\"$colony_id\",\"timestamp\":\"$timestamp\",\"note\":\"Evolution log update requires XML editing capability\"}"
1805
- }
1806
-
1807
- # queen-wisdom-import: Import wisdom from markdown QUEEN.md to XML
1808
- # Usage: queen-wisdom-import <queen_md_file> [output_xml_file]
1809
- # Returns: {"ok":true,"result":{"imported":5,"xml":"...","path":"..."}} or error
1810
- queen-wisdom-import() {
1811
- local md_file="${1:-}"
1812
- local output_file="${2:-"queen-wisdom-imported.xml"}"
1813
-
1814
- [[ -z "$md_file" ]] && { xml_json_err "Missing markdown file argument"; return 1; }
1815
- [[ -f "$md_file" ]] || { xml_json_err "Markdown file not found: $md_file"; return 1; }
1816
-
1817
- # Extract metadata from JSON block
1818
- local version last_evolved colonies
1819
- version=$(grep -A20 'METADATA' "$md_file" | grep '"version"' | sed 's/.*: "\([^"]*\)".*/\1/')
1820
- last_evolved=$(grep -A20 'METADATA' "$md_file" | grep '"last_evolved"' | sed 's/.*: "\([^"]*\)".*/\1/')
1821
-
1822
- # Generate timestamps
1823
- local created modified
1824
- created="${last_evolved:-$(date -u +"%Y-%m-%dT%H:%M:%SZ")}"
1825
- modified=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1826
-
1827
- # Start building XML
1828
- local xml='<?xml version="1.0" encoding="UTF-8"?>'
1829
- xml+=$'\n<queen-wisdom xmlns:qw="http://aether.colony/schemas/queen-wisdom/1.0">'
1830
-
1831
- # Metadata section
1832
- xml+=$'\n <metadata>'
1833
- xml+=$'\n <version>'"${version:-1.0.0}"'</version>'
1834
- xml+=$'\n <created>'"$created"'</created>'
1835
- xml+=$'\n <modified>'"$modified"'</modified>'
1836
- xml+=$'\n <colony_id>imported</colony_id>'
1837
- xml+=$'\n </metadata>'
1838
-
1839
- # Parse sections using simple grep/sed patterns
1840
- # Note: This is a basic implementation - full parsing would require more sophisticated handling
1841
-
1842
- local imported_count=0
1843
-
1844
- # Extract philosophies (simplified parsing)
1845
- xml+=$'\n <philosophies>'
1846
- while IFS= read -r line; do
1847
- # Look for lines starting with "- **" and extract content
1848
- if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*\*\* ]]; then
1849
- local id timestamp content
1850
- id=$(echo "$line" | sed -n 's/.*\*\*\([^*]*\)\*\*.*/\1/p')
1851
- timestamp=$(echo "$line" | sed -n 's/.*(\([^)]*\)).*/\1/p')
1852
- content=$(echo "$line" | sed -n 's/.*):[[:space:]]*\(.*\)/\1/p')
1853
- if [[ -n "$id" && -n "$content" ]]; then
1854
- xml+=$'\n <philosophy id="'"$id"'" confidence="0.8" domain="general" source="observation" created_at="'"${timestamp:-$modified}"'">'
1855
- xml+=$'\n <content>'"$content"'</content>'
1856
- xml+=$'\n </philosophy>'
1857
- ((imported_count++))
1858
- fi
1859
- fi
1860
- done < <(sed -n '/## 📜 Philosophies/,/## /p' "$md_file" 2>/dev/null | tail -n +4)
1861
- xml+=$'\n </philosophies>'
1862
-
1863
- # Extract patterns (simplified parsing)
1864
- xml+=$'\n <patterns>'
1865
- while IFS= read -r line; do
1866
- if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*\*\* ]]; then
1867
- local id timestamp content
1868
- id=$(echo "$line" | sed -n 's/.*\*\*\([^*]*\)\*\*.*/\1/p')
1869
- timestamp=$(echo "$line" | sed -n 's/.*(\([^)]*\)).*/\1/p')
1870
- content=$(echo "$line" | sed -n 's/.*):[[:space:]]*\(.*\)/\1/p')
1871
- if [[ -n "$id" && -n "$content" ]]; then
1872
- xml+=$'\n <pattern id="'"$id"'" confidence="0.7" domain="general" source="observation" created_at="'"${timestamp:-$modified}"'">'
1873
- xml+=$'\n <content>'"$content"'</content>'
1874
- xml+=$'\n </pattern>'
1875
- ((imported_count++))
1876
- fi
1877
- fi
1878
- done < <(sed -n '/## 🧭 Patterns/,/## /p' "$md_file" 2>/dev/null | tail -n +4)
1879
- xml+=$'\n </patterns>'
1880
-
1881
- # Similar for other sections (simplified for brevity)
1882
- xml+=$'\n <redirects />'
1883
- xml+=$'\n <stack-wisdom />'
1884
- xml+=$'\n <decrees />'
1885
-
1886
- # Evolution log
1887
- xml+=$'\n <evolution-log>'
1888
- xml+=$'\n <entry timestamp="'"$modified"'" colony="import" action="imported" type="markdown">'
1889
- xml+=$'\n <note>Imported from '"$md_file"' with '"$imported_count"' entries</note>'
1890
- xml+=$'\n </entry>'
1891
- xml+=$'\n </evolution-log>'
1892
-
1893
- xml+=$'\n</queen-wisdom>'
1894
-
1895
- # Write output
1896
- echo "$xml" > "$output_file"
1897
-
1898
- xml_json_ok "{\"imported\":$imported_count,\"xml\":\"(written to file)\",\"path\":\"$output_file\"}"
1899
- }
1900
-
1901
- # ============================================================================
1902
- # Prompt XML Conversion
1903
- # ============================================================================
1904
-
1905
- # prompt-to-xml: Convert a markdown prompt file to structured XML
1906
- # Usage: prompt-to-xml <markdown_file> [output_xml_file]
1907
- # Returns: {"ok":true,"result":{"xml":"...","path":"...","elements_extracted":N}} or error
1908
- prompt-to-xml() {
1909
- local md_file="${1:-}"
1910
- local output_file="${2:-}"
1911
-
1912
- [[ -z "$md_file" ]] && { xml_json_err "Missing markdown file argument"; return 1; }
1913
- [[ -f "$md_file" ]] || { xml_json_err "Markdown file not found: $md_file"; return 1; }
1914
-
1915
- # Extract prompt name from filename
1916
- local prompt_name
1917
- prompt_name=$(basename "$md_file" .md)
1918
-
1919
- # Initialize XML parts
1920
- local xml='<?xml version="1.0" encoding="UTF-8"?>'
1921
- xml+=$'\n<aether-prompt xmlns:ap="http://aether.colony/schemas/prompt/1.0">'
1922
-
1923
- # Metadata
1924
- xml+=$'\n <metadata>'
1925
- xml+=$'\n <version>1.0.0</version>'
1926
- xml+=$'\n <created>'"$(date -u +"%Y-%m-%dT%H:%M:%SZ")"'</created>'
1927
- xml+=$'\n </metadata>'
1928
-
1929
- # Name and type detection
1930
- xml+=$'\n <name>'"$prompt_name"'</name>'
1931
-
1932
- # Detect type from content patterns
1933
- local prompt_type="command"
1934
- if grep -q "worker\|caste\|Builder\|Watcher\|Scout" "$md_file" 2>/dev/null; then
1935
- prompt_type="worker"
1936
- elif grep -q "agent\|Agent" "$md_file" 2>/dev/null; then
1937
- prompt_type="agent"
1938
- fi
1939
- xml+=$'\n <type>'"$prompt_type"'</type>'
1940
-
1941
- # Try to detect caste for worker prompts
1942
- local caste=""
1943
- if [[ "$prompt_type" == "worker" ]]; then
1944
- if grep -qi "builder" "$md_file"; then
1945
- caste="builder"
1946
- elif grep -qi "watcher" "$md_file"; then
1947
- caste="watcher"
1948
- elif grep -qi "scout" "$md_file"; then
1949
- caste="scout"
1950
- elif grep -qi "chaos" "$md_file"; then
1951
- caste="chaos"
1952
- elif grep -qi "oracle" "$md_file"; then
1953
- caste="oracle"
1954
- elif grep -qi "architect" "$md_file"; then
1955
- caste="architect"
1956
- fi
1957
- [[ -n "$caste" ]] && xml+=$'\n <caste>'"$caste"'</caste>'
1958
- fi
1959
-
1960
- # Extract objective (first H1 or first paragraph)
1961
- local objective
1962
- objective=$(grep -m1 "^# " "$md_file" 2>/dev/null | sed 's/^# //' || head -1 "$md_file")
1963
- xml+=$'\n <objective>'"$(xml_escape_content "$objective")"'</objective>'
1964
-
1965
- # Extract requirements (## Requirements or numbered lists)
1966
- xml+=$'\n <requirements>'
1967
- local req_count=0
1968
- while IFS= read -r line; do
1969
- # Check for list items (bullet or numbered)
1970
- if [[ "$line" =~ ^[[:space:]]*[-*][[:space:]] ]] || [[ "$line" =~ ^[[:space:]]*[0-9]+\.[[:space:]] ]]; then
1971
- local req_desc
1972
- req_desc=$(echo "$line" | sed -E 's/^[[:space:]]*[-*][[:space:]]+//' | sed -E 's/^[[:space:]]*[0-9]+\.[[:space:]]+//')
1973
- ((req_count++))
1974
- xml+=$'\n <requirement id="req_'"$req_count"'" priority="normal">'
1975
- xml+=$'\n <description>'"$(xml_escape_content "$req_desc")"'</description>'
1976
- xml+=$'\n </requirement>'
1977
- fi
1978
- done < <(sed -n '/## Requirement/,/## /p' "$md_file" 2>/dev/null | tail -n +2)
1979
-
1980
- # If no requirements section found, add a default one
1981
- if [[ $req_count -eq 0 ]]; then
1982
- xml+=$'\n <requirement id="req_1" priority="normal">'
1983
- xml+=$'\n <description>Follow the instructions in this prompt</description>'
1984
- xml+=$'\n </requirement>'
1985
- fi
1986
- xml+=$'\n </requirements>'
1987
-
1988
- # Output specification
1989
- xml+=$'\n <output>'
1990
- xml+=$'\n <format>Markdown</format>'
1991
- xml+=$'\n </output>'
1992
-
1993
- # Verification
1994
- xml+=$'\n <verification>'
1995
- xml+=$'\n <method>Check output meets success criteria</method>'
1996
- xml+=$'\n </verification>'
1997
-
1998
- # Success criteria
1999
- xml+=$'\n <success_criteria>'
2000
- xml+=$'\n <criterion id="crit_1" required="true">'
2001
- xml+=$'\n <description>Task completed as specified</description>'
2002
- xml+=$'\n </criterion>'
2003
- xml+=$'\n </success_criteria>'
2004
-
2005
- xml+=$'\n</aether-prompt>'
2006
-
2007
- # Output handling
2008
- if [[ -n "$output_file" ]]; then
2009
- echo "$xml" > "$output_file"
2010
- xml_json_ok "{\"xml\":\"(written to file)\",\"path\":\"$output_file\",\"elements_extracted\":$((req_count + 5))}"
2011
- else
2012
- local escaped_xml
2013
- escaped_xml=$(echo "$xml" | jq -Rs '.[:-1]')
2014
- xml_json_ok "{\"xml\":$escaped_xml,\"path\":null,\"elements_extracted\":$((req_count + 5))}"
2015
- fi
2016
- }
2017
-
2018
- # prompt-from-xml: Convert XML prompt to markdown format
2019
- # Usage: prompt-from-xml <xml_file> [output_md_file]
2020
- # Returns: {"ok":true,"result":{"markdown":"...","path":"..."}} or error
2021
- prompt-from-xml() {
2022
- local xml_file="${1:-}"
2023
- local output_file="${2:-}"
2024
-
2025
- [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
2026
- [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
2027
-
2028
- if [[ "$XMLLINT_AVAILABLE" != "true" ]]; then
2029
- xml_json_err "xmllint not available"
2030
- return 1
2031
- fi
2032
-
2033
- # Validate against schema
2034
- local schema_file
2035
- schema_file="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../schemas/prompt.xsd"
2036
-
2037
- if [[ -f "$schema_file" ]]; then
2038
- local validation
2039
- validation=$(xml-validate "$xml_file" "$schema_file" 2>/dev/null)
2040
- if ! echo "$validation" | jq -e '.result.valid' >/dev/null 2>&1; then
2041
- xml_json_err "XML validation failed against prompt.xsd schema"
2042
- return 1
2043
- fi
2044
- fi
2045
-
2046
- # Extract fields using XPath
2047
- local name type caste objective
2048
- name=$(xmllint --xpath "string(//ap:name)" "$xml_file" 2>/dev/null || echo "Unnamed")
2049
- type=$(xmllint --xpath "string(//ap:type)" "$xml_file" 2>/dev/null || echo "command")
2050
- caste=$(xmllint --xpath "string(//ap:caste)" "$xml_file" 2>/dev/null || echo "")
2051
- objective=$(xmllint --xpath "string(//ap:objective)" "$xml_file" 2>/dev/null || echo "")
2052
-
2053
- # Build markdown
2054
- local md="# $name"
2055
- [[ -n "$caste" ]] && md+=" ($caste $type)"
2056
- md+=$'\n\n'
2057
-
2058
- md+="## Objective"
2059
- md+=$'\n\n'
2060
- md+="$objective"
2061
- md+=$'\n\n'
2062
-
2063
- # Requirements
2064
- local req_count
2065
- req_count=$(xmllint --xpath "count(//ap:requirements/ap:requirement)" "$xml_file" 2>/dev/null || echo "0")
2066
- if [[ "$req_count" -gt 0 ]]; then
2067
- md+="## Requirements"
2068
- md+=$'\n\n'
2069
-
2070
- for i in $(seq 1 "$req_count"); do
2071
- local req_desc req_priority
2072
- req_desc=$(xmllint --xpath "string(//ap:requirements/ap:requirement[$i]/ap:description)" "$xml_file" 2>/dev/null || echo "")
2073
- req_priority=$(xmllint --xpath "string(//ap:requirements/ap:requirement[$i]/@priority)" "$xml_file" 2>/dev/null || echo "normal")
2074
-
2075
- if [[ -n "$req_desc" ]]; then
2076
- md+="$i. [$req_priority] $req_desc"
2077
- md+=$'\n'
2078
- fi
2079
- done
2080
- md+=$'\n'
2081
- fi
2082
-
2083
- # Constraints
2084
- local constraint_count
2085
- constraint_count=$(xmllint --xpath "count(//ap:constraints/ap:constraint)" "$xml_file" 2>/dev/null || echo "0")
2086
- if [[ "$constraint_count" -gt 0 ]]; then
2087
- md+="## Constraints"
2088
- md+=$'\n\n'
2089
-
2090
- for i in $(seq 1 "$constraint_count"); do
2091
- local constraint_rule constraint_strength
2092
- constraint_rule=$(xmllint --xpath "string(//ap:constraints/ap:constraint[$i]/ap:rule)" "$xml_file" 2>/dev/null || echo "")
2093
- constraint_strength=$(xmllint --xpath "string(//ap:constraints/ap:constraint[$i]/@strength)" "$xml_file" 2>/dev/null || echo "should")
2094
-
2095
- if [[ -n "$constraint_rule" ]]; then
2096
- md+="- [$constraint_strength] $constraint_rule"
2097
- md+=$'\n'
2098
- fi
2099
- done
2100
- md+=$'\n'
2101
- fi
2102
-
2103
- # Output
2104
- local output_format
2105
- output_format=$(xmllint --xpath "string(//ap:output/ap:format)" "$xml_file" 2>/dev/null || echo "Markdown")
2106
- md+="## Output"
2107
- md+=$'\n\n'
2108
- md+="Format: $output_format"
2109
- md+=$'\n\n'
2110
-
2111
- # Verification
2112
- md+="## Verification"
2113
- md+=$'\n\n'
2114
- local verification_method
2115
- verification_method=$(xmllint --xpath "string(//ap:verification/ap:method)" "$xml_file" 2>/dev/null || echo "Manual review")
2116
- md+="$verification_method"
2117
- md+=$'\n\n'
2118
-
2119
- # Success criteria
2120
- md+="## Success Criteria"
2121
- md+=$'\n\n'
2122
-
2123
- local crit_count
2124
- crit_count=$(xmllint --xpath "count(//ap:success_criteria/ap:criterion)" "$xml_file" 2>/dev/null || echo "0")
2125
- for i in $(seq 1 "$crit_count"); do
2126
- local crit_desc crit_required
2127
- crit_desc=$(xmllint --xpath "string(//ap:success_criteria/ap:criterion[$i]/ap:description)" "$xml_file" 2>/dev/null || echo "")
2128
- crit_required=$(xmllint --xpath "string(//ap:success_criteria/ap:criterion[$i]/@required)" "$xml_file" 2>/dev/null || echo "true")
2129
-
2130
- if [[ -n "$crit_desc" ]]; then
2131
- [[ "$crit_required" == "true" ]] && md+="- [required] " || md+="- [optional] "
2132
- md+="$crit_desc"
2133
- md+=$'\n'
2134
- fi
2135
- done
2136
-
2137
- # Output handling
2138
- if [[ -n "$output_file" ]]; then
2139
- echo "$md" > "$output_file"
2140
- xml_json_ok "{\"markdown\":\"(written to file)\",\"path\":\"$output_file\"}"
2141
- else
2142
- local escaped_md
2143
- escaped_md=$(echo "$md" | jq -Rs '.[:-1]')
2144
- xml_json_ok "{\"markdown\":$escaped_md,\"path\":null}"
2145
- fi
2146
- }
2147
-
2148
- # prompt-validate: Validate a prompt XML file against the schema
2149
- # Usage: prompt-validate <xml_file>
2150
- # Returns: {"ok":true,"result":{"valid":true,"errors":[]}} or error
2151
- prompt-validate() {
2152
- local xml_file="${1:-}"
2153
-
2154
- [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
2155
- [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
2156
-
2157
- local schema_file
2158
- schema_file="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../schemas/prompt.xsd"
2159
-
2160
- [[ -f "$schema_file" ]] || { xml_json_err "Schema file not found: $schema_file"; return 1; }
2161
-
2162
- xml-validate "$xml_file" "$schema_file"
2163
- }
2164
-
2165
- # Helper function to escape XML content
2166
- xml_escape_content() {
2167
- local content="$1"
2168
- # Basic XML escaping
2169
- content="${content//&/&amp;}"
2170
- content="${content//\</&lt;}"
2171
- content="${content//\>/&gt;}"
2172
- content="${content//\"/&quot;}"
2173
- echo "$content"
2174
- }
2175
-
2176
- # ============================================================================
2177
- # Export Functions
2178
- # ============================================================================
2179
-
2180
- # Functions are available when this file is sourced.
2181
- # Export is disabled by default to avoid polluting stdout during tests.
2182
- # Set XML_UTILS_EXPORT=1 to enable function export for subshells.
2183
- if [[ "${XML_UTILS_EXPORT:-}" == "1" ]]; then
2184
- export -f xml-validate xml-well-formed xml-to-json json-to-xml 2>/dev/null || true
2185
- export -f xml-query xml-query-attr xml-merge xml-format 2>/dev/null || true
2186
- export -f xml-escape xml-unescape xml-detect-tools 2>/dev/null || true
2187
- export -f pheromone-to-xml pheromone-from-xml pheromone-export 2>/dev/null || true
2188
- export -f queen-wisdom-to-xml queen-wisdom-from-xml 2>/dev/null || true
2189
- export -f queen-wisdom-to-markdown queen-wisdom-validate-entry 2>/dev/null || true
2190
- export -f queen-wisdom-promote queen-wisdom-import 2>/dev/null || true
2191
- export -f registry-to-xml registry-from-xml 2>/dev/null || true
2192
- export -f generate-colony-namespace generate-cross-colony-prefix 2>/dev/null || true
2193
- export -f prefix-pheromone-id extract-session-from-namespace 2>/dev/null || true
2194
- export -f validate-colony-namespace 2>/dev/null || true
2195
- export -f prompt-to-xml prompt-from-xml prompt-validate 2>/dev/null || true
2196
- fi