@intentius/chant 0.28.0 → 0.30.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 (106) hide show
  1. package/dist/cli/handlers/components.d.ts.map +1 -1
  2. package/dist/cli/handlers/graph.d.ts.map +1 -1
  3. package/dist/cli/handlers/lifecycle.d.ts +5 -3
  4. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  5. package/dist/cli/main.d.ts.map +1 -1
  6. package/dist/cli/registry.d.ts +14 -0
  7. package/dist/cli/registry.d.ts.map +1 -1
  8. package/dist/config.d.ts +46 -4
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/deep-observation.d.ts +257 -0
  11. package/dist/deep-observation.d.ts.map +1 -0
  12. package/dist/discovery/fold-import.d.ts +153 -17
  13. package/dist/discovery/fold-import.d.ts.map +1 -1
  14. package/dist/discovery/sandbox/config-wire.d.ts +3 -2
  15. package/dist/discovery/sandbox/config-wire.d.ts.map +1 -1
  16. package/dist/env.d.ts +5 -2
  17. package/dist/env.d.ts.map +1 -1
  18. package/dist/fold/fold.d.ts +35 -3
  19. package/dist/fold/fold.d.ts.map +1 -1
  20. package/dist/fold/subset.d.ts +9 -0
  21. package/dist/fold/subset.d.ts.map +1 -1
  22. package/dist/graph-ir.d.ts +73 -4
  23. package/dist/graph-ir.d.ts.map +1 -1
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/kubectl-context.d.ts +27 -0
  27. package/dist/kubectl-context.d.ts.map +1 -1
  28. package/dist/lexicon.d.ts +78 -6
  29. package/dist/lexicon.d.ts.map +1 -1
  30. package/dist/lifecycle/change-set.d.ts +26 -5
  31. package/dist/lifecycle/change-set.d.ts.map +1 -1
  32. package/dist/lifecycle/deep-diff.d.ts +103 -0
  33. package/dist/lifecycle/deep-diff.d.ts.map +1 -0
  34. package/dist/lifecycle/deep-observe.d.ts +62 -0
  35. package/dist/lifecycle/deep-observe.d.ts.map +1 -0
  36. package/dist/lifecycle/index.d.ts +3 -0
  37. package/dist/lifecycle/index.d.ts.map +1 -1
  38. package/dist/lifecycle/live-diff.d.ts +25 -1
  39. package/dist/lifecycle/live-diff.d.ts.map +1 -1
  40. package/dist/lifecycle/observation-baseline.d.ts +118 -0
  41. package/dist/lifecycle/observation-baseline.d.ts.map +1 -0
  42. package/dist/lifecycle/observe.d.ts +4 -2
  43. package/dist/lifecycle/observe.d.ts.map +1 -1
  44. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  45. package/dist/lifecycle/status.d.ts +26 -1
  46. package/dist/lifecycle/status.d.ts.map +1 -1
  47. package/dist/lifecycle/types.d.ts +8 -0
  48. package/dist/lifecycle/types.d.ts.map +1 -1
  49. package/dist/live-endpoint.d.ts +92 -0
  50. package/dist/live-endpoint.d.ts.map +1 -0
  51. package/dist/observation.d.ts +123 -0
  52. package/dist/observation.d.ts.map +1 -0
  53. package/dist/stack-output.d.ts.map +1 -1
  54. package/package.json +1 -1
  55. package/src/cli/handlers/components.test.ts +63 -4
  56. package/src/cli/handlers/components.ts +78 -35
  57. package/src/cli/handlers/graph.test.ts +155 -6
  58. package/src/cli/handlers/graph.ts +124 -29
  59. package/src/cli/handlers/lifecycle.test.ts +410 -6
  60. package/src/cli/handlers/lifecycle.ts +475 -182
  61. package/src/cli/main.test.ts +6 -0
  62. package/src/cli/main.ts +12 -0
  63. package/src/cli/registry.ts +14 -0
  64. package/src/config.test.ts +75 -0
  65. package/src/config.ts +61 -3
  66. package/src/deep-observation.test.ts +234 -0
  67. package/src/deep-observation.ts +489 -0
  68. package/src/discovery/fold-composite.test.ts +594 -0
  69. package/src/discovery/fold-import.test.ts +372 -1
  70. package/src/discovery/fold-import.ts +1216 -116
  71. package/src/discovery/sandbox/config-wire.ts +3 -2
  72. package/src/env.test.ts +12 -0
  73. package/src/env.ts +12 -4
  74. package/src/fold/fold.test.ts +105 -0
  75. package/src/fold/fold.ts +100 -20
  76. package/src/fold/subset.test.ts +38 -7
  77. package/src/fold/subset.ts +9 -0
  78. package/src/graph-ir-live.test.ts +28 -1
  79. package/src/graph-ir.ts +115 -12
  80. package/src/index.ts +2 -0
  81. package/src/kubectl-context.ts +81 -0
  82. package/src/lexicon.ts +100 -6
  83. package/src/lifecycle/change-set.test.ts +93 -1
  84. package/src/lifecycle/change-set.ts +65 -13
  85. package/src/lifecycle/deep-diff.test.ts +157 -0
  86. package/src/lifecycle/deep-diff.ts +213 -0
  87. package/src/lifecycle/deep-observe.test.ts +174 -0
  88. package/src/lifecycle/deep-observe.ts +173 -0
  89. package/src/lifecycle/index.ts +3 -0
  90. package/src/lifecycle/live-diff.test.ts +39 -0
  91. package/src/lifecycle/live-diff.ts +51 -5
  92. package/src/lifecycle/observation-baseline.test.ts +99 -0
  93. package/src/lifecycle/observation-baseline.ts +217 -0
  94. package/src/lifecycle/observe.test.ts +74 -3
  95. package/src/lifecycle/observe.ts +82 -22
  96. package/src/lifecycle/snapshot.test.ts +39 -1
  97. package/src/lifecycle/snapshot.ts +40 -20
  98. package/src/lifecycle/status.test.ts +89 -8
  99. package/src/lifecycle/status.ts +53 -3
  100. package/src/lifecycle/types.ts +8 -0
  101. package/src/live-endpoint.test.ts +115 -0
  102. package/src/live-endpoint.ts +148 -0
  103. package/src/observation.test.ts +96 -0
  104. package/src/observation.ts +213 -0
  105. package/src/stack-output.test.ts +55 -0
  106. package/src/stack-output.ts +41 -20
