@intentius/chant-lexicon-aws 0.50.0 → 0.51.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,26 @@
1
+ import type { DisruptionQuery, DisruptionVerdict } from "@intentius/chant/lexicon";
2
+ /** The slice of a `lexicon-aws.json` entry that carries replacement semantics. */
3
+ export interface DisruptionSpec {
4
+ resourceType: string;
5
+ kind: string;
6
+ /** Read-only attributes (TS key → CF attr name). An output changing is a symptom, never a cause. */
7
+ attrs?: Record<string, string>;
8
+ createOnly?: string[];
9
+ conditionalCreateOnly?: string[];
10
+ replacementStrategy?: "delete_then_create" | "create_then_delete";
11
+ }
12
+ /**
13
+ * Classify one pending update against the type's compiled schema. Pure — the
14
+ * spec table is passed in, so this is testable without generated artifacts.
15
+ */
16
+ export declare function classifyAwsChange(query: DisruptionQuery, specs: Map<string, DisruptionSpec>): DisruptionVerdict;
17
+ /**
18
+ * `LexiconPlugin.classifyDisruption` for aws. Loads the compiled registry once
19
+ * and answers every query from it. A registry that cannot be read leaves every
20
+ * change `unknown` — the honest answer, and the one the contract requires.
21
+ */
22
+ export declare function awsDisruption(options: {
23
+ environment: string;
24
+ changes: DisruptionQuery[];
25
+ }): Record<string, DisruptionVerdict>;
26
+ //# sourceMappingURL=disruption.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disruption.d.ts","sourceRoot":"","sources":["../src/disruption.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAInF,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,oGAAoG;IACpG,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,mBAAmB,CAAC,EAAE,oBAAoB,GAAG,oBAAoB,CAAC;CACnE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,eAAe,EACtB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GACjC,iBAAiB,CAuDnB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAOpC"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "4ed3c883e89ff20517d344e836a3fdd6c35e52e8ea0276cb02c3157710823bba",
4
+ "manifest.json": "0c488f36f907e56798a03315c3dec265068b2db4233aa839f03db166e2c17b54",
5
5
  "meta.json": "e7c751a09a871dc5a16e9aeea0b0a149513a7a2a051c923e9a5acf193dc2aef8",
6
6
  "types/index.d.ts": "6d6b37bd14a9b40fb3ff5c66e1cb49be1ae1032de47a4e167cf5f411c943b7f5",
7
7
  "rules/hardcoded-region.ts": "5a0eaf7ab391231fe6cd51426ece29539cb4b36f31c8dd060956638fed55722a",
@@ -72,5 +72,5 @@
72
72
  "skills/chant-aws-eks.md": "8789255709ff004ad0a875fd5999edcdc66fc6e33d710db058d9f42703bcfdfe",
73
73
  "skills/chant-aws-carve-terraform.md": "0ae4376590db47979be144d3ac6cdcfd56c9640d0536421aacf7330459d38e52"
74
74
  },
75
- "composite": "ba342596e044edf730f75709ef8b4f5bd2c614b6d2eba0e9f457c20f039bb70a"
75
+ "composite": "e03cda0c71c249a0fad0b4c921913fe533bf0f3ddcfc9c4758a0f8d66922fb2d"
76
76
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "AWS",
6
6
  "intrinsics": [
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAgL,MAAM,0BAA0B,CAAC;AAqC5O;yEACyE;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAiBnD,eAAO,MAAM,SAAS,EAAE,aAk9BvB,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAoN,MAAM,0BAA0B,CAAC;AAsChR;yEACyE;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAiBnD,eAAO,MAAM,SAAS,EAAE,aAg+BvB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-aws",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "description": "AWS CloudFormation lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -80,7 +80,7 @@
80
80
  "typescript": "^5.9.3"
81
81
  },
82
82
  "peerDependencies": {
83
- "@intentius/chant": "^0.50.0",
83
+ "@intentius/chant": "^0.51.0",
84
84
  "typescript": "^5.9.3"
85
85
  }
