@intentius/chant 0.20.0 → 0.22.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 (55) hide show
  1. package/dist/build.d.ts +7 -0
  2. package/dist/build.d.ts.map +1 -1
  3. package/dist/cli/commands/build.d.ts.map +1 -1
  4. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  5. package/dist/cli/commands/check-lexicon-intrinsics.d.ts +17 -0
  6. package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -1
  7. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  8. package/dist/codegen/docs-types.d.ts +2 -0
  9. package/dist/codegen/docs-types.d.ts.map +1 -1
  10. package/dist/declarable.d.ts +16 -0
  11. package/dist/declarable.d.ts.map +1 -1
  12. package/dist/discovery/entity-wire-codec.d.ts.map +1 -1
  13. package/dist/discovery/fold-import.d.ts +64 -3
  14. package/dist/discovery/fold-import.d.ts.map +1 -1
  15. package/dist/discovery/index.d.ts +12 -0
  16. package/dist/discovery/index.d.ts.map +1 -1
  17. package/dist/fold/fold.d.ts +89 -16
  18. package/dist/fold/fold.d.ts.map +1 -1
  19. package/dist/fold/foldable-helpers.d.ts +121 -0
  20. package/dist/fold/foldable-helpers.d.ts.map +1 -0
  21. package/dist/fold/subset.d.ts +58 -3
  22. package/dist/fold/subset.d.ts.map +1 -1
  23. package/dist/lexicon-schema.d.ts +2 -0
  24. package/dist/lexicon-schema.d.ts.map +1 -1
  25. package/dist/lexicon.d.ts +73 -23
  26. package/dist/lexicon.d.ts.map +1 -1
  27. package/dist/runtime.d.ts +10 -1
  28. package/dist/runtime.d.ts.map +1 -1
  29. package/dist/serializer-walker.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/src/build.ts +9 -0
  32. package/src/cli/commands/build.ts +9 -0
  33. package/src/cli/commands/check-lexicon-examples.ts +16 -1
  34. package/src/cli/commands/check-lexicon-intrinsics.test.ts +35 -1
  35. package/src/cli/commands/check-lexicon-intrinsics.ts +38 -2
  36. package/src/cli/commands/check-lexicon.ts +18 -0
  37. package/src/codegen/docs-sections.test.ts +7 -1
  38. package/src/codegen/docs-sections.ts +1 -1
  39. package/src/codegen/docs-types.ts +2 -0
  40. package/src/declarable.ts +20 -0
  41. package/src/discovery/entity-wire-codec.ts +9 -7
  42. package/src/discovery/fold-import.test.ts +900 -1
  43. package/src/discovery/fold-import.ts +441 -47
  44. package/src/discovery/index.ts +25 -1
  45. package/src/fold/fold.test.ts +277 -0
  46. package/src/fold/fold.ts +219 -58
  47. package/src/fold/foldable-helpers.ts +171 -0
  48. package/src/fold/subset-doc-parity.test.ts +27 -0
  49. package/src/fold/subset.test.ts +177 -0
  50. package/src/fold/subset.ts +139 -31
  51. package/src/lexicon-schema.test.ts +43 -0
  52. package/src/lexicon-schema.ts +5 -0
  53. package/src/lexicon.ts +74 -24
  54. package/src/runtime.ts +11 -2
  55. package/src/serializer-walker.ts +14 -0
package/src/lexicon.ts CHANGED
@@ -153,39 +153,89 @@ export interface IntrinsicDef {
153
153
  * `../cli/commands/check-lexicon-intrinsics.ts`.
154
154
  */
155
155
  readonly isTag: boolean;
156
+ /**
157
+ * chant #1044 — opt this intrinsic's PLAIN-CALL form into folding
158
+ * (`Ref(bucket)`, `Concat(a, b)` reduce to their intrinsic node instead of
159
+ * falling the whole file back to the run path).
160
+ *
161
+ * Optional, and OFF unless a lexicon writes `true`. That default is the
162
+ * point: `fold()` has no general `CallExpression` case by construction
163
+ * (epic #1019), and this field is the only thing that admits one. It is a
164
+ * closed, lexicon-declared allowlist, decided one intrinsic at a time —
165
+ * never inferred from `isTag`, from the name, or from the call's shape. An
166
+ * intrinsic with no `foldsAsCall` behaves exactly as it did before #1044.
167
+ *
168
+ * Only set it when calling the intrinsic is a pure function of its
169
+ * arguments that builds a deterministic data envelope — the whole
170
+ * correctness argument is that invoking it while folding is
171
+ * indistinguishable from invoking it during a real run of the file. An
172
+ * intrinsic that reads the environment, mutates state, or depends on
173
+ * anything but its arguments does not qualify, and neither does a tagged
174
+ * template (see {@link isTag}: the two forms are mutually exclusive, and
175
+ * `chant dev check-lexicon` rejects `isTag: true` + `foldsAsCall: true`).
176
+ *
177
+ * Registration is by name. It is not permission to invoke whatever that
178
+ * name happens to be bound to: `fold()` reduces the call to a symbolic
179
+ * envelope executing nothing, and `../discovery/fold-import.ts` resolves
180
+ * the name through the folding FILE'S OWN imports before invoking the real
181
+ * function — so the function that runs while folding is the same one the
182
+ * run path would have called, from the module the source itself named.
183
+ */
184
+ readonly foldsAsCall?: boolean;
156
185
  }
