cdk-preflight 0.0.24 → 0.0.26

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.
@@ -5,7 +5,9 @@ description: cdk-preflight に新しいルールを追加する半自動パイ
5
5
 
6
6
  # add-preflight-rule
7
7
 
8
- cdk-preflight のルール追加パイプライン。AGENTS.md の設計原則(重複禁止・証拠必須・上流昇格)を機械的に踏む。
8
+ cdk-preflight のルール追加パイプライン。AGENTS.md の設計原則(重複禁止・証拠必須)を機械的に踏む。
9
+
10
+ 対象は「違反すると実際にデプロイが失敗し、かつ同じ合成済みテンプレートを見る層が誰も止めない」制約すべて。**候補を落とせるのは下の 2 つのゲートだけ**で、「割に合うか」「L2 が持っているのでは」は判断材料にしない。上流 PR は歓迎だがゲートではなく、ルールは先にここに入る。
9
11
 
10
12
  ## 手順
11
13
 
@@ -19,7 +21,7 @@ cdk-preflight のルール追加パイプライン。AGENTS.md の設計原則
19
21
  console.log(JSON.stringify(r.diagnostics, null, 2));"
20
22
  ```
21
23
  ERROR/FATAL(source: SCHEMA / CFN_LINT)が既に出るなら**ルールは書かない**。終了し、その旨を報告する。
22
- 判定の全体像(WARN クラスのみ出るがデプロイは失敗するグレーゾーン、L2/cfn-lint/サーバー側検証との棲み分け)は AGENTS.md の「Where this pack sits among validation layers」に従う。L2 (aws-cdk-lib) が同じ検証を持っていても不採用理由にならない(原則 5)。
24
+ 判定の全体像(WARN クラスのみ出るがデプロイは失敗するグレーゾーン、L2/cfn-lint/サーバー側検証との棲み分け)は AGENTS.md の「Where this pack sits among validation layers」に従う。L2 (aws-cdk-lib) が同じ検証を持っていても不採用理由にならず、既存ルールの廃止理由にもならない(原則 5)。廃止の引き金は同梱エンジン(か CFN サーバー側検証)がカバーしたときだけで、そのとき重複ガードが自動で赤くなる。
23
25
  3. **ルール作成**: `rules/<service>/<rule-id>/` に 4 ファイル。規約:
24
26
  - `package cdk_preflight` + `import rego.v1`、診断は `make_diag_full("<rule-id>", "ERROR", name, path, msg, fix, url)`
25
27
  - ヘルパーは `_pf_<短縮名>_` プレフィックスで一意に
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: find-preflight-rules
3
- description: あるAWSサービスについて cdk-preflight が書くべきルール候補を洗い出す調査パイプライン。「他のどの層でも捕まえられない制約とは何か」の当たり判定(6レンズ)を当ててから、サービスの制約を棚卸しし、重複ガードで機械的に落として、生き残りを issue のチェックリストに記録する。実装は add-preflight-rule に引き継ぐ。
3
+ description: あるAWSサービスについて cdk-preflight が書くべきルール候補を洗い出す調査パイプライン。「他のどの層からも構造的に見えない制約」の狩り場(6レンズ)を回って制約を棚卸しし、重複ガードで機械的に落として、生き残りを issue のチェックリストに記録する。実装は add-preflight-rule に引き継ぐ。
4
4
  ---
5
5
 
6
6
  # find-preflight-rules
@@ -9,11 +9,15 @@ description: あるAWSサービスについて cdk-preflight が書くべきル
9
9
 
10
10
  出力は候補リストと採否の根拠であって、実装ではない。採用された候補は `add-preflight-rule` に渡す。候補ゼロで終わるのも正しい結果で、その場合は「なぜ無いか」を残すことが成果になる。
11
11
 
12
- ## 当たり判定:このパックにしか書けない制約とは
12
+ ## スコープ
13
13
 
14
- 先に基準を頭に入れてから調べる。順番を逆にすると、他の層が既に捕まえている制約を延々と棚卸しすることになる。
14
+ cdk-preflight が拾うのは「**違反すると実際にデプロイが失敗し、かつ同じ合成済みテンプレートを見る層が誰も止めない**」制約。これが採否の全部で、「割に合うか」「よくある間違いか」は問わない。
15
15
 
16
- 判定は「その制約が、より早い層から**構造的に見えない**か」で行う。以下の 6 レンズはその構造的理由を機構ごとに分けたもの。数値と rule ID はすべて実測(2026-09、aws-cdk-lib 2.267.0 / engine 1.7.0-beta)。
16
+ **捨てる判定を下せるのは 2 つのゲートだけ** — 重複ガード(素のエンジンが ERROR/FATAL を出す)と、実機再現ゲート(fail テンプレートが本当にデプロイに失敗する。こちらは `add-preflight-rule` の担当)。L2 に同じ検証があることは不採用理由にならないし、既存ルールの廃止理由にもならない(L1・エスケープハッチ・`addPropertyOverride`・取り込みテンプレートが L2 を素通りするため)。
17
+
18
+ ## 狩り場の地図:他の層から構造的に見えない場所
19
+
20
+ 以下の 6 レンズは「より早い層から**構造的に見えない**」理由を機構ごとに分けたもので、**候補が濃い場所を示す地図**であってフィルタではない。レンズ名を付けられない制約も捨てず、2 つのゲートに掛ける。数値と rule ID はすべて実測(2026-09、aws-cdk-lib 2.267.0 / engine 1.7.0-beta)。
17
21
 
18
22
  ### レンズ 1:CDK の検証フェーズより後にしか存在しない値
19
23
 
@@ -53,14 +57,17 @@ CDK のプラグイン経路は `accountId` と `region` をエンジンに渡
53
57
 
54
58
  空いていたもの:論理 ID の長さは `I3012` の **INFO 止まり**(=グレーゾーン)、Export 名の重複は検出なし。
55
59
 
56
- ### レンズ 6:時間で変わる制約(原則却下)
60
+ ### レンズ 6:時間で変わる制約(対象。ただし重複ガードを先に)
57
61
 
58
- EOL ランタイム、廃止インスタンスタイプ、新リージョン対応。エンジンが cfn-lint 由来の検査を持っている可能性が高いうえ、鮮度管理コストが継続的に乗る。上流に任せ、ここには置かない。例外を作るなら理由を issue に明記すること。
62
+ EOL ランタイム、廃止インスタンスタイプ、リージョン非対応。他と同じ 2 ゲートに掛ける対象で、事前の自制はしない。ただし当たりは薄い側で、理由は 2 つ:
63
+
64
+ - エンジンが cfn-lint 由来の検査を既に持っている確率が高い。実測でもインスタンスタイプのリージョン可用性は `E3628` で検出済み(region 指定時)。**先に重複ガードを通す**こと。タダで落ちるなら調査時間の節約になる
65
+ - 鮮度管理コストが継続的に乗る。腐ったら `addedOn` を見て消す運用でよく、これは廃止が安いという前提(AGENTS.md「Rule lifecycle」)に依存している
59
66
 
60
67
  ## 手順
61
68
 
62
69
  1. **制約の棚卸し**。そのサービスについて、次の順で拾う。ユーザーガイドの Limits / Quotas ページ、API リファレンスのパラメータ制約、CloudFormation リソースタイプのドキュメント(スキーマではなく散文の方)、実際のデプロイ失敗エラーメッセージ。数値・順序・組み合わせ・排他・命名・リージョン依存に印を付ける。ここで拾ったドキュメントの 1 文は**仮説であって証拠ではない**。CloudFront の棚卸し(2026-09-02)では明文化された制約 9 件のうち 3 件が実機でそのままデプロイできた(BROKEN-EXPECTATION)。候補リストは「実機ゲートに掛ける仮説の列」として書き、ドキュメントの断言を根拠に採用を確定させない
63
- 2. **6 レンズに当てる**。どのレンズにも当たらない制約は捨てる。ここで大半が落ちる。レンズ 3 と 4 を先に通すと当たりが早い
70
+ 2. **6 レンズで狩り場を回る**。レンズ 3 と 4 から始めると当たりが早い。**レンズに当てはまらない制約もここでは捨てない** — 足切りは次の重複ガードと、実装側の実機ゲートだけ
64
71
  3. **重複ガード**。残った候補ごとに最小の違反テンプレートを書き、素のエンジンにかける
65
72
  ```bash