86
86
  }
@@ -0,0 +1,198 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { awsDisruption, classifyAwsChange, type DisruptionSpec } from "./disruption";
3
+ import type { DisruptionQuery } from "@intentius/chant/lexicon";
4
+
5
+ const specs = new Map<string, DisruptionSpec>([
6
+ [
7
+ "AWS::EC2::Instance",
8
+ {
9
+ resourceType: "AWS::EC2::Instance",
10
+ kind: "resource",
11
+ attrs: { InstanceId: "InstanceId", PublicIp: "PublicIp" },
12
+ createOnly: ["ImageId", "SubnetId", "KeyName"],
13
+ conditionalCreateOnly: ["InstanceType"],
14
+ },
15
+ ],
16
+ [
17
+ "AWS::RDS::DBInstance",
18
+ {
19
+ resourceType: "AWS::RDS::DBInstance",
20
+ kind: "resource",
21
+ createOnly: ["DBInstanceIdentifier"],
22
+ replacementStrategy: "delete_then_create",
23
+ },
24
+ ],
25
+ [
26
+ "AWS::S3::BucketPolicy",
27
+ { resourceType: "AWS::S3::BucketPolicy", kind: "resource" },
28
+ ],
29
+ [
30
+ "AWS::EC2::Instance.BlockDeviceMapping",
31
+ { resourceType: "AWS::EC2::Instance.BlockDeviceMapping", kind: "property" },
32
+ ],
33
+ ]);
34
+
35
+ function query(type: string | undefined, paths: string[]): DisruptionQuery {
36
+ return {
37
+ name: "thing",
38
+ ...(type ? { type } : {}),
39
+ deltas: paths.map((path) => ({ path, oldValue: "a", newValue: "b" })),
40
+ };
41
+ }
42
+
43
+ describe("classifyAwsChange (#1665)", () => {
44
+ test("a create-only property is a replace, and says which one", () => {
45
+ const v = classifyAwsChange(query("AWS::EC2::Instance", ["attributes.ImageId"]), specs);
46
+ expect(v.disruption).toBe("replace");
47
+ expect(v.because).toEqual(["attributes.ImageId"]);
48
+ expect(v.detail).toContain("ImageId is create-only");
49
+ });
50
+
51
+ // `delete_then_create` is the destructive form: the old resource is gone
52
+ // before the new one exists.
53
+ test("delete_then_create replacement is a destroy", () => {
54
+ const v = classifyAwsChange(
55
+ query("AWS::RDS::DBInstance", ["attributes.DBInstanceIdentifier"]),
56
+ specs,
57
+ );
58
+ expect(v.disruption).toBe("destroy");
59
+ expect(v.detail).toContain("deleting first");
60
+ });
61
+
62
+ test("a mutable property is in-place", () => {
63
+ const v = classifyAwsChange(query("AWS::EC2::Instance", ["attributes.Monitoring"]), specs);
64
+ expect(v.disruption).toBe("in-place");
65
+ expect(v.detail).toContain("no create-only property of AWS::EC2::Instance changed");
66
+ });
67
+
68
+ test("replacement wins over an in-place property changing alongside it", () => {
69
+ const v = classifyAwsChange(
70
+ query("AWS::EC2::Instance", ["attributes.Monitoring", "attributes.SubnetId"]),
71
+ specs,
72
+ );
73
+ expect(v.disruption).toBe("replace");
74
+ expect(v.because).toEqual(["attributes.SubnetId"]);
75
+ });
76
+
77
+ // The schema says "depends on the value", which is not an answer. Reporting
78
+ // that as in-place would be the exact failure the `unknown` level exists for.
79
+ test("a conditionally create-only property is unknown, not in-place", () => {
80
+ const v = classifyAwsChange(
81
+ query("AWS::EC2::Instance", ["attributes.InstanceType"]),
82
+ specs,
83
+ );
84
+ expect(v.disruption).toBe("unknown");
85
+ expect(v.because).toEqual(["attributes.InstanceType"]);
86
+ expect(v.detail).toContain("conditionally create-only");
87
+ });
88
+
89
+ test("a type with no schema on record is unknown", () => {
90
+ const v = classifyAwsChange(query("AWS::Nowhere::Thing", ["attributes.Anything"]), specs);
91
+ expect(v.disruption).toBe("unknown");
92
+ expect(v.detail).toContain("no CloudFormation registry schema on record");
93
+ });
94
+
95
+ test("a property-type entry is not a resource, so it is unknown", () => {
96
+ const v = classifyAwsChange(
97
+ query("AWS::EC2::Instance.BlockDeviceMapping", ["attributes.Ebs"]),
98
+ specs,
99
+ );
100
+ expect(v.disruption).toBe("unknown");
101
+ });
102
+
103
+ test("an entry with no observed type is unknown", () => {
104
+ const v = classifyAwsChange(query(undefined, ["attributes.ImageId"]), specs);
105
+ expect(v.disruption).toBe("unknown");
106
+ expect(v.detail).toContain("no resource type");
107
+ });
108
+
109
+ // A read-only attribute changing is a symptom of something else, never the
110
+ // cause of a replacement — a re-created instance reports a new PublicIp.
111
+ test("read-only attributes are not evidence of anything", () => {
112
+ const v = classifyAwsChange(query("AWS::EC2::Instance", ["attributes.PublicIp"]), specs);
113
+ expect(v.disruption).toBe("in-place");
114
+ expect(v.detail).toContain("only status, identity, or read-only attributes changed");
115
+ });
116
+
117
+ test("core's own observation fields are not properties", () => {
118
+ const v = classifyAwsChange(
119
+ query("AWS::EC2::Instance", ["status", "lastUpdated", "physicalId"]),
120
+ specs,
121
+ );
122
+ expect(v.disruption).toBe("in-place");
123
+ expect(v.detail).toContain("only status, identity, or read-only attributes changed");
124
+ });
125
+
126
+ test("a type whose schema declares no create-only property is in-place", () => {
127
+ const v = classifyAwsChange(query("AWS::S3::BucketPolicy", ["attributes.PolicyDocument"]), specs);
128
+ expect(v.disruption).toBe("in-place");
129
+ });
130
+
131
+ // createOnlyProperties can point at a nested pointer path (`Config/Engine`);
132
+ // the observation reports the top-level property.
133
+ test("a nested create-only pointer matches on its head segment", () => {
134
+ const nested = new Map<string, DisruptionSpec>([
135
+ [
136
+ "AWS::Fake::Thing",
137
+ { resourceType: "AWS::Fake::Thing", kind: "resource", createOnly: ["Config/Engine"] },
138
+ ],
139
+ ]);
140
+ const v = classifyAwsChange(query("AWS::Fake::Thing", ["attributes.Config"]), nested);
141
+ expect(v.disruption).toBe("replace");
142
+ });
143
+
144
+ test("duplicate hits name the property once but keep every path", () => {
145
+ const v = classifyAwsChange(
146
+ query("AWS::EC2::Instance", ["attributes.ImageId", "attributes.KeyName"]),
147
+ specs,
148
+ );
149
+ expect(v.because).toEqual(["attributes.ImageId", "attributes.KeyName"]);
150
+ expect(v.detail).toContain("ImageId, KeyName");
151
+ });
152
+ });
153
+
154
+ // The plugin method against the registry the codegen actually compiled — the
155
+ // evidence that the verdicts come from CloudFormation's own schema and not
156
+ // from a table in this file.
157
+ describe("awsDisruption over the generated registry", () => {
158
+ const change = (name: string, type: string, paths: string[]): DisruptionQuery => ({
159
+ name,
160
+ type,
161
+ deltas: paths.map((path) => ({ path, oldValue: "a", newValue: "b" })),
162
+ });
163
+
164
+ test("classifies a whole batch off the compiled schema", () => {
165
+ const out = awsDisruption({
166
+ environment: "prod",
167
+ changes: [
168
+ // createOnlyProperties
169
+ change("web", "AWS::EC2::Instance", ["attributes.ImageId"]),
170
+ // conditionalCreateOnlyProperties — the schema says "it depends"
171
+ change("sized", "AWS::EC2::Instance", ["attributes.InstanceType"]),
172
+ // not create-only for this type
173
+ change("mon", "AWS::EC2::Instance", ["attributes.Monitoring"]),
174
+ // no schema on record
175
+ change("nope", "AWS::Nowhere::Thing", ["attributes.Whatever"]),
176
+ ],
177
+ });
178
+
179
+ expect(out.web.disruption).toBe("replace");
180
+ expect(out.web.because).toEqual(["attributes.ImageId"]);
181
+ expect(out.sized.disruption).toBe("unknown");
182
+ expect(out.sized.detail).toContain("conditionally create-only");
183
+ expect(out.mon.disruption).toBe("in-place");
184
+ expect(out.nope.disruption).toBe("unknown");
185
+ });
186
+
187
+ test("every query gets a verdict, so nothing silently drops out of the plan", () => {
188
+ const changes = [
189
+ change("a", "AWS::S3::Bucket", ["attributes.BucketName"]),
190
+ change("b", "AWS::IAM::Role", ["attributes.RoleName"]),
191
+ ];
192
+ const out = awsDisruption({ environment: "prod", changes });
193
+ expect(Object.keys(out).sort()).toEqual(["a", "b"]);
194
+ // BucketName and RoleName are both create-only in the Registry schema.
195
+ expect(out.a.disruption).toBe("replace");
196
+ expect(out.b.disruption).toBe("replace");
197
+ });
198
+ });
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Replacement semantics for a pending update (#1665).
3
+ *
4
+ * CloudFormation already knows this and says so in the Registry schema:
5
+ * `createOnlyProperties` is the type's complete list of properties that cannot
6
+ * be updated without building a new resource, and `replacementStrategy` says
7
+ * whether the new one is created before the old is removed or after. The
8
+ * codegen compiles both into `lexicon-aws.json` alongside the types, which is
9
+ * why this is a lookup rather than a rule anyone maintains by hand.
10
+ *
11
+ * `conditionalCreateOnlyProperties` is the interesting case: the schema is
12
+ * saying "depends on the value", which is not an answer, so it degrades to
13
+ * `unknown` with the property named. That is the whole point of the contract
14
+ * having an `unknown` — a maybe reported as `in-place` is worse than no
15
+ * report.
16
+ *
17
+ * `rolling` is not produced here. Nothing in the Registry schema expresses a
18
+ * workload roll; that verdict belongs to a lexicon whose spec does.
19
+ */
20
+ import { createRequire } from "module";
21
+ import type { DisruptionQuery, DisruptionVerdict } from "@intentius/chant/lexicon";
22
+
23
+ const require = createRequire(import.meta.url);
24
+
25
+ /** The slice of a `lexicon-aws.json` entry that carries replacement semantics. */
26
+ export interface DisruptionSpec {
27
+ resourceType: string;
28
+ kind: string;
29
+ /** Read-only attributes (TS key → CF attr name). An output changing is a symptom, never a cause. */
30
+ attrs?: Record<string, string>;
31
+ createOnly?: string[];
32
+ conditionalCreateOnly?: string[];
33
+ replacementStrategy?: "delete_then_create" | "create_then_delete";
34
+ }
35
+
36
+ /**
37
+ * Classify one pending update against the type's compiled schema. Pure — the
38
+ * spec table is passed in, so this is testable without generated artifacts.
39
+ */
40
+ export function classifyAwsChange(
41
+ query: DisruptionQuery,
42
+ specs: Map<string, DisruptionSpec>,
43
+ ): DisruptionVerdict {
44
+ const spec = query.type ? specs.get(query.type) : undefined;
45
+ if (!spec || spec.kind !== "resource") {
46
+ return {
47
+ disruption: "unknown",
48
+ detail: query.type
49
+ ? `no CloudFormation registry schema on record for ${query.type}`
50
+ : "the observation reported no resource type",
51
+ };
52
+ }
53
+
54
+ const readOnly = readOnlyNames(spec);
55
+ const considered: Array<{ path: string; property: string }> = [];
56
+ for (const delta of query.deltas) {
57
+ const property = propertyName(delta.path);
58
+ if (!property || readOnly.has(property)) continue;
59
+ considered.push({ path: delta.path, property });
60
+ }
61
+
62
+ const createOnly = new Set(spec.createOnly?.map(headSegment));
63
+ const forced = considered.filter((c) => createOnly.has(c.property));
64
+ if (forced.length > 0) {
65
+ const names = [...new Set(forced.map((c) => c.property))];
66
+ const destroys = spec.replacementStrategy === "delete_then_create";
67
+ return {
68
+ disruption: destroys ? "destroy" : "replace",
69
+ because: forced.map((c) => c.path),
70
+ detail: destroys
71
+ ? `${names.join(", ")} is create-only and ${spec.resourceType} replaces by deleting first — the resource is gone before the new one exists`
72
+ : `${names.join(", ")} is create-only — CloudFormation builds a new ${spec.resourceType} and removes the old one`,
73
+ };
74
+ }
75
+
76
+ const conditional = new Set(spec.conditionalCreateOnly?.map(headSegment));
77
+ const maybe = considered.filter((c) => conditional.has(c.property));
78
+ if (maybe.length > 0) {
79
+ const names = [...new Set(maybe.map((c) => c.property))];
80
+ return {
81
+ disruption: "unknown",
82
+ because: maybe.map((c) => c.path),
83
+ detail: `the schema marks ${names.join(", ")} conditionally create-only — whether this replaces depends on the value, and the schema does not say which way`,
84
+ };
85
+ }
86
+
87
+ if (considered.length === 0) {
88
+ return {
89
+ disruption: "in-place",
90
+ detail: "no configured property differs — only status, identity, or read-only attributes changed",
91
+ };
92
+ }
93
+
94
+ return {
95
+ disruption: "in-place",
96
+ detail: `no create-only property of ${spec.resourceType} changed`,
97
+ };
98
+ }
99
+
100
+ /**
101
+ * `LexiconPlugin.classifyDisruption` for aws. Loads the compiled registry once
102
+ * and answers every query from it. A registry that cannot be read leaves every
103
+ * change `unknown` — the honest answer, and the one the contract requires.
104
+ */
105
+ export function awsDisruption(options: {
106
+ environment: string;
107
+ changes: DisruptionQuery[];
108
+ }): Record<string, DisruptionVerdict> {
109
+ const specs = registryByType();
110
+ const out: Record<string, DisruptionVerdict> = {};
111
+ for (const change of options.changes) {
112
+ out[change.name] = classifyAwsChange(change, specs);
113
+ }
114
+ return out;
115
+ }
116
+
117
+ let cached: Map<string, DisruptionSpec> | undefined;
118
+
119
+ function registryByType(): Map<string, DisruptionSpec> {
120
+ if (cached) return cached;
121
+ try {
122
+ const manifest = require("./generated/lexicon-aws.json") as Record<string, DisruptionSpec>;
123
+ cached = new Map(Object.values(manifest).map((e) => [e.resourceType, e]));
124
+ } catch {
125
+ cached = new Map();
126
+ }
127
+ return cached;
128
+ }
129
+
130
+ /** Every read-only attribute name the schema declares, top segment only. */
131
+ function readOnlyNames(spec: DisruptionSpec): Set<string> {
132
+ const names = new Set<string>();
133
+ for (const attr of Object.values(spec.attrs ?? {})) names.add(attr.split(".")[0]);
134
+ return names;
135
+ }
136
+
137
+ /**
138
+ * The property a core delta path names, or undefined when the path is not
139
+ * about a property at all. Core's own observation fields (`status`,
140
+ * `physicalId`, `lastUpdated`) sit at the root of the change and say nothing
141
+ * about configuration; only what is under `attributes.` was read off the
142
+ * resource.
143
+ */
144
+ function propertyName(path: string): string | undefined {
145
+ const prefix = "attributes.";
146
+ if (!path.startsWith(prefix)) return undefined;
147
+ const rest = path.slice(prefix.length);
148
+ return rest ? headSegment(rest) : undefined;
149
+ }
150
+
151
+ /** The first segment of a JSON-pointer-ish property path (`Config/Engine` → `Config`). */
152
+ function headSegment(path: string): string {
153
+ return path.split(/[/.]/)[0];
154
+ }
package/src/lsp/hover.ts CHANGED
@@ -62,11 +62,8 @@ function resourceHover(className: string, entry: LexiconEntry): HoverInfo | unde
62
62
  }
