@intentius/chant 0.38.0 → 0.39.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 (85) hide show
  1. package/dist/build.d.ts +21 -0
  2. package/dist/build.d.ts.map +1 -1
  3. package/dist/cli/commands/build.d.ts.map +1 -1
  4. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  5. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  6. package/dist/cli/handlers/run-client.d.ts.map +1 -1
  7. package/dist/cli/handlers/search.d.ts +30 -1
  8. package/dist/cli/handlers/search.d.ts.map +1 -1
  9. package/dist/cli/main.d.ts.map +1 -1
  10. package/dist/cli/plugins.d.ts +20 -0
  11. package/dist/cli/plugins.d.ts.map +1 -1
  12. package/dist/codegen/registry.d.ts +23 -0
  13. package/dist/codegen/registry.d.ts.map +1 -0
  14. package/dist/components/sandbox/driver.d.ts.map +1 -1
  15. package/dist/composite.d.ts +23 -4
  16. package/dist/composite.d.ts.map +1 -1
  17. package/dist/deep-observation.d.ts +11 -0
  18. package/dist/deep-observation.d.ts.map +1 -1
  19. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  20. package/dist/graph-declared.d.ts.map +1 -1
  21. package/dist/graph-ir.d.ts +17 -3
  22. package/dist/graph-ir.d.ts.map +1 -1
  23. package/dist/graph-refs.d.ts +24 -0
  24. package/dist/graph-refs.d.ts.map +1 -1
  25. package/dist/kubectl-context.d.ts.map +1 -1
  26. package/dist/lexicon-config.d.ts +61 -0
  27. package/dist/lexicon-config.d.ts.map +1 -0
  28. package/dist/lexicon.d.ts +19 -0
  29. package/dist/lexicon.d.ts.map +1 -1
  30. package/dist/lifecycle/deep-diff.d.ts +11 -0
  31. package/dist/lifecycle/deep-diff.d.ts.map +1 -1
  32. package/dist/lifecycle/digest.d.ts.map +1 -1
  33. package/dist/lifecycle/identity.d.ts +52 -0
  34. package/dist/lifecycle/identity.d.ts.map +1 -0
  35. package/dist/lifecycle/observe.d.ts +5 -0
  36. package/dist/lifecycle/observe.d.ts.map +1 -1
  37. package/dist/lifecycle/replay.d.ts.map +1 -1
  38. package/dist/lifecycle/types.d.ts +30 -0
  39. package/dist/lifecycle/types.d.ts.map +1 -1
  40. package/dist/managed-fields.d.ts +11 -0
  41. package/dist/managed-fields.d.ts.map +1 -1
  42. package/package.json +1 -1
  43. package/src/build.ts +24 -0
  44. package/src/cli/commands/build.ts +10 -0
  45. package/src/cli/commands/check-lexicon.ts +20 -1
  46. package/src/cli/handlers/lifecycle.ts +10 -0
  47. package/src/cli/handlers/run-client.ts +3 -1
  48. package/src/cli/handlers/search-kind.test.ts +45 -0
  49. package/src/cli/handlers/search.ts +102 -4
  50. package/src/cli/main.ts +32 -10
  51. package/src/cli/param-flag-scope.test.ts +69 -0
  52. package/src/cli/plugins.test.ts +33 -1
  53. package/src/cli/plugins.ts +55 -0
  54. package/src/codegen/registry.test.ts +56 -0
  55. package/src/codegen/registry.ts +69 -0
  56. package/src/components/SPRAWL-VALIDATION.md +5 -5
  57. package/src/components/sandbox/driver.test.ts +27 -0
  58. package/src/components/sandbox/driver.ts +12 -0
  59. package/src/composite.ts +33 -4
  60. package/src/deep-observation.ts +11 -0
  61. package/src/discovery/sandbox/driver.test.ts +34 -0
  62. package/src/discovery/sandbox/driver.ts +19 -0
  63. package/src/graph-declared.test.ts +86 -0
  64. package/src/graph-declared.ts +14 -2
  65. package/src/graph-ir.ts +32 -8
  66. package/src/graph-refs.test.ts +56 -0
  67. package/src/graph-refs.ts +37 -1
  68. package/src/kubectl-context.ts +4 -1
  69. package/src/lexicon-config.test.ts +111 -0
  70. package/src/lexicon-config.ts +92 -0
  71. package/src/lexicon.ts +20 -0
  72. package/src/lifecycle/deep-diff.test.ts +48 -1
  73. package/src/lifecycle/deep-diff.ts +16 -0
  74. package/src/lifecycle/digest.test.ts +81 -0
  75. package/src/lifecycle/digest.ts +34 -3
  76. package/src/lifecycle/identity.test.ts +39 -0
  77. package/src/lifecycle/identity.ts +61 -0
  78. package/src/lifecycle/observe.test.ts +75 -1
  79. package/src/lifecycle/observe.ts +28 -2
  80. package/src/lifecycle/replay.test.ts +251 -0
  81. package/src/lifecycle/replay.ts +67 -19
  82. package/src/lifecycle/types.ts +26 -0
  83. package/src/managed-fields.test.ts +50 -0
  84. package/src/managed-fields.ts +25 -6
  85. package/src/meta/peer-deps.test.ts +111 -14
