@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.
Files changed (220) hide show
  1. package/dist/audit/discover.d.ts +26 -2
  2. package/dist/audit/discover.d.ts.map +1 -1
  3. package/dist/audit/report-model.d.ts +10 -0
  4. package/dist/audit/report-model.d.ts.map +1 -1
  5. package/dist/audit/rules-doc.d.ts.map +1 -1
  6. package/dist/cli/build-params-cli.d.ts +16 -7
  7. package/dist/cli/build-params-cli.d.ts.map +1 -1
  8. package/dist/cli/commands/audit.d.ts +20 -0
  9. package/dist/cli/commands/audit.d.ts.map +1 -1
  10. package/dist/cli/commands/build.d.ts +9 -0
  11. package/dist/cli/commands/build.d.ts.map +1 -1
  12. package/dist/cli/commands/check-lexicon-docs.d.ts +13 -0
  13. package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
  14. package/dist/cli/commands/check-lexicon-examples.d.ts +24 -14
  15. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  16. package/dist/cli/commands/check-lexicon.d.ts +14 -0
  17. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  18. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
  19. package/dist/cli/commands/init-lexicon/templates/docs.d.ts +5 -0
  20. package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
  21. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  22. package/dist/cli/commands/lexicon-surface-diff.d.ts +6 -0
  23. package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -1
  24. package/dist/cli/commands/lint.d.ts.map +1 -1
  25. package/dist/cli/commands/onboard.d.ts +28 -0
  26. package/dist/cli/commands/onboard.d.ts.map +1 -1
  27. package/dist/cli/handlers/build.d.ts.map +1 -1
  28. package/dist/cli/handlers/graph.d.ts.map +1 -1
  29. package/dist/cli/handlers/lifecycle.d.ts +13 -0
  30. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  31. package/dist/cli/handlers/run.d.ts.map +1 -1
  32. package/dist/cli/handlers/search.d.ts +27 -0
  33. package/dist/cli/handlers/search.d.ts.map +1 -1
  34. package/dist/cli/main.d.ts.map +1 -1
  35. package/dist/cli/registry.d.ts +7 -0
  36. package/dist/cli/registry.d.ts.map +1 -1
  37. package/dist/codegen/docs-pages.d.ts +39 -0
  38. package/dist/codegen/docs-pages.d.ts.map +1 -0
  39. package/dist/codegen/docs-sections.d.ts.map +1 -1
  40. package/dist/codegen/docs-sidebar.d.ts +13 -2
  41. package/dist/codegen/docs-sidebar.d.ts.map +1 -1
  42. package/dist/codegen/docs-types.d.ts +30 -10
  43. package/dist/codegen/docs-types.d.ts.map +1 -1
  44. package/dist/codegen/docs.d.ts +14 -2
  45. package/dist/codegen/docs.d.ts.map +1 -1
  46. package/dist/codegen/lexicon-regen.d.ts +11 -0
  47. package/dist/codegen/lexicon-regen.d.ts.map +1 -1
  48. package/dist/codegen/surface-snapshot.d.ts +27 -3
  49. package/dist/codegen/surface-snapshot.d.ts.map +1 -1
  50. package/dist/codegen/validate.d.ts +21 -3
  51. package/dist/codegen/validate.d.ts.map +1 -1
  52. package/dist/components/capability-plugin.d.ts +13 -2
  53. package/dist/components/capability-plugin.d.ts.map +1 -1
  54. package/dist/components/driver.d.ts +38 -0
  55. package/dist/components/driver.d.ts.map +1 -1
  56. package/dist/components/index.d.ts +1 -1
  57. package/dist/components/index.d.ts.map +1 -1
  58. package/dist/components/starter-plugin.d.ts.map +1 -1
  59. package/dist/config.d.ts +85 -5
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/discovery/fold-import.d.ts.map +1 -1
  62. package/dist/env.d.ts +12 -1
  63. package/dist/env.d.ts.map +1 -1
  64. package/dist/fold/fold.d.ts +100 -1
  65. package/dist/fold/fold.d.ts.map +1 -1
  66. package/dist/graph-ir.d.ts +12 -0
  67. package/dist/graph-ir.d.ts.map +1 -1
  68. package/dist/graph-ops.d.ts +17 -0
  69. package/dist/graph-ops.d.ts.map +1 -0
  70. package/dist/lexicon.d.ts +182 -2
  71. package/dist/lexicon.d.ts.map +1 -1
  72. package/dist/lifecycle/change-set.d.ts +23 -2
  73. package/dist/lifecycle/change-set.d.ts.map +1 -1
  74. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  75. package/dist/lifecycle/index.d.ts +1 -0
  76. package/dist/lifecycle/index.d.ts.map +1 -1
  77. package/dist/lifecycle/observe.d.ts +8 -0
  78. package/dist/lifecycle/observe.d.ts.map +1 -1
  79. package/dist/lifecycle/replay.d.ts.map +1 -1
  80. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  81. package/dist/lifecycle/teardown.d.ts +130 -0
  82. package/dist/lifecycle/teardown.d.ts.map +1 -0
  83. package/dist/lifecycle/types.d.ts +7 -0
  84. package/dist/lifecycle/types.d.ts.map +1 -1
  85. package/dist/lint/engine.d.ts +6 -2
  86. package/dist/lint/engine.d.ts.map +1 -1
  87. package/dist/lint/policy.d.ts.map +1 -1
  88. package/dist/lint/rule.d.ts +31 -0
  89. package/dist/lint/rule.d.ts.map +1 -1
  90. package/dist/lint/rules/cor021-env-literal-name.d.ts +3 -0
  91. package/dist/lint/rules/cor021-env-literal-name.d.ts.map +1 -0
  92. package/dist/lint/rules/index.d.ts +2 -1
  93. package/dist/lint/rules/index.d.ts.map +1 -1
  94. package/dist/managed-fields.d.ts +33 -21
  95. package/dist/managed-fields.d.ts.map +1 -1
  96. package/dist/observation.d.ts +23 -1
  97. package/dist/observation.d.ts.map +1 -1
  98. package/dist/op/builders.d.ts +36 -7
  99. package/dist/op/builders.d.ts.map +1 -1
  100. package/dist/op/index.d.ts +1 -1
  101. package/dist/op/index.d.ts.map +1 -1
  102. package/dist/testing.d.ts +136 -0
  103. package/dist/testing.d.ts.map +1 -0
  104. package/dist/yaml.d.ts +0 -8
  105. package/dist/yaml.d.ts.map +1 -1
  106. package/package.json +6 -1
  107. package/src/__snapshots__/okf.test.ts.snap +20 -0
  108. package/src/audit/discover.test.ts +55 -1
  109. package/src/audit/discover.ts +70 -2
  110. package/src/audit/edge-init-safety.test.ts +149 -18
  111. package/src/audit/report-model.ts +12 -1
  112. package/src/audit/rules-doc.ts +1 -0
  113. package/src/build-params.test.ts +52 -0
  114. package/src/cli/build-params-cli.test.ts +25 -8
  115. package/src/cli/build-params-cli.ts +37 -23
  116. package/src/cli/commands/__fixtures__/audit-coverage/.github/workflows/ci.yml +5 -0
  117. package/src/cli/commands/__fixtures__/audit-coverage/Dockerfile +2 -0
  118. package/src/cli/commands/__fixtures__/audit-coverage/infra/main.tf +3 -0
  119. package/src/cli/commands/__fixtures__/audit-coverage/infra/stack.json +6 -0
  120. package/src/cli/commands/__fixtures__/audit-coverage/k8s/deploy.yaml +12 -0
  121. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/pages/getting-started.mdx +16 -0
  122. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +10 -2
  123. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +3 -1
  124. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +2 -1
  125. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +2 -1
  126. package/src/cli/commands/audit.test.ts +83 -1
  127. package/src/cli/commands/audit.ts +121 -28
  128. package/src/cli/commands/build.test.ts +275 -5
  129. package/src/cli/commands/build.ts +86 -14
  130. package/src/cli/commands/check-lexicon-docs.ts +30 -0
  131. package/src/cli/commands/check-lexicon-examples.test.ts +73 -7
  132. package/src/cli/commands/check-lexicon-examples.ts +66 -17
  133. package/src/cli/commands/check-lexicon.test.ts +46 -2
  134. package/src/cli/commands/check-lexicon.ts +64 -4
  135. package/src/cli/commands/init-lexicon/templates/codegen.ts +5 -2
  136. package/src/cli/commands/init-lexicon/templates/docs.ts +35 -2
  137. package/src/cli/commands/init-lexicon.test.ts +42 -1
  138. package/src/cli/commands/init-lexicon.ts +3 -1
  139. package/src/cli/commands/lexicon-rolling-upgrade.ts +1 -1
  140. package/src/cli/commands/lexicon-surface-diff.test.ts +1 -1
  141. package/src/cli/commands/lexicon-surface-diff.ts +10 -1
  142. package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
  143. package/src/cli/commands/lint.ts +19 -6
  144. package/src/cli/commands/onboard.test.ts +319 -225
  145. package/src/cli/commands/onboard.ts +144 -58
  146. package/src/cli/handlers/build.test.ts +3 -3
  147. package/src/cli/handlers/build.ts +2 -0
  148. package/src/cli/handlers/components.ts +2 -2
  149. package/src/cli/handlers/graph.test.ts +44 -0
  150. package/src/cli/handlers/graph.ts +24 -4
  151. package/src/cli/handlers/lifecycle.test.ts +348 -1
  152. package/src/cli/handlers/lifecycle.ts +235 -3
  153. package/src/cli/handlers/run.test.ts +5 -5
  154. package/src/cli/handlers/run.ts +1 -0
  155. package/src/cli/handlers/search-live.test.ts +217 -0
  156. package/src/cli/handlers/search.test.ts +41 -1
  157. package/src/cli/handlers/search.ts +60 -8
  158. package/src/cli/main.ts +17 -4
  159. package/src/cli/registry.ts +7 -0
  160. package/src/cli/security-boundary.test.ts +2 -1
  161. package/src/codegen/docs-pages.test.ts +194 -0
  162. package/src/codegen/docs-pages.ts +138 -0
  163. package/src/codegen/docs-sections.ts +0 -8
  164. package/src/codegen/docs-sidebar.ts +50 -37
  165. package/src/codegen/docs-types.ts +32 -4
  166. package/src/codegen/docs.ts +78 -40
  167. package/src/codegen/lexicon-regen.ts +22 -2
  168. package/src/codegen/rolling-upgrade.test.ts +1 -1
  169. package/src/codegen/rolling-upgrade.ts +1 -1
  170. package/src/codegen/surface-snapshot.test.ts +139 -0
  171. package/src/codegen/surface-snapshot.ts +297 -122
  172. package/src/codegen/validate.test.ts +49 -3
  173. package/src/codegen/validate.ts +36 -7
  174. package/src/components/auto-release.ts +1 -1
  175. package/src/components/capability-plugin.ts +23 -2
  176. package/src/components/driver.test.ts +45 -0
  177. package/src/components/driver.ts +64 -27
  178. package/src/components/index.ts +2 -0
  179. package/src/components/starter-plugin.ts +5 -2
  180. package/src/config.test.ts +40 -0
  181. package/src/config.ts +159 -8
  182. package/src/discovery/fold-import.test.ts +427 -2
  183. package/src/discovery/fold-import.ts +163 -7
  184. package/src/discovery/sandbox/driver.test.ts +11 -0
  185. package/src/env.test.ts +35 -1
  186. package/src/env.ts +17 -3
  187. package/src/fold/fold.test.ts +41 -0
  188. package/src/fold/fold.ts +372 -2
  189. package/src/graph-ir-live.test.ts +42 -0
  190. package/src/graph-ir.ts +42 -2
  191. package/src/graph-ops.test.ts +69 -0
  192. package/src/graph-ops.ts +42 -0
  193. package/src/lexicon.ts +182 -2
  194. package/src/lifecycle/change-set.test.ts +41 -0
  195. package/src/lifecycle/change-set.ts +30 -2
  196. package/src/lifecycle/deep-observe.ts +4 -0
  197. package/src/lifecycle/index.ts +1 -0
  198. package/src/lifecycle/observe.ts +29 -2
  199. package/src/lifecycle/replay.ts +3 -0
  200. package/src/lifecycle/snapshot.ts +3 -0
  201. package/src/lifecycle/teardown.test.ts +537 -0
  202. package/src/lifecycle/teardown.ts +357 -0
  203. package/src/lifecycle/types.ts +7 -0
  204. package/src/lint/engine.ts +7 -1
  205. package/src/lint/policy.ts +14 -3
  206. package/src/lint/rule.ts +23 -0
  207. package/src/lint/rules/cor021-env-literal-name.test.ts +128 -0
  208. package/src/lint/rules/cor021-env-literal-name.ts +114 -0
  209. package/src/lint/rules/index.ts +4 -1
  210. package/src/managed-fields.test.ts +15 -42
  211. package/src/managed-fields.ts +43 -26
  212. package/src/observation.test.ts +24 -8
  213. package/src/observation.ts +48 -4
  214. package/src/op/builders.ts +40 -7
  215. package/src/op/index.ts +1 -1
  216. package/src/ownership.test.ts +63 -1
  217. package/src/testing.test.ts +261 -0
  218. package/src/testing.ts +338 -0
  219. package/src/yaml.test.ts +15 -0
  220. package/src/yaml.ts +19 -2
@@ -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
- /** chant entity name. */
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 function buildChangeSet(env: string, input: DiffLiveInput): ChangeSet {
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",
@@ -13,3 +13,4 @@ export * from "./build-ledger";
13
13
  export * from "./build-ledger-store";
14
14
  export * from "./oras-referrer-lookup";
15
15
  export * from "./status";
16
+ export * from "./teardown";
@@ -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 { resources: q(obs.resources), unobserved: q(obs.unobserved), queried: q(obs.queried) };
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
  }
@@ -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