66
73
  npx ts-node --project test/tsconfig.json -e "
@@ -87,8 +94,8 @@ EOL ランタイム、廃止インスタンスタイプ、新リージョン対
87
94
  | 制約 | 落選理由 |
88
95
  |---|---|
89
96
  | ... | エンジンが F3034 で検出(重複ガード) |
90
- | ... | どのレンズにも当たらない(L2 の型で防がれ、迂回経路も無い) |
91
97
  | ... | ドキュメント上の制約だが実機では通った(BROKEN-EXPECTATION) |
98
+ | ... | そもそもデプロイは失敗しない(ベストプラクティス/実行時の問題) |
92
99
 
93
100
  ## Gray zone
94
101
  エンジンが WARN / INFO でだけ報告するもの。上流 issue の候補
@@ -96,6 +103,8 @@ EOL ランタイム、廃止インスタンスタイプ、新リージョン対
96
103
 
97
104
  ## やらないこと
98
105
 
99
- - セキュリティ・コンプライアンス目的の検査(cdk-nag の領分)
106
+ - セキュリティ・コンプライアンス目的の検査。これは「既存の仕組みが無い隙間だけを埋める」というスコープの帰結で、cdk-nag という機構が既にあるから外れる(特例ではない)
100
107
  - ベストプラクティスや推奨事項。**違反すると実際にデプロイが失敗する**ものだけが対象
101
108
  - 実装。このスキルは候補を確定させるまでで止める
109
+
110
+ 逆に、**やっていいことを狭めない**。L2 と被る、地味な制約、ニッチなプロパティ — どれも不採用理由にならない。落とせるのは 2 つのゲートだけ。
package/.jsii CHANGED
@@ -9414,7 +9414,7 @@
9414
9414
  },
9415
9415
  "name": "cdk-preflight",