63
63
 
64
64
  // Declared and inferred deprecations rest on different evidence, so they
65
- // read as separate lines rather than one merged list (#1701). The core
66
- // `LexiconEntry` gains `inferredDeprecations` in the next core release; this
67
- // lexicon builds against the published core, so widen locally until then.
68
- const withBasis = entry as LexiconEntry & { inferredDeprecations?: string[] };
69
- const inferred = new Set(withBasis.inferredDeprecations ?? []);
65
+ // read as separate lines rather than one merged list.
66
+ const inferred = new Set(entry.inferredDeprecations ?? []);
70
67
  const declared = (entry.deprecatedProperties ?? []).filter((p) => !inferred.has(p));
71
68
 
72
69
  if (declared.length > 0) {
package/src/plugin.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "module";
2
2
  import { detectTemplate } from "./detect";
3
- import type { LexiconPlugin, IntrinsicDef, ObservationResult, DeepObservationResult, DependencyObservation, ResourceMetadata, ExportedTemplate, ResourceSelector, InitTemplateSet, StackStatusObservation } from "@intentius/chant/lexicon";
3
+ import type { LexiconPlugin, IntrinsicDef, ObservationResult, DeepObservationResult, DependencyObservation, DisruptionQuery, DisruptionVerdict, ResourceMetadata, ExportedTemplate, ResourceSelector, InitTemplateSet, StackStatusObservation } from "@intentius/chant/lexicon";
4
4
  const require = createRequire(import.meta.url);
5
5
  import type { LintRule } from "@intentius/chant/lint/rule";
6
6
  import type { TemplateParser } from "@intentius/chant/import/parser";
@@ -25,6 +25,7 @@ import {
25
25
  type StackResource,
26
26
  } from "./api/read-client";
27
27
  import { awsDeepNormalizationHooks, observeResourcesDeepAws } from "./deep-observe";
28
+ import { awsDisruption } from "./disruption";
28
29
  import { awsReferenceCatalog } from "./reference-catalog";
29
30
  import { AMBIENT_KINDS } from "./ambient";
30
31
  import { canDescribe, describeOwnProperties, stampRegion } from "./properties";
@@ -765,6 +766,20 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
765
766
  return observeAwsDependencies({ observed: options.observed, region: options.region });
766
767
  },
767
768
 
769
+ /**
770
+ * What a pending update costs (#1665), read off the Registry schema the
771
+ * codegen already compiled: a changed `createOnlyProperties` entry is a
772
+ * replacement, and `replacementStrategy: delete_then_create` makes it a
773
+ * destroy. A conditionally-create-only property is `unknown` on purpose —
774
+ * the schema says "depends on the value", which is not an answer.
775
+ */
776
+ classifyDisruption(options: {
777
+ environment: string;
778
+ changes: DisruptionQuery[];
779
+ }): Record<string, DisruptionVerdict> {
780
+ return awsDisruption(options);
781
+ },
782
+
768
783
  /**
769
784
  * Resources of a managed kind that exist without being declared or
770
785
  * referenced (#1278) — the account's default security groups, an unattached