@@ -22,8 +22,10 @@ export interface ObserveResult {
22
22
  * `environment`. `owned` (default true for the managed-only diagram, epic #776)
23
23
  * restricts to resources carrying chant's ownership marker; a lexicon with no
24
24
  * marker channel logs and returns everything (its own contract). Plugins that
25
- * throw are collected into `errors` and skipped — one failing lexicon never
26
- * sinks the whole graph.
25
+ * throw are collected into `errors` — one failing lexicon never sinks the whole
26
+ * graph and every entity they were asked about is recorded as NOT-OBSERVED
27
+ * (`read-failed`, #1089) rather than dropped, so a failed read is visibly a
28
+ * hole instead of a silent absence.
27
29
  *
28
30
  * `stacks` (#57) is for a multi-stack, per-component project (e.g. loomster)
29
31
  * where there is no single stack named after the environment — AWS's
@@ -1 +1 @@
1
- {"version":3,"file":"observe.d.ts","sourceRoot":"","sources":["../../src/lifecycle/observe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAoB,MAAM,YAAY,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,eAAe,EAAE,CAAC;IAChC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,kBAAkB,EAAE,EAC7B,WAAW,EAAE,WAAW,EACxB,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAC5C,OAAO,CAAC,aAAa,CAAC,CAgExB"}
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;AAE5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,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;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,kBAAkB,EAAE,EAC7B,WAAW,EAAE,WAAW,EACxB,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAC5C,OAAO,CAAC,aAAa,CAAC,CAmFxB"}
@@ -1 +1 @@
1
- {"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../src/lifecycle/snapshot.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAsC,MAAM,YAAY,CAAC;AACzF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAiEjD,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,iBAAiB,EAAE,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,kBAAkB,EAAE,EAC7B,WAAW,EAAE,WAAW,EACxB,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACtC,OAAO,CAAC,kBAAkB,CAAC,CAkH7B"}
1
+ {"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../src/lifecycle/snapshot.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAsC,MAAM,YAAY,CAAC;AACzF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AA6DjD,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,iBAAiB,EAAE,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,kBAAkB,EAAE,EAC7B,WAAW,EAAE,WAAW,EACxB,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACtC,OAAO,CAAC,kBAAkB,CAAC,CA0I7B"}
@@ -21,6 +21,9 @@
21
21
  * lower-confidence signal rather than silently treated as "reconciled".
22
22
  * - **reconciled** — a release record exists and live evidence (via
23
23
  * ownership) confirms the component is present and owned by chant.
24
+ * - **unknown** — live evidence was requested and could not be read (#1089),
25
+ * or was not requested at all. A component chant could not observe is never
26
+ * reported `stale`: "the read failed" and "it is gone" are different facts.
24
27
  *
25
28
  * This is deliberately a light-touch reconciliation: chant's lexicons report
26
29
  * resource-level status (`ResourceMetadata`), not "the digest currently
@@ -35,6 +38,7 @@
35
38
  import type { ChangeSet, ChangeAction } from "./change-set.js";
36
39
  import { type ReleaseRecord } from "./release-ledger.js";
37
40
  import type { BuildLedgerEntry, ComponentBomSummary } from "./build-ledger.js";
41
+ import { type UnobservedReason } from "../observation.js";
38
42
  /** One row of `chant components status [env]` — the per-component join of recorded vs live. */
39
43
  export interface ComponentStatusRow {
40
44
  component: string;
@@ -61,9 +65,21 @@ export interface ComponentStatusRow {
61
65
  /**
62
66
  * Machine-readable "observed live", when live evidence was gathered (`--live`).
63
67
  * A consumer joining this row onto a graph node should read this rather than
64
- * string-matching `detail`. Absent when `--live` was not requested.
68
+ * string-matching `detail`. Absent when `--live` was not requested — and, since
69
+ * #1089, also absent when live state could not be read at all: `false` means
70
+ * "looked, not there", never "did not look". Read {@link unobserved} for that
71
+ * case; a consumer that treats absent as "unknown" already handles it.
65
72
  */
66
73
  live?: boolean;
74
+ /**
75
+ * Set when `--live` was requested and the observation could not read this
76
+ * component (#1089). `live` is absent alongside it and `reconciliation` is
77
+ * `unknown` — the row reports a hole rather than a verdict.
78
+ */
79
+ unobserved?: {
80
+ reason: UnobservedReason;
81
+ detail?: string;
82
+ };
67
83
  /**
68
84
  * The owning deploy unit's raw status, when a lexicon reported it (AWS: the
69
85
  * component's own CFN stack via `describeStackStatus`). Lets a renderer paint a
@@ -98,6 +114,15 @@ export interface ComponentStatusResult {
98
114
  export interface LiveComponentEvidence {
99
115
  /** True if this component name was observed live at all (declared+live, or orphan+live). */
100
116
  live: boolean;
117
+ /**
118
+ * Set when the observation could not read this component (#1089). `live` is
119
+ * `false` alongside it, but only because the boolean has nowhere else to go —
120
+ * every consumer must branch on this field before believing `live: false`.
121
+ */
122
+ unobserved?: {
123
+ reason: UnobservedReason;
124
+ detail?: string;
125
+ };
101
126
  /** The `ChangeSet` action chant's existing plan logic assigned, when the component maps to a tracked entity/resource name. */
102
127
  action?: ChangeAction;
103
128
  /** Ownership verdict, when known. */
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/lifecycle/status.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/lifecycle/status.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import type { ResourceMetadata, ArtifactMetadata } from "../lexicon.js";
2
+ import type { UnobservedEntity } from "../observation.js";
2
3
  export type { ResourceMetadata, ArtifactMetadata } from "../lexicon.js";
3
4
  /**
4
5
  * State snapshot for a single lexicon in an environment.
@@ -16,6 +17,13 @@ export interface LifecycleSnapshot {
16
17
  timestamp: string;
17
18
  /** Resource metadata keyed by logical name */
18
19
  resources: Record<string, ResourceMetadata>;
20
+ /**
21
+ * Declared entities this observation could not read (#1089), keyed by logical
22
+ * name. Additive and optional: a snapshot without it observed everything it
23
+ * was asked about. Present so a later diff can tell "was not there when the
24
+ * snapshot was taken" from "was never looked at".
25
+ */
26
+ unobserved?: Record<string, UnobservedEntity>;
19
27
  /** Artifact metadata keyed by server-side identifier (lexicon-specific). */
20
28
  artifacts?: Record<string, ArtifactMetadata>;
21
29
  /** Build digest at snapshot time — what was declared when this snapshot was taken */
@@ -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;AAErE,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAErE;;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,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC7C,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;AAEvD,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAErE;;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,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"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * chant #1166 — let a declared `environment` carry its own endpoint so
3
+ * `--live --env <name>` is self-sufficient.
4
+ *
5
+ * The bug this closes: `chant graph --live --env floci` (and `lifecycle
6
+ * diff`/`plan`) observe a stack by shelling out through each lexicon's
7
+ * `describeResources()`. For AWS that shell-out honors the ambient
8
+ * `AWS_ENDPOINT_URL` env var — when a project's `floci` environment is a local
9
+ * emulator (`http://localhost:4566`) but the invoking shell never exported
10
+ * that var, the AWS CLI silently targets real AWS instead. The stack named
11
+ * after the environment doesn't exist there, so `describeResources` hits its
12
+ * `stackDoesNotExist` branch and returns an empty, unremarkable "nothing is
13
+ * deployed" — indistinguishable from the truthful answer. This cost real
14
+ * debugging time validating #1162's live overlay: the observation code was
15
+ * right, the manual repro just never set the var.
16
+ *
17
+ * The fix: `environments` in `chant.config.ts` can name an endpoint per
18
+ * environment (`config.ts`'s `EnvironmentDeclaration`), and {@link
19
+ * applyLiveEndpoint} injects it into the ambient env var each observing
20
+ * lexicon's CLI shell-out actually reads — but only for a var that isn't
21
+ * already set. Ambient always wins: a shell that already exports
22
+ * `AWS_ENDPOINT_URL` sees no change in behavior.
23
+ *
24
+ * Audited (#1166) which lexicons have an ambient-env-var endpoint knob at
25
+ * all, since that's the specific footgun — a lexicon whose environment
26
+ * binding is resolved from `chant.config` itself (not an ambient var) has
27
+ * nothing to inject here:
28
+ *
29
+ * - **aws** — `AWS_ENDPOINT_URL`, read directly by
30
+ * `lexicons/aws/src/components/cloud-executor.ts` / `plugin.ts` before
31
+ * every `aws …` shell-out (`applyAwsEndpoint`/`applyAwsEndpointArgv`).
32
+ * - **fly** — `FLY_FLAPS_BASE_URL`, read by `resolveEndpoint()` in
33
+ * `lexicons/fly/src/op/activities/fly-apply.ts`, the same seam
34
+ * `describeResources` (`../describe-resources.ts`) calls through.
35
+ * - **gcp**, **k8s** — resolve their live target from `chant.config` itself
36
+ * (`k8s.profiles.<env>.context` via `resolveClusterTarget`,
37
+ * `packages/core/src/kubectl-context.ts`), not an ambient var. Nothing to
38
+ * inject: the config *is* the binding already.
39
+ * - **azure** — resolves via the `az` CLI's own logged-in
40
+ * subscription/session context; no ambient endpoint var exists to miss.
41
+ * - **temporal** — resolves its connection from `temporal.profiles.<env>`
42
+ * (`resolveProfile`, `lexicons/temporal/src/describe-resources.ts`), the
43
+ * same "config is the binding" shape as k8s/gcp.
44
+ */
45
+ import { type EnvironmentDeclaration } from "./config.js";
46
+ /**
47
+ * Per-lexicon ambient env var a `--live` read honors for its endpoint. Only
48
+ * lexicons with a genuine ambient-var footgun are listed — see the module doc
49
+ * for the full audit (gcp/k8s/azure/temporal resolve their target from
50
+ * `chant.config` instead, so they have nothing to inject).
51
+ */
52
+ export declare const LEXICON_ENDPOINT_ENV_VAR: Record<string, string>;
53
+ /** Result of {@link applyLiveEndpoint} — always call `restore()`, even when nothing was applied (it is then a no-op). */
54
+ export interface AppliedEndpoint {
55
+ /**
56
+ * One line describing what happened, or `undefined` when the environment
57
+ * declares no endpoint at all (nothing to say). Present whether the
58
+ * declared endpoint was applied OR an ambient var already won — #1166's
59
+ * "no silent anything" stance: an operator should never have to guess which
60
+ * target a `--live` read actually used.
61
+ */
62
+ notice?: string;
63
+ /** Undo whatever ambient env vars this call set. Always safe to call. */
64
+ restore: () => void;
65
+ }
66
+ /**
67
+ * Resolve `environment`'s declared endpoint (if any) from `config.environments`
68
+ * and apply it to the ambient env var of every lexicon in `lexicons` that has
69
+ * one ({@link LEXICON_ENDPOINT_ENV_VAR}) — but only when that var isn't
70
+ * already set. Ambient always wins (#1166): behavior for a shell that already
71
+ * exports `AWS_ENDPOINT_URL` is unchanged.
72
+ *
73
+ * Call before a `--live` describe/enrich pass; `restore()` in a `finally` so
74
+ * the injected value never leaks into a later invocation in the same process
75
+ * (tests, or a long-lived host like the MCP server).
76
+ */
77
+ export declare function applyLiveEndpoint(environments: EnvironmentDeclaration[] | undefined, environment: string, lexicons: readonly string[], env?: NodeJS.ProcessEnv): AppliedEndpoint;
78
+ /**
79
+ * #1166 acceptance: when a `--live` describe comes back with zero resources
80
+ * for a lexicon that had declared entities to look for, and nothing was
81
+ * already reported NOT-OBSERVED (#1089) either, that is either "genuinely
82
+ * nothing is deployed yet" or a misconfigured endpoint/credentials — the two
83
+ * are visually identical, so a caller must say so rather than stay quiet.
84
+ * Returns `undefined` when there is nothing to declare (no declared entities
85
+ * to have asked about, or the emptiness is already explained by #1089's
86
+ * `unobserved`).
87
+ */
88
+ export declare function zeroResourcesWarning(lexicon: string, environment: string, declaredCount: number, observed: {
89
+ resources: Record<string, unknown>;
90
+ unobserved: Record<string, unknown>;
91
+ }): string | undefined;
92
+ //# sourceMappingURL=live-endpoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-endpoint.d.ts","sourceRoot":"","sources":["../src/live-endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,OAAO,EAAuB,KAAK,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAE5E;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAG3D,CAAC;AAEF,yHAAyH;AACzH,MAAM,WAAW,eAAe;IAC9B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,sBAAsB,EAAE,GAAG,SAAS,EAClD,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,eAAe,CAqCjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GACpF,MAAM,GAAG,SAAS,CAKpB"}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * The observation contract (#1089) — what a lexicon's `describeResources()`
3
+ * is allowed to mean.
4
+ *
5
+ * Before this module there were two ways for an observation to return nothing
6
+ * for a declared entity and no way to tell them apart:
7
+ *
8
+ * 1. The provider was asked and said the resource does not exist.
9
+ * 2. The lexicon never looked — no reader for that kind, the read errored,
10
+ * no credentials, no cluster/subscription binding.
11
+ *
12
+ * Both arrived at the change set as "absent", and absent + declared classifies
13
+ * as `create`. So `chant lifecycle plan` proposed creating a Kubernetes CRD
14
+ * that already existed in the cluster, with nothing in the plan to say the tool
15
+ * had simply not looked (a warn on stderr is not a signal in a change set).
16
+ *
17
+ * The contract is a tri-state, per declared entity:
18
+ *
19
+ * - **OBSERVED-PRESENT** — a key in `resources`. The provider returned it.
20
+ * - **OBSERVED-ABSENT** — in neither map. The lexicon looked and the provider
21
+ * reported it missing. This is the only shape that may become a `create`.
22
+ * - **NOT-OBSERVED** — a key in `unobserved`, carrying a total
23
+ * {@link UnobservedReason}. Never a `create`, never a `delete`; consumers
24
+ * surface it and stop.
25
+ *
26
+ * Compatibility: `describeResources()` may still return the bare
27
+ * `name → ResourceMetadata` map it always did — that means "everything I was
28
+ * asked about, I looked at" and normalizes to an empty `unobserved`. Reporting
29
+ * NOT-OBSERVED requires the explicitly versioned {@link ObservationResult}
30
+ * envelope, which is discriminated by its literal `observation: "v1"` field (a
31
+ * bare map's value at any key is a `ResourceMetadata` object, never that
32
+ * string, so the two shapes can never be confused).
33
+ */
34
+ import type { ResourceMetadata } from "./lexicon.js";
35
+ /**
36
+ * Why a declared entity was not observed. Total: a lexicon that cannot observe
37
+ * an entity must pick one of these, and consumers may switch exhaustively.
38
+ *
39
+ * - `read-failed` — the provider was reached and the read errored (a non-zero
40
+ * CLI exit, a 5xx, an unparseable response).
41
+ * - `no-credentials` — no usable credentials/authorization for the target.
42
+ * - `no-binding` — the environment resolves to no concrete target (no kubectl
43
+ * context, no subscription, no stack, no endpoint).
44
+ * - `unsupported-kind` — the lexicon has no reader for this entity type (the
45
+ * K8s CRD case, Azure's nested ARM types). The resource may well exist.
46
+ * - `filtered` — the entity was reached but withheld by a caller-requested
47
+ * filter (`owned: true` against a resource carrying no chant marker). The
48
+ * result deliberately says nothing about it, which is still not absence: a
49
+ * declared resource that exists but is foreign must not classify as `create`.
50
+ */
51
+ export type UnobservedReason = "read-failed" | "no-credentials" | "no-binding" | "unsupported-kind" | "filtered";
52
+ /** Every legal {@link UnobservedReason}, for validation and conformance checks. */
53
+ export declare const UNOBSERVED_REASONS: readonly UnobservedReason[];
54
+ /** True when `value` is a legal {@link UnobservedReason}. */
55
+ export declare function isUnobservedReason(value: unknown): value is UnobservedReason;
56
+ /** One declared entity the lexicon could not observe, and why. */
57
+ export interface UnobservedEntity {
58
+ /** Declared entity type, when the lexicon knows it (it usually does — the entity is declared). */
59
+ type?: string;
60
+ /** Total verdict. */
61
+ reason: UnobservedReason;
62
+ /** Human-readable detail: the command that failed, the missing binding key, the unsupported kind. */
63
+ detail?: string;
64
+ }
65
+ /**
66
+ * The observation envelope — a `describeResources()` return value that can say
67
+ * "I did not look at this one". Explicitly versioned: `observation: "v1"`.
68
+ */
69
+ export interface ObservationResult {
70
+ /** Discriminant + wire version. Distinguishes the envelope from the bare `name → ResourceMetadata` map. */
71
+ readonly observation: "v1";
72
+ /** OBSERVED-PRESENT, keyed by chant entity name. */
73
+ resources: Record<string, ResourceMetadata>;
74
+ /** NOT-OBSERVED, keyed by chant entity name. Omit or leave empty when everything asked about was looked at. */
75
+ unobserved?: Record<string, UnobservedEntity>;
76
+ }
77
+ /**
78
+ * What `describeResources()` may return: the bare map (pre-#1089, still valid —
79
+ * "I looked at everything") or the {@link ObservationResult} envelope.
80
+ */
81
+ export type DescribeResourcesResult = Record<string, ResourceMetadata> | ObservationResult;
82
+ /** Normalized form every consumer works with. Both maps always present. */
83
+ export interface NormalizedObservation {
84
+ resources: Record<string, ResourceMetadata>;
85
+ unobserved: Record<string, UnobservedEntity>;
86
+ }
87
+ /** True when `value` is the versioned {@link ObservationResult} envelope. */
88
+ export declare function isObservationResult(value: unknown): value is ObservationResult;
89
+ /**
90
+ * Build an {@link ObservationResult}. Lexicons use this rather than writing the
91
+ * discriminant by hand.
92
+ */
93
+ export declare function observation(resources: Record<string, ResourceMetadata>, unobserved?: Record<string, UnobservedEntity>): ObservationResult;
94
+ /**
95
+ * Normalize either accepted return shape. `undefined` (a lexicon that returned
96
+ * nothing at all) normalizes to two empty maps — which reads as "everything was
97
+ * observed absent", so callers that mean "the read failed" must say so with
98
+ * {@link unobservedAll} rather than returning nothing.
99
+ */
100
+ export declare function normalizeObservation(value: DescribeResourcesResult | undefined): NormalizedObservation;
101
+ /**
102
+ * Mark every named entity NOT-OBSERVED with one reason — the whole-lexicon
103
+ * failure case (the provider CLI is missing, the cluster binding refused, the
104
+ * credentials are gone). Core applies this when `describeResources()` throws,
105
+ * so a thrown read degrades to an honest "did not look" for each declared
106
+ * entity instead of an empty map that classifies as N creates.
107
+ */
108
+ export declare function unobservedAll(names: Iterable<string>, reason: UnobservedReason, detail?: string, types?: Map<string, {
109
+ entityType: string;
110
+ }> | Record<string, string>): Record<string, UnobservedEntity>;
111
+ /**
112
+ * Union several observations of the same lexicon (the multi-stack read, where
113
+ * `describeResources` runs once per stack). Precedence is
114
+ * present > not-observed > absent: a resource found in any stack is present; an
115
+ * entity nobody could look at stays not-observed; an entity every reader looked
116
+ * for and did not find is absent.
117
+ */
118
+ export declare function mergeObservations(parts: Iterable<NormalizedObservation>): NormalizedObservation;
119
+ /** One-line human phrasing of a reason, for CLI output. */
120
+ export declare function unobservedReasonText(reason: UnobservedReason): string;
121
+ /** `name — reason (detail)`, the shared rendering for CLI and plan output. */
122
+ export declare function formatUnobserved(name: string, entry: UnobservedEntity): string;
123
+ //# sourceMappingURL=observation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observation.d.ts","sourceRoot":"","sources":["../src/observation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAElD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,gBAAgB,GACxB,aAAa,GACb,gBAAgB,GAChB,YAAY,GACZ,kBAAkB,GAClB,UAAU,CAAC;AAEf,mFAAmF;AACnF,eAAO,MAAM,kBAAkB,EAAE,SAAS,gBAAgB,EAMzD,CAAC;AAEF,6DAA6D;AAC7D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAE5E;AAED,kEAAkE;AAClE,MAAM,WAAW,gBAAgB;IAC/B,kGAAkG;IAClG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qBAAqB;IACrB,MAAM,EAAE,gBAAgB,CAAC;IACzB,qGAAqG;IACrG,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,2GAA2G;IAC3G,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC;IAC3B,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC5C,+GAA+G;IAC/G,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAC/C;AAED;;;GAGG;AACH,MAAM,MAAM,uBAAuB,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAAG,iBAAiB,CAAC;AAE3F,2EAA2E;AAC3E,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC5C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAC9C;AAED,6EAA6E;AAC7E,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,iBAAiB,CAM9E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EAC3C,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAC5C,iBAAiB,CAMnB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,uBAAuB,GAAG,SAAS,GAAG,qBAAqB,CAMtG;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,gBAAgB,EACxB,MAAM,CAAC,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACnE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAYlC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,qBAAqB,CAAC,GAAG,qBAAqB,CAW/F;AAED,2DAA2D;AAC3D,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAarE;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAG9E"}
@@ -1 +1 @@
1
- {"version":3,"file":"stack-output.d.ts","sourceRoot":"","sources":["../src/stack-output.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,iBAAiB,EAAE,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAG1D;;GAEG;AACH,eAAO,MAAM,mBAAmB,eAAkC,CAAC;AAEnE;;;GAGG;AACH,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC7C,QAAQ,CAAC,CAAC,mBAAmB,CAAC,EAAE,IAAI,CAAC;IACrC,QAAQ,CAAC,CAAC,iBAAiB,CAAC,EAAE,IAAI,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB;wEACoE;IACpE,QAAQ,CAAC,SAAS,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IACjD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;gFAC4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAqBD;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAOlE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,EACjC,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACxE,WAAW,CA0Cb"}
1
+ {"version":3,"file":"stack-output.d.ts","sourceRoot":"","sources":["../src/stack-output.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,iBAAiB,EAAgB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAChF,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAG1D;;GAEG;AACH,eAAO,MAAM,mBAAmB,eAAkC,CAAC;AAEnE;;;GAGG;AACH,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC7C,QAAQ,CAAC,CAAC,mBAAmB,CAAC,EAAE,IAAI,CAAC;IACrC,QAAQ,CAAC,CAAC,iBAAiB,CAAC,EAAE,IAAI,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB;wEACoE;IACpE,QAAQ,CAAC,SAAS,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IACjD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;gFAC4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AA2CD;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAOlE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,EACjC,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACxE,WAAW,CAyCb"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.28.0",
3
+ "version": "0.30.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",
@@ -1,4 +1,4 @@
1
- import { describe, test, expect, vi, beforeEach } from "vitest";
1
+ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
2
2
  import { createMockPlugin, staticDescribeResources } from "@intentius/chant-test-utils";
3
3
  import type { LexiconPlugin, ResourceMetadata } from "../../lexicon";
4
4
  import type { BuildResult } from "../../build";
@@ -38,9 +38,13 @@ vi.mock("../../lifecycle/release-ledger", async () => {
38
38
  };
39
39
  });
40
40
 
41
- vi.mock("../../config", () => ({
42
- loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args),
43
- }));
41
+ vi.mock("../../config", async () => {
42
+ const actual = await vi.importActual<typeof import("../../config")>("../../config");
43
+ return {
44
+ ...actual,
45
+ loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args),
46
+ };
47
+ });
44
48
 
45
49
  vi.mock("../../build", () => ({
46
50
  build: (...args: unknown[]) => buildMock(...args),
@@ -267,6 +271,61 @@ describe("components handlers", () => {
267
271
  expect(rows[0]).toMatchObject({ component: "svc", reconciliation: "reconciled" });
268
272
  });
269
273
 
274
+ // #1166 — an environment can declare its own endpoint (a local emulator
275
+ // like Floci), applied here unless the ambient shell already set it, so
276
+ // `--live` status doesn't silently read the wrong account.
277
+ describe("declared endpoint (#1166)", () => {
278
+ const prevEndpoint = process.env.AWS_ENDPOINT_URL;
279
+
280
+ afterEach(() => {
281
+ if (prevEndpoint === undefined) delete process.env.AWS_ENDPOINT_URL;
282
+ else process.env.AWS_ENDPOINT_URL = prevEndpoint;
283
+ });
284
+
285
+ test("applies the declared endpoint to AWS_ENDPOINT_URL for the live describe, then restores it", async () => {
286
+ delete process.env.AWS_ENDPOINT_URL;
287
+ readReleaseLedgerMock.mockResolvedValue({
288
+ records: [{
289
+ version: 1,
290
+ component: "svc",
291
+ env: "floci",
292
+ digest: "sha256:abc",
293
+ gitSha: "sha1",
294
+ runId: "run-1",
295
+ timestamp: "2026-01-01T00:00:00.000Z",
296
+ actor: "alice",
297
+ }],
298
+ malformed: 0,
299
+ });
300
+ buildMock.mockResolvedValue(makeBuildResult({ aws: ["svc"] }));
301
+ loadChantConfigMock.mockResolvedValue({
302
+ config: { environments: [{ name: "floci", endpoint: "http://localhost:4566" }] },
303
+ });
304
+
305
+ let seenDuringDescribe: string | undefined;
306
+ const plugins: LexiconPlugin[] = [
307
+ createMockPlugin({
308
+ name: "aws",
309
+ describeResources: async () => {
310
+ seenDuringDescribe = process.env.AWS_ENDPOINT_URL;
311
+ return { svc: meta() };
312
+ },
313
+ }),
314
+ ];
315
+
316
+ const ctx = {
317
+ args: makeArgs({ extraPositional: "floci", live: true, json: true }),
318
+ plugins,
319
+ serializers: plugins.map((p) => p.serializer),
320
+ };
321
+ const exit = await runComponentsStatus(ctx);
322
+ expect(exit).toBe(0);
323
+ expect(seenDuringDescribe).toBe("http://localhost:4566");
324
+ expect(process.env.AWS_ENDPOINT_URL).toBeUndefined();
325
+ expect(stderrBuf.join("\n")).toMatch(/environment "floci" declares endpoint http:\/\/localhost:4566/);
326
+ });
327
+ });
328
+
270
329
  test("--live flags an unrecorded live+owned component", async () => {
271
330
  readReleaseLedgerMock.mockResolvedValue({ records: [], malformed: 0 });
272
331
  listReleaseEnvironmentsMock.mockResolvedValue(["prod"]);
@@ -40,11 +40,13 @@ import { buildLedgerEntries, componentBomSummary, type BuildLedgerEntry } from "
40
40
  import { findBuildManifestByArtifactDigest } from "../../lifecycle/build-ledger-store";
41
41
  import type { ComponentBomSummary } from "../../lifecycle/build-ledger";
42
42
  import { loadChantConfig } from "../../config";
43
+ import { applyLiveEndpoint } from "../../live-endpoint";
43
44
  import { build } from "../../build";
44
45
  import { discoverComponents } from "../../components/discover";
45
46
  import { formatError, formatWarning, formatSuccess, formatBold } from "../format";
46
47
  import type { CommandContext } from "../registry";
47
- import type { ResourceMetadata, LexiconPlugin } from "../../lexicon";
48
+ import type { LexiconPlugin } from "../../lexicon";
49
+ import { normalizeObservation, unobservedAll, type NormalizedObservation } from "../../observation";
48
50
  import type { Phase, Component } from "../../components/component";
49
51
 
50
52
  /**
@@ -177,7 +179,10 @@ interface StatusJsonRow {
177
179
  } | null;
178
180
  reconciliation: string;
179
181
  detail: string;
182
+ /** Present only when live state was requested AND read (#1089) — see `unobserved`. */
180
183
  live?: boolean;
184
+ /** Why live state could not be read for this component (#1089). Mutually exclusive with `live`. */
185
+ unobserved?: { reason: string; detail?: string };
181
186
  stack?: { name: string; status?: string; healthy?: boolean };
182
187
  }
183
188
 
@@ -248,7 +253,20 @@ async function observeComponentStacks(
248
253
  stacks.map((stack) => observer.describeStackStatus!({ environment, stack }).catch(() => null)),
249
254
  );
250
255
  const determinate = observed.filter((o): o is NonNullable<typeof o> => o !== null);
251
- if (determinate.length === 0) continue;
256
+ if (determinate.length === 0) {
257
+ // Every stack read came back indeterminate (the lexicon's own "I cannot
258
+ // tell" — see describeStackStatus). Record the hole rather than dropping
259
+ // the component, which would leave a recorded component reading `stale`
260
+ // (#1089).
261
+ evidence.set(name, {
262
+ live: false,
263
+ unobserved: {
264
+ reason: "read-failed",
265
+ detail: `no determinate status for stack(s): ${stacks.join(", ")}`,
266
+ },
267
+ });
268
+ continue;
269
+ }
252
270
  const present = determinate.every((o) => o.present);
253
271
  // Surface the (first present, else first) stack's raw status for a richer
254
272
  // palette than the reconciliation verdict. One cfn-deploy stack per component
@@ -310,42 +328,66 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
310
328
 
311
329
  let liveEvidence;
312
330
  if (args.live) {
313
- const targetSerializers = serializers;
314
- const buildResult = await build(resolve(args.src ?? config.sourceDir ?? "."), targetSerializers);
315
- const merged: { env: string; entries: import("../../lifecycle/change-set").ChangeSetEntry[] } = { env: environment, entries: [] };
316
- for (const plugin of plugins) {
317
- if (!plugin.describeResources) continue;
318
- const declared = new Set<string>();
319
- const entities = new Map<string, { entityType: string; props: Record<string, unknown> }>();
320
- for (const [name, entity] of buildResult.entities) {
321
- if (entity.lexicon === plugin.name) {
322
- declared.add(name);
323
- entities.set(name, {
324
- entityType: entity.entityType,
325
- props: ("props" in entity && entity.props != null ? entity.props : {}) as Record<string, unknown>,
326
- });
331
+ // #1166 an environment can declare its own endpoint (a local emulator
332
+ // like Floci), applied here unless the ambient shell already set it, so
333
+ // `--live` status doesn't silently read the wrong account per environment.
334
+ const endpointResult = applyLiveEndpoint(
335
+ config.environments,
336
+ environment,
337
+ plugins.filter((p) => p.describeResources || p.describeStackStatus).map((p) => p.name),
338
+ );
339
+ if (endpointResult.notice) console.error(formatWarning({ message: endpointResult.notice }));
340
+ try {
341
+ const targetSerializers = serializers;
342
+ const buildResult = await build(resolve(args.src ?? config.sourceDir ?? "."), targetSerializers);
343
+ const merged: { env: string; entries: import("../../lifecycle/change-set").ChangeSetEntry[] } = { env: environment, entries: [] };
344
+ for (const plugin of plugins) {
345
+ if (!plugin.describeResources) continue;
346
+ const declared = new Set<string>();
347
+ const entities = new Map<string, { entityType: string; props: Record<string, unknown> }>();
348
+ for (const [name, entity] of buildResult.entities) {
349
+ if (entity.lexicon === plugin.name) {
350
+ declared.add(name);
351
+ entities.set(name, {
352
+ entityType: entity.entityType,
353
+ props: ("props" in entity && entity.props != null ? entity.props : {}) as Record<string, unknown>,
354
+ });
355
+ }
327
356
  }
357
+ let observed: NormalizedObservation;
358
+ try {
359
+ observed = normalizeObservation(
360
+ await plugin.describeResources({ environment, buildOutput: "", entityNames: Array.from(declared), entities }),
361
+ );
362
+ } catch (err) {
363
+ // A failed read is not an empty environment (#1089): mark every
364
+ // declared entity NOT-OBSERVED so the status rows say "unknown"
365
+ // rather than "stale" (recorded, and nothing live).
366
+ const message = err instanceof Error ? err.message : String(err);
367
+ console.error(formatWarning({ message: `${plugin.name}: describeResources failed — ${message} (components in this lexicon report unknown, not stale)` }));
368
+ observed = { resources: {}, unobserved: unobservedAll(declared, "read-failed", message, entities) };
369
+ }
370
+ const cs = buildChangeSet(environment, {
371
+ declared,
372
+ observedNow: observed.resources,
373
+ observedThen: undefined,
374
+ unobserved: observed.unobserved,
375
+ });
376
+ merged.entries.push(...cs.entries);
328
377
  }
329
- let observedNow: Record<string, ResourceMetadata>;
330
- try {
331
- observedNow = await plugin.describeResources({ environment, buildOutput: "", entityNames: Array.from(declared), entities });
332
- } catch (err) {
333
- console.error(formatWarning({ message: `${plugin.name}: describeResources failed — ${err instanceof Error ? err.message : String(err)}` }));
334
- continue;
335
- }
336
- const cs = buildChangeSet(environment, { declared, observedNow, observedThen: undefined });
337
- merged.entries.push(...cs.entries);
338
- }
339
- liveEvidence = liveEvidenceFromChangeSet(merged, liveNameMapping);
378
+ liveEvidence = liveEvidenceFromChangeSet(merged, liveNameMapping);
340
379
 
341
- // Multi-stack component projects (each component owns its own stack) are
342
- // invisible to the entity-keyed, single-stack `describeResources` above —
343
- // observe each component's own cfn-deploy stack directly and overlay it as
344
- // the authoritative presence signal (#57).
345
- const stackObserver = plugins.find((p) => p.describeStackStatus);
346
- if (stackObserver) {
347
- const stackEvidence = await observeComponentStacks(discovery.components, stackObserver, environment);
348
- liveEvidence = mergeLiveEvidence(liveEvidence, stackEvidence);
380
+ // Multi-stack component projects (each component owns its own stack) are
381
+ // invisible to the entity-keyed, single-stack `describeResources` above —
382
+ // observe each component's own cfn-deploy stack directly and overlay it as
383
+ // the authoritative presence signal (#57).
384
+ const stackObserver = plugins.find((p) => p.describeStackStatus);
385
+ if (stackObserver) {
386
+ const stackEvidence = await observeComponentStacks(discovery.components, stackObserver, environment);
387
+ liveEvidence = mergeLiveEvidence(liveEvidence, stackEvidence);
388
+ }
389
+ } finally {
390
+ endpointResult.restore();
349
391
  }
350
392
  }
351
393
 
@@ -420,6 +462,7 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
420
462
  reconciliation: row.reconciliation,
421
463
  detail: row.detail,
422
464
  ...(row.live !== undefined ? { live: row.live } : {}),
465
+ ...(row.unobserved ? { unobserved: row.unobserved } : {}),
423
466
  ...(row.stack ? { stack: row.stack } : {}),
424
467
  });
425
468
  }