@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
package/src/graph-ops.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { basename, resolve } from "node:path";
|
|
2
|
+
import type { Declarable } from "./declarable";
|
|
3
|
+
import { discoverOps } from "./op/discover";
|
|
4
|
+
import { Op } from "./op/builders";
|
|
5
|
+
import { setProvenance } from "./provenance";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* #1675 — the graph IR is built from `discover(sourceDir)`, but the Op
|
|
9
|
+
* convention keeps `*.op.ts` files OUTSIDE `sourceDir` (`ops/` or the project
|
|
10
|
+
* root beside `sourceDir: "src"`), and `chant op` / `chant graph` find them via
|
|
11
|
+
* {@link discoverOps}, which scans from the git root. Join the two scopes: every
|
|
12
|
+
* Op `discoverOps` finds that discovery did not already load becomes a
|
|
13
|
+
* `Temporal::Op` entity in the map, keyed the way discovery keys a default
|
|
14
|
+
* export (the file's basename sans `.op.ts`), so the IR carries the declared
|
|
15
|
+
* DAG whatever the layout. Ops discovery already loaded (same file) are left
|
|
16
|
+
* alone. Returns the op-discovery errors for the caller to surface.
|
|
17
|
+
*/
|
|
18
|
+
export async function mergeProjectOps(
|
|
19
|
+
entities: Map<string, Declarable>,
|
|
20
|
+
sourceFiles: readonly string[],
|
|
21
|
+
projectPath: string,
|
|
22
|
+
): Promise<{ added: string[]; errors: string[] }> {
|
|
23
|
+
const added: string[] = [];
|
|
24
|
+
let found: Awaited<ReturnType<typeof discoverOps>>;
|
|
25
|
+
try {
|
|
26
|
+
found = await discoverOps({ cwd: projectPath });
|
|
27
|
+
} catch (err) {
|
|
28
|
+
// Not a git checkout — nothing beyond sourceDir to join.
|
|
29
|
+
return { added, errors: [err instanceof Error ? err.message : String(err)] };
|
|
30
|
+
}
|
|
31
|
+
const loaded = new Set(sourceFiles.map((f) => resolve(f)));
|
|
32
|
+
for (const { config, filePath } of found.ops.values()) {
|
|
33
|
+
if (loaded.has(resolve(filePath))) continue;
|
|
34
|
+
const key = basename(filePath).replace(/\.ts$/, "").replace(/\.op$/, "");
|
|
35
|
+
if (entities.has(key)) continue;
|
|
36
|
+
const entity = Op(config) as unknown as Declarable;
|
|
37
|
+
setProvenance(entity, { sourceFile: resolve(filePath) });
|
|
38
|
+
entities.set(key, entity);
|
|
39
|
+
added.push(key);
|
|
40
|
+
}
|
|
41
|
+
return { added, errors: found.errors };
|
|
42
|
+
}
|
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
|
|
@@ -346,6 +346,47 @@ describe("buildChangeSet: not-observed is not absent (#1089)", () => {
|
|
|
346
346
|
expect(e.queried).toBe("from-entry");
|
|
347
347
|
});
|
|
348
348
|
|
|
349
|
+
// #1674 — lexicon attribution and the provider's physical id per entry.
|
|
350
|
+
test("stamps every entry with the lexicon it was built for, when given one", () => {
|
|
351
|
+
const cs = buildChangeSet("prod", {
|
|
352
|
+
declared: new Set(["web", "api"]),
|
|
353
|
+
observedNow: { web: meta(), "sg-0abc123": meta({ ownership: "foreign" }) },
|
|
354
|
+
observedThen: undefined,
|
|
355
|
+
}, { lexicon: "aws" });
|
|
356
|
+
expect(cs.entries.map((e) => e.lexicon)).toEqual(["aws", "aws", "aws"]);
|
|
357
|
+
const without = buildChangeSet("prod", { declared: new Set(["web"]), observedNow: {}, observedThen: undefined });
|
|
358
|
+
expect(without.entries[0]).not.toHaveProperty("lexicon");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("physicalId comes from the live observation — name stays the lexicon's key", () => {
|
|
362
|
+
const cs = buildChangeSet("prod", {
|
|
363
|
+
declared: new Set(["web"]),
|
|
364
|
+
observedNow: {
|
|
365
|
+
web: meta({ physicalId: "arn:aws:elasticloadbalancing:us-east-1:123:loadbalancer/app/web/abc" }),
|
|
366
|
+
"sg-0abc123": meta({ physicalId: "sg-0abc123", ownership: "foreign" }),
|
|
367
|
+
"pod-xyz": meta({ physicalId: "default/pod-xyz", ownerChain: { root: "declared", entity: "web" } }),
|
|
368
|
+
owned: meta({ physicalId: "i-0deadbeef", ownership: "owned" }),
|
|
369
|
+
},
|
|
370
|
+
observedThen: undefined,
|
|
371
|
+
});
|
|
372
|
+
const byName = Object.fromEntries(cs.entries.map((e) => [e.name, e]));
|
|
373
|
+
expect(byName.web.physicalId).toBe("arn:aws:elasticloadbalancing:us-east-1:123:loadbalancer/app/web/abc");
|
|
374
|
+
expect(byName["sg-0abc123"]).toMatchObject({ action: "adopt", physicalId: "sg-0abc123", name: "sg-0abc123" });
|
|
375
|
+
expect(byName["pod-xyz"]).toMatchObject({ action: "runtime", physicalId: "default/pod-xyz" });
|
|
376
|
+
expect(byName.owned).toMatchObject({ action: "delete", physicalId: "i-0deadbeef" });
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("physicalId is absent when no side reported one, and falls back to the snapshot's when the resource is gone", () => {
|
|
380
|
+
const cs = buildChangeSet("prod", {
|
|
381
|
+
declared: new Set(["web"]),
|
|
382
|
+
observedNow: {},
|
|
383
|
+
observedThen: { gone: meta({ physicalId: "i-gone" }) },
|
|
384
|
+
});
|
|
385
|
+
const byName = Object.fromEntries(cs.entries.map((e) => [e.name, e]));
|
|
386
|
+
expect(byName.web).not.toHaveProperty("physicalId");
|
|
387
|
+
expect(byName.gone).toMatchObject({ action: "noop", physicalId: "i-gone" });
|
|
388
|
+
});
|
|
389
|
+
|
|
349
390
|
test("a returned resource wins over an unobserved claim for the same name", () => {
|
|
350
391
|
const cs = buildChangeSet("prod", {
|
|
351
392
|
declared: new Set(["queue"]),
|
|
@@ -45,10 +45,27 @@ export type ChangeAction = "create" | "update" | "delete" | "adopt" | "runtime"
|
|
|
45
45
|
export type Ownership = "owned" | "foreign" | "unknown";
|
|
46
46
|
|
|
47
47
|
export interface ChangeSetEntry {
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* chant entity name for a declared entity. For an undeclared live resource
|
|
50
|
+
* (`adopt`, `delete`, `runtime`) this is the lexicon's live key — not an
|
|
51
|
+
* IR-joinable entity name; read `physicalId` for the provider id (#1674).
|
|
52
|
+
*/
|
|
49
53
|
name: string;
|
|
50
54
|
/** Resource type, when known from either side. */
|
|
51
55
|
type?: string;
|
|
56
|
+
/**
|
|
57
|
+
* The lexicon whose observation produced this entry (#1674). Set when the
|
|
58
|
+
* change set is built for one lexicon; `lifecycle plan` merges every
|
|
59
|
+
* lexicon's change set into one `entries[]`, and this is what keeps the
|
|
60
|
+
* attribution through the merge.
|
|
61
|
+
*/
|
|
62
|
+
lexicon?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Provider-assigned physical id (ARN, resource id, pod name) from the live
|
|
65
|
+
* observation's `ResourceMetadata.physicalId`, falling back to the snapshot's
|
|
66
|
+
* when the resource is gone (#1674). Absent when neither side reported one.
|
|
67
|
+
*/
|
|
68
|
+
physicalId?: string;
|
|
52
69
|
action: ChangeAction;
|
|
53
70
|
/** The three-way evidence the classification was derived from. */
|
|
54
71
|
evidence: {
|
|
@@ -101,7 +118,12 @@ export interface ChangeSet {
|
|
|
101
118
|
* classifies as `unobserved` and nothing else: no `create` is ever synthesized
|
|
102
119
|
* from a read that did not happen.
|
|
103
120
|
*/
|
|
104
|
-
export
|
|
121
|
+
export interface ChangeSetOptions {
|
|
122
|
+
/** Stamp every entry with the lexicon it was observed by (#1674). */
|
|
123
|
+
lexicon?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function buildChangeSet(env: string, input: DiffLiveInput, options?: ChangeSetOptions): ChangeSet {
|
|
105
127
|
const diff = diffLive(input);
|
|
106
128
|
const { declared, observedNow } = input;
|
|
107
129
|
const observedThen = input.observedThen ?? {};
|
|
@@ -184,9 +206,15 @@ export function buildChangeSet(env: string, input: DiffLiveInput): ChangeSet {
|
|
|
184
206
|
// classification above never reads it.
|
|
185
207
|
const queried = unobservedEntry?.queried ?? input.queried?.[name];
|
|
186
208
|
|
|
209
|
+
// The provider's id for the row (#1674). Live first; the snapshot's only
|
|
210
|
+
// when the resource is no longer live (a snapshot-only noop).
|
|
211
|
+
const physicalId = observedNow[name]?.physicalId ?? observedThen[name]?.physicalId;
|
|
212
|
+
|
|
187
213
|
entries.push({
|
|
188
214
|
name,
|
|
189
215
|
type,
|
|
216
|
+
...(options?.lexicon ? { lexicon: options.lexicon } : {}),
|
|
217
|
+
...(physicalId ? { physicalId } : {}),
|
|
190
218
|
action,
|
|
191
219
|
evidence,
|
|
192
220
|
deltas,
|
|
@@ -143,6 +143,10 @@ export function diffDeepObservation(
|
|
|
143
143
|
normalizedLive[name] = {
|
|
144
144
|
type: liveEntity.type || entity.entityType,
|
|
145
145
|
...(liveEntity.physicalId ? { physicalId: liveEntity.physicalId } : {}),
|
|
146
|
+
// Per-path owners (#1189) ride through unchanged; the diff looks them
|
|
147
|
+
// up by the flattened path, so a keyed list element (`[#name]`) has
|
|
148
|
+
// no owner today — its raw index path is what the reader recorded.
|
|
149
|
+
...(liveEntity.fieldOwners ? { fieldOwners: liveEntity.fieldOwners } : {}),
|
|
146
150
|
properties: normalizeDeepProperties(liveRaw, {
|
|
147
151
|
entityType: liveEntity.type || entity.entityType,
|
|
148
152
|
side: "live",
|
package/src/lifecycle/index.ts
CHANGED
package/src/lifecycle/observe.ts
CHANGED
|
@@ -31,6 +31,14 @@ export interface ObserveResult {
|
|
|
31
31
|
observations: LiveObservation[];
|
|
32
32
|
warnings: string[];
|
|
33
33
|
errors: string[];
|
|
34
|
+
/**
|
|
35
|
+
* Run-level notices from the lexicons (#1265), each said once however many
|
|
36
|
+
* stacks or lexicons reported it — "ownership filter unavailable on this
|
|
37
|
+
* read path" is the canonical one. Kept apart from `warnings`, which are
|
|
38
|
+
* per-entity, so a caller can print them where a note belongs: after the
|
|
39
|
+
* answer, not ahead of it.
|
|
40
|
+
*/
|
|
41
|
+
notes: string[];
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
/**
|
|
@@ -43,7 +51,14 @@ export interface ObserveResult {
|
|
|
43
51
|
function qualifyObservation(obs: NormalizedObservation, stackName: string): NormalizedObservation {
|
|
44
52
|
const q = <T>(m: Record<string, T>): Record<string, T> =>
|
|
45
53
|
Object.fromEntries(Object.entries(m).map(([k, v]) => [`${stackName}::${k}`, v]));
|
|
46
|
-
return {
|
|
54
|
+
return {
|
|
55
|
+
resources: q(obs.resources),
|
|
56
|
+
unobserved: q(obs.unobserved),
|
|
57
|
+
queried: q(obs.queried),
|
|
58
|
+
notes: obs.notes,
|
|
59
|
+
// Exports are already keyed by stack (#1279); nothing to qualify.
|
|
60
|
+
...(obs.stackExports ? { stackExports: obs.stackExports } : {}),
|
|
61
|
+
};
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
/**
|
|
@@ -110,6 +125,7 @@ export async function observeResources(
|
|
|
110
125
|
const observations: LiveObservation[] = [];
|
|
111
126
|
const warnings: string[] = [];
|
|
112
127
|
const errors: string[] = [];
|
|
128
|
+
const notes: string[] = [];
|
|
113
129
|
|
|
114
130
|
for (const plugin of plugins) {
|
|
115
131
|
if (!plugin.describeResources) continue;
|
|
@@ -200,6 +216,13 @@ export async function observeResources(
|
|
|
200
216
|
}),
|
|
201
217
|
);
|
|
202
218
|
}
|
|
219
|
+
// Run-level notices (#1265): one line per distinct note per run,
|
|
220
|
+
// whatever the stack count. `mergeObservations` already folded the
|
|
221
|
+
// per-stack copies; this folds across lexicons.
|
|
222
|
+
for (const note of observed.notes) {
|
|
223
|
+
const line = `[${plugin.name}] ${note}`;
|
|
224
|
+
if (!notes.includes(line)) notes.push(line);
|
|
225
|
+
}
|
|
203
226
|
// What the estate depends on but does not declare (#1273). Read after the
|
|
204
227
|
// managed resources, because the declared observation is the closure's
|
|
205
228
|
// roots — there is nothing to reference out from until it exists.
|
|
@@ -260,6 +283,7 @@ export async function observeResources(
|
|
|
260
283
|
resources: {},
|
|
261
284
|
unobserved: unobservedAll(entityNames, "read-failed", message, entities),
|
|
262
285
|
queried: {},
|
|
286
|
+
notes: [],
|
|
263
287
|
},
|
|
264
288
|
environment,
|
|
265
289
|
entityNames.length,
|
|
@@ -267,7 +291,7 @@ export async function observeResources(
|
|
|
267
291
|
}
|
|
268
292
|
}
|
|
269
293
|
|
|
270
|
-
return { observations, warnings, errors };
|
|
294
|
+
return { observations, warnings, errors, notes };
|
|
271
295
|
}
|
|
272
296
|
|
|
273
297
|
/**
|
|
@@ -451,5 +475,8 @@ function pushObservation(
|
|
|
451
475
|
: observed.resources,
|
|
452
476
|
...(unobservedNames.length > 0 ? { unobserved: observed.unobserved } : {}),
|
|
453
477
|
...(dependencies.edges.length > 0 ? { edges: dependencies.edges } : {}),
|
|
478
|
+
...(observed.stackExports && Object.keys(observed.stackExports).length > 0
|
|
479
|
+
? { stackExports: observed.stackExports }
|
|
480
|
+
: {}),
|
|
454
481
|
});
|
|
455
482
|
}
|
package/src/lifecycle/replay.ts
CHANGED
|
@@ -177,6 +177,9 @@ export async function replaySnapshots(
|
|
|
177
177
|
lexicon,
|
|
178
178
|
resources,
|
|
179
179
|
...(edges.length > 0 ? { edges } : {}),
|
|
180
|
+
...(snapshot.stackExports && Object.keys(snapshot.stackExports).length > 0
|
|
181
|
+
? { stackExports: snapshot.stackExports }
|
|
182
|
+
: {}),
|
|
180
183
|
});
|
|
181
184
|
commit ||= snapshot.commit ?? "";
|
|
182
185
|
// Report the OLDEST timestamp across stacks: a caller asking how stale this
|
|
@@ -140,6 +140,7 @@ export async function takeSnapshot(
|
|
|
140
140
|
let resources: Record<string, ResourceMetadata> = {};
|
|
141
141
|
let artifacts: Record<string, ArtifactMetadata> = {};
|
|
142
142
|
let unobserved: Record<string, UnobservedEntity> = {};
|
|
143
|
+
let stackExports: Record<string, Record<string, unknown>> | undefined;
|
|
143
144
|
|
|
144
145
|
try {
|
|
145
146
|
if (plugin.describeResources) {
|
|
@@ -163,6 +164,7 @@ export async function takeSnapshot(
|
|
|
163
164
|
// entity nobody could read must not be recorded as "was not there",
|
|
164
165
|
// because the next diff would then read it back as absent.
|
|
165
166
|
unobserved = observed.unobserved;
|
|
167
|
+
stackExports = observed.stackExports;
|
|
166
168
|
for (const [name, entry] of Object.entries(unobserved)) {
|
|
167
169
|
warnings.push(`${plugin.name}: not observed — ${formatUnobserved(name, entry)}`);
|
|
168
170
|
}
|
|
@@ -258,6 +260,7 @@ export async function takeSnapshot(
|
|
|
258
260
|
timestamp,
|
|
259
261
|
resources: withDependencies,
|
|
260
262
|
...(dependencies.edges.length > 0 ? { edges: dependencies.edges } : {}),
|
|
263
|
+
...(stackExports && Object.keys(stackExports).length > 0 ? { stackExports } : {}),
|
|
261
264
|
...(Object.keys(unobserved).length > 0 && { unobserved }),
|
|
262
265
|
...(Object.keys(artifacts).length > 0 && { artifacts }),
|
|
263
266
|
// Only written when deep. An absent field means identity, which is what
|