@@ -50,6 +50,17 @@ export interface PropertyDrift {
50
50
  * accepted value there would lose the most useful column in the report.
51
51
  */
52
52
  baseline?: unknown;
53
+ /**
54
+ * The field manager that owns this path live, where the substrate records one
55
+ * (#1189) — Kubernetes' `managedFields`, and nowhere else today.
56
+ *
57
+ * `kind` says a path is `undeclared` or `changed`; this says who did it.
58
+ * "Owned by `kubectl-client-side-apply`" and "owned by `hpa-controller`" are
59
+ * the same `kind` and mean opposite things: one is somebody bypassing the
60
+ * pipeline, the other is a controller doing its job. Absent on a substrate
61
+ * with no per-field ownership, which is every substrate but k8s.
62
+ */
63
+ owner?: string;
53
64
  }
54
65
  /** Property-level drift for one declared entity. */
55
66
  export interface DeepEntityDrift {
@@ -1 +1 @@
1
- {"version":3,"file":"deep-diff.d.ts","sourceRoot":"","sources":["../../src/lifecycle/deep-diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAIL,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC/B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEjF;;;;;;;;;GASG;AACH,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,YAAY,GAAG,QAAQ,CAAC;AAEpE,qCAAqC;AACrC,MAAM,WAAW,aAAa;IAC5B,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,iBAAiB,CAAC;IACxB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+CAA+C;IAC/C,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,oDAAoD;AACpD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,qDAAqD;IACrD,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,8EAA8E;IAC9E,UAAU,EAAE,kBAAkB,EAAE,CAAC;IACjC,0EAA0E;IAC1E,kBAAkB,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED,sFAAsF;AACtF,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC5B,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC7C,4EAA4E;IAC5E,IAAI,EAAE,yBAAyB,CAAC;IAChC,yEAAyE;IACzE,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,sBAAsB,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,cAAc,CA8F7D;AAED,+DAA+D;AAC/D,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAEjE"}
1
+ {"version":3,"file":"deep-diff.d.ts","sourceRoot":"","sources":["../../src/lifecycle/deep-diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAIL,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC/B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEjF;;;;;;;;;GASG;AACH,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,YAAY,GAAG,QAAQ,CAAC;AAEpE,qCAAqC;AACrC,MAAM,WAAW,aAAa;IAC5B,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,iBAAiB,CAAC;IACxB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+CAA+C;IAC/C,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;;;;OASG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,oDAAoD;AACpD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,qDAAqD;IACrD,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,8EAA8E;IAC9E,UAAU,EAAE,kBAAkB,EAAE,CAAC;IACjC,0EAA0E;IAC1E,kBAAkB,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED,sFAAsF;AACtF,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC5B,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC7C,4EAA4E;IAC5E,IAAI,EAAE,yBAAyB,CAAC;IAChC,yEAAyE;IACzE,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,sBAAsB,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,cAAc,CAmG7D;AAED,+DAA+D;AAC/D,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAEjE"}
@@ -1 +1 @@
1
- {"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../../src/lifecycle/digest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EAAE,WAAW,EAAkB,UAAU,EAAE,MAAM,SAAS,CAAC;AAIvE;;GAEG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAGhD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,WAAW,GAAG,WAAW,CAwBxE;AAED;;GAEG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,WAAW,GAAG,SAAS,GAChC,UAAU,CAgCZ"}
1
+ {"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../../src/lifecycle/digest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EAAE,WAAW,EAAkB,UAAU,EAAE,MAAM,SAAS,CAAC;AAIvE;;GAEG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAGhD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,WAAW,GAAG,WAAW,CA4BxE;AAED;;GAEG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,WAAW,GAAG,SAAS,GAChC,UAAU,CAiCZ"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * What makes an unqualified resource one resource (#1416).
3
+ *
4
+ * Managed resources are stack-qualified (`${stack}::${id}`, #1162) and cannot
5
+ * collide. Everything else — dependencies (#1273) and ambient resources (#1278)
6
+ * — is keyed by physical id and merged across stacks, because the account's
7
+ * default VPC route table is one resource however many stacks route through it.
8
+ *
9
+ * That is right for a resource that really is account-level, and wrong for a
10
+ * regional one. A VPC, subnet or security group in `us-west-1` is a different
11
+ * resource from one in `us-east-1`, and merging them by id drops all but the
12
+ * first sighting.
13
+ *
14
+ * On AWS the difference is invisible: VPC, subnet and security-group ids are
15
+ * globally unique, so keying by id and keying by (region, id) give the same
16
+ * answer. It shows up against an emulator that reuses ids across regions
17
+ * (lex00/floci#21), where a three-region estate's nine default subnets merged
18
+ * to three and "how many subnets are empty" came back 2 instead of 8.
19
+ *
20
+ * The rule is deliberately not a table of regional kinds. A lexicon stamps a
21
+ * region on a resource when region is part of that resource's identity — the
22
+ * AWS lexicon does it in `stampRegion` for ambient resources and its own
23
+ * property reads — and this reads that stamp back. A genuinely account-level
24
+ * resource carries no region and keeps merging as it always did, which is why
25
+ * dependencies (never stamped) are untouched by this.
26
+ */
27
+ import type { ResourceMetadata } from "../lexicon.js";
28
+ /**
29
+ * The region a resource records itself as being in, if it records one.
30
+ *
31
+ * Read from `attributes.region`, which is where a lexicon stamps it. Absent
32
+ * means the resource is account-level, or the lexicon has no region to give —
33
+ * both of which are read the same way, as "region is not part of this id".
34
+ */
35
+ export declare function regionOf(meta: Pick<ResourceMetadata, "attributes">): string | undefined;
36
+ /**
37
+ * The key an unqualified resource merges under.
38
+ *
39
+ * `${region}::${id}` for a resource that carries a region, matching the shape
40
+ * stack qualification already uses, and the bare id for one that does not.
41
+ *
42
+ * Applied only where results from more than one region are actually merged —
43
+ * `collectAmbient` across stacks, `replaySnapshots` across recorded stacks.
44
+ * Recording one region's observation qualifies nothing, so a stored snapshot
45
+ * keeps the keys it has always had and the first snapshot taken after this
46
+ * lands does not diff as "every ambient resource replaced".
47
+ *
48
+ * Idempotent, so an id that already carries this region survives a second pass
49
+ * unchanged rather than becoming `us-east-1::us-east-1::sg-1`.
50
+ */
51
+ export declare function unqualifiedKey(id: string, meta: Pick<ResourceMetadata, "attributes">): string;
52
+ //# sourceMappingURL=identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/lifecycle/identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,YAAY,CAAC,GAAG,MAAM,GAAG,SAAS,CAGvF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,YAAY,CAAC,GAAG,MAAM,CAI7F"}
@@ -60,6 +60,11 @@ export declare function observeResources(environment: string, plugins: Observati
60
60
  * Ask a lexicon what exists of the kinds it manages, beyond what is declared
61
61
  * (#1278). Once per stack for the region, merged by physical id — the same
62
62
  * ambient resource seen from two stacks is one resource.
63
+ *
64
+ * "The same" is per region (#1416). Two stacks in one region reporting the
65
+ * account's default security group is one group; two regions' default security
66
+ * groups are two, whatever their ids look like, so the merge key carries the
67
+ * region a resource records itself in.
63
68
  */
64
69
  export declare function collectAmbient(plugin: ObservationLexicon, opts: {
65
70
  environment: string;
@@ -1 +1 @@
1
- {"version":3,"file":"observe.d.ts","sourceRoot":"","sources":["../../src/lifecycle/observe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAI5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAUnD,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,eAAe,EAAE,CAAC;IAChC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAeD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,kBAAkB,EAAE,EAC7B,WAAW,EAAE,WAAW,EACxB,IAAI,CAAC,EAAE;IACL,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzE;;8CAE0C;IAC1C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,GACA,OAAO,CAAC,aAAa,CAAC,CA6JxB;AAyBD;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE;IACJ,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC3C,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/D,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,GACA,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAqB3C;AAED,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC5C,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE;IACJ,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC3C,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChE,GACA,OAAO,CAAC,qBAAqB,CAAC,CA0ChC"}
1
+ {"version":3,"file":"observe.d.ts","sourceRoot":"","sources":["../../src/lifecycle/observe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAI5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAWnD,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,eAAe,EAAE,CAAC;IAChC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAeD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,kBAAkB,EAAE,EAC7B,WAAW,EAAE,WAAW,EACxB,IAAI,CAAC,EAAE;IACL,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzE;;8CAE0C;IAC1C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,GACA,OAAO,CAAC,aAAa,CAAC,CA2KxB;AAyBD;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE;IACJ,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC3C,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/D,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,GACA,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CA2B3C;AAED,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC5C,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE;IACJ,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC3C,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChE,GACA,OAAO,CAAC,qBAAqB,CAAC,CA0ChC"}
@@ -1 +1 @@
1
- {"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../../src/lifecycle/replay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD;;;;;;;;;;;;GAYG;AACH;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAMvE;AAED,wBAAsB,eAAe,CACnC,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,GACxB,OAAO,CAAC;IAAE,YAAY,EAAE,eAAe,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAuFpH"}
1
+ {"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../../src/lifecycle/replay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD;;;;;;;;;;;;GAYG;AACH;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAMvE;AAED,wBAAsB,eAAe,CACnC,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,GACxB,OAAO,CAAC;IAAE,YAAY,EAAE,eAAe,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAsIpH"}
@@ -92,6 +92,18 @@ export interface ResourceDigest {
92
92
  export interface BuildDigest {
93
93
  /** Per-resource digest keyed by logical name */
94
94
  resources: Record<string, ResourceDigest>;
95
+ /**
96
+ * chant #1442 — lexicon name → version that produced this build.
97
+ *
98
+ * Recorded once per lexicon rather than on every {@link ResourceDigest},
99
+ * which would repeat one string across every resource of a stack for no
100
+ * added information.
101
+ *
102
+ * Optional because a digest read back from an older snapshot will not have
103
+ * it. Absent and empty are different: absent means "recorded before this
104
+ * existed", empty means "recorded, and no plugins were loaded".
105
+ */
106
+ lexiconVersions?: Record<string, string>;
95
107
  /** Resource-level dependency graph */
96
108
  dependencies: Record<string, string[]>;
97
109
  /** Cross-lexicon output bridges from BuildManifest */
@@ -115,5 +127,23 @@ export interface DigestDiff {
115
127
  changed: string[];
116
128
  /** Resources where propsHash matches */
117
129
  unchanged: string[];
130
+ /**
131
+ * chant #1442 — lexicons whose VERSION moved between the two digests, even
132
+ * where every resource is unchanged.
133
+ *
134
+ * A build whose only difference is the lexicon that emitted it is a real
135
+ * difference: the lexicon is a generated artifact pinned to an upstream
136
+ * spec, so a bump can change output with no source change. Reported
137
+ * separately from `changed` because no resource's declaration moved — the
138
+ * interpreter did.
139
+ *
140
+ * Empty when neither digest recorded versions, so a comparison against a
141
+ * pre-#1442 snapshot reports nothing rather than inventing a change.
142
+ */
143
+ lexiconVersionChanges: Array<{
144
+ lexicon: string;
145
+ previous?: string;
146
+ current?: string;
147
+ }>;
118
148
  }
119
149
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lifecycle/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAErE;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,MAAM,CAAC;AAEnD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB;;gFAE4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC5C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC9C,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC7C;;;;;OAKG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC;IACrD;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,qFAAqF;IACrF,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC1C,sCAAsC;IACtC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/E,iCAAiC;IACjC,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,4DAA4D;IAC5D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,wCAAwC;IACxC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,wCAAwC;IACxC,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lifecycle/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAErE;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,MAAM,CAAC;AAEnD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB;;gFAE4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC5C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC9C,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC7C;;;;;OAKG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC;IACrD;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,qFAAqF;IACrF,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC1C;;;;;;;;;;OAUG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,sCAAsC;IACtC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/E,iCAAiC;IACjC,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,4DAA4D;IAC5D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,wCAAwC;IACxC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,wCAAwC;IACxC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB;;;;;;;;;;;;OAYG;IACH,qBAAqB,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxF"}
@@ -81,6 +81,17 @@ export interface OwnershipSets {
81
81
  foreignOwned: ReadonlySet<string>;
82
82
  /** The subset of `foreignOwned` where the declared manifest also sets the path — drift-relevant despite foreign ownership. */
83
83
  foreignContested: ReadonlySet<string>;
84
+ /**
85
+ * Path → the name of the manager that owns it (#1189).
86
+ *
87
+ * The three sets above answer *which category* owns a path, which is all the
88
+ * prune rule needs. A reader needs the other question — "owned by
89
+ * `kubectl-client-side-apply`" and "owned by `hpa-controller`" are the same
90
+ * category and mean very different things to an operator. Last writer wins
91
+ * where several managers touch one path, matching what the API server itself
92
+ * reports.
93
+ */
94
+ owners: ReadonlyMap<string, string>;
84
95
  }
85
96
  /**
86
97
  * Build the three ownership sets for one live object. `entries` is
@@ -1 +1 @@
1
- {"version":3,"file":"managed-fields.d.ts","sourceRoot":"","sources":["../src/managed-fields.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAIrE;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,kCAAkC,EAAE,WAAW,CAAC,MAAM,CAQjE,CAAC;AAwBH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAwBhF;AAID;;;;;;;;;;GAUG;AACH,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,yDAAyD;IACzD,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAChC,kDAAkD;IAClD,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,8HAA8H;IAC9H,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACvC;AAsHD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,SAAS,sBAAsB,EAAE,EAC1C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,KAAK,OAAO,GACvD,aAAa,CAmBf;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAM7E"}
1
+ {"version":3,"file":"managed-fields.d.ts","sourceRoot":"","sources":["../src/managed-fields.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAIrE;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,kCAAkC,EAAE,WAAW,CAAC,MAAM,CAQjE,CAAC;AAwBH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAwBhF;AAID;;;;;;;;;;GAUG;AACH,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,yDAAyD;IACzD,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAChC,kDAAkD;IAClD,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,8HAA8H;IAC9H,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACtC;;;;;;;;;OASG;IACH,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC;AA6HD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,SAAS,sBAAsB,EAAE,EAC1C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,KAAK,OAAO,GACvD,aAAa,CAoBf;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAM7E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
package/src/build.ts CHANGED
@@ -185,6 +185,14 @@ export interface BuildOptions {
185
185
  */
186
186
  lexicons?: readonly string[];
187
187
 
188
+ /**
189
+ * chant #1442 — lexicon name → the version of the plugin that served it.
190
+ * Recorded on {@link BuildResult.lexiconVersions} so a build digest can say
191
+ * WHAT interpreted the declarations, not only what was declared. The CLI
192
+ * populates this from `options.plugins`; `build()` only carries it.
193
+ */
194
+ lexiconVersions?: Readonly<Record<string, string>>;
195
+
188
196
  /**
189
197
  * chant #1045 Phase 2 — opt-in: run-fallback files (or, when {@link fold}
190
198
  * isn't set, every file) execute together, isolated, in one sandboxed
@@ -230,6 +238,21 @@ export interface BuildResult {
230
238
  */
231
239
  foldDecisions: FoldDecision[];
232
240
 
241
+ /**
242
+ * chant #1442 — lexicon name → the version of the plugin that served this
243
+ * build, passed through verbatim from {@link BuildOptions.lexiconVersions}.
244
+ *
245
+ * The other half of what a build digest needs. `hashProps` fingerprints the
246
+ * declaration; this records what turned it into output. A lexicon is a
247
+ * generated artifact pinned to an upstream spec, so a bump can change
248
+ * emitted output with no source change at all — and without this the two
249
+ * builds are indistinguishable.
250
+ *
251
+ * Empty when the caller supplied no plugins (`build()` used as a library,
252
+ * and most tests).
253
+ */
254
+ lexiconVersions: Record<string, string>;
255
+
233
256
  /**
234
257
  * This build's resolved build-time parameters (#1064) — the build
235
258
  * provenance record for `params.<name>` values, alongside the existing
@@ -717,6 +740,7 @@ async function buildFromDiscoveryResult(
717
740
  manifest,
718
741
  sourceFileCount: discoveryResult.sourceFiles.length,
719
742
  foldDecisions: discoveryResult.foldDecisions,
743
+ lexiconVersions: { ...(options?.lexiconVersions ?? {}) },
720
744
  buildParams: options?.buildParams ?? [],
721
745
  };
722
746
  }
@@ -3,6 +3,7 @@ import { loadChantConfigUpward, resolveOwnershipMarker, resolveFoldEnabled, reso
3
3
  import { resolveCliBuildParams } from "../build-params-cli";
4
4
  import type { Serializer, SerializerResult } from "../../serializer";
5
5
  import type { LexiconPlugin } from "../../lexicon";
6
+ import { resolveLexiconVersions } from "../plugins";
6
7
  import { runPostSynthChecks } from "../../lint/post-synth";
7
8
  import { applyConfiguredSeverity } from "../../lint/config";
8
9
  import { loadPolicyChecks } from "../../lint/policy";
@@ -242,6 +243,14 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
242
243
  // ../plugins.ts and fold-import.ts's `lexiconPackageName`.
243
244
  const lexicons = options.plugins?.map((plugin) => plugin.name) ?? [];
244
245
 
246
+ // chant #1442 — which lexicon VERSION interpreted each declaration. The
247
+ // declaration is fingerprinted by `hashProps`; the thing that turned it into
248
+ // output was not recorded at all, so a lexicon bump that changed emitted
249
+ // output left the build digest identical. Captured here because this is the
250
+ // layer that knows which lexicons were loaded — `build()` is given only
251
+ // their names.
252
+ const lexiconVersions = resolveLexiconVersions(lexicons);
253
+
245
254
  // Run the build
246
255
  const result = await build(infraPath, options.serializers, undefined, {
247
256
  ownership,
@@ -250,6 +259,7 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
250
259
  sandbox,
251
260
  intrinsics,
252
261
  lexicons,
262
+ lexiconVersions,
253
263
  buildParams: paramsResolution.provenance,
254
264
  });
255
265
 
@@ -3,7 +3,7 @@ import { join, basename } from "path";
3
3
  import { auditIntrinsics } from "./check-lexicon-intrinsics";
4
4
  import { checkExamplesBuild } from "./check-lexicon-examples";
5
5
  import { auditDocsReachability } from "./check-lexicon-docs";
6
- import { auditMcpNames } from "./check-lexicon-mcp";
6
+ import { auditMcpNames, lexiconNameFor } from "./check-lexicon-mcp";
7
7
  import { loadLexiconFromDir, registers, safeList } from "./check-lexicon-plugin";
8
8
  import { RULE_CATALOG } from "../../audit/catalog";
9
9
 
@@ -448,6 +448,25 @@ export async function checkLexicon(dir: string): Promise<CheckResult> {
448
448
  : "no marker channel — every verdict must be unknown",
449
449
  });
450
450
 
451
+ // #1344 — a lexicon that reads its own `chant.config.ts` namespace should
452
+ // declare its shape, or a typo inside that namespace is accepted and silently
453
+ // ignored: `forgejo: { runnerLabel: … }` left the dialect on its defaults with
454
+ // nothing said. Source scan, because the read happens deep in a serializer
455
+ // rather than anywhere the plugin object can be asked.
456
+ const lexiconName = lexiconNameFor(dir);
457
+ const readsOwnNamespace = findFiles(join(dir, "src"), (n) => n.endsWith(".ts") && !n.endsWith(".test.ts"))
458
+ .some((file) => new RegExp(`config\\s*\\??\\.\\s*${lexiconName}\\b`).test(readOr(file)));
459
+ items.push({
460
+ name: "Declares a configSchema if it reads its own config namespace",
461
+ tier: 2,
462
+ pass: !readsOwnNamespace || plugin?.configSchema !== undefined,
463
+ detail: readsOwnNamespace
464
+ ? plugin?.configSchema
465
+ ? `config.${lexiconName} is declared and validated`
466
+ : `reads config.${lexiconName} but declares no schema — unknown keys there are silently ignored`
467
+ : "reads no config namespace of its own",
468
+ });
469
+
451
470
  const compositeFiles = listTsFiles(join(dir, "src/composites"), ["index.ts"]);
452
471
  items.push({
453
472
  name: "At least 1 composite in src/composites/",
@@ -597,6 +597,16 @@ async function runLifecycleDiffDigest(args: DigestDiffArgs): Promise<number> {
597
597
  for (const name of diff.unchanged) {
598
598
  console.log(name.padEnd(20) + "unchanged".padEnd(12) + (currentDigest.resources[name]?.type ?? ""));
599
599
  }
600
+
601
+ // chant #1442 — a lexicon bump can change emitted output with no source
602
+ // change, so every resource above can read "unchanged" while the build is
603
+ // not the same build. Printed under the table rather than as a row: no
604
+ // resource's declaration moved, the thing that interpreted them did.
605
+ for (const change of diff.lexiconVersionChanges) {
606
+ const from = change.previous ?? "(absent)";
607
+ const to = change.current ?? "(absent)";
608
+ console.log(`\nlexicon ${change.lexicon}: ${from} → ${to}`);
609
+ }
600
610
  }
601
611
 
602
612
  return 0;
@@ -116,7 +116,9 @@ export function resolveProfile(
116
116
  config: Record<string, unknown>,
117
117
  profileName?: string,
118
118
  ): WorkerProfile {
119
- const temporal = config.temporal as Record<string, unknown> | undefined;
119
+ // #1344 the temporal lexicon declares this namespace and core validates it
120
+ // at load, so this reads a checked value rather than asserting one.
121
+ const temporal = (config as { temporal?: Record<string, unknown> }).temporal;
120
122
  if (!temporal?.profiles) {
121
123
  throw new Error(
122
124
  'No temporal.profiles found in chant.config.ts. Add a profile to use `chant run`.',
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { resolveKindTerms, type Term } from "./search";
3
+ import type { IRNode } from "../../graph-ir";
4
+
5
+ const nodes = [
6
+ { id: "vpc", kind: "AWS::EC2::VPC", lexicon: "aws", attrs: {} },
7
+ { id: "att", kind: "AWS::EC2::VPCGatewayAttachment", lexicon: "aws", attrs: {} },
8
+ { id: "sub", kind: "AWS::EC2::Subnet", lexicon: "aws", attrs: {} },
9
+ { id: "assoc", kind: "AWS::EC2::SubnetRouteTableAssociation", lexicon: "aws", attrs: {} },
10
+ { id: "igw", kind: "AWS::EC2::InternetGateway", lexicon: "aws", attrs: {} },
11
+ ] as IRNode[];
12
+
13
+ const resolve = (a: string) => {
14
+ const t: Term = { kind: "kind", a };
15
+ resolveKindTerms([t], nodes);
16
+ return t.kinds ? [...t.kinds].sort() : undefined;
17
+ };
18
+
19
+ describe("a kind term means the kind, not any type containing its letters", () => {
20
+ it("EC2::VPC is the VPC, not the gateway attachment", () => {
21
+ // The estate has 6 VPCs and answered 9. VPCGatewayAttachment contains the
22
+ // characters "EC2::VPC" and is not a VPC.
23
+ expect(resolve("EC2::VPC")).toEqual(["AWS::EC2::VPC"]);
24
+ });
25
+
26
+ it("Subnet is the subnet, not the route table association", () => {
27
+ expect(resolve("Subnet")).toEqual(["AWS::EC2::Subnet"]);
28
+ });
29
+
30
+ it("a full type still resolves to itself", () => {
31
+ expect(resolve("AWS::EC2::Subnet")).toEqual(["AWS::EC2::Subnet"]);
32
+ });
33
+
34
+ it("a genuine substring search still works", () => {
35
+ // Nothing lines up with a `::` boundary here, so the documented substring
36
+ // rule applies and finds both gateway-ish kinds.
37
+ expect(resolve("Gateway")).toBeUndefined();
38
+ });
39
+
40
+ it("resolves inside an edge sub-term, where the negation lives", () => {
41
+ const t: Term = { kind: "edge", a: "", dir: "in", sub: { kind: "kind", a: "EC2::VPC" } };
42
+ resolveKindTerms([t], nodes);
43
+ expect(t.sub?.kinds && [...t.sub.kinds]).toEqual(["AWS::EC2::VPC"]);
44
+ });
45
+ });
@@ -1,6 +1,6 @@
1
1
  import { resolve } from "node:path";
2
2
  import { build } from "../../build";
3
- import { buildGraphIr, buildLiveGraphIr, sourceOverlayGraphs, type GraphIR, type IRNode } from "../../graph-ir";
3
+ import { buildGraphIr, buildLiveGraphIr, sourceOverlayGraphs, type GraphIR, type IRNode, type IREdge } from "../../graph-ir";
4
4
  import { buildDeclaredPerStack } from "../../graph-declared";
5
5
  import { enrichEffectiveTopology } from "../../graph-effective";
6
6
  import { reconstructEdges, mergeCatalogs, type ReferenceCatalog } from "../../graph-refs";
@@ -127,6 +127,8 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
127
127
  source = { kind: "live" };
128
128
  }
129
129
  let live = buildLiveGraphIr(observations);
130
+ // Containment edges, kept aside until after the overlay (see below).
131
+ let containmentEdges: IREdge[] = [];
130
132
  // Live-only: enrichment is a provider call, so it has no place in a replay.
131
133
  // A recorded answer that quietly reached for the API would stop being one.
132
134
  if (!args.at) {
@@ -147,13 +149,24 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
147
149
  // topology has nothing to traverse on anything the declared graph does not
148
150
  // already model.
149
151
  const catalogs = observing.map((p) => p.referenceCatalog).filter((c): c is ReferenceCatalog => !!c);
152
+ // Held back until after the overlay — see where it is merged for why.
150
153
  if (catalogs.length > 0) {
151
154
  // Merge, never replace. A lexicon reports relationships a catalog cannot
152
155
  // reconstruct (#1273) — an instance placed in a subnet it did not declare
153
156
  // carries a template `Ref` in its attributes, not the physical subnet id,
154
157
  // so no identity index resolves it. Overwriting here dropped exactly those
155
158
  // edges and left the fold with a chain missing its first hop.
156
- const reconstructed = reconstructEdges(live.nodes, mergeCatalogs(catalogs)).edges;
159
+ // Containment travels with the references here, and only here. `->`/`<-`
160
+ // asks which nodes reach which, and being inside something is a way of
161
+ // reaching it: "which subnets have no network interfaces IN them" and
162
+ // "which VPCs have no instances IN them" are containment questions, and
163
+ // with only peer references to walk the negation matched everything.
164
+ //
165
+ // `graph` still reads `.containment` on its own and draws boundary boxes,
166
+ // so nothing gains a line it did not have.
167
+ const rebuilt = reconstructEdges(live.nodes, mergeCatalogs(catalogs));
168
+ containmentEdges = rebuilt.containmentEdges;
169
+ const reconstructed = rebuilt.edges;
157
170
  const seen = new Set((live.edges ?? []).map((e) => `${e.from}|${e.to}|${e.viaAttr ?? ""}`));
158
171
  live = {
159
172
  ...live,
@@ -177,6 +190,36 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
177
190
  ? await buildDeclaredPerStack(stacks, projectPath)
178
191
  : buildGraphIr((await discover(resolve(args.src ?? config.sourceDir ?? "."))).entities, projectPath);
179
192
  ir = sourceOverlayGraphs(declared, live);
193
+ // Containment goes on AFTER the overlay, not through it.
194
+ //
195
+ // `sourceOverlayGraphs` admits a live edge only when one end is foreign,
196
+ // because "declared edges already cover every declared relationship". That
197
+ // is true of the references it was written for and false of containment:
198
+ // the declared canvas carries what the source *says*, and no source says
199
+ // "this instance is in this VPC" — the instance declares a subnet, and the
200
+ // VPC is implied by the account.
201
+ //
202
+ // So an instance and its VPC, both declared, had their containment dropped
203
+ // between them, while an instance in the account's DEFAULT VPC kept it
204
+ // because that VPC is foreign. "Which VPCs have no instances" answered
205
+ // correctly for the one VPC nobody declared and wrongly for every VPC
206
+ // somebody did.
207
+ if (containmentEdges.length > 0) {
208
+ const present = new Set(ir.nodes.map((n) => n.id));
209
+ const seen = new Set(ir.edges.map((e) => `${e.from}|${e.to}|${e.viaAttr ?? ""}`));
210
+ ir = {
211
+ ...ir,
212
+ edges: [
213
+ ...ir.edges,
214
+ ...containmentEdges.filter(
215
+ (e) =>
216
+ present.has(e.from) &&
217
+ present.has(e.to) &&
218
+ !seen.has(`${e.from}|${e.to}|${e.viaAttr ?? ""}`),
219
+ ),
220
+ ],
221
+ };
222
+ }
180
223
  // Carry live-derived attrs onto the declared canvas — some facts only exist
181
224
  // in live account state (e.g. `internetFacing` for an instance in the
182
225
  // account's default VPC, whose route table chant does not model). The
@@ -190,6 +233,9 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
190
233
  // Fold derived reachability facts (effectiveIngress, internetFacing) onto
191
234
  // instance nodes so multi-hop/launch-template joins are one node predicate (#1139).
192
235
  ir = enrichEffectiveTopology(ir);
236
+ // Resolve `kind:` terms against what is actually in this graph, before any
237
+ // node is tested against them.
238
+ resolveKindTerms(terms, ir.nodes);
193
239
  const nodeById = new Map(ir.nodes.map((n) => [n.id, n]));
194
240
  const matches = ir.nodes.filter((n) => terms.every((t) => matchTerm(n, t, ir, nodeById)));
195
241
  if (matches.length === 0) {
@@ -489,7 +535,7 @@ function describeTerm(t: Term): string {
489
535
  return leaf(t);
490
536
  }
491
537
 
492
- interface Term {
538
+ export interface Term {
493
539
  kind: "word" | "kind" | "tag" | "attr" | "edge";
494
540
  /** `!term` — the node must NOT satisfy this (#1280). */
495
541
  negated?: boolean;
@@ -498,6 +544,53 @@ interface Term {
498
544
  /** For edge terms: the direction and the sub-predicate matched at the far end. */
499
545
  dir?: "out" | "in";
500
546
  sub?: Term;
547
+ /**
548
+ * For kind terms: the kinds this term actually means, resolved against the
549
+ * graph (see {@link resolveKindTerms}). Absent when nothing resolved it, in
550
+ * which case the substring rule applies unchanged.
551
+ */
552
+ kinds?: Set<string>;
553
+ }
554
+
555
+ /**
556
+ * Decide which kinds a `kind:` term means, given what is in the graph.
557
+ *
558
+ * `kind:` is documented as a substring, and substrings of a CloudFormation type
559
+ * cross kind boundaries: `kind:EC2::VPC` also matches
560
+ * `AWS::EC2::VPCGatewayAttachment`, and `kind:EC2::Subnet` also matches
561
+ * `AWS::EC2::SubnetRouteTableAssociation`. An estate with 6 VPCs answers 9, and
562
+ * the caller has no reason to suspect the number they were given.
563
+ *
564
+ * That cost real answers. Asked how many VPCs have no instances, agents named
565
+ * the right VPCs and reported "9 VPCs in the estate" beside them; the grader
566
+ * failed the answer for contradicting the estate, which it did.
567
+ *
568
+ * So: a term that lines up with `::` boundaries means those kinds and only
569
+ * those. `EC2::VPC` is the last two segments of `AWS::EC2::VPC` and is not any
570
+ * run of segments in `AWS::EC2::VPCGatewayAttachment`, because
571
+ * `VPCGatewayAttachment` is not `VPC`.
572
+ *
573
+ * Substring is kept as the fallback, so `kind:Gateway` still finds both
574
+ * `InternetGateway` and `VPCGatewayAttachment` — a genuine substring search
575
+ * that no segment rule would serve. Most specific wins; nothing else changes.
576
+ */
577
+ export function resolveKindTerms(terms: Term[], nodes: IRNode[]): void {
578
+ const present = [...new Set(nodes.map((n) => n.kind).filter((k): k is string => !!k))];
579
+ const walk = (t: Term): void => {
580
+ if (t.sub) walk(t.sub);
581
+ if (t.kind !== "kind") return;
582
+ const want = t.a.toLowerCase().split("::").filter(Boolean);
583
+ if (want.length === 0) return;
584
+ const onBoundary = present.filter((k) => {
585
+ const have = k.toLowerCase().split("::");
586
+ for (let i = 0; i + want.length <= have.length; i++) {
587
+ if (want.every((w, j) => have[i + j] === w)) return true;
588
+ }
589
+ return false;
590
+ });
591
+ if (onBoundary.length > 0) t.kinds = new Set(onBoundary);
592
+ };
593
+ terms.forEach(walk);
501
594
  }
502
595
 
503
596
  function parseLeaf(tok: string): Term {
@@ -616,7 +709,12 @@ function matchTerm(n: IRNode, t: Term, ir?: GraphIR, byId?: Map<string, IRNode>)
616
709
  .filter((x): x is IRNode => !!x);
617
710
  return neighbors.some((m) => matchTerm(m, t.sub!, ir, byId));
618
711
  }
619
- if (t.kind === "kind") return (n.kind ?? "").toLowerCase().includes(t.a.toLowerCase());
712
+ if (t.kind === "kind") {
713
+ // Resolved kinds when the term lined up with `::` boundaries; the
714
+ // documented substring otherwise.
715
+ if (t.kinds) return t.kinds.has(n.kind ?? "");
716
+ return (n.kind ?? "").toLowerCase().includes(t.a.toLowerCase());
717
+ }
620
718
  if (t.kind === "attr") {
621
719
  const val = attrString((attrs as Record<string, unknown>)[t.a]);
622
720
  return t.b === undefined ? t.a in attrs : val.toLowerCase().includes(t.b.toLowerCase());
package/src/cli/main.ts CHANGED
@@ -6,6 +6,7 @@ import { formatSuccess, formatError } from "./format";
6
6
  import { loadPlugins, resolveProjectLexicons } from "./plugins";
7
7
  import { resolveCommand, type CommandDef, type ParsedArgs } from "./registry";
8
8
  import { loadChantConfigUpward } from "../config";
9
+ import { validateLexiconConfig, formatLexiconConfigProblems } from "../lexicon-config";
9
10
  import { armSandboxConfigEvaluation } from "../config-sandbox";
10
11
  import { armSandboxPolicyExecution } from "../lint/policy-import";
11
12
  import { ENV_VAR, unknownEnvError } from "../env";
@@ -559,16 +560,17 @@ Options:
559
560
  project source; network egress is NOT blocked (see
560
561
  docs). Default: off (also settable via
561
562
  chant.config.ts's build.sandbox: true; #1045)
562
- --param <name=value> (build) Bind a declared build-time parameter
563
- (chant.config.ts's buildParams) to a value, for
564
- source to read as params.<name> (#1064) instead of
565
- process.env — repeatable. Distinct from the AWS
566
- lexicon's deploy-time Parameter(): this resolves
567
- before synthesis, so it can change which resources
568
- are produced at all. Highest precedence.
569
- --params-file <path> (build) JSON file of { "name": value } build-time
570
- parameter values (#1064). Second precedence, after
571
- --param.
563
+ --param <name=value> (build, graph, run --components) Bind a declared
564
+ build-time parameter (chant.config.ts's buildParams)
565
+ to a value, for source to read as params.<name>
566
+ (#1064) instead of process.env — repeatable.
567
+ Distinct from the AWS lexicon's deploy-time
568
+ Parameter(): this resolves before synthesis, so it
569
+ can change which resources are produced at all.
570
+ Highest precedence.
571
+ --params-file <path> (build, graph, run --components) JSON file of
572
+ { "name": value } build-time parameter values
573
+ (#1064). Second precedence, after --param.
572
574
 
573
575
  Examples:
574
576
  chant build ./infra/
@@ -668,6 +670,26 @@ async function loadPluginsOrExit(path: string): Promise<import("../lexicon").Lex
668
670
  process.exit(1);
669
671
  }
670
672
 
673
+ // #1344 — a lexicon that declares the shape of its own `chant.config.ts`
674
+ // namespace gets it validated here, once, before any command runs. The
675
+ // config schema is `.passthrough()`, so before this an unknown key inside a
676
+ // namespace was accepted and silently ignored: `forgejo: { runnerLabel: … }`
677
+ // left the dialect on its defaults with nothing said. A namespace whose
678
+ // lexicon declares no schema keeps that passthrough.
679
+ try {
680
+ const { config } = await loadChantConfigUpward(resolve(path));
681
+ const problems = validateLexiconConfig(plugins, config);
682
+ if (problems.length > 0) {
683
+ console.error(formatError({
684
+ message: `Invalid lexicon configuration in chant.config:\n${formatLexiconConfigProblems(problems)}`,
685
+ hint: "Remove or correct the key. A lexicon's namespace accepts only the keys it declares.",
686
+ }));
687
+ process.exit(1);
688
+ }
689
+ } catch {
690
+ // No config, or one that failed to load — the caller's own handling stands.
691
+ }
692
+
671
693
  return plugins;
672
694
  }
673
695