9416
9416
  "readme": {
9417
- "markdown": "# cdk-preflight\n\n**Catch deploy-time CloudFormation failures at `cdk synth` time.**\n\nSome CloudFormation constraints are not expressed in resource provider schemas — they live only in documentation, in service API validation, or across multiple properties. Templates that violate them pass `cdk synth`, pass CloudFormation pre-deployment validation, and then fail minutes into a deployment, burning a rollback cycle.\n\ncdk-preflight is a curated [Rego rule pack](docs/rules.md) for exactly those constraints, evaluated with the CloudFormation validation engine that ships inside `aws-cdk-lib` (>= 2.267.0). By default a violation **fails `cdk synth`** — a template that is known to fail at deploy time never leaves your machine.\n\nEvery bundled rule is backed by a `fail`/`pass` template pair, and the failure has been reproduced against real AWS (or is explicitly marked `doc-only`). Rules that the built-in validation engine already covers are deliberately **not** duplicated — a test suite enforces this.\n\n## Quick start\n\n```bash\nnpm i -D cdk-preflight\nnpx cdk-preflight init # inserts Preflight.apply(app) into your CDK app\n```\n\nor add one line yourself:\n\n```ts\nimport { Preflight } from 'cdk-preflight';\n\nconst app = new App();\nPreflight.apply(app);\n```\n\nOn violation, `cdk synth` fails with one error per finding, including the construct trace:\n\n```text\nERROR idle_timeout.timeout_seconds is 5000 but must be between 1 and 4000 seconds (cdk-preflight)\n MyStack/Alb/Resource (Alb16C2F182) aws-cdk-lib.aws_elasticloadbalancingv2.CfnLoadBalancer\n\nSynthesis finished with errors\n```\n\n## Observe-only mode\n\nTo roll the rules out gradually, start with `enforce: false`: findings then surface as synth **warnings** through the CDK built-in validator, with construct traces and per-finding acknowledgement:\n\n```ts\nPreflight.apply(app, { enforce: false });\n```\n\n```text\nWARNING idle_timeout.timeout_seconds is 5000 but must be between 1 and 4000 seconds (CloudFormation Validate)\n MyStack/Alb (Alb) aws-cdk-lib.aws_elasticloadbalancingv2.CfnLoadBalancer\n Acknowledge with 'CloudFormation-Validate::pf-elbv2-lb-idle-timeout-range'\n```\n\n| Option | Default | Effect |\n|---|---|---|\n| `enforce` | `true` | Violations of bundled rules fail synthesis; set to `false` to only warn |\n| `strict` | `false` | With `enforce`: also fail on error-class findings (`ERROR`/`FATAL`, e.g. `F3034`) of the built-in validation engine itself, which the CDK currently downgrades to warnings |\n| `exclude` | `[]` | Rule ids to disable |\n| `includeUpstreamPending` | `true` | Include rules already proposed to the upstream engine but not yet merged |\n\nTo opt out of a single rule, pass its id in `exclude`. In observe-only mode, individual findings can also be suppressed with the CDK acknowledge mechanism shown in the warning text.\n\n## Bundled rules\n\nSee [docs/rules.md](docs/rules.md) for the generated rule table. Highlights:\n\n- **ELBv2**: `idle_timeout` / `deregistration_delay` / `slow_start` attribute ranges (stringly-typed Key/Value attributes are invisible to schema validation)\n- **IAM**: managed (6,144 chars) and inline (role/group/user) policy document size limits\n- **CloudFront**: `MinTTL <= DefaultTTL <= MaxTTL` ordering, ACM certificates must live in `us-east-1`\n- **Step Functions**: `Next`/`Default`/`Choices` must reference defined states (a dangling `StartAt` is already caught by the engine's built-in `E3601`)\n- **EC2**: security group TCP/UDP port ranges and `FromPort <= ToPort`\n\n## For AI agents\n\nTo add cdk-preflight to a CDK app:\n\n1. `npm i -D cdk-preflight`\n2. `npx cdk-preflight init` (finds the entry point via `cdk.json` and inserts `Preflight.apply(app)`; use `--dry-run` to preview)\n3. Run `cdk synth`. Violations fail the synth with a validation report; each finding carries a rule id, the actual value, and a suggested fix\n4. To fix a finding, prefer correcting the resource. To opt out instead: `exclude: ['<rule-id>']` disables one rule, `enforce: false` switches to warn-only reporting\n\nThe machine-readable findings are written to `cdk.out/validation-report.json` when synthesizing with `-c @aws-cdk/core:validationReportJson=true`.\n\n## How it works\n\n`Preflight.apply()` evaluates the rules with the [cloudformation-validate](https://github.com/aws-cloudformation/cloudformation-validate) Rust/WASM engine that ships inside `aws-cdk-lib` — no extra binaries, no network access at synth time. In the default enforce mode the engine is invoked through a dedicated CDK validation plugin so that violations fail synthesis; with `enforce: false` the rules are instead injected into the CDK built-in `CloudFormationValidatePlugin` and reported as warnings.\n\nConstraints that *can* be expressed in schemas or generic engine rules are contributed upstream instead of living here; each rule's `meta.yaml` tracks its upstream status, and rules retire once the engine covers them.\n\n## Requirements\n\n- `aws-cdk-lib` >= 2.267.0 (the first release that bundles the built-in CloudFormation validator)\n\n## Contributing\n\nRule authoring, the verification gates (including real-deploy reproduction), and the test layout are documented in [AGENTS.md](AGENTS.md) — written for AI coding agents and humans alike.\n\n## License\n\nApache-2.0\n"
9417
+ "markdown": "# cdk-preflight\n\n**Catch deploy-time CloudFormation failures at `cdk synth` time.**\n\nSome CloudFormation constraints are not expressed in resource provider schemas — they live only in documentation, in service API validation, or across multiple properties. Templates that violate them pass `cdk synth`, pass CloudFormation pre-deployment validation, and then fail minutes into a deployment, burning a rollback cycle.\n\ncdk-preflight is a curated [Rego rule pack](docs/rules.md) for exactly those constraints, evaluated with the CloudFormation validation engine that ships inside `aws-cdk-lib` (>= 2.267.0). By default a violation **fails `cdk synth`** — a template that is known to fail at deploy time never leaves your machine.\n\nThe pack aims at **every deploy-time failure that no existing CDK mechanism already catches** — nothing narrower. Every bundled rule is backed by a `fail`/`pass` template pair, and the failure has been reproduced against real AWS (or is explicitly marked `doc-only`). Rules that the built-in validation engine already covers are deliberately **not** duplicated — a test suite enforces this.\n\n## Quick start\n\n```bash\nnpm i -D cdk-preflight\nnpx cdk-preflight init # inserts Preflight.apply(app) into your CDK app\n```\n\nor add one line yourself:\n\n```ts\nimport { Preflight } from 'cdk-preflight';\n\nconst app = new App();\nPreflight.apply(app);\n```\n\nOn violation, `cdk synth` fails with one error per finding, including the construct trace:\n\n```text\nERROR idle_timeout.timeout_seconds is 5000 but must be between 1 and 4000 seconds (cdk-preflight)\n MyStack/Alb/Resource (Alb16C2F182) aws-cdk-lib.aws_elasticloadbalancingv2.CfnLoadBalancer\n\nSynthesis finished with errors\n```\n\n## Observe-only mode\n\nTo roll the rules out gradually, start with `enforce: false`: findings then surface as synth **warnings** through the CDK built-in validator, with construct traces and per-finding acknowledgement:\n\n```ts\nPreflight.apply(app, { enforce: false });\n```\n\n```text\nWARNING idle_timeout.timeout_seconds is 5000 but must be between 1 and 4000 seconds (CloudFormation Validate)\n MyStack/Alb (Alb) aws-cdk-lib.aws_elasticloadbalancingv2.CfnLoadBalancer\n Acknowledge with 'CloudFormation-Validate::pf-elbv2-lb-idle-timeout-range'\n```\n\n| Option | Default | Effect |\n|---|---|---|\n| `enforce` | `true` | Violations of bundled rules fail synthesis; set to `false` to only warn |\n| `strict` | `false` | With `enforce`: also fail on error-class findings (`ERROR`/`FATAL`, e.g. `F3034`) of the built-in validation engine itself, which the CDK currently downgrades to warnings |\n| `exclude` | `[]` | Rule ids to disable |\n| `includeUpstreamPending` | `true` | Include rules already proposed to the upstream engine but not yet merged |\n\nTo opt out of a single rule, pass its id in `exclude`. In observe-only mode, individual findings can also be suppressed with the CDK acknowledge mechanism shown in the warning text.\n\n## Bundled rules\n\nSee [docs/rules.md](docs/rules.md) for the generated rule table. Highlights:\n\n- **ELBv2**: `idle_timeout` / `deregistration_delay` / `slow_start` attribute ranges (stringly-typed Key/Value attributes are invisible to schema validation)\n- **IAM**: managed (6,144 chars) and inline (role/group/user) policy document size limits\n- **CloudFront**: `MinTTL <= DefaultTTL <= MaxTTL` ordering, ACM certificates must live in `us-east-1`\n- **Step Functions**: `Next`/`Default`/`Choices` must reference defined states (a dangling `StartAt` is already caught by the engine's built-in `E3601`)\n- **EC2**: security group TCP/UDP port ranges and `FromPort <= ToPort`\n\n## For AI agents\n\nTo add cdk-preflight to a CDK app:\n\n1. `npm i -D cdk-preflight`\n2. `npx cdk-preflight init` (finds the entry point via `cdk.json` and inserts `Preflight.apply(app)`; use `--dry-run` to preview)\n3. Run `cdk synth`. Violations fail the synth with a validation report; each finding carries a rule id, the actual value, and a suggested fix\n4. To fix a finding, prefer correcting the resource. To opt out instead: `exclude: ['<rule-id>']` disables one rule, `enforce: false` switches to warn-only reporting\n\nThe machine-readable findings are written to `cdk.out/validation-report.json` when synthesizing with `-c @aws-cdk/core:validationReportJson=true`.\n\n### Report missed deploy errors\n\ncdk-preflight only knows the failure modes it has benched. When a deploy fails on a constraint the pack should have caught, that report is how new rules get added — but at that moment cdk-preflight is out of the loop (it passed the synth), so the instruction has to already sit in your agent's context. Paste this into your repository's `AGENTS.md` (or `CLAUDE.md`):\n\n```markdown\n## cdk-preflight: report missed deploy errors\n\nIf `cdk deploy` fails even though `cdk synth` (with cdk-preflight enabled) passed,\nand the failure is a service-side validation rejection — CREATE_FAILED/UPDATE_FAILED\nwith a 400-class message such as \"Invalid request provided: ...\" — cdk-preflight is\nmissing a rule. Report it:\n\n1. Qualify first. NOT reportable: AccessDenied / permissions, service quotas,\n throttling, name conflicts with existing resources, stabilization timeouts.\n2. Redact account-specific data (account IDs -> 123456789012, real ARNs/domains\n -> placeholders).\n3. Search existing issues: https://github.com/badmintoncryer/cdk-preflight/issues\n4. With your user's approval, open a \"Missed deploy error\" issue with the verbatim\n error message, the resource type, a minimal template snippet, and your\n aws-cdk-lib / cdk-preflight versions.\n```\n\n## Scope and rule lifecycle\n\nA constraint belongs in the pack when violating it makes a real deployment fail *and* no layer that sees the same synthesized template already blocks it. There is no further \"is this worth a rule\" question — if the gap is real, it gets a rule.\n\nCDK L2 construct validation is deliberately **not** one of those layers. `CfnXxx` usage, escape hatches, `addPropertyOverride`, `cloudformation-include` and migrated templates all bypass L2, so an L2 guard covering the same mistake neither disqualifies a rule nor retires one.\n\nThat makes growth the normal state, and it has a consequence worth knowing before you upgrade: **new rules land in minor releases, so a minor upgrade can newly fail a `cdk synth` that passed yesterday.** That is intended, not a regression. If you need a frozen rule set, pin the version; to drop a single rule, `exclude: ['<rule-id>']`; to see everything without failing the build, `enforce: false`.\n\nRules move the other way too. Once the validation engine bundled in `aws-cdk-lib` (or CloudFormation's own pre-deploy validation) starts blocking a constraint, the rule is deleted rather than kept as a duplicate — staying on an older `aws-cdk-lib` and an older cdk-preflight keeps the old behavior.\n\n## How it works\n\n`Preflight.apply()` evaluates the rules with the [cloudformation-validate](https://github.com/aws-cloudformation/cloudformation-validate) Rust/WASM engine that ships inside `aws-cdk-lib` — no extra binaries, no network access at synth time. In the default enforce mode the engine is invoked through a dedicated CDK validation plugin so that violations fail synthesis; with `enforce: false` the rules are instead injected into the CDK built-in `CloudFormationValidatePlugin` and reported as warnings.\n\nConstraints that *can* be expressed in schemas or generic engine rules also make good upstream PRs to that engine, but nothing here waits on one — the upstream release cycle is deliberately slower than this pack's. Each rule's `meta.yaml` tracks its upstream status so that retirement stays bookkeeping.\n\n## Requirements\n\n- `aws-cdk-lib` >= 2.267.0 (the first release that bundles the built-in CloudFormation validator)\n\n## Contributing\n\nRule authoring, the verification gates (including real-deploy reproduction), and the test layout are documented in [AGENTS.md](AGENTS.md) — written for AI coding agents and humans alike.\n\n## License\n\nApache-2.0\n"
9418
9418
  },
