@intentius/chant-lexicon-aws 0.46.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/dist/api/read-client.d.ts +10 -2
  2. package/dist/api/read-client.d.ts.map +1 -1
  3. package/dist/codegen/generate-lexicon.d.ts +5 -0
  4. package/dist/codegen/generate-lexicon.d.ts.map +1 -1
  5. package/dist/components/host-delivery.d.ts +4 -3
  6. package/dist/components/host-delivery.d.ts.map +1 -1
  7. package/dist/composites/agentcore-agent.d.ts +26 -1
  8. package/dist/composites/agentcore-agent.d.ts.map +1 -1
  9. package/dist/composites/bucket-deployment.d.ts +82 -0
  10. package/dist/composites/bucket-deployment.d.ts.map +1 -0
  11. package/dist/composites/dynamodb-table.d.ts +45 -0
  12. package/dist/composites/dynamodb-table.d.ts.map +1 -0
  13. package/dist/composites/ec2-instance-bundle.d.ts +61 -0
  14. package/dist/composites/ec2-instance-bundle.d.ts.map +1 -0
  15. package/dist/composites/ecr-repository.d.ts +49 -0
  16. package/dist/composites/ecr-repository.d.ts.map +1 -0
  17. package/dist/composites/fargate-service.d.ts +16 -0
  18. package/dist/composites/fargate-service.d.ts.map +1 -1
  19. package/dist/composites/index.d.ts +16 -2
  20. package/dist/composites/index.d.ts.map +1 -1
  21. package/dist/composites/monitoring-stack.d.ts +99 -0
  22. package/dist/composites/monitoring-stack.d.ts.map +1 -0
  23. package/dist/composites/nlb-service.d.ts +48 -0
  24. package/dist/composites/nlb-service.d.ts.map +1 -0
  25. package/dist/composites/step-functions-workflow.d.ts +36 -0
  26. package/dist/composites/step-functions-workflow.d.ts.map +1 -0
  27. package/dist/deep-observe.d.ts +45 -0
  28. package/dist/deep-observe.d.ts.map +1 -1
  29. package/dist/deep-topology.d.ts +104 -0
  30. package/dist/deep-topology.d.ts.map +1 -0
  31. package/dist/effect-receipt-row.d.ts +92 -0
  32. package/dist/effect-receipt-row.d.ts.map +1 -0
  33. package/dist/index.d.ts +7 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/integrity.json +14 -6
  36. package/dist/lint/audit-catalog.d.ts.map +1 -1
  37. package/dist/lint/post-synth/cf-refs.d.ts +17 -0
  38. package/dist/lint/post-synth/cf-refs.d.ts.map +1 -1
  39. package/dist/lint/post-synth/index.d.ts.map +1 -1
  40. package/dist/lint/post-synth/waw016.d.ts +10 -2
  41. package/dist/lint/post-synth/waw016.d.ts.map +1 -1
  42. package/dist/lint/post-synth/waw061.d.ts +19 -0
  43. package/dist/lint/post-synth/waw061.d.ts.map +1 -0
  44. package/dist/lint/post-synth/waw062.d.ts +19 -0
  45. package/dist/lint/post-synth/waw062.d.ts.map +1 -0
  46. package/dist/lint/post-synth/waw063.d.ts +35 -0
  47. package/dist/lint/post-synth/waw063.d.ts.map +1 -0
  48. package/dist/lint/post-synth/waw064.d.ts +17 -0
  49. package/dist/lint/post-synth/waw064.d.ts.map +1 -0
  50. package/dist/lint/post-synth/waw065.d.ts +23 -0
  51. package/dist/lint/post-synth/waw065.d.ts.map +1 -0
  52. package/dist/lint/post-synth/waw066.d.ts +25 -0
  53. package/dist/lint/post-synth/waw066.d.ts.map +1 -0
  54. package/dist/lint/post-synth/waw067.d.ts +24 -0
  55. package/dist/lint/post-synth/waw067.d.ts.map +1 -0
  56. package/dist/lint/post-synth/waw068.d.ts +20 -0
  57. package/dist/lint/post-synth/waw068.d.ts.map +1 -0
  58. package/dist/manifest.json +1 -1
  59. package/dist/meta.json +34 -0
  60. package/dist/okf/index.md +8 -0
  61. package/dist/okf/rules/WAW061.md +17 -0
  62. package/dist/okf/rules/WAW062.md +27 -0
  63. package/dist/okf/rules/WAW063.md +19 -0
  64. package/dist/okf/rules/WAW064.md +18 -0
  65. package/dist/okf/rules/WAW065.md +25 -0
  66. package/dist/okf/rules/WAW066.md +22 -0
  67. package/dist/okf/rules/WAW067.md +20 -0
  68. package/dist/okf/rules/WAW068.md +19 -0
  69. package/dist/okf/types/Action.md +1 -0
  70. package/dist/okf/types/AutoScalingGroup.md +1 -0
  71. package/dist/okf/types/Bucket.md +1 -0
  72. package/dist/okf/types/EC2Route.md +2 -0
  73. package/dist/okf/types/ECRRepository.md +1 -0
  74. package/dist/okf/types/Function.md +1 -0
  75. package/dist/okf/types/IamPolicy.md +1 -0
  76. package/dist/okf/types/InternetGateway.md +4 -0
  77. package/dist/okf/types/LaunchTemplate.md +1 -0
  78. package/dist/okf/types/LoadBalancer.md +1 -0
  79. package/dist/okf/types/ManagedPolicy.md +2 -0
  80. package/dist/okf/types/Map.md +5 -0
  81. package/dist/okf/types/NatGateway.md +4 -0
  82. package/dist/okf/types/Role.md +2 -0
  83. package/dist/okf/types/RouteTable.md +4 -0
  84. package/dist/okf/types/SecurityGroup.md +1 -0
  85. package/dist/okf/types/StateMachine.md +4 -0
  86. package/dist/okf/types/Subnet.md +6 -0
  87. package/dist/okf/types/SubnetRouteTableAssociation.md +5 -0
  88. package/dist/okf/types/Table.md +4 -0
  89. package/dist/okf/types/TransitGateway.md +6 -0
  90. package/dist/okf/types/TransitGatewayAttachment.md +4 -0
  91. package/dist/okf/types/TransitGatewayPeeringAttachment.md +4 -0
  92. package/dist/okf/types/TransitGatewayRoute.md +5 -0
  93. package/dist/okf/types/TransitGatewayRouteTable.md +4 -0
  94. package/dist/okf/types/TransitGatewayRouteTableAssociation.md +4 -0
  95. package/dist/okf/types/TransitGatewayRouteTablePropagation.md +4 -0
  96. package/dist/okf/types/TransitGatewayVpcAttachment.md +4 -0
  97. package/dist/okf/types/Type.md +8 -0
  98. package/dist/okf/types/VPNConnection.md +4 -0
  99. package/dist/okf/types/VPNGateway.md +4 -0
  100. package/dist/okf/types/Vpc.md +1 -0
  101. package/dist/op/activities/index.d.ts +6 -0
  102. package/dist/op/activities/index.d.ts.map +1 -1
  103. package/dist/op/builders.d.ts +47 -0
  104. package/dist/op/builders.d.ts.map +1 -0
  105. package/dist/plugin.d.ts.map +1 -1
  106. package/dist/receipt-store.d.ts +109 -0
  107. package/dist/receipt-store.d.ts.map +1 -0
  108. package/dist/rules/cf-refs.ts +40 -0
  109. package/dist/rules/waw016.ts +37 -16
  110. package/dist/rules/waw061.ts +67 -0
  111. package/dist/rules/waw062.ts +0 -0
  112. package/dist/rules/waw063.ts +0 -0
  113. package/dist/rules/waw064.ts +52 -0
  114. package/dist/rules/waw065.ts +110 -0
  115. package/dist/rules/waw066.ts +155 -0
  116. package/dist/rules/waw067.ts +114 -0
  117. package/dist/rules/waw068.ts +78 -0
  118. package/dist/serializer.d.ts.map +1 -1
  119. package/dist/skills/chant-aws-carve-terraform.md +26 -23
  120. package/dist/spec/parse.d.ts +7 -0
  121. package/dist/spec/parse.d.ts.map +1 -1
  122. package/package.json +3 -3
  123. package/src/api/read-client.test.ts +9 -0
  124. package/src/api/read-client.ts +15 -2
  125. package/src/codegen/fallback.ts +1 -0
  126. package/src/codegen/generate-lexicon.ts +6 -0
  127. package/src/codegen/sam.ts +9 -0
  128. package/src/components/host-delivery.ts +4 -3
  129. package/src/composites/agentcore-agent.ts +35 -13
  130. package/src/composites/bucket-deployment.test.ts +84 -0
  131. package/src/composites/bucket-deployment.ts +153 -0
  132. package/src/composites/composites.test.ts +325 -0
  133. package/src/composites/dynamodb-table.test.ts +154 -0
  134. package/src/composites/dynamodb-table.ts +124 -0
  135. package/src/composites/ec2-instance-bundle.ts +134 -0
  136. package/src/composites/ecr-repository.test.ts +118 -0
  137. package/src/composites/ecr-repository.ts +117 -0
  138. package/src/composites/fargate-service.ts +19 -3
  139. package/src/composites/index.ts +21 -2
  140. package/src/composites/monitoring-stack.test.ts +130 -0
  141. package/src/composites/monitoring-stack.ts +164 -0
  142. package/src/composites/nlb-service.ts +117 -0
  143. package/src/composites/step-functions-workflow.ts +127 -0
  144. package/src/deep-observe.test.ts +201 -1
  145. package/src/deep-observe.ts +204 -0
  146. package/src/deep-topology.test.ts +392 -0
  147. package/src/deep-topology.ts +237 -0
  148. package/src/effect-receipt-row.test.ts +164 -0
  149. package/src/effect-receipt-row.ts +147 -0
  150. package/src/generated/lexicon-aws.json +34 -0
  151. package/src/index.ts +36 -7
  152. package/src/lint/audit-catalog.ts +16 -0
  153. package/src/lint/post-synth/cf-refs.ts +40 -0
  154. package/src/lint/post-synth/index.ts +16 -0
  155. package/src/lint/post-synth/waw016.test.ts +61 -4
  156. package/src/lint/post-synth/waw016.ts +37 -16
  157. package/src/lint/post-synth/waw061.test.ts +99 -0
  158. package/src/lint/post-synth/waw061.ts +67 -0
  159. package/src/lint/post-synth/waw062.test.ts +109 -0
  160. package/src/lint/post-synth/waw062.ts +0 -0
  161. package/src/lint/post-synth/waw063.test.ts +220 -0
  162. package/src/lint/post-synth/waw063.ts +0 -0
  163. package/src/lint/post-synth/waw064.test.ts +79 -0
  164. package/src/lint/post-synth/waw064.ts +52 -0
  165. package/src/lint/post-synth/waw065.test.ts +90 -0
  166. package/src/lint/post-synth/waw065.ts +110 -0
  167. package/src/lint/post-synth/waw066.test.ts +108 -0
  168. package/src/lint/post-synth/waw066.ts +155 -0
  169. package/src/lint/post-synth/waw067.test.ts +79 -0
  170. package/src/lint/post-synth/waw067.ts +114 -0
  171. package/src/lint/post-synth/waw068.test.ts +74 -0
  172. package/src/lint/post-synth/waw068.ts +78 -0
  173. package/src/lsp/hover.ts +17 -2
  174. package/src/op/activities/index.ts +15 -0
  175. package/src/op/builders.test.ts +61 -0
  176. package/src/op/builders.ts +72 -0
  177. package/src/plugin.ts +41 -10
  178. package/src/receipt-store.test.ts +204 -0
  179. package/src/receipt-store.ts +315 -0
  180. package/src/serializer.ts +105 -2
  181. package/src/skills/chant-aws-carve-terraform.md +26 -23
  182. package/src/spec/parse.test.ts +74 -0
  183. package/src/spec/parse.ts +26 -6
