@celilo/cli 0.1.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 (267) hide show
  1. package/README.md +1566 -0
  2. package/bin/celilo +16 -0
  3. package/drizzle/0000_complex_puma.sql +179 -0
  4. package/drizzle/0001_dizzy_wolfpack.sql +2 -0
  5. package/drizzle/0002_web_routes.sql +16 -0
  6. package/drizzle/0003_backup_storage.sql +32 -0
  7. package/drizzle/meta/0000_snapshot.json +1151 -0
  8. package/drizzle/meta/0001_snapshot.json +1167 -0
  9. package/drizzle/meta/0002_snapshot.json +1257 -0
  10. package/drizzle/meta/_journal.json +27 -0
  11. package/package.json +64 -0
  12. package/schemas/system_config.json +106 -0
  13. package/src/__integration__/container-services-cli.integration.test.ts +246 -0
  14. package/src/ansible/dependencies.test.ts +309 -0
  15. package/src/ansible/dependencies.ts +896 -0
  16. package/src/ansible/inventory.test.ts +463 -0
  17. package/src/ansible/inventory.ts +445 -0
  18. package/src/ansible/secrets.ts +222 -0
  19. package/src/ansible/validation.test.ts +92 -0
  20. package/src/ansible/validation.ts +272 -0
  21. package/src/api-clients/digitalocean.ts +94 -0
  22. package/src/api-clients/proxmox.ts +655 -0
  23. package/src/capabilities/logging-wrapper.test.ts +217 -0
  24. package/src/capabilities/lookup.test.ts +149 -0
  25. package/src/capabilities/lookup.ts +89 -0
  26. package/src/capabilities/public-web-helpers.test.ts +198 -0
  27. package/src/capabilities/public-web-publish.test.ts +458 -0
  28. package/src/capabilities/registration.test.ts +395 -0
  29. package/src/capabilities/registration.ts +200 -0
  30. package/src/capabilities/route-validation.test.ts +121 -0
  31. package/src/capabilities/route-validation.ts +96 -0
  32. package/src/capabilities/secret-ref.test.ts +313 -0
  33. package/src/capabilities/secret-validation.ts +157 -0
  34. package/src/capabilities/secrets.test.ts +750 -0
  35. package/src/capabilities/secrets.ts +244 -0
  36. package/src/capabilities/validation.test.ts +613 -0
  37. package/src/capabilities/validation.ts +160 -0
  38. package/src/capabilities/well-known.test.ts +238 -0
  39. package/src/capabilities/well-known.ts +222 -0
  40. package/src/cli/cli.test.ts +654 -0
  41. package/src/cli/command-registry.ts +742 -0
  42. package/src/cli/command-tree-parser.test.ts +180 -0
  43. package/src/cli/command-tree-parser.ts +193 -0
  44. package/src/cli/commands/backup-create.ts +137 -0
  45. package/src/cli/commands/backup-delete.ts +74 -0
  46. package/src/cli/commands/backup-import.ts +97 -0
  47. package/src/cli/commands/backup-list.ts +132 -0
  48. package/src/cli/commands/backup-name.ts +73 -0
  49. package/src/cli/commands/backup-prune.ts +98 -0
  50. package/src/cli/commands/backup-restore.ts +122 -0
  51. package/src/cli/commands/capability-info.ts +121 -0
  52. package/src/cli/commands/capability-list.ts +47 -0
  53. package/src/cli/commands/completion.ts +87 -0
  54. package/src/cli/commands/hook-run.ts +176 -0
  55. package/src/cli/commands/ipam.ts +607 -0
  56. package/src/cli/commands/machine-add.ts +235 -0
  57. package/src/cli/commands/machine-earmark.ts +82 -0
  58. package/src/cli/commands/machine-list.ts +77 -0
  59. package/src/cli/commands/machine-remove.ts +90 -0
  60. package/src/cli/commands/machine-status.ts +131 -0
  61. package/src/cli/commands/module-audit.ts +51 -0
  62. package/src/cli/commands/module-build.ts +60 -0
  63. package/src/cli/commands/module-config.ts +170 -0
  64. package/src/cli/commands/module-deploy.ts +71 -0
  65. package/src/cli/commands/module-generate.ts +236 -0
  66. package/src/cli/commands/module-health.ts +108 -0
  67. package/src/cli/commands/module-import.ts +80 -0
  68. package/src/cli/commands/module-list.ts +43 -0
  69. package/src/cli/commands/module-logs.ts +73 -0
  70. package/src/cli/commands/module-remove.ts +162 -0
  71. package/src/cli/commands/module-show.ts +208 -0
  72. package/src/cli/commands/module-status.ts +131 -0
  73. package/src/cli/commands/module-types.ts +189 -0
  74. package/src/cli/commands/module-upgrade.ts +192 -0
  75. package/src/cli/commands/package.ts +68 -0
  76. package/src/cli/commands/secret-list.ts +99 -0
  77. package/src/cli/commands/secret-set.ts +134 -0
  78. package/src/cli/commands/service-add-digitalocean.ts +133 -0
  79. package/src/cli/commands/service-add-proxmox.ts +342 -0
  80. package/src/cli/commands/service-config-get.ts +83 -0
  81. package/src/cli/commands/service-config-set.ts +145 -0
  82. package/src/cli/commands/service-list.ts +74 -0
  83. package/src/cli/commands/service-reconfigure.ts +230 -0
  84. package/src/cli/commands/service-remove.ts +103 -0
  85. package/src/cli/commands/service-verify.ts +240 -0
  86. package/src/cli/commands/status.ts +216 -0
  87. package/src/cli/commands/storage-add-local.ts +106 -0
  88. package/src/cli/commands/storage-add-s3.ts +114 -0
  89. package/src/cli/commands/storage-list.ts +72 -0
  90. package/src/cli/commands/storage-remove.ts +54 -0
  91. package/src/cli/commands/storage-set-default.ts +44 -0
  92. package/src/cli/commands/storage-verify.ts +54 -0
  93. package/src/cli/commands/system-config.ts +168 -0
  94. package/src/cli/commands/system-init.ts +314 -0
  95. package/src/cli/commands/system-secret-get.ts +98 -0
  96. package/src/cli/commands/system-secret-set.ts +76 -0
  97. package/src/cli/commands/system-vault-password.ts +34 -0
  98. package/src/cli/completion.test.ts +37 -0
  99. package/src/cli/completion.ts +482 -0
  100. package/src/cli/fuel-gauge.test.ts +208 -0
  101. package/src/cli/fuel-gauge.ts +405 -0
  102. package/src/cli/generate-zsh-completion.test.ts +95 -0
  103. package/src/cli/generate-zsh-completion.ts +497 -0
  104. package/src/cli/index.ts +1583 -0
  105. package/src/cli/interactive-config.test.ts +201 -0
  106. package/src/cli/interactive-config.ts +62 -0
  107. package/src/cli/parser.test.ts +227 -0
  108. package/src/cli/parser.ts +244 -0
  109. package/src/cli/prompts.test.ts +33 -0
  110. package/src/cli/prompts.ts +121 -0
  111. package/src/cli/types.ts +38 -0
  112. package/src/cli/validators.test.ts +235 -0
  113. package/src/cli/validators.ts +188 -0
  114. package/src/config/env.ts +41 -0
  115. package/src/config/paths.test.ts +172 -0
  116. package/src/config/paths.ts +108 -0
  117. package/src/db/client.ts +190 -0
  118. package/src/db/migrate.ts +30 -0
  119. package/src/db/schema.test.ts +221 -0
  120. package/src/db/schema.ts +434 -0
  121. package/src/hooks/capability-loader-firewall.test.ts +246 -0
  122. package/src/hooks/capability-loader.test.ts +100 -0
  123. package/src/hooks/capability-loader.ts +520 -0
  124. package/src/hooks/define-hook.test.ts +488 -0
  125. package/src/hooks/executor.test.ts +462 -0
  126. package/src/hooks/executor.ts +469 -0
  127. package/src/hooks/logger.test.ts +54 -0
  128. package/src/hooks/logger.ts +95 -0
  129. package/src/hooks/test-fixtures/failing-hook.ts +13 -0
  130. package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
  131. package/src/hooks/test-fixtures/success-hook.ts +20 -0
  132. package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
  133. package/src/hooks/test-fixtures/void-hook.ts +13 -0
  134. package/src/hooks/types.ts +89 -0
  135. package/src/infrastructure/property-extractor.test.ts +194 -0
  136. package/src/infrastructure/property-extractor.ts +151 -0
  137. package/src/ipam/allocator.test.ts +442 -0
  138. package/src/ipam/allocator.ts +369 -0
  139. package/src/ipam/auto-allocator.test.ts +247 -0
  140. package/src/ipam/auto-allocator.ts +270 -0
  141. package/src/ipam/subnet-parser.test.ts +107 -0
  142. package/src/ipam/subnet-parser.ts +136 -0
  143. package/src/manifest/contracts/index.ts +61 -0
  144. package/src/manifest/contracts/v1.ts +118 -0
  145. package/src/manifest/json-schema-roundtrip.test.ts +99 -0
  146. package/src/manifest/schema.ts +367 -0
  147. package/src/manifest/template-validator.test.ts +231 -0
  148. package/src/manifest/template-validator.ts +322 -0
  149. package/src/manifest/validate.test.ts +1180 -0
  150. package/src/manifest/validate.ts +415 -0
  151. package/src/module/import.test.ts +355 -0
  152. package/src/module/import.ts +676 -0
  153. package/src/module/packaging/audit.ts +169 -0
  154. package/src/module/packaging/build.ts +228 -0
  155. package/src/module/packaging/checksum.ts +41 -0
  156. package/src/module/packaging/extract.ts +234 -0
  157. package/src/module/packaging/signature.ts +47 -0
  158. package/src/secrets/encryption.test.ts +284 -0
  159. package/src/secrets/encryption.ts +162 -0
  160. package/src/secrets/generators.test.ts +112 -0
  161. package/src/secrets/generators.ts +127 -0
  162. package/src/secrets/master-key.test.ts +159 -0
  163. package/src/secrets/master-key.ts +114 -0
  164. package/src/secrets/storage.test.ts +115 -0
  165. package/src/secrets/storage.ts +106 -0
  166. package/src/secrets/vault.test.ts +35 -0
  167. package/src/secrets/vault.ts +42 -0
  168. package/src/services/backup-create.ts +532 -0
  169. package/src/services/backup-metadata.ts +198 -0
  170. package/src/services/backup-restore.ts +229 -0
  171. package/src/services/backup-retention.ts +84 -0
  172. package/src/services/backup-storage.ts +281 -0
  173. package/src/services/build-stream.test.ts +122 -0
  174. package/src/services/build-stream.ts +201 -0
  175. package/src/services/config-interview.ts +694 -0
  176. package/src/services/container-service.test.ts +298 -0
  177. package/src/services/container-service.ts +401 -0
  178. package/src/services/cross-module-data-manager.test.ts +405 -0
  179. package/src/services/cross-module-data-manager.ts +412 -0
  180. package/src/services/deploy-ansible.ts +88 -0
  181. package/src/services/deploy-planner.ts +153 -0
  182. package/src/services/deploy-preflight.ts +274 -0
  183. package/src/services/deploy-ssh.ts +131 -0
  184. package/src/services/deploy-terraform.test.ts +55 -0
  185. package/src/services/deploy-terraform.ts +445 -0
  186. package/src/services/deploy-validation.ts +311 -0
  187. package/src/services/dns-auto-register.ts +211 -0
  188. package/src/services/health-runner.ts +184 -0
  189. package/src/services/infrastructure-selector.test.ts +485 -0
  190. package/src/services/infrastructure-selector.ts +245 -0
  191. package/src/services/infrastructure-variable-resolver.test.ts +751 -0
  192. package/src/services/infrastructure-variable-resolver.ts +234 -0
  193. package/src/services/machine-detector.ts +328 -0
  194. package/src/services/machine-pool.test.ts +405 -0
  195. package/src/services/machine-pool.ts +316 -0
  196. package/src/services/manifest-validation.ts +120 -0
  197. package/src/services/module-build.test.ts +290 -0
  198. package/src/services/module-build.ts +431 -0
  199. package/src/services/module-config.test.ts +237 -0
  200. package/src/services/module-config.ts +298 -0
  201. package/src/services/module-deploy.ts +862 -0
  202. package/src/services/module-types-drift.test.ts +73 -0
  203. package/src/services/module-types-generator.test.ts +288 -0
  204. package/src/services/module-types-generator.ts +189 -0
  205. package/src/services/proxmox-state-recovery.ts +140 -0
  206. package/src/services/schema-validation.ts +155 -0
  207. package/src/services/secret-schema-loader.test.ts +311 -0
  208. package/src/services/secret-schema-loader.ts +239 -0
  209. package/src/services/ssh-key-manager.test.ts +283 -0
  210. package/src/services/ssh-key-manager.ts +193 -0
  211. package/src/services/storage-providers/local.ts +105 -0
  212. package/src/services/storage-providers/s3.ts +182 -0
  213. package/src/services/storage-providers/types.ts +24 -0
  214. package/src/services/system-config-schema-types.ts +25 -0
  215. package/src/services/system-config-validator.test.ts +160 -0
  216. package/src/services/system-config-validator.ts +74 -0
  217. package/src/services/system-init.test.ts +153 -0
  218. package/src/services/system-init.ts +253 -0
  219. package/src/services/terraform-safety.ts +174 -0
  220. package/src/services/zone-detector.test.ts +110 -0
  221. package/src/services/zone-detector.ts +102 -0
  222. package/src/services/zone-policy.test.ts +97 -0
  223. package/src/services/zone-policy.ts +126 -0
  224. package/src/templates/generator.test.ts +645 -0
  225. package/src/templates/generator.ts +1119 -0
  226. package/src/templates/types.ts +62 -0
  227. package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
  228. package/src/test-utils/cli-context-interactive.test.ts +152 -0
  229. package/src/test-utils/cli-context-server.test.ts +66 -0
  230. package/src/test-utils/cli-context.test.ts +273 -0
  231. package/src/test-utils/cli-context.ts +677 -0
  232. package/src/test-utils/cli-result.test.ts +282 -0
  233. package/src/test-utils/cli-result.ts +241 -0
  234. package/src/test-utils/cli.ts +55 -0
  235. package/src/test-utils/completion-harness.test.ts +126 -0
  236. package/src/test-utils/completion-harness.ts +82 -0
  237. package/src/test-utils/database.test.ts +182 -0
  238. package/src/test-utils/database.ts +126 -0
  239. package/src/test-utils/filesystem.test.ts +208 -0
  240. package/src/test-utils/filesystem.ts +142 -0
  241. package/src/test-utils/fixtures.test.ts +123 -0
  242. package/src/test-utils/fixtures.ts +160 -0
  243. package/src/test-utils/golden-diff.ts +197 -0
  244. package/src/test-utils/index.ts +77 -0
  245. package/src/test-utils/integration.ts +81 -0
  246. package/src/test-utils/module-fixtures.ts +468 -0
  247. package/src/test-utils/modules.test.ts +144 -0
  248. package/src/test-utils/modules.ts +183 -0
  249. package/src/test-utils/setup-test-db.ts +90 -0
  250. package/src/test-utils/value-extractor.test.ts +231 -0
  251. package/src/test-utils/value-extractor.ts +228 -0
  252. package/src/types/infrastructure.ts +157 -0
  253. package/src/utils/shell.test.ts +365 -0
  254. package/src/utils/shell.ts +159 -0
  255. package/src/validation/schemas.ts +166 -0
  256. package/src/variables/ansible-resolver.test.ts +142 -0
  257. package/src/variables/ansible-resolver.ts +69 -0
  258. package/src/variables/capability-self-ref.test.ts +220 -0
  259. package/src/variables/context.test.ts +1265 -0
  260. package/src/variables/context.ts +624 -0
  261. package/src/variables/declarative-derivation.test.ts +743 -0
  262. package/src/variables/declarative-derivation.ts +200 -0
  263. package/src/variables/parser.test.ts +231 -0
  264. package/src/variables/parser.ts +76 -0
  265. package/src/variables/resolver.test.ts +458 -0
  266. package/src/variables/resolver.ts +282 -0
  267. package/src/variables/types.ts +59 -0