157
186
 
158
187
  /**
159
- * Whether `chant build --fold` can ever fold a call to this intrinsic
160
- * (chant #1062, epic #1019). Today the answer is a direct function of
161
- * `isTag`: `fold()` (../fold/fold.ts) has no `CallExpression` case at all —
162
- * a plain-call intrinsic (`Ref(...)`, `Concat(...)`, `reference(...)`, …) is
163
- * always a run-fallback, no matter what it's named or registered as — while
164
- * a *registered* tagged-template intrinsic (`Sub\`...\``) folds because
165
- * `foldTaggedTemplate` recognizes its tag and recurses into the interior.
188
+ * Whether `chant build --fold` can ever fold a use of this intrinsic
189
+ * (chant #1062, epic #1019) in EITHER authored form.
190
+ *
191
+ * Two disjoint ways to qualify, one per form:
192
+ *
193
+ * - a registered tagged-template intrinsic (`Sub\`...\``) folds because
194
+ * `foldTaggedTemplate` recognizes its tag and recurses into the interior
195
+ * ({@link intrinsicTagFolds});
196
+ * - a registered plain-call intrinsic (`Ref(...)`, `Concat(...)`) folds
197
+ * only when its lexicon opted it in with `foldsAsCall`
198
+ * ({@link intrinsicCallFolds}, chant #1044). Before #1044 no plain call
199
+ * folded at all, whatever it was named or registered as.
166
200
  *
167
- * This function is the single predicate both `fold()` (deciding whether a
168
- * tag is registered for real) and the generated per-lexicon intrinsics page
169
- * (`../codegen/docs-sections.ts`'s "Folds?" column) call never two copies
170
- * of the same `isTag === true` check that could silently drift. #1044 (per-
171
- * intrinsic, per-lexicon foldability) changes this function's body to
172
- * consult something more than `isTag` for a plain call that becomes
173
- * foldable; every caller keeps working unchanged, and the generated matrix
174
- * updates the moment a lexicon's registration says a given intrinsic now
175
- * folds — no doc rewrite, no second code path to remember.
201
+ * This function is the single predicate the generated per-lexicon intrinsics
202
+ * page (`../codegen/docs-sections.ts`'s "Folds?" column) calls never a
203
+ * restated copy that could silently drift from the code. `fold()` itself
204
+ * calls the two form-specific predicates below rather than this one, because
205
+ * it always knows which form it is looking at, and a tag must not fold as a
206
+ * call (or vice versa) merely because the other form was opted in.
176
207
  *
177
- * Takes `{ isTag?: boolean }` rather than `Pick<IntrinsicDef, "isTag">`
178
- * deliberately: `IntrinsicDef.isTag` is required for new registrations
179
- * (chant #1067), but this predicate also reads `isTag` off untrusted,
180
- * possibly-older parsed JSON (`ManifestJSON` in `./codegen/docs-types.ts`,
181
- * on disk as a published lexicon's `dist/manifest.json`) that may predate
182
- * the required field. `undefined` there means the same thing it always
183
- * did — not a tag so the check below is intentionally unchanged.
208
+ * Takes a structural `{ isTag?, foldsAsCall? }` rather than
209
+ * `Pick<IntrinsicDef, ...>` deliberately: `IntrinsicDef.isTag` is required
210
+ * for new registrations (chant #1067), but these predicates also read off
211
+ * untrusted, possibly-older parsed JSON (`ManifestJSON` in
212
+ * `./codegen/docs-types.ts`, on disk as a published lexicon's
213
+ * `dist/manifest.json`) that may predate either field. `undefined` there
214
+ * means what it always did — not a tag, not opted in.
184
215
  */
