@intentius/chant 0.124.17 → 0.124.19

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 (154) hide show
  1. package/dist/__golden__/wave-loop/fan-out-applied.json +511 -0
  2. package/dist/__golden__/wave-loop/fan-out-apply-fails.json +406 -0
  3. package/dist/__golden__/wave-loop/fan-out-gated.json +707 -0
  4. package/dist/__golden__/wave-loop/fan-out-no-members.json +898 -0
  5. package/dist/__golden__/wave-loop/fan-out-only.json +514 -0
  6. package/dist/__golden__/wave-loop/fan-out-plan-fails.json +359 -0
  7. package/dist/__golden__/wave-loop/fan-out-same-root.json +311 -0
  8. package/dist/__golden__/wave-loop/op-by-apply-after-fails.json +401 -0
  9. package/dist/__golden__/wave-loop/op-by-apply-hold.json +756 -0
  10. package/dist/__golden__/wave-loop/op-by-apply-ignored.json +340 -0
  11. package/dist/__golden__/wave-loop/op-by-apply-pass.json +400 -0
  12. package/dist/__golden__/wave-loop/op-by-apply-skip.json +404 -0
  13. package/dist/__golden__/wave-loop/op-cost-member-clash.json +62 -0
  14. package/dist/__golden__/wave-loop/op-cost-member.json +191 -0
  15. package/dist/__golden__/wave-loop/op-denied-source.json +407 -0
  16. package/dist/__golden__/wave-loop/op-denied.json +399 -0
  17. package/dist/__golden__/wave-loop/op-discover-rest-short.json +92 -0
  18. package/dist/__golden__/wave-loop/op-discover-rest-stops.json +340 -0
  19. package/dist/__golden__/wave-loop/op-discover-rest.json +659 -0
  20. package/dist/__golden__/wave-loop/op-discover-wide.json +290 -0
  21. package/dist/__golden__/wave-loop/op-held-on-again.json +176 -0
  22. package/dist/__golden__/wave-loop/op-held-on-before-apply.json +689 -0
  23. package/dist/__golden__/wave-loop/op-held-on.json +1455 -0
  24. package/dist/__golden__/wave-loop/op-hooks-apply-fails.json +673 -0
  25. package/dist/__golden__/wave-loop/op-hooks-fail-after-apply.json +737 -0
  26. package/dist/__golden__/wave-loop/op-hooks-fail-before-apply.json +450 -0
  27. package/dist/__golden__/wave-loop/op-hooks-fail-before-plan.json +97 -0
  28. package/dist/__golden__/wave-loop/op-hooks-held.json +1059 -0
  29. package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +693 -0
  30. package/dist/__golden__/wave-loop/op-hooks-pass.json +735 -0
  31. package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +148 -0
  32. package/dist/__golden__/wave-loop/op-hooks-share.json +1225 -0
  33. package/dist/__golden__/wave-loop/op-outcome-held.json +202 -0
  34. package/dist/__golden__/wave-loop/op-outcome-replan-approved.json +286 -0
  35. package/dist/__golden__/wave-loop/op-outcome-replan-forever.json +823 -0
  36. package/dist/__golden__/wave-loop/op-outcome-replan.json +259 -0
  37. package/dist/__golden__/wave-loop/op-outcome-stood-down.json +196 -0
  38. package/dist/__golden__/wave-loop/op-outcome-unreadable.json +193 -0
  39. package/dist/__golden__/wave-loop/op-overridden.json +508 -0
  40. package/dist/__golden__/wave-loop/op-per-run.json +518 -0
  41. package/dist/__golden__/wave-loop/op-point.json +495 -0
  42. package/dist/__golden__/wave-loop/op-pr-review.json +397 -0
  43. package/dist/__golden__/wave-loop/op-sealed.json +125 -0
  44. package/dist/__golden__/wave-loop/op-spend-change-only.json +168 -0
  45. package/dist/__golden__/wave-loop/op-spend.json +328 -0
  46. package/dist/__golden__/wave-loop/op-wave-commands.json +1808 -0
  47. package/dist/__golden__/wave-loop/op-wide-refused.json +291 -0
  48. package/dist/__golden__/wave-loop/op-wide-waiting.json +231 -0
  49. package/dist/__golden__/wave-loop/op-wide.json +440 -0
  50. package/dist/cli/commands/import.d.ts +2 -0
  51. package/dist/cli/commands/import.d.ts.map +1 -1
  52. package/dist/cli/commands/import.js +6 -2
  53. package/dist/cli/handlers/graph.js +24 -1
  54. package/dist/cli/handlers/misc.d.ts.map +1 -1
  55. package/dist/cli/handlers/misc.js +2 -1
  56. package/dist/cli/parse-args.d.ts.map +1 -1
  57. package/dist/cli/parse-args.js +4 -0
  58. package/dist/cli/registry.d.ts +2 -0
  59. package/dist/cli/registry.d.ts.map +1 -1
  60. package/dist/components/fan-out-run.d.ts +4 -3
  61. package/dist/components/fan-out-run.d.ts.map +1 -1
  62. package/dist/components/fan-out-run.js +138 -123
  63. package/dist/graph-detail.d.ts.map +1 -1
  64. package/dist/graph-detail.js +9 -3
  65. package/dist/graph-dot.d.ts.map +1 -1
  66. package/dist/graph-dot.js +36 -2
  67. package/dist/graph-ir.d.ts +98 -3
  68. package/dist/graph-ir.d.ts.map +1 -1
  69. package/dist/graph-ir.js +71 -0
  70. package/dist/graph-lens.js +12 -1
  71. package/dist/graph-mermaid.d.ts +5 -1
  72. package/dist/graph-mermaid.d.ts.map +1 -1
  73. package/dist/graph-mermaid.js +46 -2
  74. package/dist/lexicon.d.ts +22 -1
  75. package/dist/lexicon.d.ts.map +1 -1
  76. package/dist/op/op-waves-run.d.ts +7 -1
  77. package/dist/op/op-waves-run.d.ts.map +1 -1
  78. package/dist/op/op-waves-run.js +129 -72
  79. package/dist/wave-engine.d.ts +74 -5
  80. package/dist/wave-engine.d.ts.map +1 -1
  81. package/dist/wave-engine.js +47 -5
  82. package/dist/wave-runtime.mjs +215 -215
  83. package/package.json +1 -1
  84. package/src/__golden__/wave-loop/fan-out-applied.json +511 -0
  85. package/src/__golden__/wave-loop/fan-out-apply-fails.json +406 -0
  86. package/src/__golden__/wave-loop/fan-out-gated.json +707 -0
  87. package/src/__golden__/wave-loop/fan-out-no-members.json +898 -0
  88. package/src/__golden__/wave-loop/fan-out-only.json +514 -0
  89. package/src/__golden__/wave-loop/fan-out-plan-fails.json +359 -0
  90. package/src/__golden__/wave-loop/fan-out-same-root.json +311 -0
  91. package/src/__golden__/wave-loop/op-by-apply-after-fails.json +401 -0
  92. package/src/__golden__/wave-loop/op-by-apply-hold.json +756 -0
  93. package/src/__golden__/wave-loop/op-by-apply-ignored.json +340 -0
  94. package/src/__golden__/wave-loop/op-by-apply-pass.json +400 -0
  95. package/src/__golden__/wave-loop/op-by-apply-skip.json +404 -0
  96. package/src/__golden__/wave-loop/op-cost-member-clash.json +62 -0
  97. package/src/__golden__/wave-loop/op-cost-member.json +191 -0
  98. package/src/__golden__/wave-loop/op-denied-source.json +407 -0
  99. package/src/__golden__/wave-loop/op-denied.json +399 -0
  100. package/src/__golden__/wave-loop/op-discover-rest-short.json +92 -0
  101. package/src/__golden__/wave-loop/op-discover-rest-stops.json +340 -0
  102. package/src/__golden__/wave-loop/op-discover-rest.json +659 -0
  103. package/src/__golden__/wave-loop/op-discover-wide.json +290 -0
  104. package/src/__golden__/wave-loop/op-held-on-again.json +176 -0
  105. package/src/__golden__/wave-loop/op-held-on-before-apply.json +689 -0
  106. package/src/__golden__/wave-loop/op-held-on.json +1455 -0
  107. package/src/__golden__/wave-loop/op-hooks-apply-fails.json +673 -0
  108. package/src/__golden__/wave-loop/op-hooks-fail-after-apply.json +737 -0
  109. package/src/__golden__/wave-loop/op-hooks-fail-before-apply.json +450 -0
  110. package/src/__golden__/wave-loop/op-hooks-fail-before-plan.json +97 -0
  111. package/src/__golden__/wave-loop/op-hooks-held.json +1059 -0
  112. package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +693 -0
  113. package/src/__golden__/wave-loop/op-hooks-pass.json +735 -0
  114. package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +148 -0
  115. package/src/__golden__/wave-loop/op-hooks-share.json +1225 -0
  116. package/src/__golden__/wave-loop/op-outcome-held.json +202 -0
  117. package/src/__golden__/wave-loop/op-outcome-replan-approved.json +286 -0
  118. package/src/__golden__/wave-loop/op-outcome-replan-forever.json +823 -0
  119. package/src/__golden__/wave-loop/op-outcome-replan.json +259 -0
  120. package/src/__golden__/wave-loop/op-outcome-stood-down.json +196 -0
  121. package/src/__golden__/wave-loop/op-outcome-unreadable.json +193 -0
  122. package/src/__golden__/wave-loop/op-overridden.json +508 -0
  123. package/src/__golden__/wave-loop/op-per-run.json +518 -0
  124. package/src/__golden__/wave-loop/op-point.json +495 -0
  125. package/src/__golden__/wave-loop/op-pr-review.json +397 -0
  126. package/src/__golden__/wave-loop/op-sealed.json +125 -0
  127. package/src/__golden__/wave-loop/op-spend-change-only.json +168 -0
  128. package/src/__golden__/wave-loop/op-spend.json +328 -0
  129. package/src/__golden__/wave-loop/op-wave-commands.json +1808 -0
  130. package/src/__golden__/wave-loop/op-wide-refused.json +291 -0
  131. package/src/__golden__/wave-loop/op-wide-waiting.json +231 -0
  132. package/src/__golden__/wave-loop/op-wide.json +440 -0
  133. package/src/cli/commands/import-live.test.ts +24 -0
  134. package/src/cli/commands/import.ts +8 -2
  135. package/src/cli/handlers/graph.test.ts +23 -0
  136. package/src/cli/handlers/graph.ts +22 -1
  137. package/src/cli/handlers/misc.ts +2 -1
  138. package/src/cli/main.test.ts +5 -0
  139. package/src/cli/parse-args.ts +3 -0
  140. package/src/cli/registry.ts +2 -0
  141. package/src/codegen/publish-order.test.ts +118 -4
  142. package/src/components/fan-out-run.ts +156 -128
  143. package/src/graph-ci-edges.test.ts +97 -0
  144. package/src/graph-detail.ts +9 -3
  145. package/src/graph-dot.ts +34 -2
  146. package/src/graph-ir.ts +156 -3
  147. package/src/graph-lens.ts +10 -1
  148. package/src/graph-mermaid.ts +44 -2
  149. package/src/lexicon.ts +23 -1
  150. package/src/meta/peer-deps.test.ts +39 -0
  151. package/src/op/op-waves-run.ts +184 -68
  152. package/src/wave-engine.test.ts +38 -1
  153. package/src/wave-engine.ts +101 -5
  154. package/src/wave-loop-golden.test.ts +627 -0