package/src/serializer.ts CHANGED
@@ -2,7 +2,19 @@ import type { Declarable, CoreParameter } from "@intentius/chant/declarable";
2
2
  import { isPropertyDeclarable, isResourceDeclarable } from "@intentius/chant/declarable";
3
3
  import type { Serializer, SerializerResult, SerializeContext } from "@intentius/chant/serializer";
4
4
  import { ownershipEntries, type OwnershipMarker } from "@intentius/chant/ownership";
5
+ import {
6
+ isEffectReceipt,
7
+ receiptExpectation,
8
+ referenceInputPaths,
9
+ type EffectReceiptDeclaration,
10
+ } from "@intentius/chant/effect-receipt";
5
11
  import { AWS_TAG_OWNERSHIP_KEYS, OWNERSHIP_METADATA_KEY } from "./ownership";
12
+ import {
13
+ AWS_EFFECT_RECEIPT_ENTITY_TYPE,
14
+ EFFECT_RECEIPTS_METADATA_KEY,
15
+ RECEIPT_UNRESOLVED_VALUE_NOTE,
16
+ receiptParameterName,
17
+ } from "./effect-receipt-row";
6
18
  import type { LexiconOutput } from "@intentius/chant/lexicon-output";
7
19
  import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
8
20
  import { isChildProject, type ChildProjectInstance } from "@intentius/chant/child-project";