185
- export function intrinsicFolds(def: { isTag?: boolean }): boolean {
216
+ export function intrinsicFolds(def: { isTag?: boolean; foldsAsCall?: boolean }): boolean {
217
+ return intrinsicTagFolds(def) || intrinsicCallFolds(def);
218
+ }
219
+
220
+ /** True when this intrinsic's TAGGED-TEMPLATE form folds (`Sub\`...\``) — see {@link intrinsicFolds}. */
221
+ export function intrinsicTagFolds(def: { isTag?: boolean }): boolean {
186
222
  return def.isTag === true;
187
223
  }
188
224
 
225
+ /**
226
+ * True when this intrinsic's PLAIN-CALL form folds (`Ref(...)`) — i.e. the
227
+ * lexicon opted it in via {@link IntrinsicDef.foldsAsCall} (chant #1044).
228
+ *
229
+ * `isTag: true` disqualifies regardless: a tagged template is invoked as
230
+ * `` Name`...` ``, so a call to it isn't the registered authoring form at
231
+ * all. Keeping that here rather than trusting registrations means a lexicon
232
+ * that declares both flags cannot quietly widen `fold()`'s call case — and
233
+ * `chant dev check-lexicon` fails the registration outright.
234
+ */
235
+ export function intrinsicCallFolds(def: { isTag?: boolean; foldsAsCall?: boolean }): boolean {
236
+ return def.isTag !== true && def.foldsAsCall === true;
237
+ }
238
+
189
239
  /**
190
240
  * Options passed to a MigrationSource by `chant migrate`.
191
241
  */
package/src/runtime.ts CHANGED
@@ -54,18 +54,27 @@ export function createResource(
54
54
 
55
55
  /**
56
56
  * Create a property-kind class for a given property type.
57
+ *
58
+ * Instances carry DECLARABLE_MARKER/lexicon/entityType/kind at runtime (set
59
+ * via defineProperty below), so they already satisfy `Declarable` — the
60
+ * return type just needs to say so. Before this, the signature was
61
+ * `Record<string, unknown>` with no `Declarable`, so any composite that
62
+ * returned a property-kind instance as a top-level member only type-checked
63
+ * by accident: either the caller went through an untyped `require()` (losing
64
+ * the type entirely) or never assigned the instance where its `Declarable`-ness
65
+ * was checked statically. See chant #1068.
57
66
  */
58
67
  export function createProperty(
59
68
  type: string,
60
69
  lexicon: string,
61
- ): new (props: Record<string, unknown>) => Record<string, unknown> {
70
+ ): new (props: Record<string, unknown>) => Declarable & Record<string, unknown> {
62
71
  const PropertyClass = function (this: Record<string, unknown>, props: Record<string, unknown>) {
63
72
  Object.defineProperty(this, DECLARABLE_MARKER, { value: true, enumerable: false });
64
73
  Object.defineProperty(this, "lexicon", { value: lexicon, enumerable: false });
65
74
  Object.defineProperty(this, "entityType", { value: type, enumerable: false });
66
75
  Object.defineProperty(this, "kind", { value: "property", enumerable: false });
67
76
  Object.defineProperty(this, "props", { value: props ?? {}, enumerable: false, configurable: true });
68
- } as unknown as new (props: Record<string, unknown>) => Record<string, unknown>;
77
+ } as unknown as new (props: Record<string, unknown>) => Declarable & Record<string, unknown>;
69
78
 
70
79
  Object.defineProperty(PropertyClass, "name", { value: type.split(".").pop() ?? type });
71
80
 
@@ -61,6 +61,20 @@ export function walkValue(
61
61
  if (name) {
62
62
  return visitor.resourceRef(name);
63
63
  }
64
+ // A resource-kind Declarable constructed inline rather than exported as
65
+ // its own top-level entity (e.g. K8s `new PersistentVolumeClaim({...})`
66
+ // embedded directly in a StatefulSet's `volumeClaimTemplates`) has no
67
+ // logical name to Ref — it was never a key in `entities`, so
68
+ // resolveAttrRefs() never assigns one. Falling through to the generic
69
+ // "object" branch below would walk the Declarable's own enumerable
70
+ // properties, which for a Declarable are its self-referencing attribute
71
+ // accessors (AttrRef instances whose parent is the Declarable itself,
72
+ // still unresolved) — never its authored `.props`. That either threw
73
+ // "logical name not set" (when the resource type declares any
74
+ // attributes) or silently serialized as `{}` (when it doesn't), instead
75
+ // of the embedded spec the caller wrote. Embed its own props inline
76
+ // instead, exactly like a property-kind Declarable already does.
77
+ return visitor.propertyDeclarable(decl, (v) => walkValue(v, entityNames, visitor));
64
78
  }
65
79
 
66
80
  // Handle arrays