@@ -0,0 +1,415 @@
1
+ import { KNOWN_CAPABILITY_NAMES } from '@celilo/capabilities';
2
+ import { parse as parseYaml } from 'yaml';
3
+ import type { ZodError } from 'zod';
4
+ import { validateModuleZoneRequirements } from '../services/zone-policy';
5
+ import { resolveContract, supportedContractVersions } from './contracts';
6
+ import { ModuleManifestSchema } from './schema';
7
+ import type { ModuleManifest } from './schema';
8
+
9
+ /**
10
+ * Validation result types
11
+ */
12
+ export interface ValidationSuccess {
13
+ success: true;
14
+ data: ModuleManifest;
15
+ }
16
+
17
+ export interface ValidationError {
18
+ success: false;
19
+ errors: Array<{
20
+ path: string;
21
+ message: string;
22
+ }>;
23
+ }
24
+
25
+ export type ValidationResult = ValidationSuccess | ValidationError;
26
+
27
+ /**
28
+ * Parse YAML string into unknown object
29
+ * This is the first stage - just parse the YAML
30
+ */
31
+ function parseManifestYaml(yamlContent: string): unknown {
32
+ try {
33
+ return parseYaml(yamlContent);
34
+ } catch (error) {
35
+ throw new Error(
36
+ `Failed to parse YAML: ${error instanceof Error ? error.message : 'Unknown error'}`,
37
+ );
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Format Zod errors into readable validation errors
43
+ */
44
+ function formatZodErrors(error: ZodError): ValidationError {
45
+ return {
46
+ success: false,
47
+ errors: error.errors.map((err) => ({
48
+ path: err.path.join('.'),
49
+ message: err.message,
50
+ })),
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Validate module manifest
56
+ *
57
+ * Policy function (Rule 10.1) - validates input only
58
+ * Does NOT perform any side effects or business logic
59
+ *
60
+ * @param yamlContent - Raw YAML string from manifest.yml
61
+ * @returns Validation result with parsed manifest or errors
62
+ */
63
+ export function validateManifest(yamlContent: string): ValidationResult {
64
+ if (!yamlContent || yamlContent.trim().length === 0) {
65
+ return {
66
+ success: false,
67
+ errors: [{ path: '', message: 'Manifest content cannot be empty' }],
68
+ };
69
+ }
70
+
71
+ let parsed: unknown;
72
+ try {
73
+ parsed = parseManifestYaml(yamlContent);
74
+ } catch (error) {
75
+ return {
76
+ success: false,
77
+ errors: [
78
+ {
79
+ path: '',
80
+ message: error instanceof Error ? error.message : 'Failed to parse YAML',
81
+ },
82
+ ],
83
+ };
84
+ }
85
+
86
+ const result = ModuleManifestSchema.safeParse(parsed);
87
+
88
+ if (!result.success) {
89
+ return formatZodErrors(result.error);
90
+ }
91
+
92
+ return {
93
+ success: true,
94
+ data: result.data,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Validate that required capabilities exist
100
+ *
101
+ * Policy function - checks capability requirements against available capabilities
102
+ * Does NOT perform database queries - caller provides capability list
103
+ *
104
+ * @param manifest - Validated manifest
105
+ * @param availableCapabilities - List of capability names available in the system
106
+ * @returns Validation errors if any required capabilities are missing
107
+ */
108
+ export function validateCapabilityRequirements(
109
+ manifest: ModuleManifest,
110
+ availableCapabilities: string[],
111
+ ): ValidationError | null {
112
+ const errors: Array<{ path: string; message: string }> = [];
113
+
114
+ for (const required of manifest.requires.capabilities) {
115
+ if (!availableCapabilities.includes(required.name)) {
116
+ errors.push({
117
+ path: `requires.capabilities.${required.name}`,
118
+ message: `Required capability '${required.name}' is not provided by any installed module`,
119
+ });
120
+ }
121
+ }
122
+
123
+ if (errors.length > 0) {
124
+ return { success: false, errors };
125
+ }
126
+
127
+ return null;
128
+ }
129
+
130
+ /**
131
+ * Validate that every capability name listed in `requires.capabilities` and
132
+ * `optional.capabilities` is a known capability in the framework registry.
133
+ *
134
+ * Policy function (Rule 10.1) — pure validation, no side effects.
135
+ *
136
+ * Why (HOOK_API_V2 Phase 3 / D3): catches typos and stale references in the
137
+ * manifest before they become silent runtime no-ops. Without this check, a
138
+ * misspelled capability name (`requires: [{ name: dns_register }]`) would
139
+ * pass schema validation, fail to load any provider, and give the user a
140
+ * confusing "capability not provided" error far downstream. The known
141
+ * registry lives in `@celilo/capabilities` so the TS interface and the
142
+ * runtime list stay in sync.
143
+ *
144
+ * @param manifest - Validated manifest
145
+ * @returns Validation error if any name is unknown, null otherwise
146
+ */
147
+ export function validateCapabilityNames(manifest: ModuleManifest): ValidationError | null {
148
+ const errors: Array<{ path: string; message: string }> = [];
149
+ const knownNames: readonly string[] = KNOWN_CAPABILITY_NAMES;
150
+
151
+ for (const required of manifest.requires.capabilities) {
152
+ if (!knownNames.includes(required.name)) {
153
+ errors.push({
154
+ path: `requires.capabilities.${required.name}`,
155
+ message: `Unknown capability '${required.name}'. Known capabilities: ${knownNames.join(', ')}.`,
156
+ });
157
+ }
158
+ }
159
+
160
+ for (const opt of manifest.optional?.capabilities ?? []) {
161
+ if (!knownNames.includes(opt.name)) {
162
+ errors.push({
163
+ path: `optional.capabilities.${opt.name}`,
164
+ message: `Unknown capability '${opt.name}'. Known capabilities: ${knownNames.join(', ')}.`,
165
+ });
166
+ }
167
+ }
168
+
169
+ if (errors.length > 0) {
170
+ return { success: false, errors };
171
+ }
172
+
173
+ return null;
174
+ }
175
+
176
+ /**
177
+ * Validate that variable sources are valid
178
+ *
179
+ * Policy function - checks that capability references in variables exist
180
+ *
181
+ * @param manifest - Validated manifest
182
+ * @returns Validation errors if any variable sources are invalid
183
+ */
184
+ export function validateVariableSources(manifest: ModuleManifest): ValidationError | null {
185
+ const errors: Array<{ path: string; message: string }> = [];
186
+
187
+ // Variables can import from capabilities listed in requires OR optional —
188
+ // both are declared dependencies of this module.
189
+ const declaredCapabilityNames = new Set([
190
+ ...manifest.requires.capabilities.map((c) => c.name),
191
+ ...(manifest.optional?.capabilities ?? []).map((c) => c.name),
192
+ ]);
193
+
194
+ for (const varImport of manifest.variables.imports) {
195
+ if (varImport.source === 'capability') {
196
+ const capabilityName = varImport.from.split('.')[0];
197
+
198
+ if (!capabilityName) {
199
+ errors.push({
200
+ path: `variables.imports.${varImport.name}`,
201
+ message: `Variable '${varImport.name}' has invalid capability reference: '${varImport.from}'`,
202
+ });
203
+ continue;
204
+ }
205
+
206
+ if (!declaredCapabilityNames.has(capabilityName)) {
207
+ errors.push({
208
+ path: `variables.imports.${varImport.name}`,
209
+ message: `Variable '${varImport.name}' imports capability '${capabilityName}' but module does not declare it in requires or optional`,
210
+ });
211
+ }
212
+ }
213
+ }
214
+
215
+ if (errors.length > 0) {
216
+ return { success: false, errors };
217
+ }
218
+
219
+ return null;
220
+ }
221
+
222
+ /**
223
+ * Validate zone requirements for module capabilities
224
+ *
225
+ * Policy function - checks that module provides capabilities in correct zones
226
+ *
227
+ * Zone-based policy enforcement
228
+ * - Modules providing well-known capabilities must be deployed to specific zones
229
+ * - Example: public_web must be in 'dmz' zone (defense perimeter, internet-facing)
230
+ * - Example: public_web must be in 'dmz' zone (defense perimeter)
231
+ *
232
+ * @param manifest - Validated manifest
233
+ * @returns Validation errors if zone requirements are violated
234
+ */
235
+ export function validateZoneRequirements(manifest: ModuleManifest): ValidationError | null {
236
+ const errors: Array<{ path: string; message: string }> = [];
237
+
238
+ // Check if module has infrastructure spec (requires.machine)
239
+ const hasInfrastructureSpec = manifest.requires?.machine;
240
+
241
+ // If module requires infrastructure, zone field is mandatory
242
+ if (hasInfrastructureSpec) {
243
+ const zone = manifest.requires?.machine?.zone;
244
+
245
+ if (!zone) {
246
+ errors.push({
247
+ path: 'requires.machine.zone',
248
+ message: 'Zone field is required for modules with infrastructure requirements',
249
+ });
250
+ // Can't validate zone requirements without a zone
251
+ return { success: false, errors };
252
+ }
253
+
254
+ // Validate zone requirements for provided capabilities
255
+ if (manifest.provides.capabilities.length > 0) {
256
+ const capabilityNames = manifest.provides.capabilities.map((cap) => cap.name);
257
+ const zoneValidation = validateModuleZoneRequirements(capabilityNames, zone);
258
+
259
+ if (!zoneValidation.valid && zoneValidation.error) {
260
+ errors.push({
261
+ path: 'requires.machine.zone',
262
+ message: zoneValidation.error,
263
+ });
264
+ }
265
+ }
266
+ }
267
+
268
+ if (errors.length > 0) {
269
+ return { success: false, errors };
270
+ }
271
+
272
+ return null;
273
+ }
274
+
275
+ /**
276
+ * Validate that the prefix of every variable's `derive_from` template
277
+ * matches its declared `source`.
278
+ *
279
+ * Why: a manifest like
280
+ * `source: capability, derive_from: "$system:primary_domain"`
281
+ * is incoherent — the source says the value comes from a capability but the
282
+ * derivation reads from system config. Zod can't catch this because both
283
+ * fields are individually valid; this validator enforces the coherence rule
284
+ * at runtime so the bug fails import instead of confusing a future reader.
285
+ *
286
+ * Rules:
287
+ * - `source: capability` → `derive_from` must only reference `$capability:`
288
+ * tokens (and may also include `{var}` placeholders).
289
+ * - `source: system` → `derive_from` must only reference `$system:` tokens
290
+ * (and `{var}` placeholders).
291
+ * - Other sources (`user`, `infrastructure`, `terraform`) — `derive_from` is
292
+ * optional and unconstrained; we don't check the prefix.
293
+ */
294
+ export function validateDeriveFromSources(manifest: ModuleManifest): ValidationError | null {
295
+ const errors: Array<{ path: string; message: string }> = [];
296
+
297
+ // Match $foo:bar tokens (foo is the prefix, bar is the path)
298
+ const tokenPattern = /\$([a-z_]+):/g;
299
+
300
+ for (const variable of manifest.variables.owns) {
301
+ if (!variable.derive_from) continue;
302
+
303
+ let expected: string | null = null;
304
+ if (variable.source === 'capability') expected = 'capability';
305
+ else if (variable.source === 'system') expected = 'system';
306
+ if (!expected) continue;
307
+
308
+ const tokens = [...variable.derive_from.matchAll(tokenPattern)].map((m) => m[1]);
309
+ const offending = tokens.filter((t) => t !== expected);
310
+ if (offending.length > 0) {
311
+ const unique = Array.from(new Set(offending)).join(', ');
312
+ errors.push({
313
+ path: `variables.owns.${variable.name}.derive_from`,
314
+ message: `Variable '${variable.name}' has source: ${variable.source} but derive_from references $${unique}: tokens. Either change source to match or rewrite the derivation.`,
315
+ });
316
+ }
317
+ }
318
+
319
+ if (errors.length > 0) {
320
+ return { success: false, errors };
321
+ }
322
+
323
+ return null;
324
+ }
325
+
326
+ /**
327
+ * Validate that every declared hook is part of the contract version the
328
+ * manifest targets.
329
+ *
330
+ * Why: the Zod schema's `.strict()` on the hooks block already prevents
331
+ * unknown hook names structurally, but the contract registry is the
332
+ * authoritative list of which hooks Celilo will actually invoke. This
333
+ * validator surfaces a clear error if someone declares a hook that the
334
+ * declared contract version doesn't promise to call.
335
+ */
336
+ export function validateHookContract(manifest: ModuleManifest): ValidationError | null {
337
+ const errors: Array<{ path: string; message: string }> = [];
338
+
339
+ const contract = resolveContract(manifest.celilo_contract);
340
+ if (!contract) {
341
+ errors.push({
342
+ path: 'celilo_contract',
343
+ message: `Unsupported celilo_contract version '${manifest.celilo_contract}'. Supported: ${supportedContractVersions().join(', ')}`,
344
+ });
345
+ return { success: false, errors };
346
+ }
347
+
348
+ if (!manifest.hooks) {
349
+ return null;
350
+ }
351
+
352
+ for (const hookName of Object.keys(manifest.hooks)) {
353
+ if (!(hookName in contract.hooks)) {
354
+ errors.push({
355
+ path: `hooks.${hookName}`,
356
+ message: `Hook '${hookName}' is not part of celilo_contract ${manifest.celilo_contract}. Known hooks: ${Object.keys(contract.hooks).join(', ')}`,
357
+ });
358
+ }
359
+ }
360
+
361
+ if (errors.length > 0) {
362
+ return { success: false, errors };
363
+ }
364
+
365
+ return null;
366
+ }
367
+
368
+ /**
369
+ * Validate that capability data templates in `provides.capabilities[].data`
370
+ * do not contain cross-capability references.
371
+ *
372
+ * Why (D9 firm rule): well-known and module-provided capability data
373
+ * templates must not embed `$capability:other_capability.x` references. If a
374
+ * capability value depends on another capability, the providing module
375
+ * derives it in its own variables and exposes the resolved value through
376
+ * `provides.capabilities[].data`. Cross-capability references in data
377
+ * templates create implicit ordering dependencies between capability
378
+ * providers, which the variable resolver isn't designed to handle and which
379
+ * make the dependency graph hard to reason about.
380
+ */
381
+ export function validateProvidesNoCrossCapabilityRefs(
382
+ manifest: ModuleManifest,
383
+ ): ValidationError | null {
384
+ const errors: Array<{ path: string; message: string }> = [];
385
+
386
+ function walk(node: unknown, path: string): void {
387
+ if (typeof node === 'string') {
388
+ if (node.includes('$capability:')) {
389
+ errors.push({
390
+ path,
391
+ message: `Capability data template at ${path} contains a $capability: reference. Per D9, capability data must not cross-reference other capabilities — derive the value in this module's variables instead and expose the resolved value here.`,
392
+ });
393
+ }
394
+ return;
395
+ }
396
+ if (node === null || typeof node !== 'object') return;
397
+ if (Array.isArray(node)) {
398
+ node.forEach((item, idx) => walk(item, `${path}[${idx}]`));
399
+ return;
400
+ }
401
+ for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
402
+ walk(value, `${path}.${key}`);
403
+ }
404
+ }
405
+
406
+ for (const cap of manifest.provides.capabilities) {
407
+ walk(cap.data, `provides.capabilities.${cap.name}.data`);
408
+ }
409
+
410
+ if (errors.length > 0) {
411
+ return { success: false, errors };
412
+ }
413
+
414
+ return null;
415
+ }