@intentius/chant 0.108.1 → 0.110.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 (46) hide show
  1. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  2. package/dist/composite.d.ts +5 -0
  3. package/dist/composite.d.ts.map +1 -1
  4. package/dist/discovery/collect.d.ts.map +1 -1
  5. package/dist/discovery/fold-import.d.ts +15 -0
  6. package/dist/discovery/fold-import.d.ts.map +1 -1
  7. package/dist/discovery/param-deps.d.ts +59 -4
  8. package/dist/discovery/param-deps.d.ts.map +1 -1
  9. package/dist/fold/subset.d.ts +17 -2
  10. package/dist/fold/subset.d.ts.map +1 -1
  11. package/dist/fold-provenance.d.ts +49 -4
  12. package/dist/fold-provenance.d.ts.map +1 -1
  13. package/dist/import/ir-utils.d.ts.map +1 -1
  14. package/dist/import/parser.d.ts +8 -0
  15. package/dist/import/parser.d.ts.map +1 -1
  16. package/dist/provenance.d.ts +49 -0
  17. package/dist/provenance.d.ts.map +1 -1
  18. package/dist/serializer-walker.d.ts.map +1 -1
  19. package/dist/stack-output.d.ts +5 -0
  20. package/dist/stack-output.d.ts.map +1 -1
  21. package/dist/yaml.d.ts.map +1 -1
  22. package/package.json +1 -1
  23. package/src/cli/commands/import.test.ts +5 -4
  24. package/src/cli/handlers/lifecycle.test.ts +106 -1
  25. package/src/cli/handlers/lifecycle.ts +26 -5
  26. package/src/composite.ts +37 -0
  27. package/src/discovery/collect.ts +13 -3
  28. package/src/discovery/fold-composite.test.ts +22 -0
  29. package/src/discovery/fold-host-composite.test.ts +23 -0
  30. package/src/discovery/fold-import.ts +324 -30
  31. package/src/discovery/fold-package-composite.test.ts +14 -1
  32. package/src/discovery/fold-project-provenance.test.ts +584 -0
  33. package/src/discovery/param-deps.test.ts +163 -3
  34. package/src/discovery/param-deps.ts +173 -4
  35. package/src/fold/subset-public-export.test.ts +38 -0
  36. package/src/fold/subset.ts +17 -2
  37. package/src/fold-provenance.test.ts +190 -16
  38. package/src/fold-provenance.ts +158 -13
  39. package/src/import/ir-utils.ts +6 -1
  40. package/src/import/parser.ts +8 -0
  41. package/src/provenance.ts +59 -2
  42. package/src/serializer-walker.test.ts +10 -0
  43. package/src/serializer-walker.ts +17 -1
  44. package/src/stack-output.ts +6 -1
  45. package/src/yaml.test.ts +6 -0
  46. package/src/yaml.ts +4 -0
