@intentius/chant 0.19.0 → 0.20.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 (156) hide show
  1. package/dist/build-params.d.ts +108 -0
  2. package/dist/build-params.d.ts.map +1 -0
  3. package/dist/build.d.ts +79 -1
  4. package/dist/build.d.ts.map +1 -1
  5. package/dist/cli/commands/build.d.ts +31 -0
  6. package/dist/cli/commands/build.d.ts.map +1 -1
  7. package/dist/cli/commands/check-lexicon-examples.d.ts +41 -0
  8. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -0
  9. package/dist/cli/commands/check-lexicon-intrinsics.d.ts +54 -0
  10. package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -0
  11. package/dist/cli/commands/check-lexicon.d.ts +1 -1
  12. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  13. package/dist/cli/commands/lint.d.ts +27 -0
  14. package/dist/cli/commands/lint.d.ts.map +1 -1
  15. package/dist/cli/handlers/build.d.ts.map +1 -1
  16. package/dist/cli/handlers/components.d.ts.map +1 -1
  17. package/dist/cli/handlers/lint.d.ts.map +1 -1
  18. package/dist/cli/handlers/run-client.d.ts +1 -1
  19. package/dist/cli/handlers/run-client.d.ts.map +1 -1
  20. package/dist/cli/handlers/run.d.ts.map +1 -1
  21. package/dist/cli/main.d.ts.map +1 -1
  22. package/dist/cli/plugins.d.ts +16 -0
  23. package/dist/cli/plugins.d.ts.map +1 -1
  24. package/dist/cli/registry.d.ts +10 -0
  25. package/dist/cli/registry.d.ts.map +1 -1
  26. package/dist/codegen/docs-sections.d.ts.map +1 -1
  27. package/dist/components/cli-support.d.ts +12 -5
  28. package/dist/components/cli-support.d.ts.map +1 -1
  29. package/dist/components/discover.d.ts +62 -7
  30. package/dist/components/discover.d.ts.map +1 -1
  31. package/dist/components/sandbox/driver.d.ts +12 -0
  32. package/dist/components/sandbox/driver.d.ts.map +1 -0
  33. package/dist/components/sandbox/run.d.ts +42 -0
  34. package/dist/components/sandbox/run.d.ts.map +1 -0
  35. package/dist/composite.d.ts +5 -0
  36. package/dist/composite.d.ts.map +1 -1
  37. package/dist/config.d.ts +71 -0
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/discovery/collect.d.ts.map +1 -1
  40. package/dist/discovery/entity-wire-codec.d.ts +166 -0
  41. package/dist/discovery/entity-wire-codec.d.ts.map +1 -0
  42. package/dist/discovery/entity-wire.d.ts +50 -0
  43. package/dist/discovery/entity-wire.d.ts.map +1 -0
  44. package/dist/discovery/fold-import.d.ts +210 -0
  45. package/dist/discovery/fold-import.d.ts.map +1 -0
  46. package/dist/discovery/index.d.ts +74 -1
  47. package/dist/discovery/index.d.ts.map +1 -1
  48. package/dist/discovery/sandbox/bundle.d.ts +18 -0
  49. package/dist/discovery/sandbox/bundle.d.ts.map +1 -0
  50. package/dist/discovery/sandbox/child-errors.d.ts +15 -0
  51. package/dist/discovery/sandbox/child-errors.d.ts.map +1 -0
  52. package/dist/discovery/sandbox/driver.d.ts +13 -0
  53. package/dist/discovery/sandbox/driver.d.ts.map +1 -0
  54. package/dist/discovery/sandbox/run.d.ts +69 -0
  55. package/dist/discovery/sandbox/run.d.ts.map +1 -0
  56. package/dist/errors.d.ts +9 -1
  57. package/dist/errors.d.ts.map +1 -1
  58. package/dist/fold/fold.d.ts +226 -0
  59. package/dist/fold/fold.d.ts.map +1 -0
  60. package/dist/fold/subset.d.ts +102 -0
  61. package/dist/fold/subset.d.ts.map +1 -0
  62. package/dist/index.d.ts +2 -0
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/lexicon-output.d.ts +7 -2
  65. package/dist/lexicon-output.d.ts.map +1 -1
  66. package/dist/lexicon-schema.d.ts +2 -2
  67. package/dist/lexicon-schema.d.ts.map +1 -1
  68. package/dist/lexicon.d.ts +81 -1
  69. package/dist/lexicon.d.ts.map +1 -1
  70. package/dist/lifecycle/release-ledger.d.ts +11 -0
  71. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  72. package/dist/lint/component-checks.d.ts +7 -1
  73. package/dist/lint/component-checks.d.ts.map +1 -1
  74. package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
  75. package/dist/lint/rules/evl003-dynamic-property-access.d.ts.map +1 -1
  76. package/dist/params.d.ts +60 -0
  77. package/dist/params.d.ts.map +1 -0
  78. package/dist/provenance.d.ts +21 -0
  79. package/dist/provenance.d.ts.map +1 -1
  80. package/dist/terraform/aws-resources.d.ts.map +1 -1
  81. package/package.json +2 -1
  82. package/src/build-params.test.ts +144 -0
  83. package/src/build-params.ts +207 -0
  84. package/src/build.test.ts +38 -0
  85. package/src/build.ts +144 -7
  86. package/src/cli/commands/build.test.ts +220 -2
  87. package/src/cli/commands/build.ts +111 -3
  88. package/src/cli/commands/check-lexicon-examples.test.ts +92 -0
  89. package/src/cli/commands/check-lexicon-examples.ts +103 -0
  90. package/src/cli/commands/check-lexicon-intrinsics.test.ts +145 -0
  91. package/src/cli/commands/check-lexicon-intrinsics.ts +312 -0
  92. package/src/cli/commands/check-lexicon.test.ts +34 -0
  93. package/src/cli/commands/check-lexicon.ts +119 -1
  94. package/src/cli/commands/lint.ts +31 -3
  95. package/src/cli/commands/onboard.ts +1 -1
  96. package/src/cli/component-security-boundary.test.ts +170 -0
  97. package/src/cli/handlers/build.ts +24 -3
  98. package/src/cli/handlers/components.ts +9 -2
  99. package/src/cli/handlers/dev.ts +1 -1
  100. package/src/cli/handlers/graph.ts +7 -5
  101. package/src/cli/handlers/lifecycle.ts +2 -2
  102. package/src/cli/handlers/lint.ts +2 -0
  103. package/src/cli/handlers/misc.ts +2 -2
  104. package/src/cli/handlers/run-client.ts +1 -1
  105. package/src/cli/handlers/run.ts +20 -5
  106. package/src/cli/main.test.ts +22 -0
  107. package/src/cli/main.ts +39 -0
  108. package/src/cli/plugins.ts +20 -4
  109. package/src/cli/registry.ts +10 -0
  110. package/src/cli/security-boundary.test.ts +135 -0
  111. package/src/codegen/docs-sections.test.ts +61 -0
  112. package/src/codegen/docs-sections.ts +7 -3
  113. package/src/components/cli-support.ts +22 -10
  114. package/src/components/discover.ts +127 -25
  115. package/src/components/sandbox/driver.ts +114 -0
  116. package/src/components/sandbox/run.test.ts +185 -0
  117. package/src/components/sandbox/run.ts +177 -0
  118. package/src/composite.test.ts +21 -0
  119. package/src/composite.ts +20 -1
  120. package/src/config.ts +81 -0
  121. package/src/discovery/collect.ts +17 -3
  122. package/src/discovery/entity-wire-codec.ts +485 -0
  123. package/src/discovery/entity-wire.test.ts +240 -0
  124. package/src/discovery/entity-wire.ts +67 -0
  125. package/src/discovery/fold-import.test.ts +1026 -0
  126. package/src/discovery/fold-import.ts +1805 -0
  127. package/src/discovery/index.test.ts +191 -1
  128. package/src/discovery/index.ts +233 -1
  129. package/src/discovery/sandbox/bundle.ts +218 -0
  130. package/src/discovery/sandbox/child-errors.ts +65 -0
  131. package/src/discovery/sandbox/driver.ts +147 -0
  132. package/src/discovery/sandbox/run.test.ts +179 -0
  133. package/src/discovery/sandbox/run.ts +196 -0
  134. package/src/errors.ts +9 -1
  135. package/src/fold/fold.test.ts +535 -0
  136. package/src/fold/fold.ts +648 -0
  137. package/src/fold/subset-doc-parity.test.ts +183 -0
  138. package/src/fold/subset.test.ts +241 -0
  139. package/src/fold/subset.ts +302 -0
  140. package/src/index.ts +2 -0
  141. package/src/lexicon-output.ts +7 -2
  142. package/src/lexicon-schema.test.ts +14 -0
  143. package/src/lexicon-schema.ts +3 -1
  144. package/src/lexicon.ts +82 -1
  145. package/src/lifecycle/git.test.ts +10 -5
  146. package/src/lifecycle/release-ledger.test.ts +28 -0
  147. package/src/lifecycle/release-ledger.ts +11 -0
  148. package/src/lint/component-checks.ts +8 -1
  149. package/src/lint/rules/evl001-non-literal-expression.ts +26 -110
  150. package/src/lint/rules/evl003-dynamic-property-access.ts +11 -2
  151. package/src/params.test.ts +22 -0
  152. package/src/params.ts +66 -0
  153. package/src/provenance.ts +22 -0
  154. package/src/terraform/adopt-state.test.ts +1 -1
  155. package/src/terraform/aws-resources.test.ts +11 -4
  156. package/src/terraform/aws-resources.ts +60 -0
