@intentius/chant-lexicon-aws 0.52.0 → 0.52.1

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.
@@ -155,6 +155,18 @@ export interface StackResource {
155
155
  }
156
156
  /** `DescribeStackResources` for one stack, mapped off the Query XML. */
157
157
  export declare function describeStackResources(stackName: string, options?: AwsReadClientOptions): Promise<StackResource[]>;
158
+ /** One stack's `DescribeStacks` answer: its outputs and its own tags. */
159
+ export interface StackDetail {
160
+ outputs: Record<string, string>;
161
+ /** The stack's own tags, as a flat map — where the ownership marker lives (#1222). */
162
+ tags: Record<string, string>;
163
+ }
164
+ /**
165
+ * `DescribeStacks` for one stack, outputs and tags off the same response. The
166
+ * two ride together because they come from one call: reading the ownership
167
+ * marker on the observation path (#1998) costs no extra round trip.
168
+ */
169
+ export declare function describeStackDetail(stackName: string, options?: AwsReadClientOptions): Promise<StackDetail>;
158
170
  /** `DescribeStacks` outputs for one stack, as `key → value`. */
159
171
  export declare function describeStackOutputs(stackName: string, options?: AwsReadClientOptions): Promise<Record<string, string>>;
160
172
  /** One live resource, as Cloud Control describes it. */
