


Add Features to a Multi-Language Infrastructure-as-Code Library Without Breaking Existing Users
Use when changing a typed infrastructure-as-code library with generated bindings for other languages. Puts features in the right layer, avoids binding-unsafe syntax, guards behavior changes with feature flags, keeps permissions narrow, and adds docs and tests.
To use this AI rule
## Keep Existing User Code Working and Expose the Full Platform
Work for the library's users, maintainers, and wider ecosystem, not only for the person requesting the change.
- Preserve backward compatibility; existing user code and synthesized infrastructure must keep working by default.
- Follow the principle of least surprise: APIs should match the user's mental model.
- Expose the platform's full capability surface while providing sensible, overridable defaults.
- Keep an escape hatch to the generated low-level resource so missing high-level features never block users.
- When rules are ambiguous, record the decision and reasoning in the change description instead of guessing silently.
If the requested work falls outside code authoring or review for this kind of library, stop and surface the scope mismatch. Do not apply specialized rules to unrelated work.
## Put Most Features in the Hand-Written Level 2 API
Maintain three distinct abstraction levels:
- Level 1 consists of generated wrappers around the provider's resource schema. Never edit them manually.
- Level 2 consists of hand-written, intent-based APIs with useful defaults. Most feature work belongs here.
- Level 3 consists of multi-resource patterns. Add new patterns only when the repository explicitly accepts that abstraction level.
Design Level 2 around user intent rather than the provider's raw schema. Hide generated-schema details, internal wiring, permission actions, and physical identifiers from ordinary callers while still exposing the complete provider capability set.
Represent resource contracts as interfaces so third-party implementations can behave like first-party ones. Prefer the smallest interface that satisfies a parameter: an identifier-only reference first, a full behavioral interface only when convenience operations are required, and concrete classes only in exceptional cases.
## Pick a Mixin, Facade, or Trait for Each New Feature
Place each feature by what it affects:
1. A direct low-level property passthrough with no logic belongs in a thin property mixin.
2. Logic that changes the resource's own properties or lifecycle belongs in a standalone mixin.
3. Behavior serving external consumers, such as grants, metrics, or events, belongs in a facade.
4. A cross-cutting capability that other constructs query belongs in a trait.
5. Otherwise, use a method on the Level 2 construct.
Do not blur these roles. Mixins are inward-facing, facades are outward-facing, and traits advertise service-independent capabilities.
Use static type guards based on stable runtime markers instead of `instanceof`, because objects may cross package or language boundaries. Keep abstract bases available for implementation but do not accept them as public parameters.
## Name Public APIs Consistently and Avoid Types Other Languages Cannot Use
Use consistent public names: PascalCase for classes and enums, camelCase for properties and methods, an `I` prefix for behavioral interfaces, no prefix for data structures, and upper snake case for enum members. Preserve official platform terminology and include units when a strong type is unavailable.
Property objects must be readonly, flat, and named after the construct. Every optional property needs a documented default that explains behavior rather than merely saying it is undefined. Prefer strong duration, size, and resource-reference types over raw numbers or identifier strings.
Do not expose generated low-level types through high-level properties. Avoid union types and other constructs unsupported by the binding generator; use enum-like classes, separate property objects, or factory methods instead.
Public type file locations are part of the external contract. Do not move them casually. Avoid mapped or conditional types, overloaded functions, language-specific namespaces, and exported object constants when they cannot be represented consistently in target languages.
Do not use fluent methods that return `this`; they translate poorly across languages and hide mutation behind chaining.
## Write Clear Validation Errors and Check for Unresolved Values First
Use scoped validation errors when a construct context exists and unscoped validation errors otherwise. Error codes must be compile-time literals, reusable when cause and resolution match, and messages should identify the wrong value, expected values, and the required correction.
Prefer auto-correcting safe configuration over throwing. Fail eagerly for API misuse, attach post-construction validation for accumulated state, and emit synthesis diagnostics for environmental problems. Do not catch unrecoverable construction errors; represent genuinely recoverable outcomes in return values.
Unresolved values may represent strings, numbers, lists, or resource attributes. Check whether a value is unresolved before validating length, range, iteration, or truthiness. Tokenized lists may expose marker lengths, and tokenized scalar values may appear false.
Use explicit undefined checks for optional values, a token-aware number conversion, and never include unresolved resource attributes in hashes for physical names.
For state accumulated after construction, use a deferred-value box that captures mutation call sites. Use list, value, map, or set boxes; pass them to low-level properties through token adapters; mutate them through box operations; and never mutate the construct tree inside a lazy or box callback.
## Keep Permissions Narrow and Gate Behavior Changes Behind Feature Flags
Prefer specific permission actions over full-service wildcards, scope resource identifiers to the narrowest practical prefix, and group related actions by resource scope. Use the library's grant helpers instead of assembling policy statements by hand.
Trust policies must include source-account and source-resource conditions where confused-deputy risk exists. Encryption grants should constrain use through the intended service. Emit a synthesis-time warning whenever configuration creates public access.
When an existing API's observable behavior changes, protect compatibility with a feature flag:
- Use a bug-fix flag when old behavior was incorrect.
- Use an API-default flag when old behavior remains valid but is no longer recommended, and document compatibility with the old behavior.
- Make unconfigured applications retain their previous behavior.
- Security flags may reduce trust or permissions, but must not silently broaden them; broader access requires an explicit API option.
Do not use a flag merely because a replacement construct exists. Deprecate the old construct instead, and warn about behavior changes through the standard annotation mechanism.
## Build the Affected Package First and Update Integration Snapshots
Build the affected package before running tests, lint, integration snapshots, or documentation example checks. During iteration, use the narrowest relevant module or test target; before handoff, return to the repository-required package and compatibility gates.
Know which files are generated, where stable and experimental integration tests live, and where agent-oriented design references override longer human-oriented guides. Never edit generated Level 1 sources directly.
Use repository-local package commands and working directories rather than inventing alternate build paths. Run automatic lint fixes only when their resulting diff remains within the requested scope.
When changing logical identifiers, update every affected integration snapshot, including cross-package snapshots. A successful local unit test is not evidence that generated bindings, documentation examples, or integration templates still compile.
## Document Every New Public API and Keep Examples Compiling
Document every new public class, method, property, and interface. Use a concise summary, then a blank line and explanatory body. Include parameter, return, default, related-reference, example, and generated-attribute tags when applicable.
Attribute names should begin with the resource type rather than use ambiguous names. Copy authoritative platform descriptions when available, but do not duplicate documentation on overrides that inherit it from a base interface.
Each module guide should state maturity, place a simple example near the top, and include examples for major use cases. All ordinary typed examples must compile; use the repository's explicit opt-out marker only when a fixture genuinely cannot apply.
Every feature change must update the relevant module guide. Documentation-only text does not replace API tests, and API implementation does not excuse stale examples.
## Assert Specific Properties in Unit Tests and Add Integration Tests
In unit tests, assert the specific generated properties that matter rather than comparing entire templates or accepting arbitrary values. Use partial-object, exact-object, array, regular-expression, and absence matchers deliberately.
Cover boundary values with data-driven tests. Error tests must assert the exact diagnostic and identify failure behavior in the test name. Test utility functions separately from constructs and preserve tests for deprecated APIs.
New optional properties require backward-compatibility tests proving that default behavior is unchanged. Grant helpers must be tested with created and imported roles, users, and service principals.
Integration tests should use the repository's integration-test construct without an extra synthesis call. Assertions are preferred for all new tests and required for custom resources. Avoid open network peers unless open access is the behavior under test.
Add integration coverage for new generated resource types or properties, cross-service behavior, new versions, and custom resources. Separate tests by feature area and keep snapshot extensions and locations consistent.
## Link Change Titles to Issues and Never Change Stable Identifiers
Use conventional change titles with the affected module when applicable. Feature and fix changes must reference an issue. Feature changes require unit tests, integration snapshots, and module documentation updates.
Breaking changes belong only in explicitly experimental packages and must be declared prominently in the change description. Keep one concern per change; submit cosmetic edits separately.
Never add speculative abstractions, change stable construct identifiers, leave commented-out or dead code, or silence lint rules to hide violations. Identifier changes can replace deployed resources and cause data loss.
Before adding validation to an existing API, determine whether previously accepted input could deploy successfully. If it could, gate the new rejection behind a compatibility flag. If it always failed at deployment, prefer early synthesis failure and document why the stricter validation is justified.
Before handoff, review the diff for generated-file edits, public type moves, binding-incompatible syntax, stale snapshots, missing documentation, compatibility regressions, and unrelated changes.