package/src/provenance.ts CHANGED
@@ -32,6 +32,13 @@ export interface EntityProvenance {
32
32
  * collapse a composite to a single node at coarse diagram detail levels (#494).
33
33
  */
34
34
  compositeInstance?: string;
35
+ /**
36
+ * Where the composite call that expanded this entity was written (chant
37
+ * #3597): the `WebApp({...})` expression, not the factory body. Recorded
38
+ * only when the fold interpreted the factory at a call it could see; the
39
+ * run path and a nested call made from already-folded values leave it unset.
40
+ */
41
+ compositeCall?: SourceLocation;
35
42
  /**
36
43
  * Per-path origins within the entity's `props` (chant #1443). Read with
37
44
  * {@link originOfPath}, which does the prefix resolution these keys are
@@ -80,6 +87,12 @@ export interface EntityProvenance {
80
87
  * expression for this path reads no parameter at all. The composite fixes
81
88
  * this field, and no argument at the call site moves it.
82
89
  *
90
+ * - `host` — the authored expression for this path reads a value host code
91
+ * produced (INTENTIUS/typescript-as-data#248): a method called on a real
92
+ * receiver or an eager lexicon helper (`host-call`), or a value a lexicon
93
+ * package exports (`host-value`). The author wrote the call, not the value,
94
+ * so ./fold-provenance.ts answers `unknown` rather than `direct`.
95
+ *
83
96
  * The coarse `composite` and the two fine kinds are not alternatives. A build
84
97
  * records `composite` when it expanded a composite without interpreting its
85
98
  * body (the run path, an inadmissible factory), and that is why
@@ -90,8 +103,49 @@ export type PathOrigin =
90
103
  | { kind: "authored" }
91
104
  | { kind: "composite"; composite: string; instance: string }
92
105
  | { kind: "build-param"; params: string[] }
93
- | { kind: "composite-parameter"; composite: string; parameters: string[] }
94
- | { kind: "composite-literal"; composite: string };
106
+ | {
107
+ kind: "composite-parameter";
108
+ composite: string;
109
+ parameters: string[];
110
+ /**
111
+ * Where each parameter's argument was written at the composite call
112
+ * (chant #3597), in `parameters` order. Absent when the build did not
113
+ * see the call; see {@link ArgumentLocation} for the fallback.
114
+ */
115
+ arguments?: ArgumentLocation[];
116
+ }
117
+ | { kind: "composite-literal"; composite: string }
118
+ | { kind: "host"; reason: HostOriginReason };
119
+
120
+ /** Which host code produced a value: a call it ran, or a value its package exports. */
121
+ export type HostOriginReason = "host-call" | "host-value";
122
+
123
+ /**
124
+ * A place in a source file. `file` is absolute, as
125
+ * {@link EntityProvenance.sourceFile} is; `line` and `column` are 1-based, the
126
+ * way an editor and `file:line:column` print them.
127
+ */
128
+ export interface SourceLocation {
129
+ file: string;
130
+ line: number;
131
+ column: number;
132
+ }
133
+
134
+ /**
135
+ * Where one composite parameter's argument was written at the call (chant
136
+ * #3597). When the call's argument object has a property at the parameter's
137
+ * path, the location is that property and `text` is the property as written
138
+ * (`replicas: 3`). When it does not (the parameter was left out, or arrived
139
+ * through a spread or a non-literal argument), the location is the call's own
140
+ * and `text` is absent: the call is the nearest place that governs it, and
141
+ * nothing claims a property that is not there.
142
+ */
143
+ export interface ArgumentLocation extends SourceLocation {
144
+ /** The factory parameter path, as in `parameters` (`replicas`, `iam.path`). */
145
+ parameter: string;
146
+ /** The innermost property as written at the call, when one is. */
147
+ text?: string;
148
+ }
95
149
 
96
150
  /** True when `prefix` addresses `path` or an ancestor of it, on a segment boundary. */
97
151
  function isPathPrefix(prefix: string, path: string): boolean {
@@ -114,6 +168,7 @@ export function setProvenance(entity: object, prov: EntityProvenance): void {
114
168
  existing.sourceFile ??= prov.sourceFile;
115
169
  existing.composite ??= prov.composite;
116
170
  existing.compositeInstance ??= prov.compositeInstance;
171
+ existing.compositeCall ??= prov.compositeCall;
117
172
  if (prov.paths) {
118
173
  existing.paths ??= {};
119
174
  for (const [path, origin] of Object.entries(prov.paths)) {
@@ -225,6 +280,8 @@ export function describePathOrigin(origin: PathOrigin): string {
225
280
  return `composite ${origin.composite} parameter ${origin.parameters.join(", ")}`;
226
281
  case "composite-literal":
227
282
  return `composite ${origin.composite} literal`;
283
+ case "host":
284
+ return origin.reason;
228
285
  }
229
286
  }
230
287
 
@@ -118,6 +118,16 @@ describe("walkValue", () => {
118
118
  });
119
119
  });
120
120
 
121
+ test("an __attrRef envelope naming a property reads the attribute that property's AttrRef carries", () => {
122
+ // A fold writes `db.Endpoint_Address` as the property name; the attribute
123
+ // is `Endpoint.Address`.
124
+ const TestDb = createResource("Test::Db", "test", { Endpoint_Address: "Endpoint.Address" });
125
+ const db = new TestDb({}) as unknown as Declarable;
126
+ const names = new Map<Declarable, string>([[db, "MyDB"]]);
127
+ const value = { __attrRef: { entity: "MyDB", attribute: "Endpoint_Address" } };
128
+ expect(walkValue(value, names, mockVisitor)).toEqual({ __getAtt: ["MyDB", "Endpoint.Address"] });
129
+ });
130
+
121
131
  test("resolves standalone __attrRef envelope in plain object", () => {
122
132
  const names = new Map<Declarable, string>();
123
133
  const value = { __attrRef: { entity: "MyBucket", attribute: "DomainName" } };
@@ -21,6 +21,22 @@ export interface SerializerVisitor {
21
21
  propertyDeclarable(entity: Declarable, walk: (v: unknown) => unknown): unknown;
22
22
  }
23
23
 
24
+ /**
25
+ * The attribute an `{__attrRef}` envelope names. A fold writes the envelope
26
+ * with the property it read (`db.Endpoint_Address`), and the property's own
27
+ * AttrRef carries the attribute (`Endpoint.Address`); they differ when the
28
+ * attribute has a dot. The envelope's name is kept when the entity or
29
+ * property is not found.
30
+ */
31
+ function envelopeAttribute(entityName: string, property: string, entityNames: Map<Declarable, string>): string {
32
+ for (const [entity, name] of entityNames) {
33
+ if (name !== entityName) continue;
34
+ const held = (entity as unknown as Record<string, unknown>)[property];
35
+ return isAttrRefLike(held) ? held.attribute : property;
36
+ }
37
+ return property;
38
+ }
39
+
24
40
  /**
25
41
  * Recursively walk a value, converting AttrRefs, Intrinsics, Declarables,
26
42
  * arrays, and objects using the provided visitor.
@@ -100,7 +116,7 @@ export function walkValue(
100
116
  // Handle serialized AttrRef envelopes (produced by AttrRef.toJSON() inside intrinsics)
101
117
  if (typeof value === "object" && "__attrRef" in value) {
102
118
  const ref = (value as { __attrRef: { entity: string; attribute: string } }).__attrRef;
103
- return visitor.attrRef(ref.entity, ref.attribute);
119
+ return visitor.attrRef(ref.entity, envelopeAttribute(ref.entity, ref.attribute, entityNames));
104
120
  }
105
121
 
106
122
  // Handle objects
@@ -37,6 +37,10 @@ export interface StackOutput extends Declarable {
37
37
  * `Output.Condition`) — a condition name, or the condition Declarable
38
38
  * itself, resolved to its logical name at serialization (#2068/#2069). */
39
39
  readonly condition?: string | Declarable;
40
+ /** The output's key in the template, when it is not the export name. A
41
+ * template may use one key for an output and a parameter or resource; a
42
+ * module cannot export both under that name (#3604). */
43
+ readonly name?: string;
40
44
  }
41
45
 
42
46
  /** What a wrapping intrinsic's lexicon gets derived from: either a nested
@@ -112,7 +116,7 @@ export function isStackOutput(value: unknown): value is StackOutput {
112
116
  */
113
117
  export function stackOutput(
114
118
  ref: AttrRef | Intrinsic | string,
115
- options?: { description?: string; exportName?: string; lexicon?: string; condition?: string | Declarable },
119
+ options?: { description?: string; exportName?: string; lexicon?: string; condition?: string | Declarable; name?: string },
116
120
  ): StackOutput {
117
121
  // Duck-type, not `instanceof` (chant #1137): AttrRef also implements
118
122
  // Intrinsic (a global-symbol marker), so `isIntrinsic(ref)` alone already
@@ -152,6 +156,7 @@ export function stackOutput(
152
156
  description: options?.description,
153
157
  exportName: options?.exportName,
154
158
  condition: options?.condition,
159
+ ...(options?.name !== undefined ? { name: options.name } : {}),
155
160
  };
156
161
 
157
162
  return output;
package/src/yaml.test.ts CHANGED
@@ -26,6 +26,12 @@ describe("emitYAML", () => {
26
26
  expect(emitYAML([undefined, 1], 0)).toBe("\n- null\n- 1");
27
27
  });
28
28
 
29
+ test("an empty mapping in a list is an item, not nothing", () => {
30
+ expect(emitYAML([{}], 0)).toBe("\n- {}");
31
+ expect(emitYAML({ egress: [{}] }, 0)).toBe("\negress:\n - {}");
32
+ expect(emitYAML([{ a: undefined }, { b: 1 }], 0)).toBe("\n- {}\n- b: 1");
33
+ });
34
+
29
35
  test("booleans", () => {
30
36
  expect(emitYAML(true, 0)).toBe("true");
31
37
  expect(emitYAML(false, 0)).toBe("false");
package/src/yaml.ts CHANGED
@@ -127,6 +127,10 @@ export function emitYAML(value: unknown, indent: number): string {
127
127
  lines.push(`${prefix} ${key}: ${emitted}`);
128
128
  }
129
129
  }
130
+ } else {
131
+ // An empty mapping is still an item: `egress: [{}]` allows all
132
+ // egress, and dropping it leaves an empty list.
133
+ lines.push(`${prefix}- {}`);
130
134
  }
131
135
  } else {
132
136
  lines.push(`${prefix}- ${emitYAML(item, indent + 1).trimStart()}`);