@@ -1 +1 @@
1
- {"version":3,"file":"read-client.d.ts","sourceRoot":"","sources":["../../src/api/read-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"read-client.d.ts","sourceRoot":"","sources":["../../src/api/read-client.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -19,7 +19,7 @@ export { output, isLexiconOutput } from "@intentius/chant/lexicon-output";
19
19
  export type { LexiconOutput } from "@intentius/chant/lexicon-output";
20
20
  export { awsPlugin } from "./plugin.js";
21
21
  export { observeResourcesDeepAws, awsDeepNormalizationHooks, hasOwnershipMarker, DEEP_READABLE_TYPES, AWS_READ_ONLY_NAMES, AWS_SERVICE_DEFAULTS, } from "./deep-observe.js";
22
- export { describeStackResources, describeStackOutputs, getResource, listResources, parseResourceDescription, resolveEndpointOverride, serviceEndpointEnvVar, AwsReadError, type AwsReadHttp, type AwsReadClientOptions, type CloudControlDescription, type StackResource, type AwsCredentials, type AwsCredentialResolver, type AwsCredentialSource, } from "./api/read-client.js";
22
+ export { describeStackResources, describeStackOutputs, describeStackDetail, getResource, listResources, parseResourceDescription, resolveEndpointOverride, serviceEndpointEnvVar, AwsReadError, type AwsReadHttp, type AwsReadClientOptions, type CloudControlDescription, type StackResource, type StackDetail, type AwsCredentials, type AwsCredentialResolver, type AwsCredentialSource, } from "./api/read-client.js";
23
23
  export { signRequest, resolveCredentials, EMPTY_PAYLOAD_SHA256, type SigV4Request, } from "./api/sigv4.js";
24
24
  export { Sub, Ref, GetAtt, If, Join, Select, Split, Base64, GetAZs, SubIntrinsic, RefIntrinsic, GetAttIntrinsic, IfIntrinsic, JoinIntrinsic, SelectIntrinsic, SplitIntrinsic, Base64Intrinsic, GetAZsIntrinsic, } from "./intrinsics.js";
25
25
  export { AWS, StackName, Region, AccountId, StackId, URLSuffix, NoValue, NotificationARNs, Partition, } from "./pseudo.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "779ecfda7e4a8ff34566425ad83a8f689e6506cfe63edc1d838aed6a8b027a37",
5
- "meta.json": "e7c751a09a871dc5a16e9aeea0b0a149513a7a2a051c923e9a5acf193dc2aef8",
4
+ "manifest.json": "7e9b7fc19e101d213bdb44c46c6dcfec531317089ae045d95f9295f052094537",
5
+ "meta.json": "49cbb2ca9b454391a7644e14ee9cfd91bcf906c6af683bf46a182aa85047ecc4",
6
6
  "types/index.d.ts": "6d6b37bd14a9b40fb3ff5c66e1cb49be1ae1032de47a4e167cf5f411c943b7f5",
7
7
  "rules/hardcoded-region.ts": "5a0eaf7ab391231fe6cd51426ece29539cb4b36f31c8dd060956638fed55722a",
8
8
  "rules/iam-wildcard.ts": "135d7217d278fef50939e605c5da32603ba35674b8c4b5c4d66a06c77903e945",
@@ -15,7 +15,7 @@
15
15
  "rules/waw013.ts": "f9b37b955917415a2eb5f4aed7f28b76aff0a6abdb05228dcbb8bc77bc67672a",
16
16
  "rules/waw014.ts": "889f1b06899b50f98941d05b029954cf375ac8a130933b98c9586079096216aa",
17
17
  "rules/waw015.ts": "071c940fb6432cd8b42a44d083921e9491d5063417839109c10b2bd13dd2032e",
18
- "rules/waw016.ts": "5d6b614bcdf3e26d87dd6c4ec67d7c5e698b3bf05db82dc05d8e006da1e0e488",
18
+ "rules/waw016.ts": "b9538a9c44a58b687a08f74328caecba4236ad3c24c97759e6d69a3b9ce487c0",
19
19
  "rules/waw017.ts": "658554008954f71036e23b514dece7063142198fbdca125f11d09026f1d21ab2",
20
20
  "rules/waw018.ts": "a4aa83a25104f039a6c5e0f3e1f5b8b65e493b0ba162c985988190a726b90d2f",
21
21
  "rules/waw019.ts": "f14ebf024a3cce7005d54c762e4e0979537d6a854fb123344107d40d92a94b5c",
@@ -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": "11e032b348578d5cacba6857e6f4d5ec5fc81ad8d4df4e69f206e5350d473f8e"
75
+ "composite": "08ba36641ef07502fb99277e611e788e146497535b48e45c0c7373f43699ea3b"
76
76
  }
@@ -9,6 +9,10 @@
9
9
  * which also matches descriptions that mention the deprecation of a sibling
10
10
  * property, an enum value, or the thing the property configures. Declared is a
11
11
  * warning; inferred is reported at info and worded as a guess.
12
+ *
13
+ * A declared name is a flattened Registry pointer path (#1988), so most of them
14
+ * are nested — `Tags/TagKey`, `Source/Decryption/Url`. The template is walked
15
+ * along that path; matching top-level keys reached fewer than half of them.
12
16
  */
13
17
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
14
18
  /** What a deprecation classification rests on. */
@@ -1 +1 @@
1
- {"version":3,"file":"waw016.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/waw016.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAG9G,kDAAkD;AAClD,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,UAAU,CAAC;AAuCvD;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,gBAAgB,EACrB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,GACrD,mBAAmB,EAAE,CAiCvB;AAED,eAAO,MAAM,MAAM,EAAE,cAOpB,CAAC"}
1
+ {"version":3,"file":"waw016.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/waw016.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAG9G,kDAAkD;AAClD,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,UAAU,CAAC;AA6DvD;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,gBAAgB,EACrB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,GACrD,mBAAmB,EAAE,CAmCvB;AAED,eAAO,MAAM,MAAM,EAAE,cAOpB,CAAC"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws",
3
- "version": "0.52.0",
3
+ "version": "0.52.1",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "AWS",
6
6
  "intrinsics": [
package/dist/meta.json CHANGED
@@ -13765,8 +13765,8 @@
13765
13765
  "Id"
13766
13766
  ],
13767
13767
  "deprecatedProperties": [
13768
- "DistributionConfig/properties/CustomOrigin",
13769
- "DistributionConfig/properties/S3Origin"
13768
+ "DistributionConfig/CustomOrigin",
13769
+ "DistributionConfig/S3Origin"
13770
13770
  ],
13771
13771
  "tagging": {
13772
13772
  "taggable": true,
@@ -21366,9 +21366,9 @@
21366
21366
  "Id"
21367
21367
  ],
21368
21368
  "deprecatedProperties": [
21369
- "/definitions/ContinuousDeploymentPolicyConfig/properties/Type",
21370
- "/definitions/ContinuousDeploymentPolicyConfig/properties/SingleHeaderPolicyConfig",
21371
- "/definitions/ContinuousDeploymentPolicyConfig/properties/SingleWeightPolicyConfig"
21369
+ "ContinuousDeploymentPolicyConfig/Type",
21370
+ "ContinuousDeploymentPolicyConfig/SingleHeaderPolicyConfig",
21371
+ "ContinuousDeploymentPolicyConfig/SingleWeightPolicyConfig"
21372
21372
  ]
21373
21373
  },
21374
21374
  "ContinuousDeploymentPolicyConfig": {
@@ -68321,8 +68321,8 @@
68321
68321
  "DomainName"
68322
68322
  ],
68323
68323
  "conditionalCreateOnly": [
68324
- "EncryptionAtRestOptions/properties",
68325
- "AdvancedSecurityOptions/properties/Enabled"
68324
+ "EncryptionAtRestOptions",
68325
+ "AdvancedSecurityOptions/Enabled"
68326
68326
  ],
68327
68327
  "tagging": {
68328
68328
  "taggable": true,
@@ -14,3 +14,4 @@ Deprecated property usage — flags properties marked as deprecated in the Cloud
14
14
 
15
15
  - [Map](/types/Map.md)
16
16
  - [Type](/types/Type.md)
17
+ - [Url](/types/Url.md)
@@ -22,3 +22,7 @@ resource_type: AWS::Lambda::Url
22
22
 
23
23
  - `FunctionArn`
24
24
  - `FunctionUrl`
25
+
26
+ ## Governed by
27
+
28
+ - [WAW016](/rules/WAW016.md): Deprecated property usage — flags properties marked as deprecated in the CloudFormation Registry
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAoN,MAAM,0BAA0B,CAAC;AAuChR;yEACyE;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAiBnD,eAAO,MAAM,SAAS,EAAE,aAm/BvB,CAAC"}
@@ -9,6 +9,10 @@
9
9
  * which also matches descriptions that mention the deprecation of a sibling
10
10
  * property, an enum value, or the thing the property configures. Declared is a
11
11
  * warning; inferred is reported at info and worded as a guess.
12
+ *
13
+ * A declared name is a flattened Registry pointer path (#1988), so most of them
14
+ * are nested — `Tags/TagKey`, `Source/Decryption/Url`. The template is walked
15
+ * along that path; matching top-level keys reached fewer than half of them.
12
16
  */
13
17
 
14
18
  import { readFileSync } from "fs";
@@ -56,6 +60,28 @@ function loadDeprecatedProperties(): Map<string, Map<string, DeprecationBasis>>
56
60
  return map;
57
61
  }
58
62
 
63
+ /**
64
+ * True when `value` expresses `segments` — the flattened Registry pointer path
65
+ * (#1988), which is a single key for a top-level property and `/`-joined for a
66
+ * nested one.
67
+ *
68
+ * An array met where the pointer named a property is descended anyway: the
69
+ * Registry elides the wildcard in some schemas, so `Tags/TagKey` addresses a
70
+ * key inside the Tags array's items.
71
+ */
72
+ function expressesPath(value: unknown, segments: string[]): boolean {
73
+ if (segments.length === 0) return true;
74
+ if (Array.isArray(value)) {
75
+ const rest = segments[0] === "*" ? segments.slice(1) : segments;
76
+ return value.some((item) => expressesPath(item, rest));
77
+ }
78
+ if (segments[0] === "*") return false;
79
+ if (typeof value !== "object" || value === null) return false;
80
+ const obj = value as Record<string, unknown>;
81
+ if (!Object.prototype.hasOwnProperty.call(obj, segments[0])) return false;
82
+ return expressesPath(obj[segments[0]], segments.slice(1));
83
+ }
84
+
59
85
  /**
60
86
  * Core detection logic — exported for direct testing with synthetic data.
61
87
  */
@@ -76,9 +102,11 @@ export function checkDeprecatedProperties(
76
102
  if (!deprProps) continue;
77
103
 
78
104
  const props = resource.Properties ?? {};
79
- for (const propName of Object.keys(props)) {
80
- const basis = deprProps.get(propName);
81
- if (!basis) continue;
105
+ // Walk each declared path into the template, rather than matching
106
+ // top-level template keys: over half the declared names are nested
107
+ // (#1988), and a key comparison could never reach them.
108
+ for (const [propName, basis] of deprProps) {
109
+ if (!expressesPath(props, propName.split("/"))) continue;
82
110
 
83
111
  diagnostics.push({
84
112
  checkId: "WAW016",
@@ -5,6 +5,7 @@
5
5
  * resources with properties and attributes, property types from definitions,
6
6
  * and enum types from string enum definitions.
7
7
  */
8
+ import type { CFNSchema } from "./fetch.js";
8
9
  import { constraintsIsEmpty as coreConstraintsIsEmpty, type PropertyConstraints } from "@intentius/chant/codegen/json-schema";
9
10
  export type { PropertyConstraints } from "@intentius/chant/codegen/json-schema";
10
11
  export interface ParsedProperty {
@@ -69,4 +70,19 @@ export declare function cfnShortName(typeName: string): string;
69
70
  * Extract service name: "AWS::S3::Bucket" → "S3"
70
71
  */
71
72
  export declare function cfnServiceName(typeName: string): string;
73
+ /**
74
+ * Flatten a CloudFormation Registry JSON pointer to the property path a
75
+ * template expresses, `/`-joined.
76
+ *
77
+ * Three shapes occur upstream. `/properties/BucketName` is the plain one.
78
+ * A pointer can re-enter the `properties` keyword mid-path
79
+ * (`/properties/DistributionConfig/properties/S3Origin`), which a template
80
+ * never writes. And a pointer can be scoped to a shared definition
81
+ * (`/definitions/ContinuousDeploymentPolicyConfig/properties/Type`), which a
82
+ * template expresses only through whichever property carries that definition.
83
+ *
84
+ * Array positions stay as `*`. `readOnlyProperties` attribute names are
85
+ * generated from this output, so dropping the wildcard would rename them.
86
+ */
87
+ export declare function stripPointerPath(path: string, schema?: CFNSchema): string;
72
88
  //# sourceMappingURL=parse.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/spec/parse.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAGL,kBAAkB,IAAI,sBAAsB,EAE5C,KAAK,mBAAmB,EAIzB,MAAM,sCAAsC,CAAC;AAG9C,YAAY,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAkBhF,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,mBAAmB,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,cAAc,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B;;;;;OAKG;IACH,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,mBAAmB,CAAC,EAAE,oBAAoB,GAAG,oBAAoB,CAAC;IAClE,OAAO,CAAC,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,WAAW,EAAE,OAAO,CAAC;QAAC,YAAY,EAAE,OAAO,CAAA;KAAE,CAAC;CAC9E;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,cAAc,CAAC;IACzB,aAAa,EAAE,kBAAkB,EAAE,CAAC;IACpC,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,iBAAiB,CAmJvE;AAiBD,eAAO,MAAM,kBAAkB,+BAAyB,CAAC;AAMzD;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGrD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGvD"}
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/spec/parse.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAoC,MAAM,SAAS,CAAC;AAC3E,OAAO,EAGL,kBAAkB,IAAI,sBAAsB,EAE5C,KAAK,mBAAmB,EAIzB,MAAM,sCAAsC,CAAC;AAG9C,YAAY,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAkBhF,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,mBAAmB,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,cAAc,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B;;;;;OAKG;IACH,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,mBAAmB,CAAC,EAAE,oBAAoB,GAAG,oBAAoB,CAAC;IAClE,OAAO,CAAC,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,WAAW,EAAE,OAAO,CAAC;QAAC,YAAY,EAAE,OAAO,CAAA;KAAE,CAAC;CAC9E;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,cAAc,CAAC;IACzB,aAAa,EAAE,kBAAkB,EAAE,CAAC;IACpC,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,iBAAiB,CAmJvE;AAiBD,eAAO,MAAM,kBAAkB,+BAAyB,CAAC;AAMzD;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGrD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGvD;AAuDD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,MAAM,CAYzE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-aws",
3
- "version": "0.52.0",
3
+ "version": "0.52.1",
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.52.0",
83
+ "@intentius/chant": "^0.52.1",
84
84
  "typescript": "^5.9.3"
85
85
  }
86
86
  }
@@ -328,17 +328,40 @@ export async function describeStackResources(
328
328
  }));
329
329
  }
330
330
 
331
- /** `DescribeStacks` outputs for one stack, as `key value`. */
332
- export async function describeStackOutputs(
331
+ /** One stack's `DescribeStacks` answer: its outputs and its own tags. */
332
+ export interface StackDetail {
333
+ outputs: Record<string, string>;
334
+ /** The stack's own tags, as a flat map — where the ownership marker lives (#1222). */
335
+ tags: Record<string, string>;
336
+ }
337
+
338
+ /**
339
+ * `DescribeStacks` for one stack, outputs and tags off the same response. The
340
+ * two ride together because they come from one call: reading the ownership
341
+ * marker on the observation path (#1998) costs no extra round trip.
342
+ */
343
+ export async function describeStackDetail(
333
344
  stackName: string,
334
345
  options: AwsReadClientOptions = {},
335
- ): Promise<Record<string, string>> {
346
+ ): Promise<StackDetail> {
336
347
  const xml = await cfnQuery("DescribeStacks", { StackName: stackName }, options);
337
348
  const outputs: Record<string, string> = {};
338
349
  for (const m of xmlMembers(xml, "Outputs")) {
339
350
  if (m.OutputKey) outputs[m.OutputKey] = m.OutputValue ?? "";
340
351
  }
341
- return outputs;
352
+ const tags: Record<string, string> = {};
353
+ for (const m of xmlMembers(xml, "Tags")) {
354
+ if (m.Key) tags[m.Key] = m.Value ?? "";
355
+ }
356
+ return { outputs, tags };
357
+ }
358
+
359
+ /** `DescribeStacks` outputs for one stack, as `key → value`. */
360
+ export async function describeStackOutputs(
361
+ stackName: string,
362
+ options: AwsReadClientOptions = {},
363
+ ): Promise<Record<string, string>> {
364
+ return (await describeStackDetail(stackName, options)).outputs;
342
365
  }
343
366
 
344
367
  /* ── Cloud Control ────────────────────────────────────────────────────────── */
@@ -13765,8 +13765,8 @@
13765
13765
  "Id"
13766
13766
  ],
13767
13767
  "deprecatedProperties": [
13768
- "DistributionConfig/properties/CustomOrigin",
13769
- "DistributionConfig/properties/S3Origin"
13768
+ "DistributionConfig/CustomOrigin",
13769
+ "DistributionConfig/S3Origin"
13770
13770
  ],
13771
13771
  "tagging": {
13772
13772
  "taggable": true,
@@ -21366,9 +21366,9 @@
21366
21366
  "Id"
21367
21367
  ],
21368
21368
  "deprecatedProperties": [
21369
- "/definitions/ContinuousDeploymentPolicyConfig/properties/Type",
21370
- "/definitions/ContinuousDeploymentPolicyConfig/properties/SingleHeaderPolicyConfig",
21371
- "/definitions/ContinuousDeploymentPolicyConfig/properties/SingleWeightPolicyConfig"
21369
+ "ContinuousDeploymentPolicyConfig/Type",
21370
+ "ContinuousDeploymentPolicyConfig/SingleHeaderPolicyConfig",
21371
+ "ContinuousDeploymentPolicyConfig/SingleWeightPolicyConfig"
21372
21372
  ]
21373
21373
  },
21374
21374
  "ContinuousDeploymentPolicyConfig": {
@@ -68321,8 +68321,8 @@
68321
68321
  "DomainName"
68322
68322
  ],
68323
68323
  "conditionalCreateOnly": [
68324
- "EncryptionAtRestOptions/properties",
68325
- "AdvancedSecurityOptions/properties/Enabled"
68324
+ "EncryptionAtRestOptions",
68325
+ "AdvancedSecurityOptions/Enabled"
68326
68326
  ],
68327
68327
  "tagging": {
68328
68328
  "taggable": true,
package/src/index.ts CHANGED
@@ -67,6 +67,7 @@ export {
67
67
  export {
68
68
  describeStackResources,
69
69
  describeStackOutputs,
70
+ describeStackDetail,
70
71
  getResource,
71
72
  listResources,
72
73
  parseResourceDescription,
@@ -77,6 +78,7 @@ export {
77
78
  type AwsReadClientOptions,
78
79
  type CloudControlDescription,
79
80
  type StackResource,
81
+ type StackDetail,
80
82
  type AwsCredentials,
81
83
  type AwsCredentialResolver,
82
84
  type AwsCredentialSource,
@@ -33,6 +33,7 @@ const { liveEvidenceFromChangeSet, reconcileStatus } = await import("@intentius/
33
33
  const { describeObservationConformance } = await import("@intentius/chant-test-utils");
34
34
  const { observeResources } = await import("@intentius/chant/lifecycle/observe");
35
35
  const { DECLARABLE_MARKER } = await import("@intentius/chant/declarable");
36
+ const { assertLiveEntity, LiveAssertionError } = await import("@intentius/chant/lifecycle/assert-live");
36
37
 
37
38
  const liveTemplate = {
38
39
  AWSTemplateFormatVersion: "2010-09-09",
@@ -58,10 +59,19 @@ const stackResourcesXml = (rows: Array<{ logicalId: string; type: string; physic
58
59
  )
59
60
  .join("")}</StackResources></DescribeStackResourcesResponse>`;
60
61
 
61
- const stackOutputsXml = (outputs: Record<string, string> = {}) =>
62
+ const stackOutputsXml = (outputs: Record<string, string> = {}, tags: Record<string, string> = {}) =>
62
63
  `<DescribeStacksResponse><Outputs>${Object.entries(outputs)
63
64
  .map(([k, v]) => `<member><OutputKey>${k}</OutputKey><OutputValue>${v}</OutputValue></member>`)
64
- .join("")}</Outputs></DescribeStacksResponse>`;
65
+ .join("")}</Outputs><Tags>${Object.entries(tags)
66
+ .map(([k, v]) => `<member><Key>${k}</Key><Value>${v}</Value></member>`)
67
+ .join("")}</Tags></DescribeStacksResponse>`;
68
+
69
+ /** The stack tags a chant apply stamps for one identity (#1222). */
70
+ const ownedStackTags = (stack: string, env: string) => ({
71
+ "chant:managed-by": "chant",
72
+ "chant:stack": stack,
73
+ "chant:env": env,
74
+ });
65
75
 
66
76
  const queryErrorXml = (code: string, message: string) =>
67
77
  `<ErrorResponse><Error><Code>${code}</Code><Message>${message}</Message></Error></ErrorResponse>`;
@@ -122,9 +132,10 @@ describe("aws lifecycle integration (#163)", () => {
122
132
  }),
123
133
  );
124
134
  expect(observedNow.MyBucket?.type).toBe("AWS::S3::Bucket");
125
- // Ownership verdicts are total (#1089): DescribeStackResources carries no
126
- // tags, so the verdict is an explicit `unknown`, not a missing field.
127
- expect(observedNow.MyBucket?.ownership).toBe("unknown");
135
+ // Ownership verdicts are total (#1089). The stack answered and carries no
136
+ // chant tags, so the verdict is `foreign` read, not merely unattempted.
137
+ expect(observedNow.MyBucket?.ownership).toBe("foreign");
138
+ expect(observedNow.MyBucket?.marker).toBeUndefined();
128
139
 
129
140
  // Declared "MyQueue" is absent from live → create; live "MyBucket" is
130
141
  // undeclared and unmarked → adopt (never delete without ownership).
@@ -371,16 +382,108 @@ describe("aws lifecycle integration (#163)", () => {
371
382
  { errors: [], warnings: [], entities, outputs: new Map([["aws", ""]]) } as never,
372
383
  { owned: true, stacks: ["prod-net", "prod-data", "prod-app", "prod-edge"] },
373
384
  );
374
- const ownership = result.notes.filter((n) => n.includes("ownership filter unavailable"));
385
+ const ownership = result.notes.filter((n) => n.includes("ownership filter not applied"));
375
386
  expect(ownership).toHaveLength(1);
376
387
  expect(ownership[0]).toBe(
377
- "[aws] ownership filter unavailable on describeResources (no tags from describe-stack-resources) — returning all, each with an explicit `unknown` verdict; use `chant import --from <env> --owned` for ownership-filtered export",
388
+ "[aws] ownership filter not applied on describeResources (this stack's own tags carry no chant marker, or DescribeStacks did not answer) — returning all, each with the verdict the read supports; use `chant import --from <env> --owned` for ownership-filtered export",
378
389
  );
379
- expect(result.warnings.join("\n")).not.toContain("ownership filter unavailable");
390
+ expect(result.warnings.join("\n")).not.toContain("ownership filter not applied");
380
391
  expect(warn).not.toHaveBeenCalled();
381
392
  expect(error).not.toHaveBeenCalled();
382
393
  });
383
394
 
395
+ test("an owned read of a marked stack resolves the marker and carries no note", async () => {
396
+ stubCfn((action) =>
397
+ action === "DescribeStackResources"
398
+ ? { text: stackResourcesXml([{ logicalId: "MyBucket", type: "AWS::S3::Bucket", physicalId: "my-bucket" }]) }
399
+ : { text: stackOutputsXml({}, ownedStackTags("shop", "prod")) },
400
+ );
401
+ const observed = normalizeObservation(
402
+ await awsPlugin.describeResources!({
403
+ environment: "prod",
404
+ buildOutput: "",
405
+ entityNames: ["MyBucket"],
406
+ entities: new Map(),
407
+ owned: true,
408
+ }),
409
+ );
410
+ expect(observed.resources.MyBucket?.ownership).toBe("owned");
411
+ expect(observed.resources.MyBucket?.marker).toEqual({ stack: "shop", env: "prod" });
412
+ expect(observed.notes).toEqual([]);
413
+ });
414
+
415
+ // --- The ownership marker on the thin read (#1998) ---
416
+
417
+ test("a stack tagged for another environment is reported foreign, not unknown", async () => {
418
+ stubCfn((action) =>
419
+ action === "DescribeStackResources"
420
+ ? { text: stackResourcesXml([{ logicalId: "MyBucket", type: "AWS::S3::Bucket", physicalId: "my-bucket" }]) }
421
+ : { text: stackOutputsXml({}, ownedStackTags("shop", "staging")) },
422
+ );
423
+
424
+ const meta = normalizeObservation(
425
+ await awsPlugin.describeResources!({
426
+ environment: "prod",
427
+ buildOutput: "",
428
+ entityNames: ["MyBucket"],
429
+ entities: new Map(),
430
+ }),
431
+ ).resources.MyBucket;
432
+ // The marker is the stack's, read back verbatim — which is what makes the
433
+ // env mismatch visible at all. Before this the field was absent and every
434
+ // caller had to pass the resource through unverified.
435
+ expect(meta?.marker).toEqual({ stack: "shop", env: "staging" });
436
+
437
+ await expect(
438
+ assertLiveEntity({
439
+ plugin: awsPlugin,
440
+ name: "MyBucket",
441
+ entityType: "AWS::S3::Bucket",
442
+ props: {},
443
+ buildOutput: "",
444
+ environment: "prod",
445
+ marker: { stack: "shop", env: "prod" },
446
+ }),
447
+ ).rejects.toThrow(LiveAssertionError);
448
+ });
449
+
450
+ test("assertLive passes when the stack's marker is this deploy's", async () => {
451
+ stubCfn((action) =>
452
+ action === "DescribeStackResources"
453
+ ? { text: stackResourcesXml([{ logicalId: "MyBucket", type: "AWS::S3::Bucket", physicalId: "my-bucket" }]) }
454
+ : { text: stackOutputsXml({}, ownedStackTags("shop", "prod")) },
455
+ );
456
+ const meta = await assertLiveEntity({
457
+ plugin: awsPlugin,
458
+ name: "MyBucket",
459
+ entityType: "AWS::S3::Bucket",
460
+ props: {},
461
+ buildOutput: "",
462
+ environment: "prod",
463
+ marker: { stack: "shop", env: "prod" },
464
+ });
465
+ expect(meta.marker).toEqual({ stack: "shop", env: "prod" });
466
+ expect(meta.ownership).toBe("owned");
467
+ });
468
+
469
+ test("a stack whose tags could not be read stays unknown, never a guess", async () => {
470
+ stubCfn((action) =>
471
+ action === "DescribeStackResources"
472
+ ? { text: stackResourcesXml([{ logicalId: "MyBucket", type: "AWS::S3::Bucket", physicalId: "my-bucket" }]) }
473
+ : { status: 400, text: queryErrorXml("Throttling", "Rate exceeded") },
474
+ );
475
+ const meta = normalizeObservation(
476
+ await awsPlugin.describeResources!({
477
+ environment: "prod",
478
+ buildOutput: "",
479
+ entityNames: ["MyBucket"],
480
+ entities: new Map(),
481
+ }),
482
+ ).resources.MyBucket;
483
+ expect(meta?.ownership).toBe("unknown");
484
+ expect(meta?.marker).toBeUndefined();
485
+ });
486
+
384
487
  test("an unowned read carries no ownership note", async () => {
385
488
  stubCfn((action) =>
386
489
  action === "DescribeStackResources"
@@ -428,10 +531,10 @@ describeObservationConformance({
428
531
  },
429
532
  },
430
533
  {
431
- // describe-stack-resources also carries no tags to read a stack/env
432
- // identity from, so `marker` (#1222) stays absent here stack-level
433
- // handling reads the stack's own tags instead.
434
- name: "a healthy stack read",
534
+ // describe-stack-resources carries no per-resource tags, so a stack whose
535
+ // own tags carry no marker leaves `marker` (#1222) absent — absent means
536
+ // absent, never a guess.
537
+ name: "a healthy read of an unmarked stack",
435
538
  declared: ["MyBucket"],
436
539
  expectPresent: ["MyBucket"],
437
540
  expectNoMarker: ["MyBucket"],
@@ -450,13 +553,54 @@ describeObservationConformance({
450
553
  },
451
554
  },
452
555
  {
453
- // aws declares a marker channel on the deep read and on live export, but
454
- // not here: describe-stack-resources returns no tags, so the filter has
455
- // nothing to filter on. The suite holds it to that — an `owned` verdict
456
- // from this path would be a claim the transport cannot support (#1348).
457
- name: "an owned read on a path with no marker channel",
556
+ // The stack's own tags are this path's marker channel (#1998), so every
557
+ // member of a marked stack carries that stack's identity.
558
+ name: "a healthy read of a marked stack",
458
559
  declared: ["MyBucket"],
459
560
  expectPresent: ["MyBucket"],
561
+ expectMarker: { MyBucket: { stack: "shop", env: "prod" } },
562
+ run: () => {
563
+ stubCfn((action) =>
564
+ action === "DescribeStackResources"
565
+ ? { text: stackResourcesXml([{ logicalId: "MyBucket", type: "AWS::S3::Bucket", physicalId: "my-bucket" }]) }
566
+ : { text: stackOutputsXml({}, ownedStackTags("shop", "prod")) },
567
+ );
568
+ return awsPlugin.describeResources!({
569
+ environment: "prod",
570
+ buildOutput: "",
571
+ entityNames: ["MyBucket"],
572
+ entities: new Map(),
573
+ });
574
+ },
575
+ },
576
+ {
577
+ // aws declares a marker channel here now, so the suite holds the verdict
578
+ // to `owned`/`foreign` — an `unknown` would mean the declaration is a
579
+ // claim the transport does not support (#1348).
580
+ name: "an owned read of a marked stack",
581
+ declared: ["MyBucket"],
582
+ expectPresent: ["MyBucket"],
583
+ owned: true,
584
+ run: () => {
585
+ stubCfn((action) =>
586
+ action === "DescribeStackResources"
587
+ ? { text: stackResourcesXml([{ logicalId: "MyBucket", type: "AWS::S3::Bucket", physicalId: "my-bucket" }]) }
588
+ : { text: stackOutputsXml({}, ownedStackTags("shop", "prod")) },
589
+ );
590
+ return awsPlugin.describeResources!({
591
+ environment: "prod",
592
+ buildOutput: "",
593
+ entityNames: ["MyBucket"],
594
+ entities: new Map(),
595
+ owned: true,
596
+ });
597
+ },
598
+ },
599
+ {
600
+ name: "an owned read of an unmarked stack",
601
+ declared: ["MyBucket"],
602
+ expectPresent: ["MyBucket"],
603
+ expectNoMarker: ["MyBucket"],
460
604
  owned: true,
461
605
  run: () => {
462
606
  stubCfn((action) =>
@@ -1,4 +1,7 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
2
5
  import { createPostSynthContext } from "@intentius/chant-test-utils";
3
6
  import { waw016, checkDeprecatedProperties, type DeprecationBasis } from "./waw016";
4
7
 
@@ -195,4 +198,151 @@ describe("WAW016: Deprecated Property Usage", () => {
195
198
  expect(bySeverity.get("warning")).toContain("AccessControl");
196
199
  expect(bySeverity.get("info")).toContain("ObjectLockConfiguration");
197
200
  });
201
+
202
+ // --- Nested pointer paths (#1988) ---
203
+
204
+ test("a Tags/TagKey entry matches the key inside the Tags array", () => {
205
+ const ctx = makeCtx({
206
+ Resources: {
207
+ Block: {
208
+ Type: "AWS::AppStream::AppBlock",
209
+ Properties: {
210
+ Name: "block",
211
+ Tags: [{ TagKey: "team", TagValue: "platform" }],
212
+ },
213
+ },
214
+ },
215
+ });
216
+ const deprecated = new Map([
217
+ ["AWS::AppStream::AppBlock", basisMap([["Tags/TagKey", "declared"], ["Tags/TagValue", "declared"]])],
218
+ ]);
219
+ const diags = checkDeprecatedProperties(ctx, deprecated);
220
+ expect(diags).toHaveLength(2);
221
+ expect(diags.map((d) => d.message).join(" ")).toContain("Tags/TagKey");
222
+ expect(diags[0].severity).toBe("warning");
223
+ expect(diags[0].entity).toBe("Block");
224
+ });
225
+
226
+ test("a Tags array carrying only the current keys is clean", () => {
227
+ const ctx = makeCtx({
228
+ Resources: {
229
+ Block: {
230
+ Type: "AWS::AppStream::AppBlock",
231
+ Properties: { Tags: [{ Key: "team", Value: "platform" }] },
232
+ },
233
+ },
234
+ });
235
+ const deprecated = new Map([
236
+ ["AWS::AppStream::AppBlock", basisMap([["Tags/TagKey", "declared"]])],
237
+ ]);
238
+ expect(checkDeprecatedProperties(ctx, deprecated)).toHaveLength(0);
239
+ });
240
+
241
+ test("a nested object path matches", () => {
242
+ const ctx = makeCtx({
243
+ Resources: {
244
+ Flow: {
245
+ Type: "AWS::MediaConnect::Flow",
246
+ Properties: {
247
+ Source: { Name: "src", Decryption: { Url: "https://example.invalid" } },
248
+ },
249
+ },
250
+ },
251
+ });
252
+ const deprecated = new Map([
253
+ ["AWS::MediaConnect::Flow", basisMap([["Source/Decryption/Url", "declared"], ["Source/SenderIpAddress", "declared"]])],
254
+ ]);
255
+ const diags = checkDeprecatedProperties(ctx, deprecated);
256
+ expect(diags).toHaveLength(1);
257
+ expect(diags[0].message).toContain("Source/Decryption/Url");
258
+ });
259
+
260
+ test("a wildcard segment requires an array member that carries the property", () => {
261
+ const deprecated = new Map([
262
+ ["AWS::EC2::SecurityGroup", basisMap([["SecurityGroupEgress/*/SourceSecurityGroupId", "declared"]])],
263
+ ]);
264
+ const hit = makeCtx({
265
+ Resources: {
266
+ Sg: {
267
+ Type: "AWS::EC2::SecurityGroup",
268
+ Properties: {
269
+ SecurityGroupEgress: [{ IpProtocol: "tcp" }, { IpProtocol: "tcp", SourceSecurityGroupId: "sg-1" }],
270
+ },
271
+ },
272
+ },
273
+ });
274
+ expect(checkDeprecatedProperties(hit, deprecated)).toHaveLength(1);
275
+
276
+ const miss = makeCtx({
277
+ Resources: {
278
+ Sg: {
279
+ Type: "AWS::EC2::SecurityGroup",
280
+ Properties: { SecurityGroupEgress: [{ IpProtocol: "tcp" }] },
281
+ },
282
+ },
283
+ });
284
+ expect(checkDeprecatedProperties(miss, deprecated)).toHaveLength(0);
285
+ });
286
+
287
+ test("a nested path is not matched by a same-named top-level key", () => {
288
+ const ctx = makeCtx({
289
+ Resources: {
290
+ Insight: {
291
+ Type: "AWS::SecurityHub::Insight",
292
+ Properties: { Keyword: "unused-top-level" },
293
+ },
294
+ },
295
+ });
296
+ const deprecated = new Map([
297
+ ["AWS::SecurityHub::Insight", basisMap([["Filters/Keyword", "declared"]])],
298
+ ]);
299
+ expect(checkDeprecatedProperties(ctx, deprecated)).toHaveLength(0);
300
+ });
301
+ });
302
+
303
+ /**
304
+ * The reachability count (#1988). Over half the declared names carry a nested
305
+ * pointer path, and a flattening regression makes them silently unmatchable
306
+ * rather than failing anything — so the count of unreachable names is pinned
307
+ * against the real generated data.
308
+ */
309
+ describe("WAW016: declared deprecation reachability", () => {
310
+ const pkgDir = dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url)))));
311
+ const lexiconPath = join(pkgDir, "src", "generated", "lexicon-aws.json");
312
+ const hasGenerated = existsSync(lexiconPath);
313
+
314
+ interface Entry {
315
+ kind?: string;
316
+ resourceType?: string;
317
+ deprecatedProperties?: string[];
318
+ inferredDeprecations?: string[];
319
+ }
320
+
321
+ /** A path a template can express: named segments and array wildcards, no JSON Schema keywords. */
322
+ function isTemplatePath(path: string): boolean {
323
+ const segments = path.split("/");
324
+ if (segments.length === 0 || segments[0] === "" || segments[0] === "*") return false;
325
+ return segments.every((s) => s === "*" || (s.length > 0 && s !== "properties" && s !== "definitions"));
326
+ }
327
+
328
+ test.skipIf(!hasGenerated)("every declared deprecated name is a path a template can express", () => {
329
+ const data = JSON.parse(readFileSync(lexiconPath, "utf-8")) as Record<string, Entry>;
330
+ const declared: string[] = [];
331
+ for (const entry of Object.values(data)) {
332
+ if (entry.kind !== "resource" || !entry.resourceType) continue;
333
+ const inferred = new Set(entry.inferredDeprecations ?? []);
334
+ for (const name of entry.deprecatedProperties ?? []) {
335
+ if (!inferred.has(name)) declared.push(`${entry.resourceType} :: ${name}`);
336
+ }
337
+ }
338
+
339
+ const nested = declared.filter((d) => d.split(" :: ")[1].includes("/"));
340
+ const unreachable = declared.filter((d) => !isTemplatePath(d.split(" :: ")[1]));
341
+
342
+ // The spec pin moves, so the totals are a floor rather than an exact
343
+ // figure; the unreachable count is the one that must stay at zero.
344
+ expect(declared.length).toBeGreaterThan(0);
345
+ expect(nested.length).toBeGreaterThan(declared.length / 2);
346
+ expect(unreachable).toEqual([]);
347
+ });
198
348
  });
@@ -9,6 +9,10 @@
9
9
  * which also matches descriptions that mention the deprecation of a sibling
10
10
  * property, an enum value, or the thing the property configures. Declared is a
11
11
  * warning; inferred is reported at info and worded as a guess.
12
+ *
13
+ * A declared name is a flattened Registry pointer path (#1988), so most of them
14
+ * are nested — `Tags/TagKey`, `Source/Decryption/Url`. The template is walked
15
+ * along that path; matching top-level keys reached fewer than half of them.
12
16
  */
13
17
 
14
18
  import { readFileSync } from "fs";
@@ -56,6 +60,28 @@ function loadDeprecatedProperties(): Map<string, Map<string, DeprecationBasis>>
56
60
  return map;
57
61
  }
58
62
 
63
+ /**
64
+ * True when `value` expresses `segments` — the flattened Registry pointer path
65
+ * (#1988), which is a single key for a top-level property and `/`-joined for a
66
+ * nested one.
67
+ *
68
+ * An array met where the pointer named a property is descended anyway: the
69
+ * Registry elides the wildcard in some schemas, so `Tags/TagKey` addresses a
70
+ * key inside the Tags array's items.
71
+ */
72
+ function expressesPath(value: unknown, segments: string[]): boolean {
73
+ if (segments.length === 0) return true;
74
+ if (Array.isArray(value)) {
75
+ const rest = segments[0] === "*" ? segments.slice(1) : segments;
76
+ return value.some((item) => expressesPath(item, rest));
77
+ }
78
+ if (segments[0] === "*") return false;
79
+ if (typeof value !== "object" || value === null) return false;
80
+ const obj = value as Record<string, unknown>;
81
+ if (!Object.prototype.hasOwnProperty.call(obj, segments[0])) return false;
82
+ return expressesPath(obj[segments[0]], segments.slice(1));
83
+ }
84
+
59
85
  /**
60
86
  * Core detection logic — exported for direct testing with synthetic data.
61
87
  */
@@ -76,9 +102,11 @@ export function checkDeprecatedProperties(
76
102
  if (!deprProps) continue;
77
103
 
78
104
  const props = resource.Properties ?? {};
79
- for (const propName of Object.keys(props)) {
80
- const basis = deprProps.get(propName);
81
- if (!basis) continue;
105
+ // Walk each declared path into the template, rather than matching
106
+ // top-level template keys: over half the declared names are nested
107
+ // (#1988), and a key comparison could never reach them.
108
+ for (const [propName, basis] of deprProps) {
109
+ if (!expressesPath(props, propName.split("/"))) continue;
82
110
 
83
111
  diagnostics.push({
84
112
  checkId: "WAW016",
package/src/plugin.ts CHANGED
@@ -19,7 +19,7 @@ import { applyAwsEndpointArgv } from "./components/cloud-executor";
19
19
  import { stackDoesNotExist } from "./stack-errors";
20
20
  import {
21
21
  AwsReadError,
22
- describeStackOutputs,
22
+ describeStackDetail,
23
23
  describeStackResources,
24
24
  type AwsReadClientOptions,
25
25
  type StackResource,
@@ -37,6 +37,7 @@ import { parseStackTemplate } from "./import/live-export";
37
37
  import { awsCompletions } from "./lsp/completions";
38
38
  import { awsHover } from "./lsp/hover";
39
39
  import { AWS_TAG_OWNERSHIP_KEYS } from "./ownership";
40
+ import { readOwnership } from "@intentius/chant/ownership";
40
41
 
41
42
  /** Re-exported from ./stack-errors so the long-standing import path (and its
42
43
  * tests) keep working now that the deep reader shares the classifier. */
@@ -49,19 +50,24 @@ export { stackDoesNotExist } from "./stack-errors";
49
50
  * import parsing, and code generation for AWS CloudFormation.
50
51
  */
51
52
  /**
52
- * describe-stack-resources returns no tags, so the ownership filter cannot be
53
- * applied on this read path. Returned as a run-level note rather than printed
54
- * (#1265): core says it once per run, after the answer, however many stacks
55
- * were read. The text is the contract consumers grep for.
53
+ * The stack's own tags did not resolve a marker for this read, so `owned: true`
54
+ * withheld nothing. Returned as a run-level note rather than printed (#1265):
55
+ * core says it once per run, after the answer, however many stacks were read.
56
+ * The text is the contract consumers grep for.
56
57
  */
57
- const OWNERSHIP_UNAVAILABLE_NOTE =
58
- "ownership filter unavailable on describeResources (no tags from describe-stack-resources) — returning all, each with an explicit `unknown` verdict; use `chant import --from <env> --owned` for ownership-filtered export";
58
+ const OWNERSHIP_UNRESOLVED_NOTE =
59
+ "ownership filter not applied on describeResources (this stack's own tags carry no chant marker, or DescribeStacks did not answer) — returning all, each with the verdict the read supports; use `chant import --from <env> --owned` for ownership-filtered export";
59
60
 
60
61
  export const awsPlugin: LexiconPlugin = {
61
62
  name: "aws",
62
- // The thin read is sourced from describe-stack-resources, which returns no
63
- // tags so `owned` on that path can only answer `unknown`, and does (#1348).
64
- ownershipChannel: { keys: AWS_TAG_OWNERSHIP_KEYS, reads: ["observeResourcesDeep", "exportResources"] },
63
+ // The thin read's marker channel is the STACK's own tags (#1998), read off
64
+ // the DescribeStacks call it already makes for the outputs.
65
+ // describe-stack-resources carries no per-resource tags, so the stack is the
66
+ // granularity — the same boundary teardown verifies on (#1222).
67
+ ownershipChannel: {
68
+ keys: AWS_TAG_OWNERSHIP_KEYS,
69
+ reads: ["describeResources", "observeResourcesDeep", "exportResources"],
70
+ },
65
71
  serializer: awsSerializer,
66
72
  // Local emulator (#920): Floci + the AWS env that redirects the SDK / observe.
67
73
  emulator: FLOCI_EMULATOR,
@@ -568,14 +574,11 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
568
574
  ...(options.region ? { region: options.region } : {}),
569
575
  };
570
576
 
571
- // describe-stack-resources does not return tags, so ownership cannot be
572
- // determined here. Degrade to detect-only rather than silently filtering,
573
- // and say so as a note on the observation. It is a property of the
574
- // environment, not of each stack: a four-stack project once printed four
575
- // identical copies ahead of every answer (#1265) — enough that an agent
576
- // piping `graph --format ir` with `2>&1` had to skip lines to find the
577
- // JSON. Core dedupes the note across stacks and prints it with the footer.
578
- const notes = options.owned ? [OWNERSHIP_UNAVAILABLE_NOTE] : undefined;
577
+ // The note for the read paths that never reach the stack's tags at all: a
578
+ // missing stack, and a failed stack read. Core dedupes it across stacks and
579
+ // prints it once per run with the footer, rather than ahead of every answer
580
+ // (#1265).
581
+ const unresolvedNotes = options.owned ? [OWNERSHIP_UNRESOLVED_NOTE] : undefined;
579
582
 
580
583
  // Derive stack name. A multi-stack project passes the explicit CloudFormation
581
584
  // stack this observation targets (see `stacks` in ChantConfig); otherwise the
@@ -614,7 +617,7 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
614
617
  { ...resources, ...identity.resources, ...receiptObs.resources },
615
618
  receiptHoles,
616
619
  identity.queried,
617
- notes,
620
+ unresolvedNotes,
618
621
  );
619
622
  }
620
623
  // Any other failure (credentials, throttling, a region that can't be
@@ -636,23 +639,41 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
636
639
  { ...receiptObs.resources },
637
640
  { ...stackHoles, ...receiptObs.unobserved },
638
641
  undefined,
639
- notes,
642
+ unresolvedNotes,
640
643
  );
641
644
  }
642
645
 
643
646
  // Map logical names from build to stack resources
644
647
  const stackResourceMap = new Map(stackResources.map((r) => [r.logicalId, r]));
645
648
 
646
- // Get stack outputs. A stack whose resources read fine but whose outputs do
647
- // not is still a usable observation, so this failure is swallowed exactly as
648
- // the non-zero exit code used to be.
649
+ // Get the stack's outputs and its own tags one DescribeStacks call. A
650
+ // stack whose resources read fine but whose DescribeStacks does not is
651
+ // still a usable observation, so this failure is swallowed exactly as the
652
+ // non-zero exit code used to be. `undefined` tags mean the call did not
653
+ // answer, which is an unread channel rather than an absent marker.
649
654
  let stackOutputs: Record<string, string> = {};
655
+ let stackTags: Record<string, string> | undefined;
650
656
  try {
651
- stackOutputs = await describeStackOutputs(stackName, client);
657
+ const detail = await describeStackDetail(stackName, client);
658
+ stackOutputs = detail.outputs;
659
+ stackTags = detail.tags;
652
660
  } catch {
653
661
  stackOutputs = {};
662
+ stackTags = undefined;
654
663
  }
655
664
 
665
+ // The marker channel on this path (#1998). describe-stack-resources carries
666
+ // no per-resource tags, but the stack's own tags carry the identity the
667
+ // apply paths stamped from the template's `Metadata["chant:ownership"]`
668
+ // block — the same channel teardown verifies on (#1222). Every member of a
669
+ // marked stack belongs to that stack's identity, so the verdict and the
670
+ // marker are the stack's, read back verbatim.
671
+ const stackMarker = stackTags ? readOwnership(stackTags, AWS_TAG_OWNERSHIP_KEYS) : undefined;
672
+ const stackOwnership: ResourceMetadata["ownership"] =
673
+ stackTags === undefined ? "unknown" : stackMarker ? "owned" : "foreign";
674
+ // `owned: true` withheld nothing, so say why whenever the marker did not resolve.
675
+ const notes = options.owned && stackMarker === undefined ? unresolvedNotes : undefined;
676
+
656
677
  // The outputs are the stack's, not any member's (#1279). They used to be
657
678
  // copied onto every resource's `attributes`, so a VPC carried the stack's
658
679
  // `expWebIp` and no `CidrBlock`. They ride the envelope once, keyed by the
@@ -672,14 +693,13 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
672
693
  physicalId: stackResource.physicalId ?? "",
673
694
  status: stackResource.status ?? "",
674
695
  lastUpdated: stackResource.timestamp ?? "",
675
- // Total verdict (#1089): describe-stack-resources returns no tags, so
676
- // this path cannot read the ownership marker. Say `unknown` explicitly
677
- // rather than leaving the field off and letting each consumer guess
678
- // the change set never escalates `unknown` to a delete.
679
- // For the same reason `marker` (#1222) stays absent here: no tags, no
680
- // stack/env identity to read, and absent means absent — never a guess.
681
- // aws teardown is stack-level and reads the stack's own tags instead.
682
- ownership: "unknown",
696
+ // Total verdict (#1089), from the stack's own tags. `unknown` is kept
697
+ // for the one case that earns it DescribeStacks did not answer, so
698
+ // the channel was never read and the change set never escalates
699
+ // `unknown` to a delete. `marker` is set only where the stack really
700
+ // carries one: absent means absent, never a guess.
701
+ ownership: stackOwnership,
702
+ ...(stackMarker ? { marker: stackMarker } : {}),
683
703
  };
684
704
  }
685
705
 
@@ -3,7 +3,9 @@ import {
3
3
  parseCFNSchema,
4
4
  cfnShortName,
5
5
  cfnServiceName,
6
+ stripPointerPath,
6
7
  } from "./parse";
8
+ import type { CFNSchema } from "./fetch";
7
9
 
8
10
  // Sample Registry JSON Schema for testing
9
11
  const sampleBucketSchema = JSON.stringify({
@@ -401,6 +403,84 @@ describe("parseCFNSchema", () => {
401
403
  });
402
404
  });
403
405
 
406
+ describe("stripPointerPath (#1988)", () => {
407
+ const schema = {
408
+ typeName: "AWS::CloudFront::ContinuousDeploymentPolicy",
409
+ properties: {
410
+ ContinuousDeploymentPolicyConfig: { $ref: "#/definitions/ContinuousDeploymentPolicyConfig" },
411
+ Id: { type: "string" },
412
+ },
413
+ definitions: {
414
+ ContinuousDeploymentPolicyConfig: {
415
+ type: "object",
416
+ properties: {
417
+ Enabled: { type: "boolean" },
418
+ TrafficConfig: { $ref: "#/definitions/TrafficConfig" },
419
+ },
420
+ },
421
+ TrafficConfig: {
422
+ type: "object",
423
+ properties: { Type: { type: "string" } },
424
+ },
425
+ },
426
+ } as unknown as CFNSchema;
427
+
428
+ test("strips the top-level prefix", () => {
429
+ expect(stripPointerPath("/properties/BucketName")).toBe("BucketName");
430
+ });
431
+
432
+ test("keeps a nested path as a template writes it", () => {
433
+ expect(stripPointerPath("/properties/Tags/TagKey")).toBe("Tags/TagKey");
434
+ expect(stripPointerPath("/properties/Source/Decryption/Url")).toBe("Source/Decryption/Url");
435
+ });
436
+
437
+ test("drops a mid-path properties keyword", () => {
438
+ expect(stripPointerPath("/properties/DistributionConfig/properties/S3Origin")).toBe(
439
+ "DistributionConfig/S3Origin",
440
+ );
441
+ expect(stripPointerPath("/properties/EncryptionAtRestOptions/properties")).toBe(
442
+ "EncryptionAtRestOptions",
443
+ );
444
+ });
445
+
446
+ test("keeps array wildcards", () => {
447
+ expect(stripPointerPath("/properties/SecurityGroupEgress/*/SourceSecurityGroupId")).toBe(
448
+ "SecurityGroupEgress/*/SourceSecurityGroupId",
449
+ );
450
+ });
451
+
452
+ test("resolves a definition-scoped pointer through the property that carries it", () => {
453
+ expect(stripPointerPath("/definitions/ContinuousDeploymentPolicyConfig/properties/Enabled", schema)).toBe(
454
+ "ContinuousDeploymentPolicyConfig/Enabled",
455
+ );
456
+ expect(stripPointerPath("/definitions/TrafficConfig/properties/Type", schema)).toBe(
457
+ "ContinuousDeploymentPolicyConfig/TrafficConfig/Type",
458
+ );
459
+ });
460
+
461
+ test("leaves a definition nothing declared reaches as the Registry wrote it", () => {
462
+ expect(stripPointerPath("/definitions/Orphan/properties/Type", schema)).toBe(
463
+ "/definitions/Orphan/properties/Type",
464
+ );
465
+ expect(stripPointerPath("/definitions/Orphan/properties/Type")).toBe(
466
+ "/definitions/Orphan/properties/Type",
467
+ );
468
+ });
469
+
470
+ test("a definition reached through an array carries the wildcard", () => {
471
+ const arraySchema = {
472
+ typeName: "AWS::Test::Thing",
473
+ properties: { Rules: { type: "array", items: { $ref: "#/definitions/Rule" } } },
474
+ definitions: { Rule: { type: "object", properties: { Legacy: { type: "string" } } } },
475
+ } as unknown as CFNSchema;
476
+ expect(stripPointerPath("/definitions/Rule/properties/Legacy", arraySchema)).toBe("Rules/*/Legacy");
477
+ });
478
+
479
+ test("passes a non-pointer through", () => {
480
+ expect(stripPointerPath("BucketName")).toBe("BucketName");
481
+ });
482
+ });
483
+
404
484
  describe("cfnShortName", () => {
405
485
  test("extracts short name from full type", () => {
406
486
  expect(cfnShortName("AWS::S3::Bucket")).toBe("Bucket");
package/src/spec/parse.ts CHANGED
@@ -119,7 +119,7 @@ export function parseCFNSchema(data: string | Buffer): SchemaParseResult {
119
119
  const attrs: ParsedAttribute[] = [];
120
120
  const seenAttrs = new Set<string>();
121
121
  for (const path of schema.readOnlyProperties ?? []) {
122
- const attrName = stripPointerPath(path);
122
+ const attrName = stripPointerPath(path, schema);
123
123
 
124
124
  // Flatten nested paths: "Endpoint/Address" → attr name "Endpoint.Address"
125
125
  const cfnAttr = attrName.replace(/\//g, ".");
@@ -180,7 +180,7 @@ export function parseCFNSchema(data: string | Buffer): SchemaParseResult {
180
180
  // can mention the deprecation of a sibling property, of an enum value, or of
181
181
  // something the property merely configures.
182
182
  const deprecatedSet = new Set<string>(
183
- stripPointerPaths(schema.deprecatedProperties ?? []),
183
+ stripPointerPaths(schema.deprecatedProperties ?? [], schema),
184
184
  );
185
185
  const inferredDeprecations: string[] = [];
186
186
 
@@ -226,12 +226,12 @@ export function parseCFNSchema(data: string | Buffer): SchemaParseResult {
226
226
  typeName: schema.typeName,
227
227
  properties: props,
228
228
  attributes: attrs,
229
- createOnly: stripPointerPaths(schema.createOnlyProperties ?? []),
230
- writeOnly: stripPointerPaths(schema.writeOnlyProperties ?? []),
231
- primaryIdentifier: stripPointerPaths(schema.primaryIdentifier ?? []),
229
+ createOnly: stripPointerPaths(schema.createOnlyProperties ?? [], schema),
230
+ writeOnly: stripPointerPaths(schema.writeOnlyProperties ?? [], schema),
231
+ primaryIdentifier: stripPointerPaths(schema.primaryIdentifier ?? [], schema),
232
232
  deprecatedProperties: [...deprecatedSet],
233
233
  inferredDeprecations,
234
- conditionalCreateOnly: stripPointerPaths(schema.conditionalCreateOnlyProperties ?? []),
234
+ conditionalCreateOnly: stripPointerPaths(schema.conditionalCreateOnlyProperties ?? [], schema),
235
235
  ...(replacementStrategy && { replacementStrategy }),
236
236
  ...(tagging && { tagging }),
237
237
  },
@@ -277,15 +277,88 @@ export function cfnServiceName(typeName: string): string {
277
277
  return parts.length >= 2 ? parts[1] : typeName;
278
278
  }
279
279
 
280
+ /** Bound on the `$ref` walk that resolves a definition-scoped pointer. */
281
+ const MAX_DEFINITION_PATH_DEPTH = 6;
282
+
283
+ /** The definition a `#/definitions/Name` ref names, or undefined for anything else. */
284
+ function refDefinitionName(ref: string | undefined): string | undefined {
285
+ const prefix = "#/definitions/";
286
+ return ref?.startsWith(prefix) ? ref.slice(prefix.length) : undefined;
287
+ }
288
+
289
+ function pushProperties(
290
+ queue: Array<{ path: string[]; node: SchemaProperty }>,
291
+ path: string[],
292
+ properties: Record<string, SchemaProperty>,
293
+ ): void {
294
+ for (const [name, prop] of Object.entries(properties)) queue.push({ path: [...path, name], node: prop });
295
+ }
296
+
280
297
  /**
281
- * Strip JSON pointer path prefix: "/properties/BucketName" "BucketName"
298
+ * The property path that reaches `defName`, searched breadth-first from the
299
+ * top-level properties so the shortest one wins. Undefined when nothing
300
+ * declared reaches the definition; the pointer is then left as the Registry
301
+ * wrote it rather than naming a key no template has.
282
302
  */
283
- function stripPointerPath(path: string): string {
284
- const prefix = "/properties/";
285
- return path.startsWith(prefix) ? path.slice(prefix.length) : path;
303
+ function definitionPropertyPath(defName: string, schema: CFNSchema): string | undefined {
304
+ const defs = schema.definitions ?? {};
305
+ const expanded = new Set<string>();
306
+ const queue: Array<{ path: string[]; node: SchemaProperty }> = [];
307
+ pushProperties(queue, [], schema.properties ?? {});
308
+
309
+ while (queue.length > 0) {
310
+ const { path, node } = queue.shift()!;
311
+ if (path.length > MAX_DEFINITION_PATH_DEPTH) continue;
312
+
313
+ const ref = refDefinitionName(node.$ref);
314
+ if (ref === defName) return path.join("/");
315
+ if (ref !== undefined) {
316
+ // Shortest path wins, so a definition already reached needs no second visit.
317
+ if (expanded.has(ref)) continue;
318
+ expanded.add(ref);
319
+ const def = defs[ref];
320
+ if (def?.properties) pushProperties(queue, path, def.properties);
321
+ continue;
322
+ }
323
+
324
+ if (node.items) {
325
+ queue.push({ path: [...path, "*"], node: node.items });
326
+ continue;
327
+ }
328
+ if (node.properties) pushProperties(queue, path, node.properties);
329
+ }
330
+ return undefined;
331
+ }
332
+
333
+ /**
334
+ * Flatten a CloudFormation Registry JSON pointer to the property path a
335
+ * template expresses, `/`-joined.
336
+ *
337
+ * Three shapes occur upstream. `/properties/BucketName` is the plain one.
338
+ * A pointer can re-enter the `properties` keyword mid-path
339
+ * (`/properties/DistributionConfig/properties/S3Origin`), which a template
340
+ * never writes. And a pointer can be scoped to a shared definition
341
+ * (`/definitions/ContinuousDeploymentPolicyConfig/properties/Type`), which a
342
+ * template expresses only through whichever property carries that definition.
343
+ *
344
+ * Array positions stay as `*`. `readOnlyProperties` attribute names are
345
+ * generated from this output, so dropping the wildcard would rename them.
346
+ */
347
+ export function stripPointerPath(path: string, schema?: CFNSchema): string {
348
+ if (!path.startsWith("/")) return path;
349
+ const segments = path.split("/").filter((s) => s.length > 0);
350
+
351
+ if (segments[0] === "definitions") {
352
+ const prefix = segments[1] && schema ? definitionPropertyPath(segments[1], schema) : undefined;
353
+ if (prefix === undefined) return path;
354
+ return [prefix, ...segments.slice(2).filter((s) => s !== "properties")].join("/");
355
+ }
356
+
357
+ if (segments[0] !== "properties") return path;
358
+ return segments.slice(1).filter((s) => s !== "properties").join("/");
286
359
  }
287
360
 
288
- function stripPointerPaths(paths: string[]): string[] {
361
+ function stripPointerPaths(paths: string[], schema?: CFNSchema): string[] {
289
362
  if (paths.length === 0) return [];
290
- return paths.map(stripPointerPath);
363
+ return paths.map((p) => stripPointerPath(p, schema));
291
364
  }