@intentius/chant 0.45.0 → 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/cli/commands/build.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/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/handlers/lifecycle.d.ts +13 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- 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/lexicon-regen.d.ts +11 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/validate.d.ts +10 -0
- package/dist/codegen/validate.d.ts.map +1 -1
- package/dist/config.d.ts +28 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/env.d.ts +12 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/lexicon.d.ts +182 -2
- package/dist/lexicon.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/teardown.d.ts +130 -0
- package/dist/lifecycle/teardown.d.ts.map +1 -0
- package/dist/lint/engine.d.ts +6 -2
- package/dist/lint/engine.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/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/package.json +6 -1
- package/src/cli/commands/build.test.ts +131 -0
- package/src/cli/commands/build.ts +20 -0
- package/src/cli/commands/check-lexicon.test.ts +45 -1
- package/src/cli/commands/check-lexicon.ts +45 -0
- package/src/cli/commands/lexicon-surface-diff.ts +9 -0
- package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
- package/src/cli/commands/lint.ts +19 -6
- package/src/cli/handlers/graph.ts +2 -2
- package/src/cli/handlers/lifecycle.test.ts +231 -1
- package/src/cli/handlers/lifecycle.ts +219 -3
- package/src/cli/handlers/search.ts +5 -2
- package/src/cli/main.ts +12 -1
- package/src/cli/registry.ts +7 -0
- package/src/codegen/lexicon-regen.ts +19 -1
- package/src/codegen/validate.test.ts +33 -0
- package/src/codegen/validate.ts +21 -2
- package/src/config.test.ts +40 -0
- package/src/config.ts +58 -1
- package/src/env.test.ts +35 -1
- package/src/env.ts +17 -3
- package/src/lexicon.ts +182 -2
- package/src/lifecycle/index.ts +1 -0
- package/src/lifecycle/teardown.test.ts +537 -0
- package/src/lifecycle/teardown.ts +357 -0
- package/src/lint/engine.ts +7 -1
- 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/op/builders.ts +40 -7
- package/src/op/index.ts +1 -1
- package/src/testing.test.ts +261 -0
- package/src/testing.ts +338 -0
package/src/codegen/validate.ts
CHANGED
|
@@ -27,6 +27,17 @@ export interface ValidateResult {
|
|
|
27
27
|
*/
|
|
28
28
|
export const RELEASE_GATE_ENV = "CHANT_RELEASE_GATE";
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Set by `chant dev surface-diff --update-snapshot` for the validate run it
|
|
32
|
+
* performs before rewriting the baseline (chant #1825). When set to "1", the
|
|
33
|
+
* `surface-matches-snapshot` check is skipped. That check fails on exactly the
|
|
34
|
+
* staleness the update run exists to fix, and with an `"always"` gate (#1475)
|
|
35
|
+
* the two would deadlock: validate cannot pass until the snapshot is updated,
|
|
36
|
+
* and the snapshot cannot be updated until validate passes. Every other check
|
|
37
|
+
* still runs, so a broken generate cannot be baselined.
|
|
38
|
+
*/
|
|
39
|
+
export const SNAPSHOT_UPDATE_ENV = "CHANT_SNAPSHOT_UPDATE";
|
|
40
|
+
|
|
30
41
|
export interface LexiconValidationConfig {
|
|
31
42
|
/** Filename of the lexicon JSON (e.g. "lexicon-mydom.json") */
|
|
32
43
|
lexiconJsonFilename: string;
|
|
@@ -201,10 +212,18 @@ export async function validateLexiconArtifacts(config: LexiconValidationConfig):
|
|
|
201
212
|
// Runs on the artifacts already on disk — no second generation — and is
|
|
202
213
|
// skipped for a lexicon with no committed snapshot, which is the case for a
|
|
203
214
|
// new lexicon before its first baseline.
|
|
215
|
+
//
|
|
216
|
+
// A re-baseline run (`chant dev surface-diff --update-snapshot`) sets
|
|
217
|
+
// {@link SNAPSHOT_UPDATE_ENV} and is exempt: the check would fail on the
|
|
218
|
+
// stale snapshot that run is about to rewrite, deadlocking the documented
|
|
219
|
+
// update flow against an `"always"` gate (#1825). Only this check is exempt —
|
|
220
|
+
// every other check above still gates the run.
|
|
204
221
|
const snapshotPath = join(config.basePath, "surface.snapshot.json");
|
|
222
|
+
const env = config.env ?? process.env;
|
|
205
223
|
const surfaceGate =
|
|
206
|
-
config.checkSurfaceSnapshot === "always" ||
|
|
207
|
-
|
|
224
|
+
(config.checkSurfaceSnapshot === "always" ||
|
|
225
|
+
(config.checkSurfaceSnapshot === true && env[RELEASE_GATE_ENV] === "1")) &&
|
|
226
|
+
env[SNAPSHOT_UPDATE_ENV] !== "1";
|
|
208
227
|
if (surfaceGate && lexiconData && existsSync(snapshotPath) && existsSync(dtsPath)) {
|
|
209
228
|
try {
|
|
210
229
|
const fresh = extractSurface(readFileSync(lexiconPath, "utf-8"), readFileSync(dtsPath, "utf-8"));
|
package/src/config.test.ts
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
environmentName,
|
|
10
10
|
environmentNames,
|
|
11
11
|
environmentEndpoint,
|
|
12
|
+
matchesEnvironmentPattern,
|
|
13
|
+
matchesDeclaredEnvironment,
|
|
12
14
|
} from "./config";
|
|
13
15
|
import { writeFileSync, mkdirSync, rmSync } from "fs";
|
|
14
16
|
import { join } from "path";
|
|
@@ -336,6 +338,44 @@ describe("environmentName / environmentNames / environmentEndpoint (#1166)", ()
|
|
|
336
338
|
expect(environmentEndpoint(environments, "unknown")).toBeUndefined(); // not declared at all
|
|
337
339
|
expect(environmentEndpoint(undefined, "floci")).toBeUndefined();
|
|
338
340
|
});
|
|
341
|
+
|
|
342
|
+
test("environmentEndpoint resolves through a pattern entry, literal entry winning (#1221)", () => {
|
|
343
|
+
const environments = [
|
|
344
|
+
{ name: "pr-special", endpoint: "http://special:1111" },
|
|
345
|
+
{ name: "pr-*", endpoint: "http://preview:4566" },
|
|
346
|
+
];
|
|
347
|
+
expect(environmentEndpoint(environments, "pr-42")).toBe("http://preview:4566");
|
|
348
|
+
expect(environmentEndpoint(environments, "pr-special")).toBe("http://special:1111"); // literal wins over the pattern
|
|
349
|
+
expect(environmentEndpoint(environments, "prod")).toBeUndefined();
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe("environment glob patterns (#1221)", () => {
|
|
354
|
+
test("matchesEnvironmentPattern: a * matches any run of characters, anchored", () => {
|
|
355
|
+
expect(matchesEnvironmentPattern("pr-*", "pr-42")).toBe(true);
|
|
356
|
+
expect(matchesEnvironmentPattern("pr-*", "pr-")).toBe(true); // empty run is a run
|
|
357
|
+
expect(matchesEnvironmentPattern("pr-*", "pr")).toBe(false);
|
|
358
|
+
expect(matchesEnvironmentPattern("pr-*", "xpr-42")).toBe(false); // anchored at the start
|
|
359
|
+
expect(matchesEnvironmentPattern("*-preview", "42-preview")).toBe(true);
|
|
360
|
+
expect(matchesEnvironmentPattern("*-preview", "42-preview-old")).toBe(false); // anchored at the end
|
|
361
|
+
expect(matchesEnvironmentPattern("test-*-eu", "test-a-eu")).toBe(true);
|
|
362
|
+
expect(matchesEnvironmentPattern("test-*-eu", "test-a-us")).toBe(false);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("matchesEnvironmentPattern without a * is plain equality", () => {
|
|
366
|
+
expect(matchesEnvironmentPattern("prod", "prod")).toBe(true);
|
|
367
|
+
expect(matchesEnvironmentPattern("prod", "prod2")).toBe(false);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("matchesDeclaredEnvironment: literal first, then pattern, across entry forms", () => {
|
|
371
|
+
const environments = ["dev", "prod", "pr-*", { name: "test-*", endpoint: "http://localhost:4566" }];
|
|
372
|
+
expect(matchesDeclaredEnvironment(environments, "prod")).toBe(true);
|
|
373
|
+
expect(matchesDeclaredEnvironment(environments, "pr-42")).toBe(true);
|
|
374
|
+
expect(matchesDeclaredEnvironment(environments, "test-suite-a")).toBe(true); // object-form pattern entry
|
|
375
|
+
expect(matchesDeclaredEnvironment(environments, "stage")).toBe(false);
|
|
376
|
+
expect(matchesDeclaredEnvironment(undefined, "prod")).toBe(false);
|
|
377
|
+
expect(matchesDeclaredEnvironment([], "prod")).toBe(false);
|
|
378
|
+
});
|
|
339
379
|
});
|
|
340
380
|
|
|
341
381
|
describe("resolveFoldEnabled (#1134 — fold is the default build path)", () => {
|
package/src/config.ts
CHANGED
|
@@ -44,6 +44,56 @@ export function environmentNames(environments: EnvironmentDeclaration[] | undefi
|
|
|
44
44
|
return environments?.map(environmentName);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Whether a declared environment name is a glob pattern rather than a literal
|
|
49
|
+
* (#1221) — it contains at least one `*`.
|
|
50
|
+
*/
|
|
51
|
+
export function isEnvironmentPattern(declaredName: string): boolean {
|
|
52
|
+
return declaredName.includes("*");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Anchored `*`-glob match (#1221): each `*` in `pattern` matches any run of
|
|
57
|
+
* characters (possibly empty); everything else matches itself. Deliberately
|
|
58
|
+
* the whole surface — no `?`, no character classes, no regex. A pattern with
|
|
59
|
+
* no `*` degenerates to string equality.
|
|
60
|
+
*/
|
|
61
|
+
export function matchesEnvironmentPattern(pattern: string, name: string): boolean {
|
|
62
|
+
const parts = pattern.split("*");
|
|
63
|
+
if (parts.length === 1) return pattern === name;
|
|
64
|
+
const first = parts[0];
|
|
65
|
+
const last = parts[parts.length - 1];
|
|
66
|
+
if (!name.startsWith(first)) return false;
|
|
67
|
+
let cursor = first.length;
|
|
68
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
69
|
+
const part = parts[i];
|
|
70
|
+
if (part === "") continue;
|
|
71
|
+
const at = name.indexOf(part, cursor);
|
|
72
|
+
if (at === -1) return false;
|
|
73
|
+
cursor = at + part.length;
|
|
74
|
+
}
|
|
75
|
+
return name.length - cursor >= last.length && name.endsWith(last);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Environment-membership test (#1221): is `name` covered by this project's
|
|
80
|
+
* declared `environments`? A literal entry matches by equality; an entry
|
|
81
|
+
* containing `*` matches as a glob pattern (`"pr-*"` covers `pr-42`).
|
|
82
|
+
* Literals are checked first across the whole list, then patterns — so a
|
|
83
|
+
* name that is declared outright never depends on pattern order. `undefined`
|
|
84
|
+
* / empty `environments` returns `false`; callers that treat "no declared
|
|
85
|
+
* environments" as "anything goes" (e.g. {@link unknownEnvError} in
|
|
86
|
+
* `./env.ts`) guard that case themselves.
|
|
87
|
+
*/
|
|
88
|
+
export function matchesDeclaredEnvironment(
|
|
89
|
+
environments: EnvironmentDeclaration[] | undefined,
|
|
90
|
+
name: string,
|
|
91
|
+
): boolean {
|
|
92
|
+
const names = environmentNames(environments) ?? [];
|
|
93
|
+
if (names.includes(name)) return true;
|
|
94
|
+
return names.some((declared) => isEnvironmentPattern(declared) && matchesEnvironmentPattern(declared, name));
|
|
95
|
+
}
|
|
96
|
+
|
|
47
97
|
/**
|
|
48
98
|
* The endpoint `name` declares (chant #1166) — `undefined` for a bare-string
|
|
49
99
|
* entry, an entry with no `endpoint` set, or a name this project doesn't
|
|
@@ -51,9 +101,16 @@ export function environmentNames(environments: EnvironmentDeclaration[] | undefi
|
|
|
51
101
|
* it injects this into the ambient env var each observing lexicon's CLI
|
|
52
102
|
* shell-out reads (e.g. `AWS_ENDPOINT_URL`), unless that var is already set —
|
|
53
103
|
* ambient always wins.
|
|
104
|
+
*
|
|
105
|
+
* A pattern entry (#1221) can carry an endpoint too: `{ name: "pr-*",
|
|
106
|
+
* endpoint }` supplies the endpoint for every `pr-<n>` environment. A literal
|
|
107
|
+
* entry always wins over a pattern — declaring `pr-special` alongside
|
|
108
|
+
* `pr-*` resolves `pr-special` to the literal entry's endpoint (or none).
|
|
54
109
|
*/
|
|
55
110
|
export function environmentEndpoint(environments: EnvironmentDeclaration[] | undefined, name: string): string | undefined {
|
|
56
|
-
const found =
|
|
111
|
+
const found =
|
|
112
|
+
environments?.find((e) => environmentName(e) === name) ??
|
|
113
|
+
environments?.find((e) => isEnvironmentPattern(environmentName(e)) && matchesEnvironmentPattern(environmentName(e), name));
|
|
57
114
|
return found && typeof found !== "string" ? found.endpoint : undefined;
|
|
58
115
|
}
|
|
59
116
|
|
package/src/env.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { describe, test, expect } from "vitest";
|
|
|
2
2
|
import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
-
import { env, unknownEnvError, ENV_VAR } from "./env";
|
|
5
|
+
import { env, unknownEnvError, isProdLikeEnvironment, ENV_VAR } from "./env";
|
|
6
6
|
import { discover } from "./discovery/index";
|
|
7
7
|
|
|
8
8
|
const withEnv = async (value: string | undefined, fn: () => void | Promise<void>): Promise<void> => {
|
|
@@ -52,6 +52,25 @@ describe("unknownEnvError", () => {
|
|
|
52
52
|
unknownEnvError("stage", ["prod", { name: "floci", endpoint: "http://localhost:4566" }]),
|
|
53
53
|
).toMatch(/Unknown environment "stage".*prod, floci/);
|
|
54
54
|
});
|
|
55
|
+
|
|
56
|
+
// #1221 — an entry containing `*` is a glob pattern, so an unbounded family
|
|
57
|
+
// (per-PR environments, per-suite test environments) is declarable without
|
|
58
|
+
// listing each name.
|
|
59
|
+
test("accepts an env covered by a declared glob pattern (#1221)", () => {
|
|
60
|
+
expect(unknownEnvError("pr-42", ["dev", "prod", "pr-*"])).toBeUndefined();
|
|
61
|
+
expect(unknownEnvError("test-suite-a", ["prod", { name: "test-*", endpoint: "http://localhost:4566" }])).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("rejects an env no pattern covers, listing the patterns among the declared entries (#1221)", () => {
|
|
65
|
+
expect(unknownEnvError("stage", ["dev", "prod", "pr-*"])).toMatch(/Unknown environment "stage".*dev, prod, pr-\*/);
|
|
66
|
+
expect(unknownEnvError("pr", ["pr-*"])).toMatch(/Unknown environment "pr"/); // prefix alone is not covered
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("a literal entry still matches by equality, never as a pattern (#1221)", () => {
|
|
70
|
+
// `prod` contains no `*`: nothing about pattern support changes it.
|
|
71
|
+
expect(unknownEnvError("prod", ["prod", "pr-*"])).toBeUndefined();
|
|
72
|
+
expect(unknownEnvError("production", ["prod"])).toMatch(/Unknown environment "production"/);
|
|
73
|
+
});
|
|
55
74
|
});
|
|
56
75
|
|
|
57
76
|
describe("env-aware discovery (#505)", () => {
|
|
@@ -87,3 +106,18 @@ describe("env-aware discovery (#505)", () => {
|
|
|
87
106
|
}
|
|
88
107
|
});
|
|
89
108
|
});
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
describe("isProdLikeEnvironment (#1222)", () => {
|
|
112
|
+
test("matches prod, production, and separator-delimited variants", () => {
|
|
113
|
+
for (const name of ["prod", "production", "Prod", "PRODUCTION", "prod-eu", "us-prod", "prod2", "production-east", "eu_prod"]) {
|
|
114
|
+
expect(isProdLikeEnvironment(name)).toBe(true);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("does not match names that merely contain the letters", () => {
|
|
119
|
+
for (const name of ["dev", "staging", "preprod", "product-demo", "reproduction", "pr-123"]) {
|
|
120
|
+
expect(isProdLikeEnvironment(name)).toBe(false);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
package/src/env.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* pinhole renders/diffs them to show environment drift (INTENTIUS/pinhole#3).
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { environmentNames, type EnvironmentDeclaration } from "./config";
|
|
21
|
+
import { environmentNames, matchesDeclaredEnvironment, type EnvironmentDeclaration } from "./config";
|
|
22
22
|
|
|
23
23
|
/** The environment variable the CLI sets from `--env`. */
|
|
24
24
|
export const ENV_VAR = "CHANT_ENV";
|
|
@@ -34,14 +34,28 @@ export function env(fallback?: string): string | undefined {
|
|
|
34
34
|
* when it's valid (or when the project declares no environments, in which case
|
|
35
35
|
* any name is accepted). `declared` entries may be a bare name or `{ name,
|
|
36
36
|
* endpoint }` (#1166) — {@link environmentNames} reduces either to the names
|
|
37
|
-
* this checks against.
|
|
37
|
+
* this checks against. An entry containing `*` is a glob pattern (#1221):
|
|
38
|
+
* `"pr-*"` legalizes every `pr-<n>` environment. Literal entries are checked
|
|
39
|
+
* first, then patterns — see {@link matchesDeclaredEnvironment}.
|
|
38
40
|
*/
|
|
39
41
|
export function unknownEnvError(
|
|
40
42
|
requested: string | undefined,
|
|
41
43
|
declared: EnvironmentDeclaration[] | undefined,
|
|
42
44
|
): string | undefined {
|
|
43
45
|
if (!requested || !declared || declared.length === 0) return undefined;
|
|
46
|
+
if (matchesDeclaredEnvironment(declared, requested)) return undefined;
|
|
44
47
|
const names = environmentNames(declared) ?? [];
|
|
45
|
-
if (names.includes(requested)) return undefined;
|
|
46
48
|
return `Unknown environment "${requested}". Declared environments: ${names.join(", ")}.`;
|
|
47
49
|
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* True when an environment name looks like production — `prod`, `production`,
|
|
53
|
+
* and separator-delimited variants (`prod-eu`, `us-prod`, `production2`).
|
|
54
|
+
* `chant lifecycle teardown <env> --yes` demands an extra confirmation for
|
|
55
|
+
* these (#1222): a typo that survives the declared-environments check should
|
|
56
|
+
* still not delete production on one flag. Name-shaped, deliberately — chant
|
|
57
|
+
* has no other signal for which environment is the one that pays the bills.
|
|
58
|
+
*/
|
|
59
|
+
export function isProdLikeEnvironment(name: string): boolean {
|
|
60
|
+
return /(^|[-_./])prod(uction)?([-_./0-9]|$)/i.test(name);
|
|
61
|
+
}
|
package/src/lexicon.ts
CHANGED
|
@@ -11,12 +11,12 @@ import type { CompletionContext, CompletionItem, HoverContext, HoverInfo, CodeAc
|
|
|
11
11
|
import type { McpToolContribution, McpResourceContribution } from "./mcp/types";
|
|
12
12
|
import type { DriverComponent } from "./components/driver";
|
|
13
13
|
import type { EmulatorDeclaration } from "./op/emulator-lifecycle";
|
|
14
|
-
import type { OwnershipChannel } from "./ownership";
|
|
14
|
+
import type { OwnershipChannel, OwnershipMarker } from "./ownership";
|
|
15
15
|
import type { LexiconConfigSchema } from "./lexicon-config";
|
|
16
16
|
import type { RuleMeta } from "./audit/catalog";
|
|
17
17
|
import type { ReferenceCatalog } from "./graph-refs";
|
|
18
18
|
import type { IREdge } from "./graph-ir";
|
|
19
|
-
import type { DescribeResourcesResult } from "./observation";
|
|
19
|
+
import type { DescribeResourcesResult, UnobservedReason } from "./observation";
|
|
20
20
|
import type { DeepNormalizationHooks, DeepObservationResult } from "./deep-observation";
|
|
21
21
|
import type { OwnerChainVerdict } from "./owner-chain";
|
|
22
22
|
import type { CommandGroup } from "./cli/command-group";
|
|
@@ -540,6 +540,23 @@ export interface LexiconPlugin {
|
|
|
540
540
|
*/
|
|
541
541
|
auditCatalog?(): Record<string, RuleMeta>;
|
|
542
542
|
|
|
543
|
+
/**
|
|
544
|
+
* Machine-readable spec-coverage accounting for `check-lexicon` (#1330).
|
|
545
|
+
*
|
|
546
|
+
* `coverage()` prints a report for humans; this returns the one fact the
|
|
547
|
+
* completeness gate cares about: which upstream spec kinds are neither
|
|
548
|
+
* modeled as declarables nor on the lexicon's exclusion list. fountain held
|
|
549
|
+
* this line in a lexicon-local vitest assertion (`coverage.test.ts`), which
|
|
550
|
+
* is a convention rather than a contract — the same class of gap #1342
|
|
551
|
+
* closed for LSP providers.
|
|
552
|
+
*
|
|
553
|
+
* Implementations must work offline from committed snapshots (fountain
|
|
554
|
+
* reads `spec/fountain-openapi.snapshot.json` plus its surface baseline):
|
|
555
|
+
* `check-lexicon` runs on every PR, so no network I/O. Omit when the
|
|
556
|
+
* lexicon has no kind-level spec accounting; the check passes vacuously.
|
|
557
|
+
*/
|
|
558
|
+
coverageReport?(): Promise<{ unaccountedKinds?: string[] }>;
|
|
559
|
+
|
|
543
560
|
/** Return intrinsic function definitions */
|
|
544
561
|
intrinsics?(): IntrinsicDef[];
|
|
545
562
|
|
|
@@ -862,6 +879,86 @@ export interface LexiconPlugin {
|
|
|
862
879
|
*/
|
|
863
880
|
describeStackStatus?(options: { environment: string; stack: string }): Promise<StackStatusObservation | null>;
|
|
864
881
|
|
|
882
|
+
/**
|
|
883
|
+
* Enumerate the resources this lexicon would delete for one marker identity
|
|
884
|
+
* (#1222). Opt-in, and read-only here: this method names the would-delete
|
|
885
|
+
* set, it never deletes. `chant lifecycle teardown <env>` calls it to plan;
|
|
886
|
+
* the execution half ({@link executeTeardown}) deletes from the same
|
|
887
|
+
* enumeration.
|
|
888
|
+
*
|
|
889
|
+
* Selection is marker-scoped by construction. `marker` carries this
|
|
890
|
+
* project's ownership stack plus the requested environment, and every
|
|
891
|
+
* returned candidate must have been read carrying exactly that identity on
|
|
892
|
+
* this lexicon's marker channel — managed-by present, stack equal, env
|
|
893
|
+
* equal. A resource whose marker is absent, foreign-stack, or foreign-env is
|
|
894
|
+
* not a candidate, ever. Core re-checks each candidate's `marker` and drops
|
|
895
|
+
* mismatches, so a buggy implementation cannot widen the set.
|
|
896
|
+
*
|
|
897
|
+
* The #1089 discipline applies: a kind this lexicon stamps but cannot read
|
|
898
|
+
* back (no reader for the kind, the read errored, no credentials) is a
|
|
899
|
+
* `hole`, named with a total {@link UnobservedReason} — never silently
|
|
900
|
+
* absent, because "absent from the plan" reads as "safe", and an unreadable
|
|
901
|
+
* kind is unknown, not safe.
|
|
902
|
+
*
|
|
903
|
+
* A lexicon without this capability still takes part in teardown planning:
|
|
904
|
+
* core falls back to {@link describeResources} and filters on
|
|
905
|
+
* {@link ResourceMetadata.marker}. Implement this when that read is the
|
|
906
|
+
* wrong shape for deletion — aws, whose thin read carries no tags and whose
|
|
907
|
+
* teardown is stack-level, is the motivating case.
|
|
908
|
+
*/
|
|
909
|
+
teardownOwned?(options: {
|
|
910
|
+
environment: string;
|
|
911
|
+
/** The identity to select on: this project's ownership stack + the env being torn down. */
|
|
912
|
+
marker: OwnershipMarker;
|
|
913
|
+
/** Deployed stack name, for a multi-stack project (see `stacks` in {@link ChantConfig}). */
|
|
914
|
+
stack?: string;
|
|
915
|
+
/** Region that stack is deployed in (#1261's contract). */
|
|
916
|
+
region?: string;
|
|
917
|
+
/**
|
|
918
|
+
* Every deployed stack a multi-stack project declares (see `stacks` in
|
|
919
|
+
* {@link ChantConfig}), for a lexicon whose enumeration is stack-shaped
|
|
920
|
+
* (aws). When absent or empty, the single-stack convention applies:
|
|
921
|
+
* `stack`, else the stack named after the environment.
|
|
922
|
+
*/
|
|
923
|
+
stacks?: Array<{ name: string; region?: string }>;
|
|
924
|
+
}): Promise<TeardownEnumeration>;
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Delete the teardown candidates core hands over — the execution half of
|
|
928
|
+
* `chant lifecycle teardown <env> --yes` (#1222). Opt-in, and the sibling of
|
|
929
|
+
* {@link teardownOwned}: that method names the would-delete set, this one
|
|
930
|
+
* deletes it. A lexicon that enumerates but does not implement this reports
|
|
931
|
+
* its candidates as skipped rather than pretending.
|
|
932
|
+
*
|
|
933
|
+
* `candidates` is the marker-verified set core computed from the plan — an
|
|
934
|
+
* implementation deletes those and only those, in whatever order its target
|
|
935
|
+
* requires (k8s deletes namespaces last; fly deletes apps last). It never
|
|
936
|
+
* re-widens the set: a live resource not in `candidates` is not this call's
|
|
937
|
+
* business, whatever its labels say.
|
|
938
|
+
*
|
|
939
|
+
* Every candidate gets exactly one outcome per call, keyed by `name`:
|
|
940
|
+
* `deleted` (including already-gone — deletion is idempotent), `failed`
|
|
941
|
+
* (the delete errored; core runs one bounded retry pass over these), or
|
|
942
|
+
* `not-prunable` with a reason (the live object no longer carries the
|
|
943
|
+
* requested identity, the kind cannot be addressed, the target refuses).
|
|
944
|
+
* A candidate the implementation says nothing about is reported as failed
|
|
945
|
+
* by core — silence is never success.
|
|
946
|
+
*/
|
|
947
|
+
executeTeardown?(options: {
|
|
948
|
+
environment: string;
|
|
949
|
+
/** The identity everything was selected on: ownership stack + env. */
|
|
950
|
+
marker: OwnershipMarker;
|
|
951
|
+
/** The marker-verified candidates to delete — from {@link teardownOwned} / the plan. */
|
|
952
|
+
candidates: TeardownCandidate[];
|
|
953
|
+
/** Deployed stack name, for a multi-stack project (see `stacks` in {@link ChantConfig}). */
|
|
954
|
+
stack?: string;
|
|
955
|
+
/** Region that stack is deployed in (#1261's contract). */
|
|
956
|
+
region?: string;
|
|
957
|
+
/** Every declared deployed stack, mirroring {@link teardownOwned} — how a
|
|
958
|
+
* stack-shaped execution (aws) finds each candidate's region. */
|
|
959
|
+
stacks?: Array<{ name: string; region?: string }>;
|
|
960
|
+
}): Promise<TeardownExecution>;
|
|
961
|
+
|
|
865
962
|
/**
|
|
866
963
|
* Where this lexicon can stamp and read chant's ownership marker (#1348).
|
|
867
964
|
* Data, not a method.
|
|
@@ -962,6 +1059,74 @@ export interface LexiconPlugin {
|
|
|
962
1059
|
}): Promise<ExportedTemplate>;
|
|
963
1060
|
}
|
|
964
1061
|
|
|
1062
|
+
/**
|
|
1063
|
+
* One resource {@link LexiconPlugin.teardownOwned} would delete (#1222).
|
|
1064
|
+
* Identity only — no delete happens on this path.
|
|
1065
|
+
*/
|
|
1066
|
+
export interface TeardownCandidate {
|
|
1067
|
+
/** chant entity name where a declared mapping exists, else the provider-side name. */
|
|
1068
|
+
name: string;
|
|
1069
|
+
/** Resource type (e.g. "AWS::S3::Bucket", "K8s::Apps::Deployment"). */
|
|
1070
|
+
type: string;
|
|
1071
|
+
/** Provider-side identifier, when the read surfaces one. */
|
|
1072
|
+
physicalId?: string;
|
|
1073
|
+
/**
|
|
1074
|
+
* The stack/env identity read off the resource's own marker — read back,
|
|
1075
|
+
* never inferred. Core verifies it equals the requested identity and drops
|
|
1076
|
+
* the candidate otherwise.
|
|
1077
|
+
*/
|
|
1078
|
+
marker: OwnershipMarker;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* One kind or entity a teardown enumeration could not read (#1089). A hole is
|
|
1083
|
+
* a claim of ignorance, not of absence: the plan must print it loudly, and the
|
|
1084
|
+
* execution half must refuse to call the env clean while holes exist.
|
|
1085
|
+
*/
|
|
1086
|
+
export interface TeardownHole {
|
|
1087
|
+
/** The unreadable kind or entity name. */
|
|
1088
|
+
name: string;
|
|
1089
|
+
/** Resource type, when known. */
|
|
1090
|
+
type?: string;
|
|
1091
|
+
/** Total verdict — the same vocabulary the observation envelope uses. */
|
|
1092
|
+
reason: UnobservedReason;
|
|
1093
|
+
/** Human-readable detail: the failing command, the unsupported kind. */
|
|
1094
|
+
detail?: string;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/** What {@link LexiconPlugin.teardownOwned} returns: the would-delete set plus its holes. */
|
|
1098
|
+
export interface TeardownEnumeration {
|
|
1099
|
+
candidates: TeardownCandidate[];
|
|
1100
|
+
/** Omit or leave empty when every stamped kind was readable. */
|
|
1101
|
+
holes?: TeardownHole[];
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* One candidate's fate after {@link LexiconPlugin.executeTeardown} (#1222).
|
|
1106
|
+
* `name` keys it back to the candidate it answers for.
|
|
1107
|
+
*/
|
|
1108
|
+
export interface TeardownOutcome {
|
|
1109
|
+
/** The candidate's `name`, verbatim. */
|
|
1110
|
+
name: string;
|
|
1111
|
+
/** Resource type, when the implementation carries it through. */
|
|
1112
|
+
type?: string;
|
|
1113
|
+
/** Provider-side identifier, when known. */
|
|
1114
|
+
physicalId?: string;
|
|
1115
|
+
/**
|
|
1116
|
+
* - `deleted` — gone, including already-gone (deletion is idempotent);
|
|
1117
|
+
* - `failed` — the delete errored (core retries these once);
|
|
1118
|
+
* - `not-prunable` — deliberately not deleted; `detail` says why.
|
|
1119
|
+
*/
|
|
1120
|
+
outcome: "deleted" | "failed" | "not-prunable";
|
|
1121
|
+
/** The error for `failed`, the reason for `not-prunable`. */
|
|
1122
|
+
detail?: string;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/** What {@link LexiconPlugin.executeTeardown} returns: one outcome per candidate. */
|
|
1126
|
+
export interface TeardownExecution {
|
|
1127
|
+
outcomes: TeardownOutcome[];
|
|
1128
|
+
}
|
|
1129
|
+
|
|
965
1130
|
/**
|
|
966
1131
|
* The observation view of a lexicon — every capability except live export.
|
|
967
1132
|
*
|
|
@@ -1047,6 +1212,21 @@ export interface ResourceMetadata {
|
|
|
1047
1212
|
* a delete, and never escalates `unknown` to one.
|
|
1048
1213
|
*/
|
|
1049
1214
|
ownership?: "owned" | "foreign" | "unknown";
|
|
1215
|
+
/**
|
|
1216
|
+
* The stack/env identity read off the resource's own ownership marker
|
|
1217
|
+
* (#1222) — the tags/labels/metadata chant stamped at synthesis, read back
|
|
1218
|
+
* verbatim on the same channel. This is what marker-scoped selection keys
|
|
1219
|
+
* on: {@link ownership} says "chant's", `marker` says *which* stack and env.
|
|
1220
|
+
*
|
|
1221
|
+
* Set only when the live model actually carries the channel and the
|
|
1222
|
+
* managed-by marker is present — an absent channel means an absent field,
|
|
1223
|
+
* never a guess. In particular aws's thin read is sourced from
|
|
1224
|
+
* `describe-stack-resources`, which returns no tags at all, so aws never
|
|
1225
|
+
* populates this here; aws teardown is stack-level and reads the stack's own
|
|
1226
|
+
* tags instead (later PR). A populated `marker` does not by itself imply an
|
|
1227
|
+
* {@link ownership} verdict on paths that do not declare a marker channel.
|
|
1228
|
+
*/
|
|
1229
|
+
marker?: OwnershipMarker;
|
|
1050
1230
|
/**
|
|
1051
1231
|
* Where this resource's owner-reference chain leads, for a live resource
|
|
1052
1232
|
* that is not itself declared (#1077). A lexicon that maintains an
|
package/src/lifecycle/index.ts
CHANGED