@@ -154,6 +166,80 @@ function toProperties(
154
166
  }
155
167
 
156
168
 
169
+ /** One rendered receipt row — a CFN-resource-shaped object that lives in the
170
+ * template's `Metadata`, never in `Resources` (#1832: the applier writes from
171
+ * `Resources`, and the `effect()` step is a receipt's sole writer). */
172
+ interface ReceiptRow {
173
+ Type: typeof AWS_EFFECT_RECEIPT_ENTITY_TYPE;
174
+ Properties: {
175
+ Name: string;
176
+ Type: "String";
177
+ Value: string;
178
+ Tags: Array<{ Key: string; Value: string }>;
179
+ };
180
+ }
181
+
182
+ /** The rendered `Value`: the synthesis-time expectation when the receipt is
183
+ * fully static, a placeholder note when reference inputs resolve later —
184
+ * never a digest hashed over placeholders (epic #1703, decision 5). */
185
+ function receiptRowValue(receipt: EffectReceiptDeclaration): string {
186
+ if (receipt.flavor === "hash" && referenceInputPaths(receipt).length > 0) {
187
+ return RECEIPT_UNRESOLVED_VALUE_NOTE;
188
+ }
189
+ return receiptExpectation(receipt);
190
+ }
191
+
192
+ /**
193
+ * Render the effect receipts (#1835) the build withheld from the apply-bound
194
+ * entity set (`SerializeContext.receipts`, #1832) as `AWS::SSM::Parameter`
195
+ * rows: plain `String`, named `/chant-receipts/<stack>/<env>/<effect>` from
196
+ * the resolved ownership marker (epic decision 4 — the same fields that stamp
197
+ * tags), carrying the ownership tags. Visibility only: the rows go under the
198
+ * template's `Metadata`, and the receipt store (./receipt-store.ts) is what
199
+ * actually writes the parameter — through the `effect()` step, on success,
200
+ * last.
201
+ *
202
+ * The env segment is explicit: a receipt with no resolved `ownership.env` is
203
+ * an error here, never a guessed path.
204
+ */
205
+ function renderReceiptRows(
206
+ receipts: ReadonlyMap<string, Declarable>,
207
+ ownership: OwnershipMarker | undefined,
208
+ ): Record<string, ReceiptRow> {
209
+ const rows: Record<string, ReceiptRow> = {};
210
+ const names = [...receipts.keys()].join(", ");
211
+ if (!ownership) {
212
+ throw new Error(
213
+ `aws receipts (${names}): no ownership marker resolved — the receipt path is ` +
214
+ `/chant-receipts/<stack>/<env>/<effect>, derived from the same ownership fields that ` +
215
+ `stamp markers (chant #1703, decision 4). Set ownership: { stack, env } in chant.config.ts.`,
216
+ );
217
+ }
218
+ if (!ownership.env) {
219
+ throw new Error(
220
+ `aws receipts (${names}): ownership resolved no env — the receipt path's <env> segment is ` +
221
+ `explicit (chant #1703, decision 4). Set ownership.env in chant.config.ts (a literal, or ` +
222
+ `{ param: "env" } with --param env=<name>).`,
223
+ );
224
+ }
225
+ const tags = Object.entries(ownershipEntries(AWS_TAG_OWNERSHIP_KEYS, ownership)).map(
226
+ ([Key, Value]) => ({ Key, Value }),
227
+ );
228
+ for (const [name, entity] of receipts) {
229
+ if (!isEffectReceipt(entity)) continue;
230
+ rows[name] = {
231
+ Type: AWS_EFFECT_RECEIPT_ENTITY_TYPE,
232
+ Properties: {
233
+ Name: receiptParameterName(ownership.stack, ownership.env, entity.effect),
234
+ Type: "String",
235
+ Value: receiptRowValue(entity),
236
+ Tags: tags,
237
+ },
238
+ };
239
+ }
240
+ return rows;
241
+ }
242
+
157
243
  /**
158
244
  * Serialize a set of entities into a CFTemplate object (without JSON.stringify).
159
245
  */