@@ -0,0 +1,440 @@
1
+ {
2
+ "jobs": [
3
+ {
4
+ "label": "decide, approved and spent",
5
+ "job": {
6
+ "wave": 3,
7
+ "decide": true
8
+ },
9
+ "result": {
10
+ "exitCode": 0,
11
+ "applied": [],
12
+ "failed": [],
13
+ "moved": [],
14
+ "hooks": [],
15
+ "decision": {
16
+ "wave": 3,
17
+ "name": "prod",
18
+ "op": "migrations",
19
+ "gate": "migrations-wave-3",
20
+ "policy": "always",
21
+ "approval": "ledger",
22
+ "policySource": "base",
23
+ "base": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
24
+ "digest": "jcs1-sha256:106d25f35ee6bc02e9aa253d0b2633a764396833c82e0c5ec5a4ad83a2a85b4e",
25
+ "members": [
26
+ {
27
+ "member": "t3",
28
+ "planDigest": "sha256:3333333333333333333333333333333333333333333333333333333333333333",
29
+ "op": "migrate"
30
+ },
31
+ {
32
+ "member": "t1",
33
+ "planDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
34
+ "op": "migrate"
35
+ },
36
+ {
37
+ "member": "t2",
38
+ "planDigest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
39
+ "op": "migrate"
40
+ }
41
+ ],
42
+ "status": "approved",
43
+ "spend": "base",
44
+ "via": "ledger",
45
+ "approvedBy": "alice",
46
+ "approvedAt": "2026-10-10T00:00:00Z",
47
+ "spentAt": "2026-10-11T00:00:00.000Z"
48
+ }
49
+ },
50
+ "ledger": {
51
+ "appended": [],
52
+ "resolved": [],
53
+ "applied": [
54
+ {
55
+ "version": 1,
56
+ "kind": "applied",
57
+ "op": "migrations",
58
+ "gate": "migrations-wave-3",
59
+ "planDigest": "jcs1-sha256:106d25f35ee6bc02e9aa253d0b2633a764396833c82e0c5ec5a4ad83a2a85b4e",
60
+ "approvedAt": "2026-10-10T00:00:00Z",
61
+ "approvedBy": "alice",
62
+ "timestamp": "2026-10-11T00:00:00.000Z",
63
+ "wave": {
64
+ "number": 3,
65
+ "name": "prod"
66
+ }
67
+ }
68
+ ]
69
+ },
70
+ "calls": 3,
71
+ "logs": 2
72
+ },
73
+ {
74
+ "label": "share 2",
75
+ "job": {
76
+ "wave": 3,
77
+ "share": 2
78
+ },
79
+ "result": {
80
+ "exitCode": 0,
81
+ "applied": [
82
+ "t3"
83
+ ],
84
+ "failed": [],
85
+ "moved": [],
86
+ "hooks": [],
87
+ "decision": {
88
+ "wave": 3,
89
+ "name": "prod",
90
+ "op": "migrations",
91
+ "gate": "migrations-wave-3",
92
+ "policy": "always",
93
+ "approval": "ledger",
94
+ "policySource": "base",
95
+ "base": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
96
+ "digest": "jcs1-sha256:106d25f35ee6bc02e9aa253d0b2633a764396833c82e0c5ec5a4ad83a2a85b4e",
97
+ "members": [
98
+ {
99
+ "member": "t3",
100
+ "planDigest": "sha256:3333333333333333333333333333333333333333333333333333333333333333",
101
+ "op": "migrate"
102
+ },
103
+ {
104
+ "member": "t1",
105
+ "planDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
106
+ "op": "migrate"
107
+ },
108
+ {
109
+ "member": "t2",
110
+ "planDigest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
111
+ "op": "migrate"
112
+ }
113
+ ],
114
+ "status": "approved",
115
+ "spend": "base",
116
+ "via": "ledger",
117
+ "approvedBy": "alice",
118
+ "approvedAt": "2026-10-10T00:00:00Z",
119
+ "spentAt": "2026-10-11T00:00:00.000Z"
120
+ }
121
+ },
122
+ "ledger": {
123
+ "appended": [],
124
+ "resolved": [],
125
+ "applied": [
126
+ {
127
+ "version": 1,
128
+ "kind": "applied",
129
+ "op": "migrations",
130
+ "gate": "migrations-wave-3",
131
+ "planDigest": "jcs1-sha256:106d25f35ee6bc02e9aa253d0b2633a764396833c82e0c5ec5a4ad83a2a85b4e",
132
+ "approvedAt": "2026-10-10T00:00:00Z",
133
+ "approvedBy": "alice",
134
+ "timestamp": "2026-10-11T00:00:00.000Z",
135
+ "wave": {
136
+ "number": 3,
137
+ "name": "prod"
138
+ }
139
+ }
140
+ ]
141
+ },
142
+ "calls": 2,
143
+ "logs": 0
144
+ },
145
+ {
146
+ "label": "share 1, t2 moved",
147
+ "job": {
148
+ "wave": 3,
149
+ "share": 1
150
+ },
151
+ "result": {
152
+ "exitCode": 4,
153
+ "applied": [
154
+ "t1"
155
+ ],
156
+ "failed": [],
157
+ "moved": [
158
+ "t2"
159
+ ],
160
+ "hooks": [],
161
+ "decision": {
162
+ "wave": 3,
163
+ "name": "prod",
164
+ "op": "migrations",
165
+ "gate": "migrations-wave-3",
166
+ "policy": "always",
167
+ "approval": "ledger",
168
+ "policySource": "base",
169
+ "base": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
170
+ "digest": "jcs1-sha256:106d25f35ee6bc02e9aa253d0b2633a764396833c82e0c5ec5a4ad83a2a85b4e",
171
+ "members": [
172
+ {
173
+ "member": "t3",
174
+ "planDigest": "sha256:3333333333333333333333333333333333333333333333333333333333333333",
175
+ "op": "migrate"
176
+ },
177
+ {
178
+ "member": "t1",
179
+ "planDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
180
+ "op": "migrate"
181
+ },
182
+ {
183
+ "member": "t2",
184
+ "planDigest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
185
+ "op": "migrate"
186
+ }
187
+ ],
188
+ "status": "approved",
189
+ "spend": "base",
190
+ "via": "ledger",
191
+ "approvedBy": "alice",
192
+ "approvedAt": "2026-10-10T00:00:00Z",
193
+ "spentAt": "2026-10-11T00:00:00.000Z"
194
+ }
195
+ },
196
+ "ledger": {
197
+ "appended": [],
198
+ "resolved": [],
199
+ "applied": [
200
+ {
201
+ "version": 1,
202
+ "kind": "applied",
203
+ "op": "migrations",
204
+ "gate": "migrations-wave-3",
205
+ "planDigest": "jcs1-sha256:106d25f35ee6bc02e9aa253d0b2633a764396833c82e0c5ec5a4ad83a2a85b4e",
206
+ "approvedAt": "2026-10-10T00:00:00Z",
207
+ "approvedBy": "alice",
208
+ "timestamp": "2026-10-11T00:00:00.000Z",
209
+ "wave": {
210
+ "number": 3,
211
+ "name": "prod"
212
+ }
213
+ }
214
+ ]
215
+ },
216
+ "calls": 3,
217
+ "logs": 1
218
+ }
219
+ ],
220
+ "calls": [
221
+ {
222
+ "argv": [
223
+ "tool",
224
+ "plan",
225
+ "t3",
226
+ "--out",
227
+ ".chant/op-waves/migrations/wave-3/t3.plan.json"
228
+ ]
229
+ },
230
+ {
231
+ "argv": [
232
+ "tool",
233
+ "plan",
234
+ "t1",
235
+ "--out",
236
+ ".chant/op-waves/migrations/wave-3/t1.plan.json"
237
+ ]
238
+ },
239
+ {
240
+ "argv": [
241
+ "tool",
242
+ "plan",
243
+ "t2",
244
+ "--out",
245
+ ".chant/op-waves/migrations/wave-3/t2.plan.json"
246
+ ]
247
+ },
248
+ {
249
+ "argv": [
250
+ "tool",
251
+ "plan",
252
+ "t3",
253
+ "--out",
254
+ ".chant/op-waves/migrations/wave-3/t3.plan.json"
255
+ ]
256
+ },
257
+ {
258
+ "argv": [
259
+ "tool",
260
+ "apply",
261
+ "t3",
262
+ "--plan",
263
+ ".chant/op-waves/migrations/wave-3/t3.plan.json"
264
+ ],
265
+ "env": {
266
+ "CHANT_HOOKS": ".chant/op-waves/migrations/wave-3/t3.hooks.json",
267
+ "CHANT_APPLY_OUTCOME": ".chant/op-waves/migrations/wave-3/t3.hooks-outcome.json"
268
+ },
269
+ "read": {
270
+ "CHANT_HOOKS": {
271
+ "version": 1,
272
+ "opWaves": "migrations",
273
+ "op": "migrate",
274
+ "wave": "prod",
275
+ "number": 3,
276
+ "base": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
277
+ "digest": "jcs1-sha256:106d25f35ee6bc02e9aa253d0b2633a764396833c82e0c5ec5a4ad83a2a85b4e",
278
+ "approved": true,
279
+ "runBy": "chant",
280
+ "runs": [
281
+ {
282
+ "target": "t3",
283
+ "op": "migrate",
284
+ "plan": ".chant/op-waves/migrations/wave-3/t3.plan.json"
285
+ }
286
+ ],
287
+ "runsFile": ".chant/op-waves/migrations/wave-3/runs.json",
288
+ "hooks": {},
289
+ "outcomes": []
290
+ }
291
+ }
292
+ },
293
+ {
294
+ "argv": [
295
+ "tool",
296
+ "plan",
297
+ "t1",
298
+ "--out",
299
+ ".chant/op-waves/migrations/wave-3/t1.plan.json"
300
+ ]
301
+ },
302
+ {
303
+ "argv": [
304
+ "tool",
305
+ "plan",
306
+ "t2",
307
+ "--out",
308
+ ".chant/op-waves/migrations/wave-3/t2.plan.json"
309
+ ]
310
+ },
311
+ {
312
+ "argv": [
313
+ "tool",
314
+ "apply",
315
+ "t1",
316
+ "--plan",
317
+ ".chant/op-waves/migrations/wave-3/t1.plan.json"
318
+ ],
319
+ "env": {
320
+ "CHANT_HOOKS": ".chant/op-waves/migrations/wave-3/t1.hooks.json",
321
+ "CHANT_APPLY_OUTCOME": ".chant/op-waves/migrations/wave-3/t1.hooks-outcome.json"
322
+ },
323
+ "read": {
324
+ "CHANT_HOOKS": {
325
+ "version": 1,
326
+ "opWaves": "migrations",
327
+ "op": "migrate",
328
+ "wave": "prod",
329
+ "number": 3,
330
+ "base": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
331
+ "digest": "jcs1-sha256:106d25f35ee6bc02e9aa253d0b2633a764396833c82e0c5ec5a4ad83a2a85b4e",
332
+ "approved": true,
333
+ "runBy": "chant",
334
+ "runs": [
335
+ {
336
+ "target": "t1",
337
+ "op": "migrate",
338
+ "plan": ".chant/op-waves/migrations/wave-3/t1.plan.json"
339
+ }
340
+ ],
341
+ "runsFile": ".chant/op-waves/migrations/wave-3/runs.json",
342
+ "hooks": {},
343
+ "outcomes": []
344
+ }
345
+ }
346
+ }
347
+ ],
348
+ "logs": [
349
+ "wave 3 (prod): gate always (read at bbbbbbbbbbbb), digest jcs1-sha256:106d25f35ee6bc02e9aa253d0b2633a764396833c82e0c5ec5a4ad83a2a85b4e: approved by alice",
350
+ "wave 3 (prod) applies under the approval by alice at 2026-10-10T00:00:00Z, which is now spent",
351
+ "t2: its plan moved since wave 3 was decided, so it was not applied. decided: sha256:2222222222222222222222222222222222222222222222222222222222222222; planned now: sha256:9999999999999999999999999999999999999999999999999999999999999999"
352
+ ],
353
+ "files": {
354
+ ".chant/op-waves/migrations/wave-3/t1.hooks.json": {
355
+ "version": 1,
356
+ "opWaves": "migrations",
357
+ "op": "migrate",
358
+ "wave": "prod",
359
+ "number": 3,
360
+ "base": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
361
+ "digest": "jcs1-sha256:106d25f35ee6bc02e9aa253d0b2633a764396833c82e0c5ec5a4ad83a2a85b4e",
362
+ "approved": true,
363
+ "runBy": "chant",
364
+ "runs": [
365
+ {
366
+ "target": "t1",
367
+ "op": "migrate",
368
+ "plan": ".chant/op-waves/migrations/wave-3/t1.plan.json"
369
+ }
370
+ ],
371
+ "runsFile": ".chant/op-waves/migrations/wave-3/runs.json",
372
+ "hooks": {},
373
+ "outcomes": []
374
+ },
375
+ ".chant/op-waves/migrations/wave-3/t1.plan.json": {
376
+ "planDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111"
377
+ },
378
+ ".chant/op-waves/migrations/wave-3/t2.plan.json": {
379
+ "planDigest": "sha256:9999999999999999999999999999999999999999999999999999999999999999"
380
+ },
381
+ ".chant/op-waves/migrations/wave-3/t3.hooks.json": {
382
+ "version": 1,
383
+ "opWaves": "migrations",
384
+ "op": "migrate",
385
+ "wave": "prod",
386
+ "number": 3,
387
+ "base": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
388
+ "digest": "jcs1-sha256:106d25f35ee6bc02e9aa253d0b2633a764396833c82e0c5ec5a4ad83a2a85b4e",
389
+ "approved": true,
390
+ "runBy": "chant",
391
+ "runs": [
392
+ {
393
+ "target": "t3",
394
+ "op": "migrate",
395
+ "plan": ".chant/op-waves/migrations/wave-3/t3.plan.json"
396
+ }
397
+ ],
398
+ "runsFile": ".chant/op-waves/migrations/wave-3/runs.json",
399
+ "hooks": {},
400
+ "outcomes": []
401
+ },
402
+ ".chant/op-waves/migrations/wave-3/t3.plan.json": {
403
+ "planDigest": "sha256:3333333333333333333333333333333333333333333333333333333333333333"
404
+ },
405
+ ".chant/op-waves/migrations/wave-3.json": {
406
+ "wave": 3,
407
+ "name": "prod",
408
+ "op": "migrations",
409
+ "gate": "migrations-wave-3",
410
+ "policy": "always",
411
+ "approval": "ledger",
412
+ "policySource": "base",
413
+ "base": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
414
+ "digest": "jcs1-sha256:106d25f35ee6bc02e9aa253d0b2633a764396833c82e0c5ec5a4ad83a2a85b4e",
415
+ "members": [
416
+ {
417
+ "member": "t3",
418
+ "planDigest": "sha256:3333333333333333333333333333333333333333333333333333333333333333",
419
+ "op": "migrate"
420
+ },
421
+ {
422
+ "member": "t1",
423
+ "planDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
424
+ "op": "migrate"
425
+ },
426
+ {
427
+ "member": "t2",
428
+ "planDigest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
429
+ "op": "migrate"
430
+ }
431
+ ],
432
+ "status": "approved",
433
+ "spend": "base",
434
+ "via": "ledger",
435
+ "approvedBy": "alice",
436
+ "approvedAt": "2026-10-10T00:00:00Z",
437
+ "spentAt": "2026-10-11T00:00:00.000Z"
438
+ }
439
+ }
440
+ }
@@ -143,6 +143,30 @@ describe("liveImportFromPlugins (#114)", () => {
143
143
  expect(result.error).toContain("No resources exported");
144
144
  });
