cdk-preflight 0.0.157 → 0.0.158
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.
- package/.claude/skills/add-preflight-rule/SKILL.md +1 -1
- package/.jsii +2 -2
- package/AGENTS.md +10 -0
- package/lib/index.js +1 -1
- package/lib/rules.generated.js +65 -65
- package/package.json +1 -1
|
@@ -23,7 +23,7 @@ cdk-preflight のルール追加パイプライン。AGENTS.md の設計原則
|
|
|
23
23
|
- `package cdk_preflight` + `import rego.v1`、診断は `make_diag_full("<rule-id>", "ERROR", name, path, msg, fix, url)`
|
|
24
24
|
- ヘルパーは `_pf_<短縮名>_` プレフィックスで一意に
|
|
25
25
|
- `walk` ビルトインは無い。`to_number`/`object.get`/`flatten_list`/`resolve` で明示的に書く
|
|
26
|
-
- **個数を根拠にするなら数える前にガードを通す**。`flatten_list()` は `Fn::If` を真の枝に潰す(`AWS::NoValue` に落ちる要素まで数える)し、`Ref`/`Fn::Split` を 1 要素で返す。生読みの `count(object.get(...))` はもっと悪く、**オブジェクトに対してキーの数を返す**ので `Fn::If` のマーカーが「3 要素」に化ける。閾値なら `_pf_countable_list(name, path)`、`count(...) > 0` の存在ゲートなら `_pf_unconditional_list(name, path)`、取り出し済みの配列なら `_pf_countable_items(v)`(すべて `rules/_lib/lists.rego`)。`count(...) == 0`
|
|
26
|
+
- **個数を根拠にするなら数える前にガードを通す**。`flatten_list()` は `Fn::If` を真の枝に潰す(`AWS::NoValue` に落ちる要素まで数える)し、`Ref`/`Fn::Split` を 1 要素で返す。生読みの `count(object.get(...))` はもっと悪く、**オブジェクトに対してキーの数を返す**ので `Fn::If` のマーカーが「3 要素」に化ける。閾値なら `_pf_countable_list(name, path)`、`count(...) > 0` の存在ゲートなら `_pf_unconditional_list(name, path)`、取り出し済みの配列なら `_pf_countable_items(v)`(すべて `rules/_lib/lists.rego`)。`count(...) == 0` の非空検査と `count(...) < N` の下限検査は不要(過大カウントでは鳴らなくなる方向にしか動かない)。**`resolve()` で取った値にガードを足しても無駄**(`Fn::If` はそこに届く前に真の枝へ潰れている)ので、その場合は `is_array()` だけ置く。詳細は AGENTS.md(#258 / #272)
|
|
27
27
|
- fail テンプレートはこのルール**だけ**に違反、pass テンプレートは完全クリーン
|
|
28
28
|
- **数値・長さ・個数の制約は境界ちょうどで書く**。fail は「違反する値のうち限界に最も近いもの」、pass は「限界そのもの」— 20 文字下限なら fail=19 文字 / pass=20 文字であって、fail=5 文字 / pass=26 文字ではない。緩いペアはルールの向きしか証明しない(`count(v) < 20` は fail が 5 文字なら定数が `< 10` でも鳴り、pass が 26 文字なら `< 25` でも黙る)ので、定数も比較演算子も固定できないし、実機ゲートの証拠も弱くなる(19 文字が CREATE に失敗して初めて下限 20 が実証される)。ルールが両端を見ているレンジは fail に両端を置く(fail の判定は「自分のルールの診断が 1 件以上」なので、違反リソースを 2 つ並べてよい)。エンジンのスキーマが既に持っている側の端は入れない — ルールもそこは見ていない(原則 1)し、重複ガードが赤くなる。**しきい値はリテラルでなくてもよい** — 限界が別プロパティの値(`MinValue` と `MaxValue`、コンテナの `Memory` とタスクの `Memory`)なら、fail は 2 つの値を違反側に 1 だけずらし、pass は同値にする(同値で既に違反する `>=` / `<=` は逆で、fail が同値・pass が 1 ずれ)。`MinValue 10 / MaxValue 1` ではルールが `mn > mx + 5` でも鳴るので定数も演算子も固定できない。順序の無い制約(プロパティ欠落、enum の値違い、リソース間の不整合)には境界が無いので対象外
|
|
29
29
|
4. **ローカルゲート**: まず `npx ts-node --transpile-only --project test/tsconfig.json scripts/rule-check.ts check <service|rule-id>...` を回す。`rules/` を直接読んで 1 エンジンに全ルールを載せ、fail が自分のルールで鳴るか / pass が全ルール無音か / どちらも組み込みエンジンに止められないかを返す(`bundle-rules` も meta.yaml の evidence も要らないので、実機ゲート前の直しはここで回す。80 本で数秒)。全部 `ok` になってから `npx projen bundle-rules && npx jest test/rules test/structure.test.ts`(`test/rules` は `rules.test.ts` と、ルール表を回す `rules.shard*.test.ts` の両方に当たる)(`bundle-rules` は `src/rules.generated.ts` / `docs/rules.md` に加えて **README のルール数バッジと対応リソースタイプ一覧も書き換える**。手で直さない。古いままなら `structure.test.ts` が落ちる)。**`rules/<service>/` を新設したときは `npx projen` も回す** — `.github/workflows/monthly-verify.yml` のサービス行列はそこから生成されるので、`bundle-rules` だけでは更新されず PR の self-mutation チェックが赤くなる(2026-09-13、Athena 追加で踏んだ)。`structure.test.ts` は手順 3 の境界値を機械で見る(rego のしきい値ごとに fail へ「限界に最も近い違反値」、pass へ「限界そのもの」が現れるか)。しきい値の検出が意味を持たないルールだけ `rules/_boundary-exceptions.txt` に理由付きで逃がす。**jest は `-t` で対象を絞る**。フルスイートは PR 直前の 1 回だけでよく、実測では 401 回中 73 回がフル実行で合計 5.8 時間を溶かしている。
|
package/.jsii
CHANGED
|
@@ -9600,6 +9600,6 @@
|
|
|
9600
9600
|
"symbolId": "src/index:PreflightOptions"
|
|
9601
9601
|
}
|
|
9602
9602
|
},
|
|
9603
|
-
"version": "0.0.
|
|
9604
|
-
"fingerprint": "
|
|
9603
|
+
"version": "0.0.158",
|
|
9604
|
+
"fingerprint": "m9vWzrW3d18LT2GCNqYQxBShwcyyzGvqC/yGwl6uWPw="
|
|
9605
9605
|
}
|
package/AGENTS.md
CHANGED
|
@@ -145,6 +145,16 @@ If you are a subagent running one of these phases, do not spawn further agents.
|
|
|
145
145
|
- **Proving a property absent** (measured 2026-09-02, 1.7.0-beta): `resolve()` is undefined for a missing key AND for an unresolvable value (e.g. `Ref` to a no-default parameter), so `not resolve(...)` misfires on present-but-unresolvable values. The only reliable absence proof is the preprocessed document: `props := input.resources[name].properties; is_object(props); object.get(props, "<Key>", "__pf_absent") == "__pf_absent"`. The `is_object` guard fails closed if the internal shape ever changes. This is the sanctioned exception to "don't depend on `input` internals". Reference implementations: `pf-ecs-container-memory-required`, `pf-ecs-fargate-task-cpu-memory`.
|
|
146
146
|
- `flatten_list()` returns an **empty list for an absent path** — it cannot distinguish "absent" from "present and empty" (measured 2026-09-02). Use the `input.resources` idiom above when the distinction matters (`pf-ecs-container-definitions-empty`).
|
|
147
147
|
- **`flatten_list()` element counts are not the counts CloudFormation deploys** (measured 2026-09-25, issue #258). Two separate distortions: `Fn::If` is **collapsed to its true branch**, so an element that becomes `AWS::NoValue` is still counted (over-count); and `Ref` / `Fn::Split` come back as a **single element** whatever the imported list holds (under-count). The `{"__conditional": "C", "__if_true": ..., "__if_false": null}` marker only exists in the raw `input.resources[<name>].properties` document — `flatten_list()` never shows it. So a rule that makes a claim about *how many* entries there are must guard first: `_pf_countable_list(name, path)` (`rules/_lib/lists.rego`) is true only when the raw value is a literal array with no conditional element, and `_pf_unconditional_list(name, path)` is the looser guard for `count(...) > 0` presence gates, where a `Ref` list is still definitely present. Reference implementations: `pf-codebuild-secondary-artifacts-max-12` (max), `pf-msk-client-subnets-count` (min), `pf-ec2-instance-eni-exclusive` (presence gate); regression probe in `test/flatten-list-count.test.ts`. A `count(...) == 0` "must not be empty" check needs no guard — both distortions can only make it miss, never misfire. The same hazard reaches a nested array pulled out with `object.get` and it bites harder there: **`count()` of a marker object returns its key count**, so `count(object.get(s, "ClientAliases", []))` on an `Fn::If` reads as 3. Guard those with `_pf_countable_items(<the extracted value>)`, which requires an actual array (`pf-ecs-svc-connect-client-aliases-max`).
|
|
148
|
+
- **Which guard works depends on how the value was reached** (measured 2026-09-24, issue #272). The markers only survive on some paths, so a guard on the wrong path is dead code that looks like protection:
|
|
149
|
+
|
|
150
|
+
| 読み方 | `Fn::If` | 解決できない `Ref` |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| 生の `input.resources[<name>].properties` | `__conditional` マーカー(3 キー) | `__dynamic` マーカー(1 キー) |
|
|
153
|
+
| `flatten_list()` の `it.value` の入れ子 | **真の枝に潰れて消える** | `__dynamic` が残る |
|
|
154
|
+
| `resolve()` | **真の枝に潰れて消える** | **undefined**(ルールごと降りる) |
|
|
155
|
+
|
|
156
|
+
So `_pf_countable_items()` earns its place on a **raw** read (it catches both) and on a `flatten_list` value (it catches the `Ref` half via `is_array`); after `resolve()` it can only repeat an `is_array()` you should already have. The `Fn::If` over-count on a `flatten_list` / `resolve` value is **not fixed and not detectable** from those paths — it needs a raw lookup by path, which is what `_pf_countable_list(name, path)` does. Two more measurements worth knowing: `Fn::Split` over a *literal* string is expanded into a real array in the raw document (only `Fn::Split` over a `Ref` stays a marker), and `resolve()` of an `Fn::GetAtt` returns a 2-key marker object, so `count()` on it silently yields 2.
|
|
157
|
+
- **`count(...) < N` minimum checks need no guard.** An over-count can only push the count up, and if the counted `k` is already below `N` then the deployed count (`<= k`) is below it too — the diagnosis holds. Guarding there only buys a false negative. `< 1` is the same thing as `== 0`.
|
|
148
158
|
- **Intrinsics inside `input.resources` values are marker objects, never strings** (measured 2026-09-03, 1.7.0-beta): `Fn::GetAtt` appears as `{"__kind": "getatt:Arn", "__ref": "B"}` and `Fn::Sub` as `{"__dynamic": "Sub:..."}` at any nesting depth. A plain `is_string(v)` guard therefore safely mutes rules that judge string shapes (ARN format, prefixes) on values users wired via intrinsics — no logical-ID false positives. Reference implementations: `pf-iam-policy-resource-format`, `pf-iam-policy-action-format`.
|
|
149
159
|
- **Dynamic reference strings are markerized too** (measured 2026-09-03): a literal `{{resolve:ssm:/path}}` property value surfaces as `{"__dynamic": "dynamic reference: {{resolve:ssm:/path}}", "__param_type": "String"}` and `resolve()` is undefined for it. A rule that judges the reference text itself (e.g. the SSM parameter path) must read the raw property with `object.get` and extract the `__dynamic` string. Reference implementation: `pf-ec2-instance-ami-arch`.
|
|
150
160
|
- **Deploy-region rules**: the enforce plugin injects a generated module defining `deploy_region` (and `deploy_account` when the account id is concrete) whenever the app-level region is concrete (`deployEnvironmentModule` in `src/private/enforce.ts`; the test harness injects `us-east-1` for fixtures). Rules must read it as `data.cdk_preflight.deploy_region` — the data reference is undefined when not injected, so the rule skips; a bare `deploy_region` variable would be a compile error. `data.cdk_preflight.deploy_account` works the same way (`pf-kms-key-policy-lockout` treats any account as ours when it is absent, `pf-kms-alias-target` skips its account check). Consequences: region-dependent rules are silent in warn mode (`enforce: false` hands rules to the CDK built-in plugin, which we cannot inject into) and in region-agnostic apps; say so in `meta.yaml#repro.evidence`. Reference implementations: `pf-dynamodb-global-table-replica-region`, `pf-dynamodb-kinesis-stream-region`.
|
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.
|
|
20
|
+
static [JSII_RTTI_SYMBOL_1] = { fqn: "cdk-preflight.Preflight", version: "0.0.158" };
|
|
21
21
|
/**
|
|
22
22
|
* Register the cdk-preflight rules on an App or Stage.
|
|
23
23
|
*/
|