@@ -163,6 +249,7 @@ function serializeToTemplate(
163
249
  extraParameters?: Record<string, CFParameter>,
164
250
  extraOutputs?: Record<string, CFOutput>,
165
251
  ownership?: OwnershipMarker,
252
+ receiptRows?: Record<string, ReceiptRow>,
166
253
  ): CFTemplate {
167
254
  const template: CFTemplate = {
168
255
  AWSTemplateFormatVersion: "2010-09-09",
@@ -196,6 +283,14 @@ function serializeToTemplate(
196
283
  [OWNERSHIP_METADATA_KEY]: ownershipEntries(AWS_TAG_OWNERSHIP_KEYS, ownership),
197
284
  };
198
285
  }
286
+ // Effect receipt rows (#1835) — visibility only, deliberately outside
287
+ // `Resources`: the applier's desired and prune sets both read `Resources`,
288
+ // and the `effect()` step is a receipt's sole writer (#1832, epic #1703
289
+ // decision 3). The observation leg (plugin.ts) reads the paths back from
290
+ // this block, so the identity is derived exactly once, here.
291
+ if (receiptRows && Object.keys(receiptRows).length > 0) {
292
+ template.Metadata = { ...(template.Metadata ?? {}), [EFFECT_RECEIPTS_METADATA_KEY]: receiptRows };
293
+ }
199
294
  for (const [, entity] of entities) {
200
295
  if (isDefaultTags(entity)) {
201
296
  defaultTagEntries.push(...entity.tags);
@@ -412,6 +507,14 @@ export const awsSerializer: Serializer = {
412
507
  serialize(entities: Map<string, Declarable>, outputs?: LexiconOutput[], context?: SerializeContext): string | SerializerResult {
413
508
  const ownership = context?.ownership;
414
509
 
510
+ // Effect receipts (#1835): withheld from `entities` by the build (#1832),
511
+ // rendered here as Metadata rows — never as `Resources` an applier would
512
+ // write. Env-less ownership is an error, not a guessed path segment.
513
+ const receiptRows =
514
+ context?.receipts && context.receipts.size > 0
515
+ ? renderReceiptRows(context.receipts, ownership)
516
+ : undefined;
517
+
415
518
  // Check if any entities are child projects (nested stacks)
416
519
  const childProjects = new Map<string, ChildProjectInstance>();
417
520
  let hasChildProjects = false;
@@ -425,7 +528,7 @@ export const awsSerializer: Serializer = {
425
528
 
426
529
  // No nested stacks — use the simple path
427
530
  if (!hasChildProjects) {
428
- const template = serializeToTemplate(entities, outputs, undefined, undefined, ownership);
531
+ const template = serializeToTemplate(entities, outputs, undefined, undefined, ownership, receiptRows);
429
532
  return JSON.stringify(template, null, 2);
430
533
  }
431
534
 
@@ -465,7 +568,7 @@ export const awsSerializer: Serializer = {
465
568
  }
466
569
 
467
570
  // Serialize the parent template (ChildProjectInstance entities become CF::Stack resources)
468
- const parentTemplate = serializeToTemplate(entities, outputs, parentParams, undefined, ownership);
571
+ const parentTemplate = serializeToTemplate(entities, outputs, parentParams, undefined, ownership, receiptRows);
469
572
  const primary = JSON.stringify(parentTemplate, null, 2);
470
573
 
471
574
  return {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  skill: chant-aws-carve-terraform
3
- description: Demo carving a resource out of Terraform into native chant — advise, emit, bridge, apply — fully offline
3
+ description: Demo carving a resource out of Terraform into native chant — advise, emit, audit, bridge, apply — fully offline
4
4
  user-invocable: true
5
5
  ---
6
6
 
@@ -15,7 +15,7 @@ graduation.
15
15
  ## When to use
16
16
 
17
17
  - Someone asks "how does chant move things off Terraform?"
18
- - You want to show the advise → emit → bridge → apply loop end to end.
18
+ - You want to show the advise → emit → audit → bridge → apply loop end to end.
19
19
  - You are evaluating whether a Terraform estate is worth carving.
20
20
 
21
21
  ## Preconditions
@@ -27,7 +27,7 @@ graduation.
27
27
  ## The fastest path: run the bundled demo
28
28
 
29
29
  The `examples/terraform-carve-out` example ships a runnable estate + state and a
30
- script that runs all four steps with commentary:
30
+ script that runs all five steps with commentary:
31
31
 
32
32
  ```bash
33
33
  cd examples/terraform-carve-out
@@ -54,28 +54,30 @@ From `examples/terraform-carve-out`, with `TF=./terraform`:
54
54
  --state ./terraform/terraform.tfstate --output ./carveout
55
55
  ```
56
56
  Show `./carveout/src/assets.ts` — a real `new Bucket({ BucketName, Tags })` with
57
- CloudFormation-style properties mapped from the Terraform state attributes.
58
- Emit also scaffolds `./carveout` into a buildable chant project
59
- (`chant.config.ts`, `package.json`), so `npm install && npm run build` works
60
- there as-is. Explain: a Terraform-managed resource is not in any
61
- CloudFormation stack, so the correct source of its live shape is the state
62
- file, not a cloud read.
63
-
64
- 3. **Lint the inherited resource — optional, offer it.**
57
+ CloudFormation-style properties mapped from the Terraform state attributes,
58
+ and the `aws_s3_bucket_versioning` sub-resource folded into
59
+ `VersioningConfiguration`. Emit also scaffolds `./carveout` into a buildable
60
+ chant project (`chant.config.ts`, `package.json`) and persists a carve
61
+ manifest (`*.carve.json`) bridge and apply read the target from it, so
62
+ `--select` is only needed once. Explain: a Terraform-managed resource is not
63
+ in any CloudFormation stack, so the correct source of its live shape is the
64
+ state file, not a cloud read.
65
+
66
+ 3. **Audit the inherited resource.**
65
67
  ```bash
66
- chant lint ./carveout/src --lexicon aws
68
+ chant build ./carveout/src --lexicon aws
67
69
  ```
68
- After emit, offer to lint the carved source. chant audits the resource you
69
- inherited from Terraform against the AWS lexicon's rules. Findings have a
70
- severity: `error` (must fix before `chant build` will emit e.g. an S3
71
- bucket with no public-access block) and `warning`/`info` (advisory — e.g.
72
- DynamoDB point-in-time recovery). This is a feature of carving: chant
73
- immediately tells the person what is wrong with what they adopted. Whether to
74
- fix an advisory finding is their call; errors block the build until resolved.
70
+ The first build fails, deliberately: the post-synth audit refuses the
71
+ adopted bucket because Terraform managed it without a public-access block or
72
+ a TLS-only policy. This is a feature of carving: chant immediately tells the
73
+ person what is wrong with what they adopted. Show the fix (add
74
+ `PublicAccessBlockConfiguration` and a companion `S3BucketPolicy` with an
75
+ `aws:SecureTransport = false` Deny the tutorial page has the exact code),
76
+ then re-run the build and show the valid CloudFormation template.
75
77
 
76
78
  4. **Bridge — patch the surviving Terraform.**
77
79
  ```bash
78
- chant carve bridge --from ./terraform --select aws_s3_bucket.assets --output ./carveout
80
+ chant carve bridge --from ./terraform --output ./carveout
79
81
  ```
80
82
  Show the generated `data "aws_s3_bucket" "assets"` and the rewired survivor.
81
83
  Emphasize it is dry-run: nothing in `./terraform` changed. `--apply-rewrites`
@@ -83,11 +85,12 @@ From `examples/terraform-carve-out`, with `TF=./terraform`:
83
85
 
84
86
  5. **Apply — graduation plan.**
85
87
  ```bash
86
- chant carve apply --from ./terraform --select aws_s3_bucket.assets --env prod --stack assets
88
+ chant carve apply --from ./terraform --output ./carveout --env prod --stack assets
87
89
  ```
88
90
  Show the ownership marker (`chant:managed-by/stack/env`) and the finalized
89
- runbook. Stress this makes no cloud call the apply is the person's own
90
- lifecycle; chant just plans it.
91
+ runbook. `--write-source` additionally stamps the marker tags into the
92
+ emitted source. Stress this makes no cloud call — the apply is the person's
93
+ own lifecycle; chant just plans it.
91
94
 
92
95
  ## Key points to land
93
96
 
@@ -187,6 +187,80 @@ describe("parseCFNSchema", () => {
187
187
  });
188
188
  const result = parseCFNSchema(schema);
189
189
  expect(result.resource.deprecatedProperties).toEqual([]);
190
+ expect(result.resource.inferredDeprecations).toEqual([]);
191
+ });
192
+
193
+ // --- Deprecation basis (#1701) ---
194
+
195
+ test("a declared deprecation is not recorded as inferred", () => {
196
+ const schema = JSON.stringify({
197
+ typeName: "AWS::Test::Declared",
198
+ properties: {
199
+ OldProp: { type: "string", description: "An old property." },
200
+ },
201
+ deprecatedProperties: ["/properties/OldProp"],
202
+ additionalProperties: false,
203
+ });
204
+ const result = parseCFNSchema(schema);
205
+ expect(result.resource.deprecatedProperties).toEqual(["OldProp"]);
206
+ expect(result.resource.inferredDeprecations).toEqual([]);
207
+ });
208
+
209
+ test("a description-mined deprecation is recorded as inferred", () => {
210
+ const schema = JSON.stringify({
211
+ typeName: "AWS::Test::Mined",
212
+ properties: {
213
+ OldProp: { type: "string", description: "This property is deprecated. Use NewProp instead." },
214
+ NewProp: { type: "string", description: "The replacement property" },
215
+ },
216
+ additionalProperties: false,
217
+ });
218
+ const result = parseCFNSchema(schema);
219
+ expect(result.resource.deprecatedProperties).toEqual(["OldProp"]);
220
+ expect(result.resource.inferredDeprecations).toEqual(["OldProp"]);
221
+ });
222
+
223
+ test("a property both declared and matched by the regex counts as declared", () => {
224
+ // sampleBucketSchema declares AccessControl and its description says "legacy".
225
+ const result = parseCFNSchema(sampleBucketSchema);
226
+ expect(result.resource.deprecatedProperties).toContain("AccessControl");
227
+ expect(result.resource.inferredDeprecations).not.toContain("AccessControl");
228
+ });
229
+
230
+ test("inferredDeprecations is a subset of deprecatedProperties", () => {
231
+ const schema = JSON.stringify({
232
+ typeName: "AWS::Test::Mixed",
233
+ properties: {
234
+ DeclaredProp: { type: "string", description: "Plain prose." },
235
+ MinedProp: { type: "string", description: "This is a legacy property." },
236
+ },
237
+ deprecatedProperties: ["/properties/DeclaredProp"],
238
+ additionalProperties: false,
239
+ });
240
+ const result = parseCFNSchema(schema);
241
+ const all = new Set(result.resource.deprecatedProperties);
242
+ for (const p of result.resource.inferredDeprecations) {
243
+ expect(all.has(p)).toBe(true);
244
+ }
245
+ expect(result.resource.inferredDeprecations).toEqual(["MinedProp"]);
246
+ });
247
+
248
+ test("a description naming a sibling's deprecation lands in inferred, not declared", () => {
249
+ // Shape of AWS::Neptune::DBCluster.DBPort, where the description announces
250
+ // that the sibling `Port` is going away and DBPort is the replacement.
251
+ const schema = JSON.stringify({
252
+ typeName: "AWS::Test::Sibling",
253
+ properties: {
254
+ DBPort: {
255
+ type: "integer",
256
+ description: "The port. Note: `Port` will soon be deprecated. Rename it to `DBPort`.",
257
+ },
258
+ },
259
+ additionalProperties: false,
260
+ });
261
+ const result = parseCFNSchema(schema);
262
+ expect(result.resource.inferredDeprecations).toEqual(["DBPort"]);
263
+ expect(result.resource.deprecatedProperties).toEqual(["DBPort"]);
190
264
  });
191
265
 
192
266
  // --- Tagging metadata ---
package/src/spec/parse.ts CHANGED
@@ -30,6 +30,13 @@ export type { PropertyConstraints } from "@intentius/chant/codegen/json-schema";
30
30
  */
31
31
  const MAX_PROPERTY_TYPE_DEPTH = 3;
32
32
 
33
+ /**
34
+ * Prose signals that a property description is describing a deprecation.
35
+ * A hit is a guess, not a reading — see `ParsedResource.inferredDeprecations`.
36
+ */
37
+ const DEPRECATION_RE =
38
+ /\bdeprecated\b|\blegacy\b|no longer (available|recommended|used|supported)|is not recommended|has been discontinued/i;
39
+
33
40
  export interface ParsedProperty {
34
41
  name: string;
35
42
  tsType: string;
@@ -63,6 +70,13 @@ export interface ParsedResource {
63
70
  writeOnly: string[];
64
71
  primaryIdentifier: string[];
65
72
  deprecatedProperties: string[];
73
+ /**
74
+ * The subset of `deprecatedProperties` that no upstream declaration backs.
75
+ * These names come from {@link DEPRECATION_RE} matching the property
76
+ * description, so they are an inference rather than a reading. Kept apart so
77
+ * a consumer can tell the two apart and calibrate what it says (#1701).
78
+ */
79
+ inferredDeprecations: string[];
66
80
  conditionalCreateOnly: string[];
67
81
  replacementStrategy?: "delete_then_create" | "create_then_delete";
68
82
  tagging?: { taggable: boolean; tagOnCreate: boolean; tagUpdatable: boolean };
@@ -160,19 +174,24 @@ export function parseCFNSchema(data: string | Buffer): SchemaParseResult {
160
174
  }
161
175
  }
162
176
 
163
- // --- Deprecated properties: explicit + description-mined ---
177
+ // --- Deprecated properties: declared upstream, plus description-mined ---
178
+ // The declared half is what the Registry schema states. The mined half is a
179
+ // regex over English prose and is recorded separately (#1701): a description
180
+ // can mention the deprecation of a sibling property, of an enum value, or of
181
+ // something the property merely configures.
164
182
  const deprecatedSet = new Set<string>(
165
183
  stripPointerPaths(schema.deprecatedProperties ?? []),
166
184
  );
167
-
168
- const DEPRECATION_RE = /\bdeprecated\b|\blegacy\b|no longer (available|recommended|used|supported)|is not recommended|has been discontinued/i;
185
+ const inferredDeprecations: string[] = [];
169
186
 
170
187
  // Mine top-level property descriptions
171
188
  if (schema.properties) {
172
189
  for (const [name, prop] of Object.entries(schema.properties)) {
173
- if (prop.description && DEPRECATION_RE.test(prop.description)) {
174
- deprecatedSet.add(name);
175
- }
190
+ if (!prop.description || !DEPRECATION_RE.test(prop.description)) continue;
191
+ // A hit upstream already declares adds nothing, and is not an inference.
192
+ if (deprecatedSet.has(name)) continue;
193
+ deprecatedSet.add(name);
194
+ inferredDeprecations.push(name);
176
195
  }
177
196
  }
178
197
 
@@ -211,6 +230,7 @@ export function parseCFNSchema(data: string | Buffer): SchemaParseResult {
211
230
  writeOnly: stripPointerPaths(schema.writeOnlyProperties ?? []),
212
231
  primaryIdentifier: stripPointerPaths(schema.primaryIdentifier ?? []),
213
232
  deprecatedProperties: [...deprecatedSet],
233
+ inferredDeprecations,
214
234
  conditionalCreateOnly: stripPointerPaths(schema.conditionalCreateOnlyProperties ?? []),
215
235
  ...(replacementStrategy && { replacementStrategy }),
216
236
  ...(tagging && { tagging }),