145
145
 
146
+ // #3911 — an exporter that refuses (the sql lexicon naming objects it cannot
147
+ // declare) is the import's error, not a warning behind "No resources exported".
148
+ test("an exporter's failure is the error when nothing else exported, and --skip-undeclarable reaches it", async () => {
149
+ let seen: unknown;
150
+ const refusing: LexiconPlugin = {
151
+ ...fakeExporter("sql", sampleIR),
152
+ async exportResources(opts: { skipUndeclarable?: boolean }) {
153
+ seen = opts.skipUndeclarable;
154
+ if (!opts.skipUndeclarable) throw new Error("the server has an object of a kind chant cannot declare:\n - aggregate app.total(integer)");
155
+ return { ...sampleIR, warnings: ["aggregate app.total(integer) is left out (--skip-undeclarable)"] };
156
+ },
157
+ };
158
+ const failed = await liveImportFromPlugins([refusing], { environment: "prod", output: outputDir, force: true });
159
+ expect(failed.success).toBe(false);
160
+ expect(failed.error).toContain("sql: live export failed");
161
+ expect(failed.error).toContain(" - aggregate app.total(integer)");
162
+ expect(seen).toBeUndefined();
163
+
164
+ const skipped = await liveImportFromPlugins([refusing], { environment: "prod", output: outputDir, force: true, skipUndeclarable: true });
165
+ expect(skipped.success).toBe(true);
166
+ expect(seen).toBe(true);
167
+ expect(skipped.warnings).toContain("aggregate app.total(integer) is left out (--skip-undeclarable)");
168
+ });
169
+
146
170
  // #932 — a multi-stack reconcile imports each stack from its own live