9419
9419
  "repository": {
9420
9420
  "type": "git",
@@ -9598,6 +9598,6 @@
9598
9598
  "symbolId": "src/index:PreflightOptions"
9599
9599
  }
9600
9600
  },
9601
- "version": "0.0.24",
9602
- "fingerprint": "ZNylEr4ioiS4syvlraTIaBKna2hfpQcyl83PHZYd850="
9601
+ "version": "0.0.26",
9602
+ "fingerprint": "MbdjJdd4XrgIQknfUa+SOuCnMA4GdzQOuopkxCgnBBo="
9603
9603
  }
package/AGENTS.md CHANGED
@@ -6,54 +6,72 @@ Guide for AI agents (and humans) working on this repository.
6
6
 
7
7
  A data-first OSS: the product is the **rule pack** (`rules/`), not the loader. Rules are Rego documents evaluated by `@aws/cloudformation-validate` (the engine bundled inside `aws-cdk-lib` >= 2.267.0). The loader (`src/index.ts`) only selects rules and registers them with the CDK validation machinery.
8
8
 
9
- Scope: **deploy-failure prevention only.** Security/compliance policies (cdk-nag, Control Tower proactive controls) are out of scope. A rule belongs here only if violating it makes an actual deployment fail.
9
+ Scope: **every deploy-time failure that no existing CDK mechanism already catches.** A rule belongs here iff (a) violating it makes an actual deployment fail and (b) no layer that sees the same synthesized template already blocks it. That is the entire admission test — there is no second question about whether a rule is "worth it", how exotic the mistake looks, or whether some other layer would usually have caught the user first.
10
+
11
+ Security and compliance policy (cdk-nag, Control Tower proactive controls) stays out under the same rule rather than as a special exception: those mechanisms exist, so that gap is not ours to fill.
12
+
13
+ Growth is the normal state. The upstream engine ships slowly and carefully by design; this pack does not have to, so rules land here continuously and are deleted without ceremony once the engine covers them (see [Rule lifecycle](#rule-lifecycle-add-fast-retire-without-ceremony)). A shrinking rule count is neither a goal nor a problem.
10
14
 
11
15
  ## Design principles (do not violate)
12
16
 
13
17
  1. **No duplication of the engine.** If the bundled engine already reports a constraint as ERROR/FATAL (schema check or built-in rule), we must not re-implement it. `test/rules.test.ts` enforces this mechanically ("does not duplicate a built-in blocker"). If that test fails for a new rule, the rule is unnecessary — delete it and record why in the PR.
14
18
  2. **Every rule ships with proof.** `templates/fail.template.json` must violate exactly this rule; `templates/pass.template.json` must be clean. `meta.yaml#repro` records how the deploy-time failure was verified (`real-deploy` / `research-case` / `doc-only` — the last one requires an explanation in `evidence`).
15
- 3. **Rules graduate upstream.** Constraints expressible in schemas or generic engine rules should be PRed to [cloudformation-validate](https://github.com/aws-cloudformation/cloudformation-validate) (open an issue first). Track status in `meta.yaml#upstream` (`none` / `pending-engine` / `engine-pr` / `retired`). Shrinking this pack is success, not failure.
19
+ 3. **Upstream contribution is welcome, never a gate.** Constraints expressible in schemas or generic engine rules make good PRs to [cloudformation-validate](https://github.com/aws-cloudformation/cloudformation-validate) (open an issue first) — but a rule ships here first and never waits on one. Track status in `meta.yaml#upstream` (`none` / `pending-engine` / `engine-pr` / `retired`); the field exists so that retirement is bookkeeping rather than archaeology.
16
20
  4. **Tests are the contract.** Never merge with a red test; never weaken an assertion to make it pass. New behavior needs a new test first.
17
- 5. **The boundary is the engine, not the CDK L2 layer.** Rules validate synthesized templates, so an L2 construct that validates (or structurally prevents) the same mistake does not make a rule redundant — L1 usage, escape hatches, `addPropertyOverride`, and externally generated templates all bypass L2. Overlapping an L2 guard is fine and expected; overlapping the bundled engine is forbidden (principle 1). When a rule does overlap L2, note it in the PR so reviewers can weigh the marginal value. See "Where this pack sits among validation layers" below.
21
+ 5. **The boundary is the engine, not the CDK L2 layer.** Rules validate synthesized templates, so an L2 construct that validates (or structurally prevents) the same mistake does not make a rule redundant — L1 usage, escape hatches, `addPropertyOverride`, and externally generated templates all bypass L2. Overlapping an L2 guard is fine and expected; overlapping the bundled engine is forbidden (principle 1). L2 coverage is not a retirement trigger either — deleting a rule because an L2 construct checks it would strip the coverage from exactly the users this pack exists for. Only a layer that sees the same synthesized template retires a rule: the bundled engine, or CloudFormation's server-side validation. Noting an L2 overlap in the PR is useful context, not something to justify. See "Where this pack sits among validation layers" below.
18
22
 
19
23
  ## Where this pack sits among validation layers
20
24
 
21
- Four other validation layers exist around a CDK app. Only one of them is a boundary for this pack; the relationships are:
25
+ Five other validation layers sit around a CDK app. Exactly one of them is a boundary for this pack, and the same one is the only trigger for retiring a rule; the relationships are:
22
26
 
23
27
  | Layer | Runs at | Catches | Relation to this pack |
24
28
  |---|---|---|---|
25
- | CDK L2 construct validation | synth, only on that construct's prop path | per-construct guards (e.g. `Volume` iops checks, SNS auto-`.fifo`, Fargate hardcoding `awsvpc`) | **Not a boundary** (principle 5). Bypassed by L1 / escape hatches / external templates. Overlap allowed; record it in the PR |
29
+ | CDK L2 construct validation | synth, only on that construct's prop path | per-construct guards (e.g. `Volume` iops checks, SNS auto-`.fifo`, Fargate hardcoding `awsvpc`) | **Neither a boundary nor a retirement trigger** (principle 5). Bypassed by L1 / escape hatches / external templates, so its coverage is not our coverage. Overlap allowed and unremarkable |
26
30
  | CDK L1 generated validators (`CfnXxx`) | synth, every CDK app | type and required-property checks only — never value ranges, patterns, or cross-field rules | **No practical overlap**: pack rules are value / cross-field constraints by construction |
27
- | Bundled engine default rules (`@aws/cloudformation-validate`: SCHEMA / CFN_LINT / ENGINE) | synth, via this plugin | patched registry-schema ranges/patterns/enums + cfn-lint rules | **The hard boundary** (principle 1). If the bare engine reports ERROR/FATAL on the minimal violating template, the rule must not exist — enforced by the jest duplication guard |
31
+ | Bundled engine default rules (`@aws/cloudformation-validate`: SCHEMA / CFN_LINT / ENGINE) | synth, via this plugin | patched registry-schema ranges/patterns/enums + cfn-lint rules | **The hard boundary** (principle 1) **and the only retirement trigger**. If the bare engine reports ERROR/FATAL on the minimal violating template, the rule must not exist — enforced by the jest duplication guard, which turns red by itself when the engine catches up |
28
32
  | cfn-lint (external CLI) | outside the synth path (a separate CI step, if the user runs one) | near-parity with the bundled engine | **Not a boundary** — never disqualifies a rule. A check cfn-lint has but the engine lacks is an upstream-PR candidate *for the engine* (project policy: contribute to cloudformation-validate, not to cfn-lint) |
29
33
  | CloudFormation server-side pre-deploy validation | CreateStack / UpdateStack / CreateChangeSet, before resources are touched | property syntax against RAW registry schemas for non-excluded types; resource name conflicts | **Effectively no overlap** with this pack's domain: ~420 resource types are excluded (IAM, EC2 SG, ECS, RDS, SFN, …) and raw schemas carry none of the doc-only limits — 0/16 real-world cases detected (measured 2026-09, cdk-validation-gap-research). Never a reason to reject a rule |
30
34
  | CloudFormation + service APIs | deploy (CREATE/UPDATE) | everything else: cross-field rules, service-side business rules, quotas | **The target, not a duplicate to avoid.** A rule exists *iff* it front-runs a real deploy-time failure here; `meta.yaml#repro` (real-deploy gate) proves that equivalence |
31
35
 
32
- Selection algorithm for a new rule, in order: (1) duplication guard — run the minimal violating template through the bare engine; any built-in ERROR/FATAL kills the candidate. A **WARN-class-only** engine finding (`W…`) does *not* kill it but marks a gray zone: the engine knows about the constraint and under-classifies it, so nothing blocks the deploy (`strict` promotes only ERROR/FATAL) — prefer filing an upstream severity issue, and if a stopgap rule ships anyway, mark it `upstream: pending-engine` so it retires with the upstream fix. (2) real-deploy gate — the fail template must actually fail CREATE with the predicted service error; a fail template that deploys kills the candidate (it happened: the "30-day minimum before STANDARD_IA" and the "4096-char ZipFile" constraints are documented but not enforced, so those rules were dropped). (3) L1/L2 coverage never disqualifies, only gets noted.
36
+ Selection algorithm for a new rule, in order: (1) duplication guard — run the minimal violating template through the bare engine; any built-in ERROR/FATAL kills the candidate. A **WARN-class-only** engine finding (`W…`) does *not* kill it but marks a gray zone: the engine knows about the constraint and under-classifies it, so nothing blocks the deploy (`strict` promotes only ERROR/FATAL) — prefer filing an upstream severity issue, and if a stopgap rule ships anyway, mark it `upstream: pending-engine` so it retires with the upstream fix. (2) real-deploy gate — the fail template must actually fail CREATE with the predicted service error; a fail template that deploys kills the candidate (it happened: the "30-day minimum before STANDARD_IA" and the "4096-char ZipFile" constraints are documented but not enforced, so those rules were dropped). There is no third gate: L1/L2 coverage, marginal value, and how exotic the mistake looks never disqualify a candidate.
33
37
 
34
38
  **A doc sentence is a hypothesis, not evidence.** The CloudFront survey (2026-09-02) put nine documented constraints through the gate and three of them deployed clean: `DefaultRootObject` "can't begin with a forward slash", `GeoRestriction.Locations` marked "Required: Conditional", and "a `CacheBehavior` must include either a `CachePolicyId` or `ForwardedValues`". All three are unambiguous prose in the CloudFormation reference; the API accepts all three. Shipping on the strength of the docs alone would have produced a third of that batch as false positives — synth failures on templates that deploy. Treat `doc-only` as reserved for constraints whose repro needs resources we cannot cheaply create (a validated ACM certificate, an owned domain), never as a shortcut for a constraint that simply has not been tried. When a repro comes back failing for a *different* reason than predicted — a foreign-account ARN, a CNAME rejected for ownership — that is not evidence either; rebuild the template until the service error names the constraint, or record the confound in `evidence`.
35
39
 
36
40
  The same gate also corrects rules that are merely aimed slightly wrong. `pf-cloudfront-wafv2-webacl-scope` began life checking the ARN's region field; the service error (`Only global scoped ARNs are supported`) showed the real check is on the scope segment, which is what catches the realistic mistake — a REGIONAL web ACL created in us-east-1, where the region field looks perfectly correct.
37
41
 
38
- ### How much is an L2 overlap worth?
42
+ ### Where the bypasses are (a hunting map)
39
43
 
40
- Principle 5 says an L2 guard never disqualifies a rule. It does not say every L2 check deserves a port — "allowed" and "worth it" are different questions, and the second one decides priority once rules are being added in bulk. Note that enforce is the default mode, so a false positive is a hard synth failure for users; the bar sits above "it is permitted".
44
+ Principle 5 says an L2 guard never disqualifies a rule, and the scope rule says there is no "but is it worth it" follow-up. What remains is a practical question: **where do you find constraints that no earlier layer can reach?** The list below maps the structural bypasses, richest first. It ranks *where to look*, never *what to accept* — a candidate that only turns up on the thin ground at the bottom is still admissible, it is just a worse use of the next hour than the ground at the top.
41
45
 
42
- Rank a candidate by **how easily the L2 guard is bypassed in normal use**, not by whether one exists.
46
+ The ordering is by **how easily the L2 guard is bypassed in normal use**, not by whether one exists.
43
47
 
44
- **Highest value — CDK cannot validate it at all, by construction:**
48
+ **Richest ground — CDK cannot validate it at all, by construction:**
45
49
 
46
50
  - *Anything applied after the validation phase.* `validateTree` runs at `core/lib/private/synthesis.ts:48`, `synthesizeTree` at `:59`, and `addPropertyOverride` values are `deepMerge`d into the rendered resource inside `_toCloudFormation` (`core/lib/cfn-resource.ts:538`). A value injected by an override therefore passes through neither the L1 generated validator nor any `node.addValidation()` hook. The same ordering hides render-time transformations: IAM policy documents get a final minimization pass "just before rendering" (`aws-iam/lib/policy-document.ts:222`), so the final document size does not exist yet at validation time — and `aws-iam` accordingly has no size check at all.
47
51
  - *Entry points the type system cannot describe.* Stringly-typed Key/Value attribute lists (ELBv2 `setAttribute` and friends): the type is `[{key, value}]`, so neither TypeScript nor an L2 prop can express "this key takes 30–900". `pf-elbv2-tg-slow-start-range` is the canonical case — the L2 prop `slowStart` does validate, but `setAttribute` is the normal way to set the other attributes, so the bypass is routine rather than exotic.
48
52
  - *Opaque strings.* ASL passed via `DefinitionBody.fromString` / `fromFile`, policy document JSON, EventBridge patterns, metric expressions. The L2 accepts a string and does not parse it (and should not).
49
53
  - *No L2 at the entry point at all.* Raw `CfnXxx` usage, `cloudformation-include`, migrate output, SAM-generated templates.
50
54
 
51
- **Medium value — the information is usually missing rather than impossible to obtain:** cross-resource consistency where the counterpart was imported with `fromXxxArn`. The L2 holds only an ARN string; the template has both resources side by side with concrete values.
55
+ **Middle ground — the information is usually missing rather than impossible to obtain:** cross-resource consistency where the counterpart was imported with `fromXxxArn`. The L2 holds only an ARN string; the template has both resources side by side with concrete values.
52
56
 
53
- **Low value:** a constraint whose only entry point is a typed L2 prop that already validates it. Porting it buys coverage for hand-written L1 only, at full maintenance cost. Say so explicitly in the PR and expect a reviewer to ask why it is worth it.
57
+ **Thin ground:** a constraint whose only entry point is a typed L2 prop that already validates it. Porting it buys coverage for hand-written L1 and `cloudformation-include` only. Admissible like anything else — just expect the same hour to yield more elsewhere.
54
58
 
55
59
  **Do not repeat this mis-analysis:** "L2 cannot validate accumulated state because it validates in the constructor" is false — `node.addValidation()` hooks run during `validateTree`, after all mutation. Accumulation is not the structural wall. The wall is transformation that happens *after* `validateTree`.
56
60
 
61
+ ## Rule lifecycle: add fast, retire without ceremony
62
+
63
+ **Adding.** Two gates, both mechanical, and nothing else can reject a candidate: the duplication guard (`test/rules.test.ts`) and the real-deploy gate (`bench/verify-rule.sh`). The real-deploy gate is the one thing that did *not* get looser with the widened scope, and deliberately so — `enforce` is the default mode, so a false positive is a hard synth failure in someone's CI. The CloudFront survey is the standing evidence: 3 of 9 documented constraints deployed clean (see "A doc sentence is a hypothesis" above). Volume comes from running the bench over more services, never from lowering that bar.
64
+
65
+ **Retiring.** The trigger is the bundled engine (or CloudFormation's server-side validation) starting to block the constraint — never an L2 construct, never cfn-lint. You do not have to watch for it: the duplication guard turns red on its own the first time an `aws-cdk-lib` bump brings an engine that reports ERROR/FATAL on the rule's fail template. Then:
66
+
67
+ 1. `rm -rf rules/<service>/<rule-id>/` — delete outright, no tombstone. `upstream: retired` exists for a rule mid-flight, not for a corpse.
68
+ 2. `npx projen bundle-rules` (regenerates `src/rules.generated.ts` and `docs/rules.md`).
69
+ 3. One line in the PR body naming the engine rule id that replaced it.
70
+
71
+ Users pinned to an older `aws-cdk-lib` keep the old behavior by staying on the older cdk-preflight, so a deletion never leaves them uncovered.
72
+
73
+ **Release contract.** New rules land in **minor** releases. Because `enforce` is the default, a minor upgrade can newly fail a `cdk synth` that passed yesterday — that is the intended behavior of a pack whose whole purpose is to grow, not a regression to be apologised for. The documented escape hatches are: pin the version for a frozen rule set, `exclude: ['<rule-id>']` for one rule, `enforce: false` for warn-only. Keep that sentence in the README whenever it is reworded.
74
+
57
75
  ## Repository layout
58
76
 
59
77
  ```
package/README.md CHANGED
@@ -6,7 +6,7 @@ Some CloudFormation constraints are not expressed in resource provider schemas
6
6
 
7
7
  cdk-preflight is a curated [Rego rule pack](docs/rules.md) for exactly those constraints, evaluated with the CloudFormation validation engine that ships inside `aws-cdk-lib` (>= 2.267.0). By default a violation **fails `cdk synth`** — a template that is known to fail at deploy time never leaves your machine.
8
8
 
9
- Every bundled rule is backed by a `fail`/`pass` template pair, and the failure has been reproduced against real AWS (or is explicitly marked `doc-only`). Rules that the built-in validation engine already covers are deliberately **not** duplicated — a test suite enforces this.
9
+ The pack aims at **every deploy-time failure that no existing CDK mechanism already catches** — nothing narrower. Every bundled rule is backed by a `fail`/`pass` template pair, and the failure has been reproduced against real AWS (or is explicitly marked `doc-only`). Rules that the built-in validation engine already covers are deliberately **not** duplicated — a test suite enforces this.
10
10
 
11
11
  ## Quick start
12
12
 
@@ -77,11 +77,43 @@ To add cdk-preflight to a CDK app:
77
77
 
78
78
  The machine-readable findings are written to `cdk.out/validation-report.json` when synthesizing with `-c @aws-cdk/core:validationReportJson=true`.
79
79
 
80
+ ### Report missed deploy errors
81
+
82
+ cdk-preflight only knows the failure modes it has benched. When a deploy fails on a constraint the pack should have caught, that report is how new rules get added — but at that moment cdk-preflight is out of the loop (it passed the synth), so the instruction has to already sit in your agent's context. Paste this into your repository's `AGENTS.md` (or `CLAUDE.md`):
83
+
84
+ ```markdown
85
+ ## cdk-preflight: report missed deploy errors
86
+
87
+ If `cdk deploy` fails even though `cdk synth` (with cdk-preflight enabled) passed,
88
+ and the failure is a service-side validation rejection — CREATE_FAILED/UPDATE_FAILED
89
+ with a 400-class message such as "Invalid request provided: ..." — cdk-preflight is
90
+ missing a rule. Report it:
91
+
92
+ 1. Qualify first. NOT reportable: AccessDenied / permissions, service quotas,
93
+ throttling, name conflicts with existing resources, stabilization timeouts.
94
+ 2. Redact account-specific data (account IDs -> 123456789012, real ARNs/domains
95
+ -> placeholders).
96
+ 3. Search existing issues: https://github.com/badmintoncryer/cdk-preflight/issues
97
+ 4. With your user's approval, open a "Missed deploy error" issue with the verbatim
98
+ error message, the resource type, a minimal template snippet, and your
99
+ aws-cdk-lib / cdk-preflight versions.
100
+ ```
101
+
102
+ ## Scope and rule lifecycle
103
+
104
+ A constraint belongs in the pack when violating it makes a real deployment fail *and* no layer that sees the same synthesized template already blocks it. There is no further "is this worth a rule" question — if the gap is real, it gets a rule.
105
+
106
+ CDK L2 construct validation is deliberately **not** one of those layers. `CfnXxx` usage, escape hatches, `addPropertyOverride`, `cloudformation-include` and migrated templates all bypass L2, so an L2 guard covering the same mistake neither disqualifies a rule nor retires one.
107
+
108
+ That makes growth the normal state, and it has a consequence worth knowing before you upgrade: **new rules land in minor releases, so a minor upgrade can newly fail a `cdk synth` that passed yesterday.** That is intended, not a regression. If you need a frozen rule set, pin the version; to drop a single rule, `exclude: ['<rule-id>']`; to see everything without failing the build, `enforce: false`.
109
+
110
+ Rules move the other way too. Once the validation engine bundled in `aws-cdk-lib` (or CloudFormation's own pre-deploy validation) starts blocking a constraint, the rule is deleted rather than kept as a duplicate — staying on an older `aws-cdk-lib` and an older cdk-preflight keeps the old behavior.
111
+
80
112
  ## How it works
81
113
 
82
114
  `Preflight.apply()` evaluates the rules with the [cloudformation-validate](https://github.com/aws-cloudformation/cloudformation-validate) Rust/WASM engine that ships inside `aws-cdk-lib` — no extra binaries, no network access at synth time. In the default enforce mode the engine is invoked through a dedicated CDK validation plugin so that violations fail synthesis; with `enforce: false` the rules are instead injected into the CDK built-in `CloudFormationValidatePlugin` and reported as warnings.
83
115
 
84
- Constraints that *can* be expressed in schemas or generic engine rules are contributed upstream instead of living here; each rule's `meta.yaml` tracks its upstream status, and rules retire once the engine covers them.
116
+ Constraints that *can* be expressed in schemas or generic engine rules also make good upstream PRs to that engine, but nothing here waits on one — the upstream release cycle is deliberately slower than this pack's. Each rule's `meta.yaml` tracks its upstream status so that retirement stays bookkeeping.
85
117
 
86
118
  ## Requirements
87
119
 
package/lib/index.js CHANGED
@@ -17,7 +17,7 @@ const rules_generated_1 = require("./rules.generated");
17
17
  * Preflight.apply(app);
18
18
  */
19
19
  class Preflight {
20
- static [JSII_RTTI_SYMBOL_1] = { fqn: "cdk-preflight.Preflight", version: "0.0.24" };
20
+ static [JSII_RTTI_SYMBOL_1] = { fqn: "cdk-preflight.Preflight", version: "0.0.26" };
21
21
  /**
22
22
  * Register the cdk-preflight rules on an App or Stage.
23
23
  */
package/llms.txt CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  > A Rego rule pack that catches deploy-time CloudFormation failures at `cdk synth` time, by injecting rules for schema-invisible constraints (doc-only limits, cross-property rules) into the AWS CDK built-in validator (aws-cdk-lib >= 2.267.0).
4
4
 
5
+ Scope: every deploy-time failure that no existing CDK mechanism already catches. L2 construct validation does not count as coverage (L1, escape hatches, `addPropertyOverride` and included templates bypass it), so L2 overlap neither blocks a new rule nor retires an existing one; only the bundled engine or CloudFormation's server-side validation retires a rule, and then the rule is deleted outright. New rules land in minor releases, so a minor upgrade can newly fail a synth that passed before — pin the version, `exclude` the id, or set `enforce: false`.
6
+
5
7
  ## Install into a CDK app (agent playbook)
6
8
 
7
9
  1. `npm i -D cdk-preflight`
package/package.json CHANGED
@@ -87,7 +87,7 @@
87
87
  "publishConfig": {
88
88
  "access": "public"
89
89
  },
90
- "version": "0.0.24",
90
+ "version": "0.0.26",
91
91
  "jest": {
92
92
  "coverageProvider": "v8",
93
93
  "testMatch": [