@intentius/chant 0.44.14 → 0.46.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.
- package/dist/audit/discover.d.ts +26 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +10 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/cli/build-params-cli.d.ts +16 -7
- package/dist/cli/build-params-cli.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +20 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +9 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-docs.d.ts +13 -0
- package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +24 -14
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts +14 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts +5 -0
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lexicon-surface-diff.d.ts +6 -0
- package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts +28 -0
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +13 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts +27 -0
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +7 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-pages.d.ts +39 -0
- package/dist/codegen/docs-pages.d.ts.map +1 -0
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/codegen/docs-sidebar.d.ts +13 -2
- package/dist/codegen/docs-sidebar.d.ts.map +1 -1
- package/dist/codegen/docs-types.d.ts +30 -10
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts +14 -2
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.d.ts +11 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/surface-snapshot.d.ts +27 -3
- package/dist/codegen/surface-snapshot.d.ts.map +1 -1
- package/dist/codegen/validate.d.ts +21 -3
- package/dist/codegen/validate.d.ts.map +1 -1
- package/dist/components/capability-plugin.d.ts +13 -2
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/config.d.ts +85 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/env.d.ts +12 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +100 -1
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +12 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ops.d.ts +17 -0
- package/dist/graph-ops.d.ts.map +1 -0
- package/dist/lexicon.d.ts +182 -2
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +23 -2
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +8 -0
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/replay.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/teardown.d.ts +130 -0
- package/dist/lifecycle/teardown.d.ts.map +1 -0
- package/dist/lifecycle/types.d.ts +7 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lint/engine.d.ts +6 -2
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/policy.d.ts.map +1 -1
- package/dist/lint/rule.d.ts +31 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/lint/rules/cor021-env-literal-name.d.ts +3 -0
- package/dist/lint/rules/cor021-env-literal-name.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +2 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/managed-fields.d.ts +33 -21
- package/dist/managed-fields.d.ts.map +1 -1
- package/dist/observation.d.ts +23 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/op/builders.d.ts +36 -7
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/testing.d.ts +136 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/yaml.d.ts +0 -8
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/__snapshots__/okf.test.ts.snap +20 -0
- package/src/audit/discover.test.ts +55 -1
- package/src/audit/discover.ts +70 -2
- package/src/audit/edge-init-safety.test.ts +149 -18
- package/src/audit/report-model.ts +12 -1
- package/src/audit/rules-doc.ts +1 -0
- package/src/build-params.test.ts +52 -0
- package/src/cli/build-params-cli.test.ts +25 -8
- package/src/cli/build-params-cli.ts +37 -23
- package/src/cli/commands/__fixtures__/audit-coverage/.github/workflows/ci.yml +5 -0
- package/src/cli/commands/__fixtures__/audit-coverage/Dockerfile +2 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/main.tf +3 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/stack.json +6 -0
- package/src/cli/commands/__fixtures__/audit-coverage/k8s/deploy.yaml +12 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/pages/getting-started.mdx +16 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +10 -2
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +3 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +2 -1
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +2 -1
- package/src/cli/commands/audit.test.ts +83 -1
- package/src/cli/commands/audit.ts +121 -28
- package/src/cli/commands/build.test.ts +275 -5
- package/src/cli/commands/build.ts +86 -14
- package/src/cli/commands/check-lexicon-docs.ts +30 -0
- package/src/cli/commands/check-lexicon-examples.test.ts +73 -7
- package/src/cli/commands/check-lexicon-examples.ts +66 -17
- package/src/cli/commands/check-lexicon.test.ts +46 -2
- package/src/cli/commands/check-lexicon.ts +64 -4
- package/src/cli/commands/init-lexicon/templates/codegen.ts +5 -2
- package/src/cli/commands/init-lexicon/templates/docs.ts +35 -2
- package/src/cli/commands/init-lexicon.test.ts +42 -1
- package/src/cli/commands/init-lexicon.ts +3 -1
- package/src/cli/commands/lexicon-rolling-upgrade.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.test.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.ts +10 -1
- package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
- package/src/cli/commands/lint.ts +19 -6
- package/src/cli/commands/onboard.test.ts +319 -225
- package/src/cli/commands/onboard.ts +144 -58
- package/src/cli/handlers/build.test.ts +3 -3
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.ts +2 -2
- package/src/cli/handlers/graph.test.ts +44 -0
- package/src/cli/handlers/graph.ts +24 -4
- package/src/cli/handlers/lifecycle.test.ts +348 -1
- package/src/cli/handlers/lifecycle.ts +235 -3
- package/src/cli/handlers/run.test.ts +5 -5
- package/src/cli/handlers/run.ts +1 -0
- package/src/cli/handlers/search-live.test.ts +217 -0
- package/src/cli/handlers/search.test.ts +41 -1
- package/src/cli/handlers/search.ts +60 -8
- package/src/cli/main.ts +17 -4
- package/src/cli/registry.ts +7 -0
- package/src/cli/security-boundary.test.ts +2 -1
- package/src/codegen/docs-pages.test.ts +194 -0
- package/src/codegen/docs-pages.ts +138 -0
- package/src/codegen/docs-sections.ts +0 -8
- package/src/codegen/docs-sidebar.ts +50 -37
- package/src/codegen/docs-types.ts +32 -4
- package/src/codegen/docs.ts +78 -40
- package/src/codegen/lexicon-regen.ts +22 -2
- package/src/codegen/rolling-upgrade.test.ts +1 -1
- package/src/codegen/rolling-upgrade.ts +1 -1
- package/src/codegen/surface-snapshot.test.ts +139 -0
- package/src/codegen/surface-snapshot.ts +297 -122
- package/src/codegen/validate.test.ts +49 -3
- package/src/codegen/validate.ts +36 -7
- package/src/components/auto-release.ts +1 -1
- package/src/components/capability-plugin.ts +23 -2
- package/src/components/driver.test.ts +45 -0
- package/src/components/driver.ts +64 -27
- package/src/components/index.ts +2 -0
- package/src/components/starter-plugin.ts +5 -2
- package/src/config.test.ts +40 -0
- package/src/config.ts +159 -8
- package/src/discovery/fold-import.test.ts +427 -2
- package/src/discovery/fold-import.ts +163 -7
- package/src/discovery/sandbox/driver.test.ts +11 -0
- package/src/env.test.ts +35 -1
- package/src/env.ts +17 -3
- package/src/fold/fold.test.ts +41 -0
- package/src/fold/fold.ts +372 -2
- package/src/graph-ir-live.test.ts +42 -0
- package/src/graph-ir.ts +42 -2
- package/src/graph-ops.test.ts +69 -0
- package/src/graph-ops.ts +42 -0
- package/src/lexicon.ts +182 -2
- package/src/lifecycle/change-set.test.ts +41 -0
- package/src/lifecycle/change-set.ts +30 -2
- package/src/lifecycle/deep-observe.ts +4 -0
- package/src/lifecycle/index.ts +1 -0
- package/src/lifecycle/observe.ts +29 -2
- package/src/lifecycle/replay.ts +3 -0
- package/src/lifecycle/snapshot.ts +3 -0
- package/src/lifecycle/teardown.test.ts +537 -0
- package/src/lifecycle/teardown.ts +357 -0
- package/src/lifecycle/types.ts +7 -0
- package/src/lint/engine.ts +7 -1
- package/src/lint/policy.ts +14 -3
- package/src/lint/rule.ts +23 -0
- package/src/lint/rules/cor021-env-literal-name.test.ts +128 -0
- package/src/lint/rules/cor021-env-literal-name.ts +114 -0
- package/src/lint/rules/index.ts +4 -1
- package/src/managed-fields.test.ts +15 -42
- package/src/managed-fields.ts +43 -26
- package/src/observation.test.ts +24 -8
- package/src/observation.ts +48 -4
- package/src/op/builders.ts +40 -7
- package/src/op/index.ts +1 -1
- package/src/ownership.test.ts +63 -1
- package/src/testing.test.ts +261 -0
- package/src/testing.ts +338 -0
- package/src/yaml.test.ts +15 -0
- package/src/yaml.ts +19 -2
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teardown planning (#1222) — the enumeration half of
|
|
3
|
+
* `chant lifecycle teardown <env>`.
|
|
4
|
+
*
|
|
5
|
+
* Answers one question: which live resources carry THIS project's ownership
|
|
6
|
+
* marker for THIS environment? Selection is marker-scoped by construction —
|
|
7
|
+
* managed-by present, stack equal to the project's `ownership.stack`, env
|
|
8
|
+
* equal to the argument — so a foreign stack's resources, another env's
|
|
9
|
+
* resources, and unmarked resources are out of scope by shape, not by
|
|
10
|
+
* filtering discipline someone has to remember.
|
|
11
|
+
*
|
|
12
|
+
* Stateless: live markers only. No build, no snapshot, no ledger — the
|
|
13
|
+
* ownership record lives on the cloud resource (see ../ownership.ts), and this
|
|
14
|
+
* module reads it back from there.
|
|
15
|
+
*
|
|
16
|
+
* Two paths per lexicon:
|
|
17
|
+
* - the `teardownOwned` capability, where the lexicon enumerates its own
|
|
18
|
+
* would-delete set (and can use a read shaped for deletion — aws's
|
|
19
|
+
* stack-level path, k8s's prune selector);
|
|
20
|
+
* - a fallback over `describeResources` + the {@link ResourceMetadata.marker}
|
|
21
|
+
* field (#1222 PR 1), for lexicons that have not implemented the capability
|
|
22
|
+
* yet. Best-effort: a lexicon whose thin read only resolves declared names
|
|
23
|
+
* returns nothing here, which the plan reports as a skip, not as clean.
|
|
24
|
+
*
|
|
25
|
+
* Whichever path ran, core re-checks every candidate's marker and drops
|
|
26
|
+
* mismatches — an implementation bug can narrow the set, never widen it.
|
|
27
|
+
*
|
|
28
|
+
* The execution half is {@link executeTeardown}: it drives each lexicon's
|
|
29
|
+
* `executeTeardown` capability over the planned set, then runs one bounded
|
|
30
|
+
* retry pass over the failures. Both halves are exported as functions —
|
|
31
|
+
* #1224's test-env harness calls them in-process, not only through the verb.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { normalizeObservation, unobservedAll } from "../observation";
|
|
35
|
+
import type { ObservationLexicon, TeardownCandidate, TeardownHole, TeardownOutcome } from "../lexicon";
|
|
36
|
+
import type { OwnershipMarker } from "../ownership";
|
|
37
|
+
|
|
38
|
+
/** One would-delete row in a teardown plan, attributed to its lexicon. */
|
|
39
|
+
export interface TeardownPlanEntry extends TeardownCandidate {
|
|
40
|
+
lexicon: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One hole in a teardown plan (#1089), attributed to its lexicon. */
|
|
44
|
+
export interface TeardownPlanHole extends TeardownHole {
|
|
45
|
+
lexicon: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The plan `chant lifecycle teardown <env>` prints and #1224 consumes. */
|
|
49
|
+
export interface TeardownPlan {
|
|
50
|
+
environment: string;
|
|
51
|
+
/** The project's ownership stack — the identity everything was selected on. */
|
|
52
|
+
stack: string;
|
|
53
|
+
/** The would-delete set. Every entry's marker equals `{ stack, env: environment }`. */
|
|
54
|
+
entries: TeardownPlanEntry[];
|
|
55
|
+
/** What could not be read (#1089). A plan with holes is incomplete, not clean. */
|
|
56
|
+
holes: TeardownPlanHole[];
|
|
57
|
+
/**
|
|
58
|
+
* Lexicons that took part in neither path — no `teardownOwned`, no
|
|
59
|
+
* `describeResources`. Reported so "nothing to delete" can never quietly
|
|
60
|
+
* mean "nobody looked".
|
|
61
|
+
*/
|
|
62
|
+
skipped: string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface PlanTeardownOptions {
|
|
66
|
+
/** The environment being torn down — the marker env to select on. */
|
|
67
|
+
environment: string;
|
|
68
|
+
/** This project's ownership stack (`ownership.stack` in chant.config). */
|
|
69
|
+
stack: string;
|
|
70
|
+
plugins: ObservationLexicon[];
|
|
71
|
+
/** Deployed stack name, for a multi-stack project. */
|
|
72
|
+
deployedStack?: string;
|
|
73
|
+
/** Region that stack is deployed in. */
|
|
74
|
+
region?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Every deployed stack a multi-stack project declares (`stacks` in
|
|
77
|
+
* chant.config), for a lexicon whose teardown is stack-shaped (aws
|
|
78
|
+
* enumerates and deletes whole stacks). Forwarded to `teardownOwned` /
|
|
79
|
+
* `executeTeardown` as `stacks`.
|
|
80
|
+
*/
|
|
81
|
+
deployedStacks?: Array<{ name: string; region?: string }>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** True when `marker` is exactly the identity this plan selects on. */
|
|
85
|
+
function markerMatches(marker: OwnershipMarker | undefined, stack: string, env: string): boolean {
|
|
86
|
+
return marker !== undefined && marker.stack === stack && marker.env === env;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Enumerate what `chant lifecycle teardown <env>` would delete. Read-only —
|
|
91
|
+
* this function never deletes and never will; execution composes on top of the
|
|
92
|
+
* plan it returns.
|
|
93
|
+
*/
|
|
94
|
+
export async function planTeardown(opts: PlanTeardownOptions): Promise<TeardownPlan> {
|
|
95
|
+
const marker: OwnershipMarker = { stack: opts.stack, env: opts.environment };
|
|
96
|
+
const entries: TeardownPlanEntry[] = [];
|
|
97
|
+
const holes: TeardownPlanHole[] = [];
|
|
98
|
+
const skipped: string[] = [];
|
|
99
|
+
|
|
100
|
+
for (const plugin of opts.plugins) {
|
|
101
|
+
if (plugin.teardownOwned) {
|
|
102
|
+
let enumeration;
|
|
103
|
+
try {
|
|
104
|
+
enumeration = await plugin.teardownOwned({
|
|
105
|
+
environment: opts.environment,
|
|
106
|
+
marker,
|
|
107
|
+
...(opts.deployedStack ? { stack: opts.deployedStack } : {}),
|
|
108
|
+
...(opts.region ? { region: opts.region } : {}),
|
|
109
|
+
...(opts.deployedStacks && opts.deployedStacks.length > 0 ? { stacks: opts.deployedStacks } : {}),
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
// A failed enumeration is a hole over the whole lexicon, not a clean
|
|
113
|
+
// lexicon (#1089): nothing was read, so nothing is known.
|
|
114
|
+
holes.push({
|
|
115
|
+
lexicon: plugin.name,
|
|
116
|
+
name: "*",
|
|
117
|
+
reason: "read-failed",
|
|
118
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
for (const candidate of enumeration.candidates) {
|
|
123
|
+
// Defense in depth: the contract says every candidate carries the
|
|
124
|
+
// requested identity; a candidate that does not is dropped here so an
|
|
125
|
+
// implementation bug cannot widen the delete set.
|
|
126
|
+
if (!markerMatches(candidate.marker, opts.stack, opts.environment)) continue;
|
|
127
|
+
entries.push({ lexicon: plugin.name, ...candidate });
|
|
128
|
+
}
|
|
129
|
+
for (const hole of enumeration.holes ?? []) {
|
|
130
|
+
holes.push({ lexicon: plugin.name, ...hole });
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (plugin.describeResources) {
|
|
136
|
+
let observed;
|
|
137
|
+
try {
|
|
138
|
+
observed = normalizeObservation(
|
|
139
|
+
await plugin.describeResources({
|
|
140
|
+
environment: opts.environment,
|
|
141
|
+
buildOutput: "",
|
|
142
|
+
entityNames: [],
|
|
143
|
+
entities: new Map(),
|
|
144
|
+
owned: true,
|
|
145
|
+
...(opts.deployedStack ? { stack: opts.deployedStack } : {}),
|
|
146
|
+
...(opts.region ? { region: opts.region } : {}),
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
151
|
+
observed = {
|
|
152
|
+
resources: {},
|
|
153
|
+
unobserved: unobservedAll(["*"], "read-failed", message),
|
|
154
|
+
queried: {},
|
|
155
|
+
notes: [],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
for (const [name, meta] of Object.entries(observed.resources)) {
|
|
159
|
+
// Marker-scoped by construction: no marker, foreign stack, or foreign
|
|
160
|
+
// env means not a candidate — a resource with no readable identity is
|
|
161
|
+
// never promoted to a delete.
|
|
162
|
+
if (!markerMatches(meta.marker, opts.stack, opts.environment)) continue;
|
|
163
|
+
entries.push({
|
|
164
|
+
lexicon: plugin.name,
|
|
165
|
+
name,
|
|
166
|
+
type: meta.type,
|
|
167
|
+
...(meta.physicalId ? { physicalId: meta.physicalId } : {}),
|
|
168
|
+
marker: meta.marker!,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
for (const [name, u] of Object.entries(observed.unobserved)) {
|
|
172
|
+
holes.push({
|
|
173
|
+
lexicon: plugin.name,
|
|
174
|
+
name,
|
|
175
|
+
...(u.type ? { type: u.type } : {}),
|
|
176
|
+
reason: u.reason,
|
|
177
|
+
...(u.detail ? { detail: u.detail } : {}),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
skipped.push(plugin.name);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
entries.sort((a, b) => a.lexicon.localeCompare(b.lexicon) || a.name.localeCompare(b.name));
|
|
187
|
+
return { environment: opts.environment, stack: opts.stack, entries, holes, skipped };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** One planned entry's fate after execution, attributed to its lexicon. */
|
|
191
|
+
export interface TeardownOutcomeEntry extends TeardownPlanEntry {
|
|
192
|
+
/**
|
|
193
|
+
* `skipped` is core's verdict for a candidate whose lexicon implements no
|
|
194
|
+
* `executeTeardown` yet; the other three come from the lexicon (see
|
|
195
|
+
* {@link TeardownOutcome}).
|
|
196
|
+
*/
|
|
197
|
+
outcome: "deleted" | "failed" | "not-prunable" | "skipped";
|
|
198
|
+
/** The error for `failed`, the reason for `not-prunable`/`skipped`. */
|
|
199
|
+
detail?: string;
|
|
200
|
+
/** True when this final outcome came from the bounded retry pass. */
|
|
201
|
+
retried?: boolean;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** What `chant lifecycle teardown <env> --yes` prints and #1224 consumes. */
|
|
205
|
+
export interface TeardownReport {
|
|
206
|
+
environment: string;
|
|
207
|
+
stack: string;
|
|
208
|
+
/** The plan that was executed — holes and skipped lexicons included. */
|
|
209
|
+
plan: TeardownPlan;
|
|
210
|
+
/** One row per planned entry. Never fewer: silence is never success. */
|
|
211
|
+
outcomes: TeardownOutcomeEntry[];
|
|
212
|
+
/** Lexicons whose candidates were skipped for lack of an `executeTeardown`. */
|
|
213
|
+
unimplemented: string[];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface ExecuteTeardownOptions extends PlanTeardownOptions {
|
|
217
|
+
/**
|
|
218
|
+
* A plan already computed (the one just shown to the user). Recomputed from
|
|
219
|
+
* a fresh live read when omitted.
|
|
220
|
+
*/
|
|
221
|
+
plan?: TeardownPlan;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Run one execution pass over a lexicon's candidates and return exactly one
|
|
226
|
+
* outcome per candidate: what the lexicon reported, `failed` for anything it
|
|
227
|
+
* stayed silent about, and `failed` across the board when the call threw.
|
|
228
|
+
* Outcomes the lexicon volunteers for names core never asked about are
|
|
229
|
+
* dropped — an implementation cannot widen the set by reporting on it.
|
|
230
|
+
*/
|
|
231
|
+
async function executePass(
|
|
232
|
+
plugin: ObservationLexicon,
|
|
233
|
+
candidates: TeardownCandidate[],
|
|
234
|
+
opts: ExecuteTeardownOptions,
|
|
235
|
+
marker: OwnershipMarker,
|
|
236
|
+
): Promise<Map<string, TeardownOutcome>> {
|
|
237
|
+
const byName = new Map<string, TeardownOutcome>();
|
|
238
|
+
let reported: TeardownOutcome[];
|
|
239
|
+
try {
|
|
240
|
+
const execution = await plugin.executeTeardown!({
|
|
241
|
+
environment: opts.environment,
|
|
242
|
+
marker,
|
|
243
|
+
candidates,
|
|
244
|
+
...(opts.deployedStack ? { stack: opts.deployedStack } : {}),
|
|
245
|
+
...(opts.region ? { region: opts.region } : {}),
|
|
246
|
+
...(opts.deployedStacks && opts.deployedStacks.length > 0 ? { stacks: opts.deployedStacks } : {}),
|
|
247
|
+
});
|
|
248
|
+
reported = execution.outcomes;
|
|
249
|
+
} catch (err) {
|
|
250
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
251
|
+
for (const candidate of candidates) {
|
|
252
|
+
byName.set(candidate.name, { name: candidate.name, outcome: "failed", detail });
|
|
253
|
+
}
|
|
254
|
+
return byName;
|
|
255
|
+
}
|
|
256
|
+
const asked = new Set(candidates.map((c) => c.name));
|
|
257
|
+
for (const outcome of reported) {
|
|
258
|
+
if (!asked.has(outcome.name)) continue;
|
|
259
|
+
byName.set(outcome.name, outcome);
|
|
260
|
+
}
|
|
261
|
+
for (const candidate of candidates) {
|
|
262
|
+
if (byName.has(candidate.name)) continue;
|
|
263
|
+
byName.set(candidate.name, {
|
|
264
|
+
name: candidate.name,
|
|
265
|
+
outcome: "failed",
|
|
266
|
+
detail: "the lexicon reported no outcome for this candidate",
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return byName;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Execute a teardown: delete every planned candidate through its lexicon's
|
|
274
|
+
* `executeTeardown`, then retry the failures once. Per-lexicon ordering only —
|
|
275
|
+
* each lexicon deletes its own set in the order its target requires (k8s
|
|
276
|
+
* deletes namespaces last, fly deletes apps last); there is no global
|
|
277
|
+
* reverse-dependency ordering in v1, the bounded retry pass covers the
|
|
278
|
+
* cross-lexicon cases it would.
|
|
279
|
+
*
|
|
280
|
+
* Every planned entry comes back with an outcome. A lexicon that enumerates
|
|
281
|
+
* but implements no execution reports its candidates as `skipped` — loudly,
|
|
282
|
+
* never as clean. Failures that survive the retry stay `failed` in the
|
|
283
|
+
* report; nothing here ever swallows one.
|
|
284
|
+
*/
|
|
285
|
+
export async function executeTeardown(opts: ExecuteTeardownOptions): Promise<TeardownReport> {
|
|
286
|
+
const marker: OwnershipMarker = { stack: opts.stack, env: opts.environment };
|
|
287
|
+
const plan = opts.plan ?? (await planTeardown(opts));
|
|
288
|
+
|
|
289
|
+
const byLexicon = new Map<string, TeardownPlanEntry[]>();
|
|
290
|
+
for (const entry of plan.entries) {
|
|
291
|
+
const list = byLexicon.get(entry.lexicon) ?? [];
|
|
292
|
+
list.push(entry);
|
|
293
|
+
byLexicon.set(entry.lexicon, list);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const outcomes: TeardownOutcomeEntry[] = [];
|
|
297
|
+
const unimplemented: string[] = [];
|
|
298
|
+
|
|
299
|
+
// Plugin registration order, so a project's lexicon ordering is stable.
|
|
300
|
+
for (const plugin of opts.plugins) {
|
|
301
|
+
const entries = byLexicon.get(plugin.name);
|
|
302
|
+
if (!entries) continue;
|
|
303
|
+
byLexicon.delete(plugin.name);
|
|
304
|
+
|
|
305
|
+
if (!plugin.executeTeardown) {
|
|
306
|
+
unimplemented.push(plugin.name);
|
|
307
|
+
for (const entry of entries) {
|
|
308
|
+
outcomes.push({
|
|
309
|
+
...entry,
|
|
310
|
+
outcome: "skipped",
|
|
311
|
+
detail: `the ${plugin.name} lexicon does not implement teardown execution yet`,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const candidates: TeardownCandidate[] = entries.map(({ lexicon: _lexicon, ...candidate }) => candidate);
|
|
318
|
+
const first = await executePass(plugin, candidates, opts, marker);
|
|
319
|
+
|
|
320
|
+
// One bounded retry pass over this lexicon's failures — transient errors
|
|
321
|
+
// and ordering hiccups get a second chance, nothing gets an infinite one.
|
|
322
|
+
const failedNames = new Set(
|
|
323
|
+
[...first.values()].filter((o) => o.outcome === "failed").map((o) => o.name),
|
|
324
|
+
);
|
|
325
|
+
const retried =
|
|
326
|
+
failedNames.size > 0
|
|
327
|
+
? await executePass(plugin, candidates.filter((c) => failedNames.has(c.name)), opts, marker)
|
|
328
|
+
: new Map<string, TeardownOutcome>();
|
|
329
|
+
|
|
330
|
+
for (const entry of entries) {
|
|
331
|
+
const second = retried.get(entry.name);
|
|
332
|
+
const outcome = second ?? first.get(entry.name)!;
|
|
333
|
+
outcomes.push({
|
|
334
|
+
...entry,
|
|
335
|
+
outcome: outcome.outcome,
|
|
336
|
+
...(outcome.detail !== undefined ? { detail: outcome.detail } : {}),
|
|
337
|
+
...(second !== undefined ? { retried: true } : {}),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// A planned entry attributed to a lexicon that is not in `plugins` at
|
|
343
|
+
// execution time (a plan handed in from elsewhere). Nobody can delete it,
|
|
344
|
+
// and silence is never success.
|
|
345
|
+
for (const entries of byLexicon.values()) {
|
|
346
|
+
for (const entry of entries) {
|
|
347
|
+
outcomes.push({
|
|
348
|
+
...entry,
|
|
349
|
+
outcome: "skipped",
|
|
350
|
+
detail: `no loaded lexicon named "${entry.lexicon}" to execute this candidate`,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
outcomes.sort((a, b) => a.lexicon.localeCompare(b.lexicon) || a.name.localeCompare(b.name));
|
|
356
|
+
return { environment: opts.environment, stack: opts.stack, plan, outcomes, unimplemented };
|
|
357
|
+
}
|
package/src/lifecycle/types.ts
CHANGED
|
@@ -75,6 +75,13 @@ export interface LifecycleSnapshot {
|
|
|
75
75
|
* relationships recorded" rather than "no relationships existed".
|
|
76
76
|
*/
|
|
77
77
|
edges?: IREdge[];
|
|
78
|
+
/**
|
|
79
|
+
* What the deployed stack publishes (#1279), keyed by stack name — a
|
|
80
|
+
* CloudFormation stack's outputs. Recorded once here rather than on every
|
|
81
|
+
* resource's `attributes`, which is where they used to be copied. Absent on
|
|
82
|
+
* every snapshot written before this, and on a lexicon with no such notion.
|
|
83
|
+
*/
|
|
84
|
+
stackExports?: Record<string, Record<string, unknown>>;
|
|
78
85
|
/** Build digest at snapshot time — what was declared when this snapshot was taken */
|
|
79
86
|
digest?: BuildDigest;
|
|
80
87
|
}
|
package/src/lint/engine.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LintRule, LintDiagnostic, LintContext } from "./rule";
|
|
1
|
+
import type { LintRule, LintDiagnostic, LintContext, LintProjectConfig } from "./rule";
|
|
2
2
|
import type { IntrinsicDef } from "../lexicon";
|
|
3
3
|
import { parseFile } from "./parser";
|
|
4
4
|
import { readFileSync } from "fs";
|
|
@@ -203,6 +203,10 @@ function isDiagnosticDisabled(
|
|
|
203
203
|
* `IntrinsicDef[]` into the fold path since #1039/#1105. Optional and
|
|
204
204
|
* defaulting to none, so a caller that hasn't resolved a project's
|
|
205
205
|
* lexicons (a unit test, `bench.test.ts`) is unaffected.
|
|
206
|
+
* @param projectConfig - chant #1221 — the project's config slice for
|
|
207
|
+
* config-aware rules (COR021 reads `environments` + `ownership`), put on
|
|
208
|
+
* every file's `LintContext.projectConfig`. Optional; without it those
|
|
209
|
+
* rules stay silent.
|
|
206
210
|
* @returns LintRunResult with diagnostics and suppressed items
|
|
207
211
|
*/
|
|
208
212
|
export async function runLint(
|
|
@@ -210,6 +214,7 @@ export async function runLint(
|
|
|
210
214
|
rules: LintRule[],
|
|
211
215
|
ruleOptions?: Map<string, Record<string, unknown>>,
|
|
212
216
|
intrinsics?: readonly IntrinsicDef[],
|
|
217
|
+
projectConfig?: LintProjectConfig,
|
|
213
218
|
): Promise<LintRunResult> {
|
|
214
219
|
const allDiagnostics: LintDiagnostic[] = [];
|
|
215
220
|
const allSuppressed: Array<LintDiagnostic & { reason?: string }> = [];
|
|
@@ -231,6 +236,7 @@ export async function runLint(
|
|
|
231
236
|
filePath,
|
|
232
237
|
lexicon: undefined,
|
|
233
238
|
intrinsics,
|
|
239
|
+
projectConfig,
|
|
234
240
|
};
|
|
235
241
|
|
|
236
242
|
// Execute each rule
|
package/src/lint/policy.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* `policyGate` Op step runs this to gate an apply on the same checks.
|
|
5
5
|
*/
|
|
6
6
|
import { resolve, dirname } from "node:path";
|
|
7
|
-
import { loadChantConfigUpward } from "../config";
|
|
7
|
+
import { loadChantConfigUpward, resolveOwnershipEnv, resolveOwnershipMarker } from "../config";
|
|
8
|
+
import { resolveBuildParams } from "../build-params";
|
|
8
9
|
import { resolveProjectLexicons, loadPlugins } from "../cli/plugins";
|
|
9
10
|
import { build } from "../build";
|
|
10
11
|
import { runPostSynthChecks, isPostSynthCheck } from "./post-synth";
|
|
@@ -86,9 +87,19 @@ export async function evaluateProjectPolicies(opts: {
|
|
|
86
87
|
const loaded = await loadChantConfigUpward(buildPath);
|
|
87
88
|
const config = loaded.config;
|
|
88
89
|
const configDir = loaded.configPath ? dirname(loaded.configPath) : buildPath;
|
|
89
|
-
|
|
90
|
+
// #1396 — `ownership.env` may reference a build parameter, so the declared
|
|
91
|
+
// parameters are resolved first (from their env mappings and defaults, the
|
|
92
|
+
// same inputs an Op step has) and the build sees them too.
|
|
93
|
+
const params = resolveBuildParams(config.buildParams, { env: process.env });
|
|
94
|
+
if (params.errors.length > 0) {
|
|
95
|
+
throw new Error(`Build parameters did not resolve — cannot evaluate policy:\n ${params.errors.join("\n ")}`);
|
|
96
|
+
}
|
|
97
|
+
const env = opts.env ?? resolveOwnershipEnv(config, params.provenance);
|
|
90
98
|
|
|
91
|
-
const result = await build(buildPath, serializers
|
|
99
|
+
const result = await build(buildPath, serializers, undefined, {
|
|
100
|
+
ownership: resolveOwnershipMarker(config, params.provenance),
|
|
101
|
+
buildParams: params.provenance,
|
|
102
|
+
});
|
|
92
103
|
if (result.errors.length > 0) {
|
|
93
104
|
throw new Error("Build failed — cannot evaluate policy on a broken build");
|
|
94
105
|
}
|
package/src/lint/rule.ts
CHANGED
|
@@ -49,6 +49,21 @@ export interface LintDiagnostic {
|
|
|
49
49
|
fix?: LintFix;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The slice of the project's `chant.config` a config-aware rule reads
|
|
54
|
+
* (#1221) — threaded into {@link LintContext} by `runLint` when the caller
|
|
55
|
+
* resolved the project's config (`chant lint` does; a bare unit test or the
|
|
56
|
+
* LSP's single-file lint may not). Structurally mirrors the corresponding
|
|
57
|
+
* `ChantConfig` fields (../config.ts) without importing them, so `rule.ts`
|
|
58
|
+
* stays dependency-light for lexicon rule authors.
|
|
59
|
+
*/
|
|
60
|
+
export interface LintProjectConfig {
|
|
61
|
+
/** Declared environments — a bare name or `{ name, endpoint }` (#1166). */
|
|
62
|
+
environments?: Array<string | { name: string; endpoint?: string }>;
|
|
63
|
+
/** Ownership marking config — `env` is a literal or a build-parameter reference (#1396). */
|
|
64
|
+
ownership?: { stack?: string; env?: string | { param: string }; enabled?: boolean };
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
/**
|
|
53
68
|
* Context provided to lint rules during checking
|
|
54
69
|
*/
|
|
@@ -74,6 +89,14 @@ export interface LintContext {
|
|
|
74
89
|
* call is a violation.
|
|
75
90
|
*/
|
|
76
91
|
intrinsics?: readonly IntrinsicDef[];
|
|
92
|
+
/**
|
|
93
|
+
* chant #1221 — the project's resolved config slice for config-aware rules
|
|
94
|
+
* (COR021 reads `environments` + `ownership`). Threaded from `runLint`;
|
|
95
|
+
* undefined when the caller never loaded a project config (a unit test
|
|
96
|
+
* constructing a context directly, the LSP's single-file path), in which
|
|
97
|
+
* case config-aware rules stay silent.
|
|
98
|
+
*/
|
|
99
|
+
projectConfig?: LintProjectConfig;
|
|
77
100
|
}
|
|
78
101
|
|
|
79
102
|
/**
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
import { cor021EnvLiteralNameRule } from "./cor021-env-literal-name";
|
|
4
|
+
import type { LintContext, LintProjectConfig } from "../rule";
|
|
5
|
+
|
|
6
|
+
const MULTI_ENV_PARAM_BOUND: LintProjectConfig = {
|
|
7
|
+
environments: ["dev", "prod"],
|
|
8
|
+
ownership: { stack: "billing", env: { param: "env" } },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function createContext(code: string, projectConfig?: LintProjectConfig, filePath = "test.ts"): LintContext {
|
|
12
|
+
const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
|
|
13
|
+
return { sourceFile, entities: [], filePath, lexicon: undefined, projectConfig };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("COR021: literal name in a multi-environment project (#1221)", () => {
|
|
17
|
+
test("rule metadata", () => {
|
|
18
|
+
expect(cor021EnvLiteralNameRule.id).toBe("COR021");
|
|
19
|
+
expect(cor021EnvLiteralNameRule.severity).toBe("warning");
|
|
20
|
+
expect(cor021EnvLiteralNameRule.category).toBe("correctness");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("flags a bare string literal in a *Name property", () => {
|
|
24
|
+
const ctx = createContext(
|
|
25
|
+
`export const uploads = new Bucket({ bucketName: "billing-uploads" });`,
|
|
26
|
+
MULTI_ENV_PARAM_BOUND,
|
|
27
|
+
);
|
|
28
|
+
const diags = cor021EnvLiteralNameRule.check(ctx);
|
|
29
|
+
expect(diags).toHaveLength(1);
|
|
30
|
+
expect(diags[0].ruleId).toBe("COR021");
|
|
31
|
+
expect(diags[0].severity).toBe("warning");
|
|
32
|
+
expect(diags[0].message).toContain('"billing-uploads"');
|
|
33
|
+
expect(diags[0].message).toContain("params.env");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("flags a bare `name` property, nested objects included", () => {
|
|
37
|
+
const ctx = createContext(
|
|
38
|
+
`export const svc = new Service({ metadata: { name: "web" } });`,
|
|
39
|
+
MULTI_ENV_PARAM_BOUND,
|
|
40
|
+
);
|
|
41
|
+
const diags = cor021EnvLiteralNameRule.check(ctx);
|
|
42
|
+
expect(diags).toHaveLength(1);
|
|
43
|
+
expect(diags[0].message).toContain('name: "web"');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("flags a no-substitution template literal the same as a string literal", () => {
|
|
47
|
+
const ctx = createContext(
|
|
48
|
+
"export const uploads = new Bucket({ bucketName: `billing-uploads` });",
|
|
49
|
+
MULTI_ENV_PARAM_BOUND,
|
|
50
|
+
);
|
|
51
|
+
expect(cor021EnvLiteralNameRule.check(ctx)).toHaveLength(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("passes a template literal interpolating the env parameter", () => {
|
|
55
|
+
const ctx = createContext(
|
|
56
|
+
"export const uploads = new Bucket({ bucketName: `billing-${params.env}-uploads` });",
|
|
57
|
+
MULTI_ENV_PARAM_BOUND,
|
|
58
|
+
);
|
|
59
|
+
expect(cor021EnvLiteralNameRule.check(ctx)).toHaveLength(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("passes non-name properties and non-literal name values", () => {
|
|
63
|
+
const ctx = createContext(
|
|
64
|
+
`
|
|
65
|
+
const shared = { prefix: "billing" };
|
|
66
|
+
export const uploads = new Bucket({ bucketName: shared.prefix, region: "us-east-1" });
|
|
67
|
+
`,
|
|
68
|
+
MULTI_ENV_PARAM_BOUND,
|
|
69
|
+
);
|
|
70
|
+
expect(cor021EnvLiteralNameRule.check(ctx)).toHaveLength(0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("silent when ownership.env is a literal", () => {
|
|
74
|
+
const ctx = createContext(
|
|
75
|
+
`export const uploads = new Bucket({ bucketName: "billing-uploads" });`,
|
|
76
|
+
{ environments: ["dev", "prod"], ownership: { stack: "billing", env: "prod" } },
|
|
77
|
+
);
|
|
78
|
+
expect(cor021EnvLiteralNameRule.check(ctx)).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("silent when ownership.env is absent", () => {
|
|
82
|
+
const ctx = createContext(
|
|
83
|
+
`export const uploads = new Bucket({ bucketName: "billing-uploads" });`,
|
|
84
|
+
{ environments: ["dev", "prod"], ownership: { stack: "billing" } },
|
|
85
|
+
);
|
|
86
|
+
expect(cor021EnvLiteralNameRule.check(ctx)).toHaveLength(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("silent with fewer than two declared environments", () => {
|
|
90
|
+
const oneEnv = createContext(
|
|
91
|
+
`export const uploads = new Bucket({ bucketName: "billing-uploads" });`,
|
|
92
|
+
{ environments: ["prod"], ownership: { stack: "billing", env: { param: "env" } } },
|
|
93
|
+
);
|
|
94
|
+
expect(cor021EnvLiteralNameRule.check(oneEnv)).toHaveLength(0);
|
|
95
|
+
|
|
96
|
+
const noEnvs = createContext(
|
|
97
|
+
`export const uploads = new Bucket({ bucketName: "billing-uploads" });`,
|
|
98
|
+
{ ownership: { stack: "billing", env: { param: "env" } } },
|
|
99
|
+
);
|
|
100
|
+
expect(cor021EnvLiteralNameRule.check(noEnvs)).toHaveLength(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("silent without a project config on the context", () => {
|
|
104
|
+
const ctx = createContext(`export const uploads = new Bucket({ bucketName: "billing-uploads" });`);
|
|
105
|
+
expect(cor021EnvLiteralNameRule.check(ctx)).toHaveLength(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("names the bound parameter, whatever it is called", () => {
|
|
109
|
+
const ctx = createContext(
|
|
110
|
+
`export const uploads = new Bucket({ bucketName: "billing-uploads" });`,
|
|
111
|
+
{ environments: ["dev", "prod"], ownership: { stack: "billing", env: { param: "stage" } } },
|
|
112
|
+
);
|
|
113
|
+
const diags = cor021EnvLiteralNameRule.check(ctx);
|
|
114
|
+
expect(diags).toHaveLength(1);
|
|
115
|
+
expect(diags[0].message).toContain("params.stage");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("object-form environments entries count toward the threshold", () => {
|
|
119
|
+
const ctx = createContext(
|
|
120
|
+
`export const uploads = new Bucket({ bucketName: "billing-uploads" });`,
|
|
121
|
+
{
|
|
122
|
+
environments: ["prod", { name: "floci", endpoint: "http://localhost:4566" }],
|
|
123
|
+
ownership: { stack: "billing", env: { param: "env" } },
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
expect(cor021EnvLiteralNameRule.check(ctx)).toHaveLength(1);
|
|
127
|
+
});
|
|
128
|
+
});
|