147
171
  // CloudFormation stack; the `stack` option must reach `exportResources` so
148
172
  // the right stack is queried (not one env-named stack for all of them).
@@ -645,6 +645,8 @@ export interface LiveImportOptions {
645
645
  owned?: boolean;
646
646
  /** Keep server-defaulted fields instead of stripping to declared shape. */
647
647
  verbatim?: boolean;
648
+ /** Import what can be declared, and warn about the live objects no declaration can hold instead of failing. */
649
+ skipUndeclarable?: boolean;
648
650
  }
649
651
 
650
652
  /**
@@ -745,6 +747,8 @@ export async function liveImportFromPlugins(
745
747
  // Collect IR from every exporter, tagging which lexicon produced output.
746
748
  let irParts: ExportedTemplate[] = [];
747
749
  let generatorLexicon: LexiconPlugin | undefined;
750
+ // An exporter that fails (an sql import refusing objects it cannot declare, #3911) is the error when nothing else exported.
751
+ const failures: string[] = [];
748
752
  for (const plugin of exporters) {
749
753
  let ir: ExportedTemplate;
750
754
  try {
@@ -754,9 +758,10 @@ export async function liveImportFromPlugins(
754
758
  selector: options.selector,
755
759
  owned: options.owned,
756
760
  verbatim: options.verbatim,
761
+ ...(options.skipUndeclarable ? { skipUndeclarable: true } : {}),
757
762
  });
758
763
  } catch (err) {
759
- warnings.push(`${plugin.name}: live export failed — ${err instanceof Error ? err.message : String(err)}`);
764
+ failures.push(`${plugin.name}: live export failed — ${err instanceof Error ? err.message : String(err)}`);
760
765
  continue;
761
766
  }
762
767
  // What the exporter left out or could not carry (#3336).
@@ -771,9 +776,10 @@ export async function liveImportFromPlugins(
771
776
  success: false,
772
777
  generatedFiles: [],
773
778
  warnings,
774
- error: `No resources exported from environment "${options.environment}".`,
779
+ error: failures.length > 0 ? failures.join("\n") : `No resources exported from environment "${options.environment}".`,
775
780
  };
776
781
  }
782
+ warnings.push(...failures);
777
783
 
778
784
  if (exporters.length > 1 && irParts.length > 1) {
779
785
  warnings.push("Multiple lexicons exported resources; generated with the first. Use --lexicon to target one.");
@@ -249,6 +249,29 @@ describe("runGraph", () => {
249
249
  expect(JSON.parse(stdoutBuf.join("\n")).meta).toEqual({ collector: { pipelines: [] } });
250
250
  });
251
251
 
252
+ // #3910 — a lexicon's graphEdges answer becomes edges and dangling refs.
253
+ test("--format ir carries what a lexicon's graphEdges reports", async () => {
254
+ lintClean(); discovered();
255
+ loadPluginsMock.mockResolvedValue([{
256
+ name: "gitlab",
257
+ serializer: {},
258
+ graphEdges: () => ({
259
+ references: [
260
+ { from: "pod", to: "vpc", kind: "needs", viaAttr: "needs" },
261
+ { from: "pod", to: "group/other", kind: "triggers", viaAttr: "trigger", crossPipeline: true, dangling: true },
262
+ ],
263
+ pipelines: { ".gitlab-ci.yml": ["pod", "vpc"] },
264
+ }),
265
+ }, { name: "broken", serializer: {}, graphEdges: () => { throw new Error("boom"); } }]);
266
+ resolveLexMock.mockResolvedValue(["gitlab", "broken"]);
267
+ const exit = await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
268
+ expect(exit).toBe(0);
269
+ const ir = JSON.parse(stdoutBuf.join("\n"));
270
+ expect(ir.edges).toContainEqual({ from: "pod", to: "vpc", kind: "needs", viaAttr: "needs" });
271
+ expect(ir.dangling).toEqual([{ from: "pod", kind: "triggers", path: "trigger", value: "group/other", crossPipeline: true }]);
272
+ expect(ir.groups.byPipeline).toEqual({ ".gitlab-ci.yml": ["pod", "vpc"] });
273
+ });
274
+
252
275
  test("--format ir has no meta when no lexicon reports anything", async () => {
253
276
  lintClean(); discovered();
254
277
  loadPluginsMock.mockResolvedValue([{ name: "gcp", serializer: {}, graphMeta: () => undefined }, { name: "k8s", serializer: {} }]);
@@ -2,7 +2,7 @@ import { dirname, resolve } from "node:path";
2
2
  import { discoverOps } from "../../op/discover";
3
3
  import { discover } from "../../discovery/index";
4
4
  import { partitionByLexicon, computeStackGraph, build, mergeBuildRootEntities } from "../../build";
5
- import { buildGraphIr, buildLiveGraphIr, collectUnobserved, overlayGraphs, sourceOverlayGraphs, GRAPH_IR_VERSION, type GraphIR, type IRPipeline, type LiveObservation } from "../../graph-ir";
5
+ import { applyLexiconGraphEdges, buildGraphIr, buildLiveGraphIr, collectUnobserved, overlayGraphs, sourceOverlayGraphs, GRAPH_IR_VERSION, type GraphIR, type IRPipeline, type LexiconGraphEdges, type LiveObservation } from "../../graph-ir";
6
6
  import { behaviourOverlay } from "../../behaviour-overlay";
7
7
  import type { SerializerResult } from "../../serializer";
8
8
  import { applyBehaviourOverlay, behaviourRequestFromIr } from "../../behaviour-graph";
@@ -726,6 +726,26 @@ function withGraphMeta(ir: GraphIR, plugins: LexiconPlugin[], entities: Map<stri
726
726
  return Object.keys(added).length === 0 ? ir : { ...ir, meta: { ...(ir.meta ?? {}), ...added } };
727
727
  }
728
728
 
729
+ /**
730
+ * Add the edges the lexicons' `graphEdges` hooks resolve (#3910): CI
731
+ * dependencies written as strings (`needs`, `uses:`, `trigger:`, `include:`,
732
+ * `extends`), which the AttrRef walk in `buildGraphIr` cannot see. A hook that
733
+ * throws costs its own edges and a warning, not the graph.
734
+ */
735
+ function withLexiconEdges(ir: GraphIR, plugins: LexiconPlugin[], entities: Map<string, import("../../declarable").Declarable>): GraphIR {
736
+ const reports: LexiconGraphEdges[] = [];
737
+ for (const p of plugins) {
738
+ if (!p.graphEdges) continue;
739
+ try {
740
+ const report = p.graphEdges(entities);
741
+ if (report) reports.push(report);
742
+ } catch (err) {
743
+ console.error(formatWarning({ message: `the ${p.name} lexicon's graphEdges failed (${err instanceof Error ? err.message : String(err)}); its edges are left out` }));
744
+ }
745
+ }
746
+ return reports.length === 0 ? ir : applyLexiconGraphEdges(ir, reports);
747
+ }
748
+
729
749
  /** Returned instead of a graph when the prediction cannot be attempted honestly. */
