@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
@@ -56,6 +56,19 @@ export interface DiscoveryOptions {
56
56
  */
57
57
  intrinsics?: IntrinsicDef[];
58
58
 
59
+ /**
60
+ * chant #1063 — the lexicon NAMES loaded for this build (`["aws", "k8s"]`),
61
+ * i.e. what `resolveProjectLexicons()` returned and `loadPlugins()` then
62
+ * imported. Threaded into the fold session as the ALLOWLIST of packages a
63
+ * bare import specifier may be resolved into, so a lexicon's plain data
64
+ * exports (`Azure`/`GCP`'s pseudo-parameter namespaces, AWS's `S3Actions`,
65
+ * gitlab's `CI`) fold as identifier values instead of failing the file.
66
+ * Only meaningful when {@link fold} is set. Default: none — a caller that
67
+ * doesn't say which lexicons are active gets no bare-specifier resolution
68
+ * at all, rather than a looser fallback.
69
+ */
70
+ lexicons?: readonly string[];
71
+
59
72
  /**
60
73
  * chant #1045 Phase 2 — opt-in: whatever would otherwise reach the
61
74
  * in-process `importModule` step (every file, when {@link fold} isn't set;
@@ -156,7 +169,9 @@ export async function discover(path: string, options?: DiscoveryOptions): Promis
156
169
  // a project file imported by several others is folded exactly once, so
157
170
  // every referrer shares the identical constructed Declarable/
158
171
  // CompositeInstance objects rather than each building its own copy.
159
- const foldSession = options?.fold ? createFoldSession(options.intrinsics, buildParamValuesMap) : undefined;
172
+ const foldSession = options?.fold
173
+ ? createFoldSession(options.intrinsics, buildParamValuesMap, options.lexicons)
174
+ : undefined;
160
175
  if (options?.fold) {
161
176
  for (const file of files) {
162
177
  foldAttempts.set(file, await tryFoldFile(file, options.intrinsics, foldSession));
@@ -166,6 +181,15 @@ export async function discover(path: string, options?: DiscoveryOptions): Promis
166
181
  ? await planFoldTaint(
167
182
  files,
168
183
  new Map(files.map((file) => [file, foldAttempts.get(file)?.ok === true])),
184
+ // chant #1044 — which files' OBJECTS each successful fold captured,
185
+ // so a file forced back to run also invalidates the folds that
186
+ // already hold its instances (see planFoldTaint's doc).
187
+ new Map(
188
+ files.flatMap((file) => {
189
+ const attempt = foldAttempts.get(file);
190
+ return attempt?.ok === true ? [[file, attempt.liveSources] as const] : [];
191
+ }),
192
+ ),
169
193
  )
170
194
  : new Set<string>();
171
195
 
@@ -283,6 +283,237 @@ describe("fold — intrinsic tagged templates", () => {
283
283
  });
284
284
  });
285
285
 
286
+ /**
287
+ * chant #1044 — registered lexicon intrinsics in PLAIN-CALL form.
288
+ *
289
+ * The registry is the whole door: a name folds as a call only when an active
290
+ * lexicon registered it AND set `foldsAsCall`. Everything below that pairs a
291
+ * folding case with the near-miss that must keep rejecting.
292
+ */
293
+ describe("fold — registered call-form intrinsics (#1044)", () => {
294
+ /** Shaped like aws's real registration (lexicons/aws/src/plugin.ts). */
295
+ const REF: IntrinsicDef = { name: "Ref", isTag: false, foldsAsCall: true };
296
+ const JOIN: IntrinsicDef = { name: "Join", isTag: false, foldsAsCall: true };
297
+ /** Registered, but its lexicon never opted the call form in — the default. */
298
+ const NOT_OPTED_IN: IntrinsicDef = { name: "Reference", isTag: false };
299
+ const SUB: IntrinsicDef = { name: "Sub", isTag: true };
300
+
301
+ test("an opted-in call folds to the intrinsic node form, arguments folded in order", () => {
302
+ const consts = parseConsts(`const env = "prod"; const x = Ref(env);`);
303
+ const expr = consts.get("x");
304
+ if (!expr) throw new Error("fixture error");
305
+ expect(fold(expr, consts, [REF])).toEqual({ __intrinsic: "Ref", args: ["prod"] });
306
+ });
307
+
308
+ test("the envelope is the same family the tagged-template form produces — same __intrinsic key, positional args", () => {
309
+ const consts = parseConsts(`const x = Join("-", ["a", "b"]);`);
310
+ const expr = consts.get("x");
311
+ if (!expr) throw new Error("fixture error");
312
+ expect(fold(expr, consts, [JOIN])).toEqual({ __intrinsic: "Join", args: ["-", ["a", "b"]] });
313
+ });
314
+
315
+ test("calls nest, and a call-form intrinsic folds inside a registered tag's interpolation", () => {
316
+ const consts = parseConsts(`const env = "prod"; const x = Sub\`\${Ref(env)}-fn\`;`);
317
+ const expr = consts.get("x");
318
+ if (!expr) throw new Error("fixture error");
319
+ expect(fold(expr, consts, [SUB, REF])).toEqual({
320
+ __intrinsic: "Sub",
321
+ strings: ["", "-fn"],
322
+ values: [{ __intrinsic: "Ref", args: ["prod"] }],
323
+ });
324
+ });
325
+
326
+ test("an argument nothing can resolve stays symbolic, exactly as inside a tag", () => {
327
+ const consts = parseConsts(`const x = Ref(AWS.StackName);`);
328
+ const expr = consts.get("x");
329
+ if (!expr) throw new Error("fixture error");
330
+ expect(fold(expr, consts, [REF])).toEqual({
331
+ __intrinsic: "Ref",
332
+ args: [{ __symbol: "AWS.StackName" }],
333
+ });
334
+ });
335
+
336
+ test("an already-resolved cross-file binding wins over the symbolic path — identity must survive", () => {
337
+ const consts = parseConsts(`const x = Ref(environment);`);
338
+ const expr = consts.get("x");
339
+ if (!expr) throw new Error("fixture error");
340
+ const live = { marker: "the real, shared Parameter instance" };
341
+ const folded = fold(expr, consts, [REF], new Map([["environment", live]])) as {
342
+ __intrinsic: string;
343
+ args: unknown[];
344
+ };
345
+ expect(folded.args[0]).toBe(live);
346
+ });
347
+
348
+ test("a REGISTERED intrinsic that was never opted in is still rejected — the opt-in is the door, not registration", () => {
349
+ const consts = parseConsts(`const x = Reference("db");`);
350
+ const expr = consts.get("x");
351
+ if (!expr) throw new Error("fixture error");
352
+ let error: unknown;
353
+ try {
354
+ fold(expr, consts, [NOT_OPTED_IN]);
355
+ } catch (e) {
356
+ error = e;
357
+ }
358
+ expect(error).toBeInstanceOf(FoldError);
359
+ expect((error as FoldError).message).toContain("function call as a value is not foldable: Reference(...)");
360
+ });
361
+
362
+ test("an opted-in name is rejected when its lexicon isn't among the active ones", () => {
363
+ const consts = parseConsts(`const x = Ref("db");`);
364
+ const expr = consts.get("x");
365
+ if (!expr) throw new Error("fixture error");
366
+ expect(() => fold(expr, consts)).toThrow(FoldError);
367
+ });
368
+
369
+ test("a TAGGED-TEMPLATE intrinsic called as a plain function is rejected — the two forms are disjoint", () => {
370
+ const consts = parseConsts(`const x = Sub("literal");`);
371
+ const expr = consts.get("x");
372
+ if (!expr) throw new Error("fixture error");
373
+ expect(() => fold(expr, consts, [SUB])).toThrow(FoldError);
374
+ });
375
+
376
+ test("a call-form intrinsic used as a TAG is rejected too — opting the call in doesn't register a tag", () => {
377
+ const consts = parseConsts(`const x = Ref\`literal\`;`);
378
+ const expr = consts.get("x");
379
+ if (!expr) throw new Error("fixture error");
380
+ let error: unknown;
381
+ try {
382
+ fold(expr, consts, [REF]);
383
+ } catch (e) {
384
+ error = e;
385
+ }
386
+ expect(error).toBeInstanceOf(FoldError);
387
+ expect((error as FoldError).message).toContain("unregistered tagged template intrinsic: Ref");
388
+ });
389
+
390
+ test("a registered name reached as a METHOD (`ns.Ref(...)`) is rejected — only a bare identifier callee folds", () => {
391
+ const consts = parseConsts(`const x = aws.Ref("db");`);
392
+ const expr = consts.get("x");
393
+ if (!expr) throw new Error("fixture error");
394
+ expect(() => fold(expr, consts, [REF])).toThrow(FoldError);
395
+ });
396
+
397
+ test("an array method taking a closure is rejected — .map is arbitrary JS, not a registered intrinsic", () => {
398
+ const consts = parseConsts(`const cidrs = ["10.0.0.0/24"]; const x = cidrs.map((c) => Ref(c));`);
399
+ const expr = consts.get("x");
400
+ if (!expr) throw new Error("fixture error");
401
+ let error: unknown;
402
+ try {
403
+ fold(expr, consts, [REF]);
404
+ } catch (e) {
405
+ error = e;
406
+ }
407
+ expect(error).toBeInstanceOf(FoldError);
408
+ expect((error as FoldError).message).toContain("cidrs.map(...)");
409
+ });
410
+
411
+ test("a user-defined function is rejected however it's named", () => {
412
+ const consts = parseConsts(`const x = makeName("a", "b");`);
413
+ const expr = consts.get("x");
414
+ if (!expr) throw new Error("fixture error");
415
+ expect(() => fold(expr, consts, [REF])).toThrow(FoldError);
416
+ });
417
+
418
+ test("a registered name SHADOWED by a local const is rejected — the local binding wins, and it isn't the lexicon's", () => {
419
+ const consts = parseConsts(`const Ref = (n) => ({ mine: n }); const x = Ref("db");`);
420
+ const expr = consts.get("x");
421
+ if (!expr) throw new Error("fixture error");
422
+ expect(() => fold(expr, consts, [REF])).toThrow(FoldError);
423
+ });
424
+
425
+ test("an unfoldable argument still rejects the whole call", () => {
426
+ const consts = parseConsts(`const x = Ref(getName());`);
427
+ const expr = consts.get("x");
428
+ if (!expr) throw new Error("fixture error");
429
+ let error: unknown;
430
+ try {
431
+ fold(expr, consts, [REF]);
432
+ } catch (e) {
433
+ error = e;
434
+ }
435
+ expect(error).toBeInstanceOf(FoldError);
436
+ expect((error as FoldError).message).toContain("getName(...)");
437
+ });
438
+
439
+ test("a same-file resource passed to an intrinsic call rejects rather than folding to a look-alike envelope", () => {
440
+ const consts = parseConsts(`const bucket = new S3Bucket({ name: "b" }); const x = Ref(bucket);`);
441
+ const expr = consts.get("x");
442
+ if (!expr) throw new Error("fixture error");
443
+ expect(() => fold(expr, consts, [REF])).toThrow(FoldError);
444
+ });
445
+ });
446
+
447
+ describe("fold — registered authoring helpers (#1082)", () => {
448
+ test("a registered helper call folds to the helper-call node form, arguments folded in order", () => {
449
+ const consts = parseConsts(`const stack = "web"; const x = phase("Apply", [{ kind: "cfn-deploy", stack }]);`);
450
+ const expr = consts.get("x");
451
+ if (!expr) throw new Error("fixture error");
452
+ expect(fold(expr, consts)).toEqual({
453
+ __helper: "phase",
454
+ args: ["Apply", [{ kind: "cfn-deploy", stack: "web" }]],
455
+ });
456
+ });
457
+
458
+ test("helper calls nest, so a fan-out phase folds to nested envelopes", () => {
459
+ const consts = parseConsts(`const x = phase("Outer", [phase("Inner", []), gate("approve")]);`);
460
+ const expr = consts.get("x");
461
+ if (!expr) throw new Error("fixture error");
462
+ expect(fold(expr, consts)).toEqual({
463
+ __helper: "phase",
464
+ args: [
465
+ "Outer",
466
+ [
467
+ { __helper: "phase", args: ["Inner", []] },
468
+ { __helper: "gate", args: ["approve"] },
469
+ ],
470
+ ],
471
+ });
472
+ });
473
+
474
+ test("an unfoldable argument still rejects the whole call", () => {
475
+ const consts = parseConsts(`const x = phase("Apply", [{ stack: getName() }]);`);
476
+ const expr = consts.get("x");
477
+ if (!expr) throw new Error("fixture error");
478
+ expect(() => fold(expr, consts)).toThrow(FoldError);
479
+ });
480
+
481
+ test("an UNREGISTERED name is still rejected — the allowlist is closed, not a general call case", () => {
482
+ const consts = parseConsts(`const x = compose("Apply", []);`);
483
+ const expr = consts.get("x");
484
+ if (!expr) throw new Error("fixture error");
485
+ let error: unknown;
486
+ try {
487
+ fold(expr, consts);
488
+ } catch (e) {
489
+ error = e;
490
+ }
491
+ expect(error).toBeInstanceOf(FoldError);
492
+ expect((error as FoldError).message).toContain("function call as a value is not foldable: compose(...)");
493
+ });
494
+
495
+ test("a registered name SHADOWED by a local const is rejected — the local binding wins, and it isn't chant's", () => {
496
+ const consts = parseConsts(`const phase = (n) => ({ phase: n }); const x = phase("Apply");`);
497
+ const expr = consts.get("x");
498
+ if (!expr) throw new Error("fixture error");
499
+ expect(() => fold(expr, consts)).toThrow(FoldError);
500
+ });
501
+
502
+ test("a registered name used as a METHOD (`ns.phase(...)`) is rejected — only a bare identifier callee folds", () => {
503
+ const consts = parseConsts(`const x = helpers.phase("Apply", []);`);
504
+ const expr = consts.get("x");
505
+ if (!expr) throw new Error("fixture error");
506
+ expect(() => fold(expr, consts)).toThrow(FoldError);
507
+ });
508
+
509
+ test("a spread argument is rejected — fold has no case for one, registered callee or not", () => {
510
+ const consts = parseConsts(`const steps = []; const x = phase(...steps);`);
511
+ const expr = consts.get("x");
512
+ if (!expr) throw new Error("fixture error");
513
+ expect(() => fold(expr, consts)).toThrow(FoldError);
514
+ });
515
+ });
516
+
286
517
  describe("fold — nullish coalescing, ternary, arithmetic", () => {
287
518
  test("?? falls through only on null/undefined", () => {
288
519
  expect(foldConst(`const x = null ?? "fallback";`, "x")).toBe("fallback");
@@ -428,6 +659,52 @@ describe("fold — rejections", () => {
428
659
  });
429
660
  });
430
661
 
662
+ describe("foldResource — constructor argument positions (#1082)", () => {
663
+ test("a props object that isn't the FIRST argument still folds, reported as a positional arg list", () => {
664
+ // AWS's deploy-time Parameter is `(type, props)` — lexicons/aws/src/parameter.ts.
665
+ const src = `export const p = new Parameter("String", { description: "vpc id", defaultValue: "" });`;
666
+ const result = foldModule(src);
667
+ expect(result.p).toEqual({
668
+ ok: true,
669
+ spec: {
670
+ __resource: "Parameter",
671
+ props: { description: "vpc id", defaultValue: "" },
672
+ args: ["String", { description: "vpc id", defaultValue: "" }],
673
+ },
674
+ });
675
+ });
676
+
677
+ test("a constructor with no object-literal argument at all folds to its bare argument list", () => {
678
+ const src = `export const p = new Parameter("String");`;
679
+ const result = foldModule(src);
680
+ expect(result.p).toEqual({ ok: true, spec: { __resource: "Parameter", props: {}, args: ["String"] } });
681
+ });
682
+
683
+ test("the classic (props) shape is unchanged — no `args` list", () => {
684
+ const src = `export const b = new S3Bucket({ name: "b" });`;
685
+ const result = foldModule(src);
686
+ expect(result.b).toEqual({ ok: true, spec: { __resource: "S3Bucket", props: { name: "b" } } });
687
+ });
688
+
689
+ test("the classic (props, attributes) shape is unchanged — no `args` list", () => {
690
+ const src = `export const b = new S3Bucket({ name: "b" }, { DependsOn: "other" });`;
691
+ const result = foldModule(src);
692
+ expect(result.b).toEqual({
693
+ ok: true,
694
+ spec: { __resource: "S3Bucket", props: { name: "b" }, attributes: { DependsOn: "other" } },
695
+ });
696
+ });
697
+
698
+ test("a non-object-literal argument is folded, not rejected for its position — but must still be foldable", () => {
699
+ const src = `export const p = new Parameter(getType(), { description: "x" });`;
700
+ const result = foldModule(src);
701
+ expect(result.p?.ok).toBe(false);
702
+ if (result.p && !result.p.ok) {
703
+ expect(result.p.error).toContain("getType(...)");
704
+ }
705
+ });
706
+ });
707
+
431
708
  describe("foldModule", () => {
432
709
  test("folds an exported const resource to { ok: true, spec }", () => {
433
710
  const src = `