@intentius/chant-lexicon-aws 0.58.0 → 0.60.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.
@@ -0,0 +1,298 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import {
3
+ applyEnumOverlay,
4
+ assertOverlayCoverage,
5
+ enumOverlayByType,
6
+ enumOverlayEntries,
7
+ redundantOverlayWarnings,
8
+ type EnumOverlayEntry,
9
+ } from "./enum-overlay";
10
+ import { generate } from "./generate";
11
+
12
+ const entry = (over: Partial<EnumOverlayEntry> = {}): EnumOverlayEntry => ({
13
+ type: "AWS::Test::Resource",
14
+ pointer: "/properties/Mode",
15
+ enumName: "Mode",
16
+ note: "test",
17
+ source: { kind: "docs", url: "https://example.invalid" },
18
+ reviewed: "2026-09-06",
19
+ values: ["fast", "slow"],
20
+ ...over,
21
+ });
22
+
23
+ const schema = (extra: Record<string, unknown> = {}) =>
24
+ JSON.stringify({
25
+ typeName: "AWS::Test::Resource",
26
+ properties: { Mode: { type: "string" }, Name: { type: "string" } },
27
+ additionalProperties: false,
28
+ ...extra,
29
+ });
30
+
31
+ describe("applyEnumOverlay", () => {
32
+ test("narrows a bare string property to a named enum definition", () => {
33
+ const { data, applications } = applyEnumOverlay("AWS::Test::Resource", schema(), [entry()], {
34
+ strict: true,
35
+ });
36
+
37
+ expect(applications).toEqual([{ entry: entry(), outcome: "applied" }]);
38
+ const doc = JSON.parse(data as string);
39
+ expect(doc.definitions.Mode).toEqual({ type: "string", enum: ["fast", "slow"] });
40
+ expect(doc.properties.Mode.$ref).toBe("#/definitions/Mode");
41
+ // The enum stays on the property too, because that is what the lexicon
42
+ // registry's propertyConstraints are read from.
43
+ expect(doc.properties.Mode.enum).toEqual(["fast", "slow"]);
44
+ expect(doc.properties.Name).toEqual({ type: "string" });
45
+ });
46
+
47
+ test("reaches a property inside a definition", () => {
48
+ const nested = JSON.stringify({
49
+ typeName: "AWS::Test::Resource",
50
+ properties: { Disk: { $ref: "#/definitions/Ebs" } },
51
+ definitions: { Ebs: { type: "object", properties: { VolumeType: { type: "string" } } } },
52
+ });
53
+
54
+ const { data } = applyEnumOverlay(
55
+ "AWS::Test::Resource",
56
+ nested,
57
+ [entry({ pointer: "/definitions/Ebs/properties/VolumeType", enumName: "VolumeType", values: ["gp2", "gp3"] })],
58
+ { strict: true },
59
+ );
60
+
61
+ const doc = JSON.parse(data as string);
62
+ expect(doc.definitions.VolumeType).toEqual({ type: "string", enum: ["gp2", "gp3"] });
63
+ expect(doc.definitions.Ebs.properties.VolumeType.$ref).toBe("#/definitions/VolumeType");
64
+ });
65
+
66
+ test("the spec wins where it already declares allowed values", () => {
67
+ const withEnum = schema({
68
+ properties: { Mode: { type: "string", enum: ["fast", "slow", "medium"] } },
69
+ });
70
+
71
+ const { data, applications } = applyEnumOverlay("AWS::Test::Resource", withEnum, [entry()], {
72
+ strict: true,
73
+ });
74
+
75
+ expect(applications[0].outcome).toBe("redundant");
76
+ expect(applications[0].upstreamValues).toEqual(["fast", "slow", "medium"]);
77
+ // Untouched: no definition minted, no $ref, upstream values intact.
78
+ expect(data).toBe(withEnum);
79
+ const doc = JSON.parse(data as string);
80
+ expect(doc.definitions).toBeUndefined();
81
+ expect(doc.properties.Mode.$ref).toBeUndefined();
82
+ expect(doc.properties.Mode.enum).toEqual(["fast", "slow", "medium"]);
83
+ });
84
+
85
+ test("a redundant entry produces a warning naming the entry", () => {
86
+ const withEnum = schema({ properties: { Mode: { type: "string", enum: ["fast"] } } });
87
+ const { applications } = applyEnumOverlay("AWS::Test::Resource", withEnum, [entry()], {
88
+ strict: true,
89
+ });
90
+
91
+ const warnings = redundantOverlayWarnings(applications);
92
+ expect(warnings).toHaveLength(1);
93
+ expect(warnings[0].file).toBe("AWS::Test::Resource/properties/Mode");
94
+ expect(warnings[0].error).toContain("redundant");
95
+ });
96
+
97
+ test("a strict run fails loudly on a property the schema does not declare", () => {
98
+ expect(() =>
99
+ applyEnumOverlay("AWS::Test::Resource", schema(), [entry({ pointer: "/properties/Gone" })], {
100
+ strict: true,
101
+ }),
102
+ ).toThrow(/AWS::Test::Resource\/properties\/Gone names a property the schema does not declare/);
103
+ });
104
+
105
+ test("a fixture run records the same case as absent instead of failing", () => {
106
+ const { applications } = applyEnumOverlay(
107
+ "AWS::Test::Resource",
108
+ schema(),
109
+ [entry({ pointer: "/properties/Gone" })],
110
+ { strict: false },
111
+ );
112
+ expect(applications[0].outcome).toBe("absent");
113
+ });
114
+
115
+ test("fails when the enum name would shadow an existing definition", () => {
116
+ const withDef = schema({ definitions: { Mode: { type: "object", properties: {} } } });
117
+ expect(() =>
118
+ applyEnumOverlay("AWS::Test::Resource", withDef, [entry()], { strict: true }),
119
+ ).toThrow(/already declares/);
120
+ });
121
+
122
+ test("fails when the target property already refers to a definition", () => {
123
+ const withRef = schema({
124
+ properties: { Mode: { $ref: "#/definitions/Other" } },
125
+ definitions: { Other: { type: "string" } },
126
+ });
127
+ expect(() =>
128
+ applyEnumOverlay("AWS::Test::Resource", withRef, [entry()], { strict: true }),
129
+ ).toThrow(/already refers to/);
130
+ });
131
+
132
+ test("leaves a schema with no entries untouched", () => {
133
+ const original = schema();
134
+ const { data, applications } = applyEnumOverlay("AWS::Other::Thing", original, []);
135
+ expect(data).toBe(original);
136
+ expect(applications).toEqual([]);
137
+ });
138
+ });
139
+
140
+ describe("assertOverlayCoverage", () => {
141
+ test("a strict run fails on an entry whose type never appeared", () => {
142
+ expect(() => assertOverlayCoverage([entry()], new Set(["AWS::S3::Bucket"]), true)).toThrow(
143
+ /AWS::Test::Resource/,
144
+ );
145
+ });
146
+
147
+ test("a fixture run tolerates it", () => {
148
+ expect(() => assertOverlayCoverage([entry()], new Set(["AWS::S3::Bucket"]), false)).not.toThrow();
149
+ });
150
+
151
+ test("passes when every type was seen", () => {
152
+ expect(() =>
153
+ assertOverlayCoverage([entry()], new Set(["AWS::Test::Resource"]), true),
154
+ ).not.toThrow();
155
+ });
156
+ });
157
+
158
+ describe("the shipped overlay", () => {
159
+ const entries = enumOverlayEntries();
160
+
161
+ test("is non-empty and grouped by type without loss", () => {
162
+ expect(entries.length).toBeGreaterThan(0);
163
+ const grouped = [...enumOverlayByType(entries).values()].reduce((n, l) => n + l.length, 0);
164
+ expect(grouped).toBe(entries.length);
165
+ });
166
+
167
+ test("addresses each property at most once", () => {
168
+ const keys = entries.map((e) => `${e.type}${e.pointer}`);
169
+ expect(new Set(keys).size).toBe(keys.length);
170
+ });
171
+
172
+ test("every entry is well formed", () => {
173
+ for (const e of entries) {
174
+ expect(e.type, `${e.type} is a CloudFormation type name`).toMatch(/^[A-Za-z0-9]+(::[A-Za-z0-9]+){2}$/);
175
+ expect(e.pointer, `${e.type}${e.pointer} is a properties pointer`).toMatch(
176
+ /^\/(properties\/[A-Za-z0-9]+|definitions\/[A-Za-z0-9]+\/properties\/[A-Za-z0-9]+)$/,
177
+ );
178
+ expect(e.enumName, `${e.type}${e.pointer} names its definition`).toMatch(/^[A-Za-z0-9]+$/);
179
+ expect(e.reviewed, `${e.type}${e.pointer} records when it was read`).toMatch(/^\d{4}-\d{2}-\d{2}$/);
180
+ expect(e.note.length, `${e.type}${e.pointer} says why it is here`).toBeGreaterThan(0);
181
+ if (e.source.kind === "botocore") {
182
+ expect(e.source.service).toBeTruthy();
183
+ expect(e.source.apiVersion).toMatch(/^\d{4}-\d{2}-\d{2}$/);
184
+ expect(e.source.shape).toBeTruthy();
185
+ } else {
186
+ expect(e.source.url).toMatch(/^https:\/\//);
187
+ }
188
+ }
189
+ });
190
+
191
+ test("every value list is non-empty, unique and sorted", () => {
192
+ for (const e of entries) {
193
+ const where = `${e.type}${e.pointer}`;
194
+ expect(e.values.length, where).toBeGreaterThan(0);
195
+ expect(new Set(e.values).size, where).toBe(e.values.length);
196
+ expect(e.values, where).toEqual([...e.values].sort());
197
+ }
198
+ });
199
+
200
+ test("covers the headline property from chant #1497", () => {
201
+ const ec2 = entries.find(
202
+ (e) => e.type === "AWS::EC2::Instance" && e.pointer === "/properties/InstanceType",
203
+ );
204
+ expect(ec2).toBeDefined();
205
+ expect(ec2!.values).toContain("t3.micro");
206
+ });
207
+
208
+ test("carries every value the repo's own examples and composites already write", () => {
209
+ const written: Array<[string, string, string]> = [
210
+ ["AWS::Lambda::Function", "/properties/Runtime", "nodejs20.x"],
211
+ ["AWS::Lambda::Function", "/properties/Runtime", "nodejs18.x"],
212
+ ["AWS::Lambda::Function", "/properties/Runtime", "python3.12"],
213
+ ["AWS::EC2::Instance", "/properties/InstanceType", "t3.micro"],
214
+ ["AWS::ElasticLoadBalancingV2::TargetGroup", "/properties/Protocol", "HTTP"],
215
+ ["AWS::ElasticLoadBalancingV2::TargetGroup", "/properties/Protocol", "HTTPS"],
216
+ ["AWS::ElasticLoadBalancingV2::TargetGroup", "/properties/Protocol", "TCP"],
217
+ ["AWS::ElasticLoadBalancingV2::TargetGroup", "/properties/TargetType", "ip"],
218
+ ["AWS::ElasticLoadBalancingV2::LoadBalancer", "/properties/Scheme", "internet-facing"],
219
+ ["AWS::SNS::Subscription", "/properties/Protocol", "lambda"],
220
+ ["AWS::DynamoDB::Table", "/properties/BillingMode", "PAY_PER_REQUEST"],
221
+ ["AWS::DynamoDB::Table", "/definitions/AttributeDefinition/properties/AttributeType", "S"],
222
+ ["AWS::DynamoDB::Table", "/definitions/AttributeDefinition/properties/AttributeType", "N"],
223
+ ["AWS::DynamoDB::Table", "/definitions/KeySchema/properties/KeyType", "HASH"],
224
+ ["AWS::DynamoDB::Table", "/definitions/KeySchema/properties/KeyType", "RANGE"],
225
+ ["AWS::DynamoDB::Table", "/definitions/Projection/properties/ProjectionType", "ALL"],
226
+ ["AWS::DynamoDB::Table", "/definitions/Projection/properties/ProjectionType", "INCLUDE"],
227
+ ["AWS::DynamoDB::Table", "/definitions/StreamSpecification/properties/StreamViewType", "NEW_AND_OLD_IMAGES"],
228
+ ["AWS::DynamoDB::Table", "/definitions/StreamSpecification/properties/StreamViewType", "KEYS_ONLY"],
229
+ ["AWS::RDS::DBInstance", "/properties/Engine", "postgres"],
230
+ ["AWS::RDS::DBCluster", "/properties/Engine", "aurora-postgresql"],
231
+ ["AWS::ApplicationAutoScaling::ScalableTarget", "/properties/ServiceNamespace", "ecs"],
232
+ ["AWS::ApplicationAutoScaling::ScalableTarget", "/properties/ServiceNamespace", "dynamodb"],
233
+ ["AWS::ApplicationAutoScaling::ScalableTarget", "/properties/ScalableDimension", "ecs:service:DesiredCount"],
234
+ [
235
+ "AWS::ApplicationAutoScaling::ScalableTarget",
236
+ "/properties/ScalableDimension",
237
+ "dynamodb:table:ReadCapacityUnits",
238
+ ],
239
+ ];
240
+
241
+ for (const [type, pointer, value] of written) {
242
+ const e = entries.find((x) => x.type === type && x.pointer === pointer);
243
+ expect(e, `${type}${pointer} is in the overlay`).toBeDefined();
244
+ expect(e!.values, `${type}${pointer} accepts ${value}`).toContain(value);
245
+ }
246
+ });
247
+ });
248
+
249
+ describe("the overlay through the generation pipeline", () => {
250
+ test("a bare property comes out as a named union in the .d.ts", async () => {
251
+ const schemas = new Map<string, Buffer>([
252
+ [
253
+ "AWS::ElasticLoadBalancingV2::LoadBalancer",
254
+ Buffer.from(
255
+ JSON.stringify({
256
+ typeName: "AWS::ElasticLoadBalancingV2::LoadBalancer",
257
+ properties: { Name: { type: "string" }, Scheme: { type: "string" } },
258
+ additionalProperties: false,
259
+ }),
260
+ ),
261
+ ],
262
+ ]);
263
+
264
+ const result = await generate({ schemaSource: schemas });
265
+
266
+ expect(result.typesDTS).toContain(
267
+ 'export type LoadBalancer_Scheme = "internal" | "internet-facing";',
268
+ );
269
+ expect(result.typesDTS).toContain("Scheme?: LoadBalancer_Scheme;");
270
+
271
+ const lexicon = JSON.parse(result.lexiconJSON);
272
+ expect(lexicon["LoadBalancer"].propertyConstraints.Scheme.enum).toEqual([
273
+ "internal",
274
+ "internet-facing",
275
+ ]);
276
+ });
277
+
278
+ test("a spec-declared enum survives the pipeline unchanged", async () => {
279
+ const schemas = new Map<string, Buffer>([
280
+ [
281
+ "AWS::ElasticLoadBalancingV2::LoadBalancer",
282
+ Buffer.from(
283
+ JSON.stringify({
284
+ typeName: "AWS::ElasticLoadBalancingV2::LoadBalancer",
285
+ properties: { Scheme: { type: "string", enum: ["internal"] } },
286
+ additionalProperties: false,
287
+ }),
288
+ ),
289
+ ],
290
+ ]);
291
+
292
+ const result = await generate({ schemaSource: schemas });
293
+
294
+ expect(result.typesDTS).toContain('Scheme?: "internal";');
295
+ expect(result.typesDTS).not.toContain("export type LoadBalancer_Scheme");
296
+ expect(result.warnings.some((w) => w.error.includes("redundant"))).toBe(true);
297
+ });
298
+ });
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Curated enum overlay (chant #1497).
3
+ *
4
+ * The CloudFormation Registry spec declares `enum` on some properties and not
5
+ * on others, and the split does not follow how often a property is written:
6
+ * `AWS::SageMaker::NotebookInstance.InstanceType` is a union while
7
+ * `AWS::EC2::Instance.InstanceType` is a bare `string`. Where the spec is
8
+ * silent the generated type stops teaching the API, and a wrong value survives
9
+ * `tsc` and `chant build` to fail at deploy.
10
+ *
11
+ * This overlay is a checked-in list of `(CFN type, JSON pointer, values)`
12
+ * entries merged into the raw schema before parsing, so a curated enum reaches
13
+ * the generated `.d.ts` and the lexicon registry by exactly the path a spec
14
+ * enum reaches them. Nothing here is fetched at build time: the values live in
15
+ * `enum-overlay.json` beside this file, each with the source it was read from
16
+ * and the date it was read, so `scripts/refresh-enum-overlay.ts` can diff them
17
+ * later.
18
+ *
19
+ * Precedence: the overlay only fills gaps. An entry whose target already
20
+ * declares `enum` (from the Registry spec or from a cfn-lint patch) leaves the
21
+ * spec alone and reports itself as redundant. Upstream is refreshed on every
22
+ * `generate`, so it is the source that keeps tracking AWS; a hand list allowed
23
+ * to win would rot invisibly and would make the generated type depend on which
24
+ * of two sources happened to be newer. Reporting the overlap instead surfaces
25
+ * the entry for retirement the moment upstream catches up.
26
+ *
27
+ * An entry that matches nothing is an error, not a shrug: a renamed or removed
28
+ * property means the curated values are being applied to a property that no
29
+ * longer exists, and silence there is how an overlay goes stale.
30
+ */
31
+
32
+ import overlayDocument from "./enum-overlay.json" with { type: "json" };
33
+
34
+ /** Where an entry's values were read from, and enough detail to read them again. */
35
+ export type EnumOverlaySource =
36
+ | {
37
+ kind: "botocore";
38
+ /** Directory under `botocore/data`, e.g. `elbv2`. */
39
+ service: string;
40
+ /** API version directory, e.g. `2015-12-01`. */
41
+ apiVersion: string;
42
+ /** Shape name carrying the `enum`, e.g. `ProtocolEnum`. */
43
+ shape: string;
44
+ }
45
+ | {
46
+ kind: "docs";
47
+ /** Page the values were transcribed from. */
48
+ url: string;
49
+ };
50
+
51
+ /** One curated enum. */
52
+ export interface EnumOverlayEntry {
53
+ /** CloudFormation type name, e.g. `AWS::EC2::Instance`. */
54
+ type: string;
55
+ /**
56
+ * JSON pointer to the property inside that type's Registry schema, either
57
+ * `/properties/<Name>` or `/definitions/<Def>/properties/<Name>`.
58
+ */
59
+ pointer: string;
60
+ /**
61
+ * Name for the `definitions` entry this creates. The generator turns it into
62
+ * an exported type named `<class>_<enumName>`, e.g. `Function_Runtime`.
63
+ */
64
+ enumName: string;
65
+ /** Why this property earned a place in the first curated set. */
66
+ note: string;
67
+ source: EnumOverlaySource;
68
+ /** ISO date the values were last read from `source`. */
69
+ reviewed: string;
70
+ values: string[];
71
+ }
72
+
73
+ /** What happened to one entry during a generation run. */
74
+ export interface EnumOverlayApplication {
75
+ entry: EnumOverlayEntry;
76
+ /**
77
+ * `applied` narrowed the property; `redundant` left an upstream enum in
78
+ * place; `absent` means the schema in hand does not declare the property,
79
+ * which only a non-strict (fixture) run tolerates.
80
+ */
81
+ outcome: "applied" | "redundant" | "absent";
82
+ /** For `redundant`, the values upstream already declared. */
83
+ upstreamValues?: string[];
84
+ }
85
+
86
+ /** The curated entries, in file order. */
87
+ export function enumOverlayEntries(): EnumOverlayEntry[] {
88
+ return overlayDocument.entries as EnumOverlayEntry[];
89
+ }
90
+
91
+ /** Entries grouped by CloudFormation type name. */
92
+ export function enumOverlayByType(
93
+ entries: EnumOverlayEntry[] = enumOverlayEntries(),
94
+ ): Map<string, EnumOverlayEntry[]> {
95
+ const byType = new Map<string, EnumOverlayEntry[]>();
96
+ for (const entry of entries) {
97
+ const list = byType.get(entry.type);
98
+ if (list) list.push(entry);
99
+ else byType.set(entry.type, [entry]);
100
+ }
101
+ return byType;
102
+ }
103
+
104
+ /** A pointer segment that is not a plain object key is not addressable. */
105
+ function pointerSegments(pointer: string): string[] {
106
+ if (!pointer.startsWith("/")) {
107
+ throw new Error(`enum overlay pointer must start with "/": ${pointer}`);
108
+ }
109
+ return pointer.split("/").slice(1);
110
+ }
111
+
112
+ type SchemaNode = Record<string, unknown>;
113
+
114
+ function resolvePointer(doc: SchemaNode, pointer: string): SchemaNode | undefined {
115
+ let node: unknown = doc;
116
+ for (const segment of pointerSegments(pointer)) {
117
+ if (typeof node !== "object" || node === null || Array.isArray(node)) return undefined;
118
+ node = (node as SchemaNode)[segment];
119
+ if (node === undefined) return undefined;
120
+ }
121
+ if (typeof node !== "object" || node === null || Array.isArray(node)) return undefined;
122
+ return node as SchemaNode;
123
+ }
124
+
125
+ /**
126
+ * Apply the entries for one CloudFormation type to its raw schema bytes.
127
+ *
128
+ * Returns the (possibly rewritten) schema and one {@link EnumOverlayApplication}
129
+ * per entry. Throws when an entry cannot be honoured, which is always a
130
+ * curation bug rather than a spec quirk.
131
+ *
132
+ * `strict` is on for a run over the real schema zip and off for a run over the
133
+ * trimmed fixtures under `src/testdata/schemas`, which legitimately drop
134
+ * properties the overlay names. Only a strict run treats a missing property as
135
+ * an error.
136
+ */
137
+ export function applyEnumOverlay(
138
+ typeName: string,
139
+ data: Buffer | string,
140
+ entries: EnumOverlayEntry[],
141
+ opts: { strict?: boolean } = {},
142
+ ): { data: Buffer | string; applications: EnumOverlayApplication[] } {
143
+ if (entries.length === 0) return { data, applications: [] };
144
+ const strict = opts.strict ?? false;
145
+
146
+ const text = typeof data === "string" ? data : data.toString("utf-8");
147
+ const doc = JSON.parse(text) as SchemaNode;
148
+ const applications: EnumOverlayApplication[] = [];
149
+ let changed = false;
150
+
151
+ for (const entry of entries) {
152
+ const where = `${entry.type}${entry.pointer}`;
153
+
154
+ const target = resolvePointer(doc, entry.pointer);
155
+ if (!target) {
156
+ if (!strict) {
157
+ applications.push({ entry, outcome: "absent" });
158
+ continue;
159
+ }
160
+ throw new Error(
161
+ `enum overlay entry ${where} names a property the schema does not declare. ` +
162
+ `Either the property was renamed upstream or the entry is stale; fix or drop it in ` +
163
+ `src/codegen/enum-overlay.json.`,
164
+ );
165
+ }
166
+
167
+ if (Array.isArray(target.enum) && target.enum.length > 0) {
168
+ applications.push({
169
+ entry,
170
+ outcome: "redundant",
171
+ upstreamValues: target.enum as string[],
172
+ });
173
+ continue;
174
+ }
175
+
176
+ if (typeof target.$ref === "string") {
177
+ throw new Error(
178
+ `enum overlay entry ${where} targets a property that already refers to ` +
179
+ `${target.$ref}; the overlay cannot own its type.`,
180
+ );
181
+ }
182
+
183
+ const definitions = (doc.definitions ??= {}) as SchemaNode;
184
+ if (entry.enumName in definitions) {
185
+ throw new Error(
186
+ `enum overlay entry ${where} would define ${entry.type}.${entry.enumName}, ` +
187
+ `which the schema already declares. Pick a different enumName.`,
188
+ );
189
+ }
190
+
191
+ // The definition gives the generator a name to export; the `enum` left on
192
+ // the property is what `extractConstraints` copies into the lexicon
193
+ // registry, where the LSP and the docs build read it.
194
+ definitions[entry.enumName] = { type: "string", enum: [...entry.values] };
195
+ target.$ref = `#/definitions/${entry.enumName}`;
196
+ target.enum = [...entry.values];
197
+ changed = true;
198
+ applications.push({ entry, outcome: "applied" });
199
+ }
200
+
201
+ if (!changed) return { data, applications };
202
+ const rewritten = JSON.stringify(doc);
203
+ return {
204
+ data: typeof data === "string" ? rewritten : Buffer.from(rewritten),
205
+ applications,
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Fail on any entry the run never reached.
211
+ *
212
+ * `strict` is on for a run over the real schema zip and off for a run over a
213
+ * fixture subset, where most types are legitimately absent. A missing type in
214
+ * a full run means the resource left the Registry and the entry is dead.
215
+ */
216
+ export function assertOverlayCoverage(
217
+ entries: EnumOverlayEntry[],
218
+ seenTypes: Set<string>,
219
+ strict: boolean,
220
+ ): void {
221
+ if (!strict) return;
222
+ const missing = entries.filter((e) => !seenTypes.has(e.type));
223
+ if (missing.length === 0) return;
224
+ const names = [...new Set(missing.map((e) => e.type))].sort().join(", ");
225
+ throw new Error(
226
+ `enum overlay names ${missing.length} entr${missing.length === 1 ? "y" : "ies"} on ` +
227
+ `type(s) the spec does not contain: ${names}. Drop them from ` +
228
+ `src/codegen/enum-overlay.json or fix the type name.`,
229
+ );
230
+ }
231
+
232
+ /** One warning line per entry upstream has caught up with. */
233
+ export function redundantOverlayWarnings(
234
+ applications: EnumOverlayApplication[],
235
+ ): Array<{ file: string; error: string }> {
236
+ return applications
237
+ .filter((a) => a.outcome === "redundant")
238
+ .map((a) => ({
239
+ file: `${a.entry.type}${a.entry.pointer}`,
240
+ error:
241
+ `enum overlay entry is redundant: the spec already declares ` +
242
+ `${(a.upstreamValues ?? []).length} allowed value(s) here, and the spec wins. ` +
243
+ `Retire the entry from src/codegen/enum-overlay.json.`,
244
+ }));
245
+ }
@@ -18,6 +18,15 @@ import { assertPinnedSpec } from "../spec/pin";
18
18
  import { parseCFNSchema, cfnShortName, type SchemaParseResult } from "../spec/parse";
19
19
  import { fetchCfnLintPatches, applyPatches } from "./patches";
20
20
  import { fetchCfnLintExtensions, loadExtensionSchemas, type ExtensionConstraint } from "./extensions";
21
+ import {
22
+ applyEnumOverlay,
23
+ assertOverlayCoverage,
24
+ enumOverlayByType,
25
+ enumOverlayEntries,
26
+ redundantOverlayWarnings,
27
+ type EnumOverlayApplication,
28
+ type EnumOverlayEntry,
29
+ } from "./enum-overlay";
21
30
  import { samResources } from "./sam";
22
31
  import { fallbackResources } from "./fallback";
23
32
  import { NamingStrategy, publishedNames, propertyTypeName, extractDefName } from "./naming";
@@ -35,6 +44,18 @@ export type { GenerateOptions, GenerateResult };
35
44
  let awsConstraints = new Map<string, ExtensionConstraint[]>();
36
45
  /** chant #1459 — spec type → already-published TS name, reset per `generate()` call. */
37
46
  let awsReservedNames: Record<string, string> = {};
47
+ /**
48
+ * Curated enum overlay state (chant #1497), reset per `generate()` call.
49
+ *
50
+ * The overlay runs in `parseSchema` rather than `augmentSchemas` on purpose:
51
+ * `augmentSchemas` is skipped for a caller-supplied schema set because it
52
+ * fetches, and the overlay fetches nothing. Running it per schema keeps a
53
+ * fixture-driven run and a real run on the same code path.
54
+ */
55
+ let awsOverlay: Map<string, EnumOverlayEntry[]> = new Map();
56
+ let awsOverlayApplications: EnumOverlayApplication[] = [];
57
+ let awsOverlaySeenTypes = new Set<string>();
58
+ let awsOverlayStrict = false;
38
59
 
39
60
  const awsPipelineConfig: GeneratePipelineConfig<SchemaParseResult> = {
40
61
  fetchSchemas: async (opts) => {
@@ -46,8 +67,12 @@ const awsPipelineConfig: GeneratePipelineConfig<SchemaParseResult> = {
46
67
  return schemas;
47
68
  },
48
69
 
49
- parseSchema: (_typeName, data) => {
50
- const result = parseCFNSchema(data);
70
+ parseSchema: (typeName, data) => {
71
+ awsOverlaySeenTypes.add(typeName);
72
+ const entries = awsOverlay.get(typeName) ?? [];
73
+ const overlaid = applyEnumOverlay(typeName, data, entries, { strict: awsOverlayStrict });
74
+ awsOverlayApplications.push(...overlaid.applications);
75
+ const result = parseCFNSchema(overlaid.data);
51
76
  if (!result.resource.typeName) return null;
52
77
  return result;
53
78
  },
@@ -110,6 +135,13 @@ const awsPipelineConfig: GeneratePipelineConfig<SchemaParseResult> = {
110
135
  // Re-filter extension constraints now that we have the full type set
111
136
  // (augmentSchemas loaded them before parsing, so we already have them)
112
137
 
138
+ // A curated enum that reached nothing is a stale entry (chant #1497), and
139
+ // the only place it can be noticed is here, once every schema has been seen.
140
+ assertOverlayCoverage(enumOverlayEntries(), awsOverlaySeenTypes, awsOverlayStrict);
141
+ const applied = awsOverlayApplications.filter((a) => a.outcome === "applied").length;
142
+ log(`Applied ${applied} curated enum overlay entries`);
143
+ warnings.push(...redundantOverlayWarnings(awsOverlayApplications));
144
+
113
145
  log(`Total: ${results.length} resource schemas`);
114
146
  return { results, warnings };
115
147
  },
@@ -139,6 +171,12 @@ const awsPipelineConfig: GeneratePipelineConfig<SchemaParseResult> = {
139
171
  export async function generate(opts: GenerateOptions = {}): Promise<GenerateResult> {
140
172
  // Reset shared state
141
173
  awsConstraints = new Map();
174
+ awsOverlay = enumOverlayByType();
175
+ awsOverlayApplications = [];
176
+ awsOverlaySeenTypes = new Set();
177
+ // A fixture set is a trimmed subset, so an entry it never reaches is
178
+ // expected; a run over the real zip has no such excuse.
179
+ awsOverlayStrict = !opts.schemaSource;
142
180
  // chant #1459 — names already published keep pointing at the types that
143
181
  // published them. Skipped for a caller-supplied schema set, exactly as
144
182
  // `augmentSchemas` is (see the pipeline's `opts.schemaSource` guard): a
@@ -42,10 +42,10 @@ describe("rollback-previous (#557)", () => {
42
42
  });
43
43
 
44
44
  it("rolls an ECS service back via the executor for the {service, cluster} shape (#990)", async () => {
45
- // The ecs-fargate preset / ALB-ECS pilot (and loomster's loom-frontend)
46
- // compose rollback-previous with an ECS service, NOT a snapshot id. This
47
- // used to reach the snapshot path and throw "Cannot read properties of
48
- // undefined (reading 'includes')" on the absent snapshotId.
45
+ // The ecs-fargate preset / ALB-ECS pilot compose rollback-previous with an
46
+ // ECS service, NOT a snapshot id. This used to reach the snapshot path and
47
+ // throw "Cannot read properties of undefined (reading 'includes')" on the
48
+ // absent snapshotId.
49
49
  const mock = createMockCloudExecutor();
50
50
  const out = await createRollbackPreviousCapability(mock.executor).run(ctx, {
51
51
  service: "loom-frontend-svc",
@@ -54,8 +54,8 @@ export interface RollbackPreviousSnapshotInput {
54
54
  }
55
55
 
56
56
  /** Roll an ECS service back to its previously recorded task definition — the
57
- * shape the `ecs-fargate` preset and the ALB/ECS pilot compose (e.g. loomster's
58
- * `loom-frontend`). */
57
+ * shape the `ecs-fargate` preset and the ALB/ECS pilot compose for an
58
+ * ALB-fronted frontend service. */
59
59
  export interface RollbackPreviousEcsInput {
60
60
  /** ECS service name. */
61
61
  service: string;