@@ -0,0 +1,485 @@
1
+ /**
2
+ * chant #1045 (Phase 1) — the JSON wire format for a discovered, named,
3
+ * ref-resolved entity set. Split out of `./entity-wire.ts` in Phase 2 so this
4
+ * codec (`encodeEntitySet`/`decodeEntitySet`, no dependency on `discover()`
5
+ * or anything that pulls in the `typescript` compiler package) can be
6
+ * bundled ALONE into the sandboxed child's driver (`./sandbox/driver.ts`)
7
+ * without dragging in `discover()`'s whole fold/import graph — `entity-
8
+ * wire.ts` re-exports everything here unchanged, so nothing outside this
9
+ * pair of files needs to know about the split.
10
+ *
11
+ * `discover()` (./index.ts) produces a `Map<string, Declarable>` whose cross-
12
+ * entity references are live object identity: an `AttrRef.parent` is a
13
+ * `WeakRef<object>`, and a resource can embed ANOTHER resource directly as a
14
+ * prop value (e.g. `DependsOn: [otherResource]`) — the lexicon serializers
15
+ * detect this by looking the embedded object up (by identity) in a
16
+ * `Map<Declarable, string>` built from the very same entities map
17
+ * (`serializer-walker.ts`'s `resourceRef` dispatch; see also
18
+ * `resource-attributes.ts`'s `resolveDependsOn`). Neither a `WeakRef` nor bare
19
+ * object identity survives a process boundary.
20
+ *
21
+ * {@link encodeEntitySet} converts that live map into plain, JSON-safe data:
22
+ * every identity-based reference (an `AttrRef`, or a whole entity embedded by
23
+ * value) becomes a name-keyed marker instead. {@link decodeEntitySet} is the
24
+ * inverse — it rebuilds a live `Map<string, Declarable>` whose entities are
25
+ * BEHAVIORALLY indistinguishable from what `discover()` would have produced
26
+ * in-process: real `AttrRef` instances (several call sites downstream key off
27
+ * `instanceof AttrRef`, not just duck typing — `intrinsic-interpolation.ts`'s
28
+ * `defaultInterpolationSerializer`, `discovery/graph.ts`'s
29
+ * `buildDependencyGraph`, `build.ts`'s `detectCrossLexiconRefs`/
30
+ * `computeStackGraph` — so a plain `{__attrRef}` envelope alone is not
31
+ * enough), and whole-entity embeds restored to the SAME object reference
32
+ * (not a structurally-equal clone), so `entityNames.get(decl)` keeps working
33
+ * by identity exactly as it does today.
34
+ *
35
+ * `serializer-walker.ts`'s `walkValue` needs NO changes for this: it already
36
+ * falls back to reading a plain `{__attrRef}` envelope (added for intrinsics
37
+ * whose own `toJSON()` embeds one). `decodeEntitySet` goes further and
38
+ * reconstructs the real class so every OTHER `instanceof AttrRef` call site
39
+ * keeps working too, not just the walker.
40
+ *
41
+ * Naming happens exactly once, inside the boundary — `resolveAttrRefs`
42
+ * (./resolve.ts) runs as part of `discover()`, before `encodeEntitySet` is
43
+ * ever called (in-process), or inside the sandboxed child, over just the
44
+ * run-fallback subset (chant #1045 Phase 2, `./sandbox/run.ts`). This module
45
+ * does not re-derive names; it only carries already-resolved ones across.
46
+ *
47
+ * Scope (see the chant#1045 PR description for the full list): resource and
48
+ * property `Declarable`s, `StackOutput` (itself a marked `Declarable`), and
49
+ * `LexiconOutput` (not a `Declarable` — it is carried as its own wire form)
50
+ * all round-trip. A `ChildProjectInstance` (`nestedStack()`) does not — its
51
+ * `outputs` field is a lazy `Proxy`, not data — and {@link encodeEntitySet}
52
+ * throws rather than silently mis-encoding it. No corpus entry under
53
+ * `examples/` or any lexicon's `examples/` directory uses `nestedStack()`
54
+ * today.
55
+ */
56
+
57
+ import { DECLARABLE_MARKER, type Declarable } from "../declarable";
58
+ import { AttrRef } from "../attrref";
59
+ import { INTRINSIC_MARKER, type Intrinsic } from "../intrinsic";
60
+ import { isAttrRefLike } from "../utils";
61
+ import { isLexiconOutput, LexiconOutput } from "../lexicon-output";
62
+ import { isChildProject } from "../child-project";
63
+
64
+ /**
65
+ * A JSON-safe value tree: primitives, arrays/objects, and four marker shapes
66
+ * that stand in for what can't cross a process boundary by identity:
67
+ *
68
+ * - `__attrRef` — an `AttrRef` (attribute reference), keyed by the parent
69
+ * entity's already-resolved logical name.
70
+ * - `__entityRef` — a WHOLE entity embedded by value (e.g. `DependsOn:
71
+ * [otherResource]`), keyed by that entity's logical name.
72
+ * - `__property` — a nested, unnamed property-kind `Declarable` (not tracked
73
+ * in the entities map, so it carries its own `lexicon`/`entityType`/`props`
74
+ * inline rather than by reference).
75
+ * - `__intrinsic` — a lexicon intrinsic (`Sub`, `Join`, `Ref`, a pseudo-
76
+ * parameter, gitlab's `!reference`, github's `${{ }}` `Expression`, …).
77
+ * `value` is its already-called `toJSON()` output (Phase 0 confirmed every
78
+ * intrinsic in this codebase implements `toJSON` and holds no
79
+ * function/closure fields, so this is lossless). `yaml` is present only
80
+ * when the intrinsic ALSO implements the optional `toYAML()` method the
81
+ * gitlab and github serializers duck-type (`"toYAML" in value`) ahead of
82
+ * the generic `toJSON` dispatch, for a YAML-native form that differs from
83
+ * the JSON one (gitlab's `!reference [a, b]` tag vs. its plain `["a","b"]`
84
+ * JSON array) — capturing only `toJSON()` would silently lose that native
85
+ * form. `refs` additionally captures any `AttrRef`/whole-entity reference
86
+ * found while walking the intrinsic's OWN fields (not through `toJSON()`)
87
+ * — `buildDependencyGraph` and `detectCrossLexiconRefs`/`computeStackGraph`
88
+ * walk raw entity property trees looking for `instanceof AttrRef`/a
89
+ * tracked `Declarable`, not through `toJSON()`, so a ref nested inside e.g.
90
+ * a `Sub` template needs to still be discoverable post-decode for
91
+ * cross-lexicon output auto-detection and dependency ordering to keep
92
+ * working.
93
+ */
94
+ export type WireValue =
95
+ | null
96
+ | string
97
+ | number
98
+ | boolean
99
+ | WireValue[]
100
+ | { __attrRef: { entity: string; attribute: string } }
101
+ | { __entityRef: { entity: string } }
102
+ | { __property: { lexicon: string; entityType: string; props?: WireValue } }
103
+ | { __intrinsic: { value: WireValue; yaml?: WireValue; refs: WireValue[] } }
104
+ | { [key: string]: WireValue };
105
+
106
+ /** Wire form of one named, top-level `Declarable` entity (resource, property, or marked-Declarable output like `StackOutput`). */
107
+ export interface WireDeclarableEntity {
108
+ form: "declarable";
109
+ name: string;
110
+ lexicon: string;
111
+ entityType: string;
112
+ kind?: "resource" | "property" | "output";
113
+ props?: WireValue;
114
+ attributes?: WireValue;
115
+ /**
116
+ * `.description` of every truthy own marker symbol on the entity (e.g.
117
+ * `"chant.declarable"`, `"chant.stackOutput"`, a lexicon-specific one like
118
+ * `"chant.aws.defaultTags"`). Every marker symbol in this codebase is
119
+ * created with `Symbol.for(...)`, so `Symbol.for(description)` on decode
120
+ * reliably recovers the SAME symbol a `X_MARKER in value` check looks for —
121
+ * core doesn't need to know what any lexicon-specific marker means.
122
+ */
123
+ markers: string[];
124
+ /**
125
+ * Every other own enumerable field, beyond the fixed `lexicon`/`entityType`/
126
+ * `kind`/`props`/`attributes` shape — covers both a resource's per-attribute
127
+ * `AttrRef` fields (`vpcId`, `arn`, …) and plain-data fields on marker-
128
+ * Declarables that aren't built via `createResource`/`createProperty`
129
+ * (`StackOutput.sourceRef`/`description`, `DefaultTags.tags`,
130
+ * `Parameter.parameterType`/`description`/`defaultValue`, …).
131
+ */
132
+ extra?: Record<string, WireValue>;
133
+ }
134
+
135
+ /** Wire form of one named `LexiconOutput` entity — not a `Declarable`, so it's carried as its own shape rather than forced into {@link WireDeclarableEntity}. */
136
+ export interface WireLexiconOutputEntity {
137
+ form: "lexiconOutput";
138
+ name: string;
139
+ outputName: string;
140
+ /** The wrapped `AttrRef` or `Intrinsic` — encodes to `{__attrRef}` or `{__intrinsic}` respectively (see {@link WireValue}). */
141
+ ref: WireValue;
142
+ }
143
+
144
+ export type WireEntity = WireDeclarableEntity | WireLexiconOutputEntity;
145
+
146
+ /** A discovered, named, ref-resolved entity set, as pure JSON. */
147
+ export interface EntitySetWire {
148
+ entities: WireEntity[];
149
+ }
150
+
151
+ const CORE_FIELD_NAMES = new Set(["lexicon", "entityType", "kind", "props", "attributes"]);
152
+
153
+ // ─────────────────────────────────────────────────────────────────────────
154
+ // Encode: live entities map → JSON-safe wire data.
155
+ // ─────────────────────────────────────────────────────────────────────────
156
+
157
+ /**
158
+ * Read a `LexiconOutput`'s internal ref without re-deriving it — see the
159
+ * `_intrinsic`/`_sourceParent` fields in `../lexicon-output.ts`. When it was
160
+ * built from an `AttrRef`, `LexiconOutput` keeps only the parent `WeakRef` +
161
+ * attribute name (not the original `AttrRef` instance), so a fresh one is
162
+ * synthesized here — its logical name must be set explicitly (from
163
+ * `entityNames`, the same map every other reference in this module resolves
164
+ * names through) since it never went through `resolveAttrRefs`.
165
+ */
166
+ function lexiconOutputRef(output: LexiconOutput, entityNames: Map<unknown, string>): AttrRef | Intrinsic {
167
+ if (output._intrinsic) return output._intrinsic;
168
+ const parent = output._sourceParent?.deref();
169
+ const parentName = parent ? entityNames.get(parent) : undefined;
170
+ if (!parent || output.sourceAttribute === null || !parentName) {
171
+ throw new Error(`encodeEntitySet: LexiconOutput "${output.outputName}" has neither a resolvable AttrRef parent nor an intrinsic`);
172
+ }
173
+ const ref = new AttrRef(parent, output.sourceAttribute);
174
+ ref._setLogicalName(parentName);
175
+ return ref;
176
+ }
177
+
178
+ /** Walk `root`'s OWN fields (not through `toJSON()`) collecting any `AttrRef`/tracked-entity reference found — see {@link WireValue}'s `__intrinsic.refs` doc. */
179
+ function collectEmbeddedRefs(root: unknown, entityNames: Map<unknown, string>): WireValue[] {
180
+ const refs: WireValue[] = [];
181
+ const seen = new Set<unknown>();
182
+
183
+ function walk(value: unknown): void {
184
+ if (value === null || value === undefined || typeof value !== "object") return;
185
+ if (seen.has(value)) return;
186
+ seen.add(value);
187
+
188
+ if (isAttrRefLike(value)) {
189
+ const entity = value.getLogicalName();
190
+ if (entity) refs.push({ __attrRef: { entity, attribute: value.attribute } });
191
+ return;
192
+ }
193
+ if ("entityType" in value) {
194
+ const name = entityNames.get(value);
195
+ if (name) {
196
+ refs.push({ __entityRef: { entity: name } });
197
+ return;
198
+ }
199
+ }
200
+ if (Array.isArray(value)) {
201
+ for (const item of value) walk(item);
202
+ return;
203
+ }
204
+ for (const val of Object.values(value as Record<string, unknown>)) walk(val);
205
+ }
206
+
207
+ walk(root);
208
+ return refs;
209
+ }
210
+
211
+ /** Encode one value found in an entity's props/attributes/extra-field tree — mirrors `serializer-walker.ts`'s `walkValue` dispatch, targeting the wire format instead of a lexicon format. */
212
+ function encodeValue(value: unknown, entityNames: Map<unknown, string>): WireValue {
213
+ if (value === null || value === undefined) return null;
214
+
215
+ if (isAttrRefLike(value)) {
216
+ const entity = value.getLogicalName();
217
+ if (!entity) {
218
+ throw new Error(`encodeEntitySet: AttrRef for attribute "${value.attribute}" has no logical name — was resolveAttrRefs run before encoding?`);
219
+ }
220
+ return { __attrRef: { entity, attribute: value.attribute } };
221
+ }
222
+
223
+ if (typeof value === "object" && INTRINSIC_MARKER in value) {
224
+ const intrinsic = value as Intrinsic & { toYAML?: () => unknown };
225
+ const toJSONResult = typeof intrinsic.toJSON === "function" ? intrinsic.toJSON() : null;
226
+ const wire: { value: WireValue; yaml?: WireValue; refs: WireValue[] } = {
227
+ value: encodeValue(toJSONResult, entityNames),
228
+ refs: collectEmbeddedRefs(value, entityNames),
229
+ };
230
+ if (typeof intrinsic.toYAML === "function") {
231
+ wire.yaml = encodeValue(intrinsic.toYAML(), entityNames);
232
+ }
233
+ return { __intrinsic: wire };
234
+ }
235
+
236
+ if (typeof value === "object" && "entityType" in value) {
237
+ const decl = value as Declarable;
238
+ const name = entityNames.get(decl);
239
+ if (name) {
240
+ // A tracked, top-level entity referenced by identity (e.g. `DependsOn:
241
+ // [otherResource]`).
242
+ return { __entityRef: { entity: name } };
243
+ }
244
+ // Untracked — either genuinely property-kind (never independently named
245
+ // to begin with), or a resource-kind Declarable embedded as a bare
246
+ // nested VALUE that never became its own top-level entity (e.g. Helm's
247
+ // `Helm::Test`/`Helm::Hook` wrap a `new Pod({...})` as their `resource`
248
+ // field, and a Helm chart's own k8s resources — `new Container(...)`,
249
+ // `new PersistentVolumeClaim(...)` inside a `StatefulSet`'s props — are
250
+ // likewise embedded values, never independent entities). Neither case
251
+ // has (or can have) a resolvable logical name: no lexicon serializer's
252
+ // generic dispatch (`serializer-walker.ts`'s `walkValue`) is what reads
253
+ // such a value in practice — every one that touches it reaches straight
254
+ // into its `.props` (see `Helm::Test`'s handling in
255
+ // `lexicons/helm/src/serializer.ts`, which extracts `resource.props`
256
+ // directly rather than walking `resource` as a Declarable reference).
257
+ // Inline exactly that, discarding the unresolvable (and, in every corpus
258
+ // entry today, unread) identity layer.
259
+ const props = "props" in decl ? (decl as unknown as { props?: unknown }).props : undefined;
260
+ return { __property: { lexicon: decl.lexicon, entityType: decl.entityType, props: props !== undefined ? encodeValue(props, entityNames) : undefined } };
261
+ }
262
+
263
+ if (Array.isArray(value)) {
264
+ return value.map((item) => (item === undefined ? null : encodeValue(item, entityNames)));
265
+ }
266
+
267
+ if (typeof value === "object") {
268
+ const result: Record<string, WireValue> = {};
269
+ for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
270
+ if (val === undefined) continue;
271
+ result[key] = encodeValue(val, entityNames);
272
+ }
273
+ return result;
274
+ }
275
+
276
+ return value as WireValue;
277
+ }
278
+
279
+ function encodeDeclarable(entity: Declarable, entityNames: Map<unknown, string>): Omit<WireDeclarableEntity, "form" | "name"> {
280
+ const wire: Omit<WireDeclarableEntity, "form" | "name"> = { lexicon: entity.lexicon, entityType: entity.entityType, markers: [] };
281
+ if (entity.kind !== undefined) wire.kind = entity.kind;
282
+
283
+ if ("props" in entity && (entity as unknown as { props?: unknown }).props !== undefined) {
284
+ wire.props = encodeValue((entity as unknown as { props: unknown }).props, entityNames);
285
+ }
286
+ if ("attributes" in entity && (entity as unknown as { attributes?: unknown }).attributes !== undefined) {
287
+ wire.attributes = encodeValue((entity as unknown as { attributes: unknown }).attributes, entityNames);
288
+ }
289
+
290
+ const markers: string[] = [];
291
+ for (const sym of Object.getOwnPropertySymbols(entity)) {
292
+ if (sym.description && (entity as unknown as Record<symbol, unknown>)[sym] === true) {
293
+ markers.push(sym.description);
294
+ }
295
+ }
296
+ wire.markers = markers;
297
+
298
+ const extra: Record<string, WireValue> = {};
299
+ for (const key of Object.keys(entity)) {
300
+ if (CORE_FIELD_NAMES.has(key)) continue;
301
+ const val = (entity as unknown as Record<string, unknown>)[key];
302
+ if (val === undefined) continue;
303
+ extra[key] = encodeValue(val, entityNames);
304
+ }
305
+ if (Object.keys(extra).length > 0) wire.extra = extra;
306
+
307
+ return wire;
308
+ }
309
+
310
+ /**
311
+ * Encode a discovered, named, ref-resolved entities map into pure JSON.
312
+ *
313
+ * @throws if any entity is a `ChildProjectInstance` (`nestedStack()`) — its
314
+ * `outputs` field is a lazy `Proxy`, not representable as data. No corpus
315
+ * entry uses it today (chant#1045 Phase 1).
316
+ */
317
+ export function encodeEntitySet(entities: Map<string, Declarable>): EntitySetWire {
318
+ const entityNames = new Map<unknown, string>();
319
+ for (const [name, entity] of entities) entityNames.set(entity, name);
320
+
321
+ const wireEntities: WireEntity[] = [];
322
+ for (const [name, entity] of entities) {
323
+ if (isChildProject(entity)) {
324
+ throw new Error(
325
+ `encodeEntitySet: entity "${name}" is a child project (nestedStack()) — not yet supported by the JSON entity boundary (chant#1045 Phase 1)`,
326
+ );
327
+ }
328
+ if (isLexiconOutput(entity)) {
329
+ const ref = lexiconOutputRef(entity, entityNames);
330
+ wireEntities.push({ form: "lexiconOutput", name, outputName: entity.outputName, ref: encodeValue(ref, entityNames) });
331
+ continue;
332
+ }
333
+ wireEntities.push({ form: "declarable", name, ...encodeDeclarable(entity, entityNames) });
334
+ }
335
+
336
+ return { entities: wireEntities };
337
+ }
338
+
339
+ // ─────────────────────────────────────────────────────────────────────────
340
+ // Decode: JSON-safe wire data → live entities map.
341
+ // ─────────────────────────────────────────────────────────────────────────
342
+
343
+ /**
344
+ * Decode one wire value, resolving `__attrRef`/`__entityRef`/`__property`/
345
+ * `__intrinsic` markers against `registry` (name → already-reconstructed live
346
+ * object).
347
+ *
348
+ * Reads via an explicit cast (`asRecord`) rather than relying on TS's `in`-
349
+ * narrowing across the {@link WireValue} union: the union's catch-all plain-
350
+ * object member (`{ [key: string]: WireValue }`) structurally overlaps every
351
+ * marker shape too (an index signature accepts any key), so narrowing alone
352
+ * can't tell TS which shape's fields are actually present at runtime.
353
+ */
354
+ function decodeValue(wire: WireValue, registry: Map<string, unknown>): unknown {
355
+ if (wire === null || typeof wire !== "object") return wire;
356
+
357
+ if (Array.isArray(wire)) {
358
+ return wire.map((item) => decodeValue(item, registry));
359
+ }
360
+
361
+ const asRecord = wire as unknown as Record<string, unknown>;
362
+
363
+ if ("__attrRef" in asRecord) {
364
+ const { entity, attribute } = asRecord.__attrRef as { entity: string; attribute: string };
365
+ const parent = registry.get(entity);
366
+ if (!parent) throw new Error(`decodeEntitySet: __attrRef refers to unknown entity "${entity}"`);
367
+ const ref = new AttrRef(parent as object, attribute);
368
+ ref._setLogicalName(entity);
369
+ return ref;
370
+ }
371
+
372
+ if ("__entityRef" in asRecord) {
373
+ const { entity } = asRecord.__entityRef as { entity: string };
374
+ const target = registry.get(entity);
375
+ if (!target) throw new Error(`decodeEntitySet: __entityRef refers to unknown entity "${entity}"`);
376
+ return target;
377
+ }
378
+
379
+ if ("__property" in asRecord) {
380
+ const { lexicon, entityType, props } = asRecord.__property as { lexicon: string; entityType: string; props?: WireValue };
381
+ const obj: Record<string | symbol, unknown> = {};
382
+ Object.defineProperty(obj, DECLARABLE_MARKER, { value: true, enumerable: false });
383
+ Object.defineProperty(obj, "lexicon", { value: lexicon, enumerable: false, configurable: true });
384
+ Object.defineProperty(obj, "entityType", { value: entityType, enumerable: false, configurable: true });
385
+ Object.defineProperty(obj, "kind", { value: "property", enumerable: false, configurable: true });
386
+ if (props !== undefined) {
387
+ Object.defineProperty(obj, "props", { value: decodeValue(props, registry), enumerable: false, configurable: true });
388
+ }
389
+ return obj;
390
+ }
391
+
392
+ if ("__intrinsic" in asRecord) {
393
+ const intrinsicWire = asRecord.__intrinsic as { value: WireValue; yaml?: WireValue; refs: WireValue[] };
394
+ const decodedValue = decodeValue(intrinsicWire.value, registry);
395
+ const decodedRefs = intrinsicWire.refs.map((ref) => decodeValue(ref, registry));
396
+ const wrapper: Record<string, unknown> = {
397
+ [INTRINSIC_MARKER]: true,
398
+ __chantWireRefs: decodedRefs,
399
+ toJSON(): unknown {
400
+ return decodedValue;
401
+ },
402
+ };
403
+ // Only present when the original intrinsic ALSO implemented `toYAML()`
404
+ // (see {@link WireValue}'s doc) — gitlab/github's serializers duck-type
405
+ // this method, so it must exist on the reconstructed wrapper only when
406
+ // it existed on the original, not unconditionally.
407
+ if ("yaml" in intrinsicWire) {
408
+ const decodedYaml = decodeValue(intrinsicWire.yaml as WireValue, registry);
409
+ wrapper.toYAML = (): unknown => decodedYaml;
410
+ }
411
+ return wrapper;
412
+ }
413
+
414
+ const result: Record<string, unknown> = {};
415
+ for (const [key, val] of Object.entries(asRecord)) {
416
+ result[key] = decodeValue(val as WireValue, registry);
417
+ }
418
+ return result;
419
+ }
420
+
421
+ function decodeDeclarableShell(wire: WireDeclarableEntity): Declarable {
422
+ const obj: Record<string | symbol, unknown> = {};
423
+ for (const marker of wire.markers) {
424
+ Object.defineProperty(obj, Symbol.for(marker), { value: true, enumerable: false, configurable: true });
425
+ }
426
+ // DECLARABLE_MARKER is always one of `wire.markers` (every Declarable carries
427
+ // it), but define it defensively in case an entity somehow didn't round-trip
428
+ // it, so a decoded entity always satisfies `isDeclarable()`.
429
+ if (!(DECLARABLE_MARKER in obj)) {
430
+ Object.defineProperty(obj, DECLARABLE_MARKER, { value: true, enumerable: false });
431
+ }
432
+ Object.defineProperty(obj, "lexicon", { value: wire.lexicon, enumerable: false, configurable: true });
433
+ Object.defineProperty(obj, "entityType", { value: wire.entityType, enumerable: false, configurable: true });
434
+ if (wire.kind !== undefined) {
435
+ Object.defineProperty(obj, "kind", { value: wire.kind, enumerable: false, configurable: true });
436
+ }
437
+ return obj as unknown as Declarable;
438
+ }
439
+
440
+ /**
441
+ * Decode a JSON entity set (see {@link encodeEntitySet}) back into a live
442
+ * `Map<string, Declarable>`, functionally indistinguishable from what
443
+ * `discover()` would have produced in-process.
444
+ */
445
+ export function decodeEntitySet(wire: EntitySetWire): Map<string, Declarable> {
446
+ const registry = new Map<string, unknown>();
447
+
448
+ // Pass 1: create every declarable's shell up front, so pass 2 can resolve a
449
+ // forward (or circular, e.g. mutual DependsOn) reference to any entity by
450
+ // name regardless of declaration order — exactly like the live WeakRef graph
451
+ // discover() produces tolerates today.
452
+ for (const entry of wire.entities) {
453
+ if (entry.form === "declarable") {
454
+ registry.set(entry.name, decodeDeclarableShell(entry));
455
+ }
456
+ }
457
+
458
+ const result = new Map<string, Declarable>();
459
+ for (const entry of wire.entities) {
460
+ if (entry.form === "declarable") {
461
+ const obj = registry.get(entry.name) as Record<string | symbol, unknown>;
462
+ if (entry.props !== undefined) {
463
+ Object.defineProperty(obj, "props", { value: decodeValue(entry.props, registry), enumerable: false, configurable: true });
464
+ }
465
+ if (entry.attributes !== undefined) {
466
+ Object.defineProperty(obj, "attributes", { value: decodeValue(entry.attributes, registry), enumerable: false, configurable: true });
467
+ }
468
+ for (const [key, val] of Object.entries(entry.extra ?? {})) {
469
+ obj[key] = decodeValue(val, registry);
470
+ }
471
+ result.set(entry.name, obj as unknown as Declarable);
472
+ continue;
473
+ }
474
+
475
+ // LexiconOutput — constructed via its real constructor from the decoded
476
+ // ref, so it derives sourceLexicon/_sourceParent/sourceAttribute exactly
477
+ // the way constructing it from a live AttrRef/Intrinsic would.
478
+ const ref = decodeValue(entry.ref, registry) as AttrRef | Intrinsic;
479
+ const output = new LexiconOutput(ref, entry.outputName);
480
+ registry.set(entry.name, output);
481
+ result.set(entry.name, output as unknown as Declarable);
482
+ }
483
+
484
+ return result;
485
+ }