730
750
  const REFUSED = Symbol("behaviour-refused");
731
751
 
@@ -867,6 +887,7 @@ async function runGraphView(
867
887
  const plugins = ctx.plugins.length > 0
868
888
  ? ctx.plugins
869
889
  : await loadPlugins(await projectLexiconsOrNone(projectPath));
890
+ ir = withLexiconEdges(ir, plugins as LexiconPlugin[], result.entities);
870
891
  const predicted = await predictOntoIr(ir, plugins as LexiconPlugin[], ctx.args, {
871
892
  // No environment on the declared path: the file is not deployed anywhere
872
893
  // yet, and naming one would claim it was.
@@ -349,7 +349,7 @@ export async function runImport(ctx: CommandContext): Promise<number> {
349
349
  const { config } = await loadChantConfig(resolve("."));
350
350
  if (config.stacks && config.stacks.length > 0) {
351
351
  const perStack = await importFromLiveStacks(
352
- { environment: args.migrateFrom, lexicon: args.lexicon, force: args.force, selector, owned: args.owned, verbatim: args.verbatim },
352
+ { environment: args.migrateFrom, lexicon: args.lexicon, force: args.force, selector, owned: args.owned, verbatim: args.verbatim, skipUndeclarable: args.skipUndeclarable },
353
353
  config.stacks,
354
354
  );
355
355
  let anyFailure = false;
@@ -369,6 +369,7 @@ export async function runImport(ctx: CommandContext): Promise<number> {
369
369
  selector,
370
370
  owned: args.owned,
371
371
  verbatim: args.verbatim,
372
+ skipUndeclarable: args.skipUndeclarable,
372
373
  });
373
374
 
374
375
  printImportResult(result);
@@ -16,6 +16,11 @@ describe("parseArgs", () => {
16
16
  expect(() => parseArgs(["graph", "--traffic"])).toThrow(/--traffic needs a level/);
17
17
  });
18
18
 
19
+ test("--skip-undeclarable reaches live import (#3911)", () => {
20
+ expect(parseArgs(["import", "--from", "prod"]).skipUndeclarable).toBeUndefined();
21
+ expect(parseArgs(["import", "--from", "prod", "--skip-undeclarable"]).skipUndeclarable).toBe(true);
22
+ });
23
+
19
24
  test("--fold and --no-fold set the tri-state fold option (#1134)", () => {
20
25
  expect(parseArgs(["build", "src"]).fold).toBeUndefined();
21
26
  expect(parseArgs(["build", "src", "--fold"]).fold).toBe(true);
@@ -33,6 +33,7 @@ const BOOLEAN_FLAGS = new Set([
33
33
  "--explain",
34
34
  "--owned",
35
35
  "--verbatim",
36
+ "--skip-undeclarable",
36
37
  "--apply-rewrites",
37
38
  "--write",
38
39
  "--strict",
@@ -216,6 +217,8 @@ export function parseArgs(args: string[]): ParsedArgs {
216
217
  if (!result.namespace || result.namespace.startsWith("-")) throw new Error("--namespace needs a namespace: --namespace <ns>");
217
218
  } else if (arg === "--verbatim") {
218
219
  result.verbatim = true;
220
+ } else if (arg === "--skip-undeclarable") {
221
+ result.skipUndeclarable = true;
219
222
  } else if (arg === "--state") {
220
223
  result.statePath = args[++i];
221
224
  } else if (arg === "--select") {
@@ -158,6 +158,8 @@ export interface ParsedArgs {
158
158
  confirmProd?: boolean;
159
159
  /** `chant import --verbatim` — keep server-defaulted fields in live import */
160
160
  verbatim?: boolean;
161
+ /** `chant import --skip-undeclarable` — import what can be declared and warn about the live objects that cannot, instead of failing */
162
+ skipUndeclarable?: boolean;
161
163
  /** `chant lifecycle … --src <dir>` — build root override for lifecycle commands */
162
164
  src?: string;
163
165
  /** `--env <name>` — active environment: sets CHANT_ENV so env-aware source