@intentius/chant 0.41.1 → 0.41.3

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.
@@ -82,4 +82,20 @@ export interface CapabilityPlugin {
82
82
  * descriptive error rather than registered half-working.
83
83
  */
84
84
  export declare function isCapabilityPlugin(value: unknown): value is CapabilityPlugin;
85
+ /**
86
+ * The calling module's own package version, read from the nearest
87
+ * `package.json` above it (chant #1505).
88
+ *
89
+ * `CapabilityPlugin.version` documents itself as the plugin package's semver,
90
+ * but chant's packages release in lockstep, so a hardcoded literal goes stale
91
+ * on every `just release` — the aws plugin shipped `"1.0.0"` from the day it
92
+ * was extracted from the starter set (#681), and the k8s plugin's authoring-
93
+ * time `"0.41.0"` was stale one release later. Walking up from the module's
94
+ * own URL survives both the `src/` (development condition) and `dist/`
95
+ * layouts, and an npm install, without any build-time stamping.
96
+ *
97
+ * Returns `"0.0.0"` when no versioned `package.json` is found — a visible
98
+ * sentinel rather than a guess; nothing gates on the field.
99
+ */
100
+ export declare function ownPackageVersion(moduleUrl: string): string;
85
101
  //# sourceMappingURL=capability-plugin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"capability-plugin.d.ts","sourceRoot":"","sources":["../../src/components/capability-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IACjC,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,gKAAgK;IAChK,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yIAAyI;IACzI,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,gBAAgB;IAE/B,uFAAuF;IACvF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB,mGAAmG;IACnG,YAAY,IAAI,KAAK,CAAC,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;IAGlD,qIAAqI;IACrI,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAE/B,gLAAgL;IAChL,QAAQ,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IAE/C,2FAA2F;IAC3F,IAAI,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAa5E"}
1
+ {"version":3,"file":"capability-plugin.d.ts","sourceRoot":"","sources":["../../src/components/capability-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IACjC,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,gKAAgK;IAChK,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yIAAyI;IACzI,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,gBAAgB;IAE/B,uFAAuF;IACvF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB,mGAAmG;IACnG,YAAY,IAAI,KAAK,CAAC,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;IAGlD,qIAAqI;IACrI,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAE/B,gLAAgL;IAChL,QAAQ,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IAE/C,2FAA2F;IAC3F,IAAI,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAa5E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAa3D"}
@@ -72,6 +72,11 @@ export interface ComponentGraphResult {
72
72
  * so a renderer can deep-link a component node to source (`chant graph
73
73
  * --components --format ir` sets `sourceLoc` from this). */
74
74
  files?: Record<string, string>;
75
+ /** Component name → the live resource names it owns (#1491): the declared
76
+ * `liveNames`, or `[name]` when undeclared — the identity-join fallback the
77
+ * `Component` contract already specifies. This is what lets a consumer join
78
+ * the component DAG to the resource graph without guessing at kinds. */
79
+ liveNames?: Record<string, string[]>;
75
80
  error?: string;
76
81
  }
77
82
  /** Compute the components' dependency graph under `path`, for `chant graph --components`. */
@@ -1 +1 @@
1
- {"version":3,"file":"cli-support.d.ts","sourceRoot":"","sources":["../../src/components/cli-support.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli-support.d.ts","sourceRoot":"","sources":["../../src/components/cli-support.ts"],"names":[],"mappings":"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"}
@@ -25,7 +25,7 @@
25
25
  * from verb modules, so neither needed to change for this migration —
26
26
  * the compatibility guarantee #559 asks for.
27
27
  */
28
- import type { CapabilityPlugin } from "./capability-plugin.js";
28
+ import { type CapabilityPlugin } from "./capability-plugin.js";
29
29
  /**
30
30
  * Every `kind` in the starter verb set, grouped by family, per epic #551 and
31
31
  * docs/components/capabilities.mdx. Source of truth (moved here from
@@ -1 +1 @@
1
- {"version":3,"file":"starter-plugin.d.ts","sourceRoot":"","sources":["../../src/components/starter-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAkBH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB;;;;;;;CAaxB,CAAC;AA2BX;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,gBAKrC,CAAC"}
1
+ {"version":3,"file":"starter-plugin.d.ts","sourceRoot":"","sources":["../../src/components/starter-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAkBH,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE/E;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB;;;;;;;CAaxB,CAAC;AA2BX;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,gBAOrC,CAAC"}
@@ -18,4 +18,20 @@ export declare const DETAIL: {
18
18
  };
19
19
  /** Apply a detail tier to the base (declarable-level) IR. */
20
20
  export declare function applyDetail(ir: GraphIR, level: DetailLevel): GraphIR;
21
+ /**
22
+ * chant #1489 — the message to print when `--detail 3` changed nothing.
23
+ *
24
+ * T3's only addition over T2 is the producer attribute on `$ref`-derived
25
+ * edges. A graph whose resources link by name or label convention instead of
26
+ * attribute references (the k8s lexicon end to end) has nothing to annotate,
27
+ * so levels 2 and 3 come out byte-identical — which reads as a broken dial
28
+ * from any consumer stepping through the levels (behold#131 was filed over
29
+ * exactly this). Accepting a value that changes nothing without saying so is
30
+ * the bug; this names it.
31
+ *
32
+ * Returns the warning text, or undefined when detail 3 did add something.
33
+ * Callers print it through their own sink; the compare is over the two IRs
34
+ * the caller already holds, so this stays a pure function.
35
+ */
36
+ export declare function detailInertNotice(base: GraphIR, detailed: GraphIR): string | undefined;
21
37
  //# sourceMappingURL=graph-detail.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"graph-detail.d.ts","sourceRoot":"","sources":["../src/graph-detail.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAkB,MAAM,YAAY,CAAC;AAE1D;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAExC,eAAO,MAAM,MAAM;;;;;CAKT,CAAC;AAEX,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAYpE"}
1
+ {"version":3,"file":"graph-detail.d.ts","sourceRoot":"","sources":["../src/graph-detail.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAkB,MAAM,YAAY,CAAC;AAE1D;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAExC,eAAO,MAAM,MAAM;;;;;CAKT,CAAC;AAEX,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAYpE;AA+HD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAWtF"}
@@ -30,11 +30,13 @@ export interface K8sConfigShape {
30
30
  profiles?: Record<string, K8sClusterProfile>;
31
31
  }
32
32
  /**
33
- * Thrown when an environment declares a cluster binding but the ambient
34
- * kubectl context disagrees with it. Refusing here instead of silently
35
- * observing whichever cluster is ambient — is the fix for #1100: a
36
- * wrong-cluster read reports every declared resource as missing, which #1089
37
- * then classifies as a confident (and wrong) list of `create` actions.
33
+ * Historically thrown when an environment declared a cluster binding but the
34
+ * ambient kubectl context disagreed with it (#1100/#1155). As of #1488 the
35
+ * resolver no longer polices ambient at all a declared binding is USED, not
36
+ * checked so {@link resolveClusterTarget} never throws this anymore. The
37
+ * class stays exported because the k8s lexicon's read/write paths still catch
38
+ * it (converting a refusal into NOT-OBSERVED), and a custom connector may
39
+ * still throw it to get that classification.
38
40
  */
39
41
  export declare class ClusterBindingMismatchError extends Error {
40
42
  readonly environment: string;
@@ -57,8 +59,11 @@ export interface ResolvedClusterTarget {
57
59
  * `kubectl config current-context`; the k8s lexicon's typed API client
58
60
  * (chant #1074) supplies one that reads the parsed kubeconfig instead, so a
59
61
  * client that never needs the `kubectl` binary does not acquire a dependency
60
- * on it just to check the binding. Both answer the same question, so the
61
- * refusal semantics below are identical either way.
62
+ * on it just to check the binding.
63
+ *
64
+ * As of #1488 the resolver itself no longer reads ambient (a declared binding
65
+ * is used directly), so this hook only matters to callers that surface the
66
+ * ambient context in their own messages.
62
67
  */
63
68
  export type AmbientContextReader = () => Promise<string | undefined>;
64
69
  /** Options for {@link resolveClusterTarget}. */
@@ -79,13 +84,16 @@ export interface ResolveClusterTargetOptions {
79
84
  * - No binding declared: returns `{ source: "ambient" }` — unchanged
80
85
  * behavior — but logs a visible warning identifying the caller and
81
86
  * environment, so the fallback is never silent (#1100 acceptance).
82
- * - Binding declared and the ambient context agrees (or ambient can't be
83
- * determined): returns `{ context: bound, source: "bound" }`. Callers
84
- * should pass this context explicitly on every kubectl invocation rather
85
- * than relying on it also being ambient.
86
- * - Binding declared and the ambient context disagrees: throws
87
- * {@link ClusterBindingMismatchError} a loud refusal instead of quietly
88
- * reading the wrong cluster.
87
+ * - Binding declared: returns `{ context: bound, source: "bound" }`,
88
+ * regardless of what is ambient (#1488). The declared binding is the
89
+ * selection, not a check: callers pass this context explicitly on every
90
+ * read and write, so whichever cluster `kubectl` happens to be pointed at
91
+ * is irrelevant. Any k3d cluster another project creates mid-session used
92
+ * to steal the ambient context and turn a healthy estate grey with
93
+ * `read-failed` as the only explanation; now it cannot. A bound context
94
+ * that is absent from the kubeconfig fails downstream in the typed client
95
+ * with an error naming the context and the `k8s.profiles.<env>.context`
96
+ * binding — never by falling back to ambient.
89
97
  */
90
98
  export declare function resolveClusterTarget(config: Record<string, unknown>, environment: string, lexiconName: string, options?: ResolveClusterTargetOptions): Promise<ResolvedClusterTarget>;
91
99
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"kubectl-context.d.ts","sourceRoot":"","sources":["../src/kubectl-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAItD,wFAAwF;AACxF,MAAM,WAAW,iBAAiB;IAChC,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wFAAwF;AACxF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC9C;AAED;;;;;;GAMG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;aAElC,WAAW,EAAE,MAAM;aACnB,eAAe,EAAE,MAAM;aACvB,cAAc,EAAE,MAAM;gBAFtB,WAAW,EAAE,MAAM,EACnB,eAAe,EAAE,MAAM,EACvB,cAAc,EAAE,MAAM;CAWzC;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;AAarE,gDAAgD;AAChD,MAAM,WAAW,2BAA2B;IAC1C;;;OAGG;IACH,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,qBAAqB,CAAC,CAsBhC;AAID;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB;AAC5B,kFAAkF;AAChF;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE;AACpB,4DAA4D;GAC1D;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAmBrE;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,kBAAkB,CAuCvE"}
1
+ {"version":3,"file":"kubectl-context.d.ts","sourceRoot":"","sources":["../src/kubectl-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAItD,wFAAwF;AACxF,MAAM,WAAW,iBAAiB;IAChC,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wFAAwF;AACxF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC9C;AAED;;;;;;;;GAQG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;aAElC,WAAW,EAAE,MAAM;aACnB,eAAe,EAAE,MAAM;aACvB,cAAc,EAAE,MAAM;gBAFtB,WAAW,EAAE,MAAM,EACnB,eAAe,EAAE,MAAM,EACvB,cAAc,EAAE,MAAM;CAWzC;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;CAC7B;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;AAarE,gDAAgD;AAChD,MAAM,WAAW,2BAA2B;IAC1C;;;OAGG;IACH,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,qBAAqB,CAAC,CAiBhC;AAID;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB;AAC5B,kFAAkF;AAChF;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE;AACpB,4DAA4D;GAC1D;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAmBrE;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,kBAAkB,CAuCvE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.41.1",
3
+ "version": "0.41.3",
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",
@@ -9,7 +9,7 @@ import { observeResources } from "../../lifecycle/observe";
9
9
  import { replaySnapshots, hasSnapshot } from "../../lifecycle/replay";
10
10
  import { loadChantConfig, environmentNames, loadChantConfigUpward, type ChantConfig } from "../../config";
11
11
  import { applyLiveEndpoint } from "../../live-endpoint";
12
- import { applyDetail, type DetailLevel } from "../../graph-detail";
12
+ import { applyDetail, detailInertNotice, type DetailLevel } from "../../graph-detail";
13
13
  import { applyLens, parseLens } from "../../graph-lens";
14
14
  import { toMermaid } from "../../graph-mermaid";
15
15
  import { toDot } from "../../graph-dot";
@@ -343,7 +343,16 @@ async function runGraphLive(
343
343
  return 1;
344
344
  }
345
345
  }
346
- ir = applyDetail(ir, (args.detail ?? 2) as DetailLevel);
346
+ {
347
+ const base = ir;
348
+ ir = applyDetail(ir, (args.detail ?? 2) as DetailLevel);
349
+ // #1489 — a --detail 3 that changed nothing says so instead of silently
350
+ // emitting the same bytes as --detail 2.
351
+ if (args.detail === 3) {
352
+ const notice = detailInertNotice(base, ir);
353
+ if (notice) console.error(formatWarning({ message: notice }));
354
+ }
355
+ }
347
356
 
348
357
  // Containment grouping (#779) → boundary boxes. Built after lens/detail and
349
358
  // filtered to surviving nodes, so a lens can't leave dangling group refs.
@@ -452,7 +461,10 @@ async function runComponentGraphView(
452
461
  id: name,
453
462
  kind: "Component",
454
463
  lexicon: "chant",
455
- attrs: { wave: waveOf.get(name) ?? null },
464
+ // `liveNames` (#1491): the resources this component owns — declared, or
465
+ // the contract's `[name]` identity fallback — so a consumer can join
466
+ // the component DAG to the resource graph instead of guessing at kinds.
467
+ attrs: { wave: waveOf.get(name) ?? null, liveNames: graph.liveNames?.[name] ?? [name] },
456
468
  // Deep-link the node to its `*.component.ts` (behold's inspect panel).
457
469
  ...(graph.files?.[name] ? { sourceLoc: { file: graph.files[name] } } : {}),
458
470
  })),
@@ -558,7 +570,15 @@ async function runGraphView(
558
570
  return 1;
559
571
  }
560
572
  }
561
- ir = applyDetail(ir, level as DetailLevel);
573
+ {
574
+ const base = ir;
575
+ ir = applyDetail(ir, level as DetailLevel);
576
+ // #1489 — same say-so as the live path: an inert --detail 3 names itself.
577
+ if (level === 3) {
578
+ const notice = detailInertNotice(base, ir);
579
+ if (notice) console.error(formatWarning({ message: notice }));
580
+ }
581
+ }
562
582
  return emitIr(ir, ctx, format);
563
583
  }
564
584
 
@@ -233,3 +233,24 @@ describe("MalformedCapabilityPluginError", () => {
233
233
  expect(err.name).toBe("MalformedCapabilityPluginError");
234
234
  });
235
235
  });
236
+
237
+ // #1505 — plugin versions track the lockstep release instead of a literal that
238
+ // goes stale on every `just release` (aws shipped "1.0.0" from its extraction;
239
+ // the k8s plugin's authoring-time literal was stale one release later).
240
+ describe("ownPackageVersion (#1505)", () => {
241
+ test("resolves this module's own package version — core's package.json, exactly", async () => {
242
+ const { ownPackageVersion } = await import("./capability-plugin");
243
+ const { readFileSync } = await import("node:fs");
244
+ const { version } = JSON.parse(
245
+ readFileSync(new URL("../../package.json", import.meta.url), "utf-8"),
246
+ ) as { version: string };
247
+ expect(ownPackageVersion(import.meta.url)).toBe(version);
248
+ });
249
+
250
+ test("the starter plugin reports that version, not a literal", async () => {
251
+ const { starterCapabilityPlugin } = await import("./starter-plugin");
252
+ const { ownPackageVersion } = await import("./capability-plugin");
253
+ expect(starterCapabilityPlugin.version).toBe(ownPackageVersion(import.meta.url));
254
+ expect(starterCapabilityPlugin.version).not.toBe("1.0.0");
255
+ });
256
+ });
@@ -28,6 +28,9 @@
28
28
  * migration path and the "no behavior change" guarantee.
29
29
  */
30
30
 
31
+ import { readFileSync } from "node:fs";
32
+ import { dirname, join } from "node:path";
33
+ import { fileURLToPath } from "node:url";
31
34
  import type { Capability } from "./capability";
32
35
 
33
36
  /**
@@ -106,3 +109,33 @@ export function isCapabilityPlugin(value: unknown): value is CapabilityPlugin {
106
109
  const obj = value as Record<string, unknown>;
107
110
  return typeof obj.capabilities === "function";
108
111
  }
112
+
113
+ /**
114
+ * The calling module's own package version, read from the nearest
115
+ * `package.json` above it (chant #1505).
116
+ *
117
+ * `CapabilityPlugin.version` documents itself as the plugin package's semver,
118
+ * but chant's packages release in lockstep, so a hardcoded literal goes stale
119
+ * on every `just release` — the aws plugin shipped `"1.0.0"` from the day it
120
+ * was extracted from the starter set (#681), and the k8s plugin's authoring-
121
+ * time `"0.41.0"` was stale one release later. Walking up from the module's
122
+ * own URL survives both the `src/` (development condition) and `dist/`
123
+ * layouts, and an npm install, without any build-time stamping.
124
+ *
125
+ * Returns `"0.0.0"` when no versioned `package.json` is found — a visible
126
+ * sentinel rather than a guess; nothing gates on the field.
127
+ */
128
+ export function ownPackageVersion(moduleUrl: string): string {
129
+ let dir = dirname(fileURLToPath(moduleUrl));
130
+ for (;;) {
131
+ try {
132
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8")) as { version?: unknown };
133
+ if (typeof pkg.version === "string") return pkg.version;
134
+ } catch {
135
+ // No package.json here (or unreadable) — keep walking up.
136
+ }
137
+ const parent = dirname(dir);
138
+ if (parent === dir) return "0.0.0";
139
+ dir = parent;
140
+ }
141
+ }
@@ -275,6 +275,23 @@ describe("computeComponentGraph", () => {
275
275
  expect(result.success).toBe(false);
276
276
  expect(result.error).toMatch(/unknown component|ghost/i);
277
277
  });
278
+
279
+ test("carries each component's liveNames, with the [name] identity fallback (#1491)", async () => {
280
+ await writeFile(
281
+ join(testDir, "pg.component.ts"),
282
+ `export const pg = { name: "postgres", liveNames: ["pgDeployment", "pgService", "pgClaim"], dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "shell", reason: "test" }] }] };`,
283
+ );
284
+ await writeFile(
285
+ join(testDir, "plain.component.ts"),
286
+ `export const plain = { name: "plain", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "shell", reason: "test" }] }] };`,
287
+ );
288
+
289
+ const result = await computeComponentGraph(testDir);
290
+
291
+ expect(result.success).toBe(true);
292
+ expect(result.liveNames?.postgres).toEqual(["pgDeployment", "pgService", "pgClaim"]);
293
+ expect(result.liveNames?.plain).toEqual(["plain"]);
294
+ });
278
295
  });
279
296
 
280
297
  // ── runComponents (#585) ─────────────────────────────────────────────────────
@@ -136,6 +136,11 @@ export interface ComponentGraphResult {
136
136
  * so a renderer can deep-link a component node to source (`chant graph
137
137
  * --components --format ir` sets `sourceLoc` from this). */
138
138
  files?: Record<string, string>;
139
+ /** Component name → the live resource names it owns (#1491): the declared
140
+ * `liveNames`, or `[name]` when undeclared — the identity-join fallback the
141
+ * `Component` contract already specifies. This is what lets a consumer join
142
+ * the component DAG to the resource graph without guessing at kinds. */
143
+ liveNames?: Record<string, string[]>;
139
144
  error?: string;
140
145
  }
141
146
 
@@ -154,8 +159,13 @@ export async function computeComponentGraph(path: string, sandbox?: boolean): Pr
154
159
 
155
160
  // component name → its declaring file, relative to `path`, for node deep-links.
156
161
  const files: Record<string, string> = {};
162
+ // component name → owned live resource names (#1491), declared or the
163
+ // contract's identity fallback.
164
+ const liveNames: Record<string, string[]> = {};
157
165
  for (const [name, discovered] of result.components) {
158
166
  files[name] = relative(path, discovered.filePath);
167
+ const declared = discovered.component.liveNames;
168
+ liveNames[name] = declared && declared.length > 0 ? [...declared] : [name];
159
169
  }
160
170
 
161
171
  try {
@@ -164,7 +174,7 @@ export async function computeComponentGraph(path: string, sandbox?: boolean): Pr
164
174
  for (const c of driverComponents) {
165
175
  for (const dep of c.dependsOn ?? []) edges.push({ from: c.name, to: dep });
166
176
  }
167
- return { success: true, order, waves, edges, files };
177
+ return { success: true, order, waves, edges, files, liveNames };
168
178
  } catch (err) {
169
179
  if (err instanceof UnknownDependencyError || err instanceof DependencyCycleError) {
170
180
  return { success: false, order: [], waves: [], edges: [], error: err.message };
@@ -42,7 +42,7 @@ import {
42
42
  healthGateCapability,
43
43
  shellCapability,
44
44
  } from "./verbs/index";
45
- import type { CapabilityPlugin } from "./capability-plugin";
45
+ import { ownPackageVersion, type CapabilityPlugin } from "./capability-plugin";
46
46
 
47
47
  /**
48
48
  * Every `kind` in the starter verb set, grouped by family, per epic #551 and
@@ -99,7 +99,9 @@ function starterCapabilities(): Array<Capability<never, unknown>> {
99
99
  */
100
100
  export const starterCapabilityPlugin: CapabilityPlugin = {
101
101
  name: "starter",
102
- version: "1.0.0",
102
+ // The core package's own version (#1505) — lockstep releases bump it, so a
103
+ // literal here would go stale every `just release`.
104
+ version: ownPackageVersion(import.meta.url),
103
105
  capabilities: starterCapabilities,
104
106
  families: () => STARTER_VERB_FAMILIES,
105
107
  };
@@ -1,5 +1,5 @@
1
1
  import { describe, test, expect } from "vitest";
2
- import { applyDetail, DETAIL } from "./graph-detail";
2
+ import { applyDetail, DETAIL, detailInertNotice } from "./graph-detail";
3
3
  import type { GraphIR } from "./graph-ir";
4
4
 
5
5
  // A small graph: a gcp vpc/subnet pair plus a k8s namespace and deployment that
@@ -90,3 +90,39 @@ describe("applyDetail", () => {
90
90
  });
91
91
  });
92
92
  });
93
+
94
+ // #1489 — an inert --detail 3 names itself instead of silently emitting the
95
+ // same bytes as --detail 2 (the k8s lexicon links by name/label convention, so
96
+ // its graphs never have a producer attribute to annotate).
97
+ describe("detailInertNotice (#1489)", () => {
98
+ test("detail 3 that added toAttr annotations: no notice", () => {
99
+ const detailed = applyDetail(base, DETAIL.ATTRIBUTES);
100
+ expect(detailInertNotice(base, detailed)).toBeUndefined();
101
+ });
102
+
103
+ test("edges without producer attributes: notice names the convention-linking cause", () => {
104
+ const labelLinked: GraphIR = {
105
+ nodes: [
106
+ { id: "svc", kind: "Service", lexicon: "k8s", attrs: { spec: { selector: { app: "web" } } } },
107
+ { id: "web", kind: "Deployment", lexicon: "k8s", attrs: { metadata: { labels: { app: "web" } } } },
108
+ ],
109
+ edges: [{ from: "svc", to: "web", kind: "ref", viaAttr: "spec.selector" }],
110
+ groups: {},
111
+ };
112
+ const detailed = applyDetail(labelLinked, DETAIL.ATTRIBUTES);
113
+ const notice = detailInertNotice(labelLinked, detailed);
114
+ expect(notice).toContain("--detail 3");
115
+ expect(notice).toContain("1 edge(s)");
116
+ expect(notice).toContain("identical to --detail 2");
117
+ });
118
+
119
+ test("no edges at all: notice says so", () => {
120
+ const edgeless: GraphIR = {
121
+ nodes: [{ id: "ns", kind: "Namespace", lexicon: "k8s", attrs: {} }],
122
+ edges: [],
123
+ groups: {},
124
+ };
125
+ const detailed = applyDetail(edgeless, DETAIL.ATTRIBUTES);
126
+ expect(detailInertNotice(edgeless, detailed)).toContain("no edges at all");
127
+ });
128
+ });
@@ -158,3 +158,31 @@ function findRefAttr(attrs: Record<string, unknown>, producer: string): string |
158
158
  visit(attrs);
159
159
  return found;
160
160
  }
161
+
162
+ /**
163
+ * chant #1489 — the message to print when `--detail 3` changed nothing.
164
+ *
165
+ * T3's only addition over T2 is the producer attribute on `$ref`-derived
166
+ * edges. A graph whose resources link by name or label convention instead of
167
+ * attribute references (the k8s lexicon end to end) has nothing to annotate,
168
+ * so levels 2 and 3 come out byte-identical — which reads as a broken dial
169
+ * from any consumer stepping through the levels (behold#131 was filed over
170
+ * exactly this). Accepting a value that changes nothing without saying so is
171
+ * the bug; this names it.
172
+ *
173
+ * Returns the warning text, or undefined when detail 3 did add something.
174
+ * Callers print it through their own sink; the compare is over the two IRs
175
+ * the caller already holds, so this stays a pure function.
176
+ */
177
+ export function detailInertNotice(base: GraphIR, detailed: GraphIR): string | undefined {
178
+ if (JSON.stringify(detailed) !== JSON.stringify(base)) return undefined;
179
+ const edges = base.edges.length;
180
+ const why =
181
+ edges === 0
182
+ ? "this graph has no edges at all"
183
+ : `none of this graph's ${edges} edge(s) reference a producer attribute — they link by name or label convention`;
184
+ return (
185
+ `--detail 3 adds the producer attribute to reference edges, but ${why}, ` +
186
+ `so the output is identical to --detail 2.`
187
+ );
188
+ }
@@ -16,7 +16,7 @@ vi.mock("node:child_process", async () => {
16
16
 
17
17
  const { resolveClusterTarget, ClusterBindingMismatchError } = await import("./kubectl-context");
18
18
 
19
- describe("resolveClusterTarget (chant #1100)", () => {
19
+ describe("resolveClusterTarget (chant #1100, #1488)", () => {
20
20
  beforeEach(() => {
21
21
  execMock.mockReset();
22
22
  });
@@ -34,9 +34,7 @@ describe("resolveClusterTarget (chant #1100)", () => {
34
34
  warnSpy.mockRestore();
35
35
  });
36
36
 
37
- test("bound and ambient context matches: returns the bound context", async () => {
38
- execMock.mockResolvedValue({ stdout: "prod-eks\n", stderr: "" });
39
-
37
+ test("bound: returns the bound context without ever probing the ambient one (#1488)", async () => {
40
38
  const target = await resolveClusterTarget(
41
39
  { k8s: { profiles: { prod: { context: "prod-eks" } } } },
42
40
  "prod",
@@ -44,11 +42,14 @@ describe("resolveClusterTarget (chant #1100)", () => {
44
42
  );
45
43
 
46
44
  expect(target).toEqual({ context: "prod-eks", source: "bound" });
47
- expect(execMock).toHaveBeenCalledWith(expect.stringContaining("current-context"));
45
+ expect(execMock).not.toHaveBeenCalled();
48
46
  });
49
47
 
50
- test("bound and ambient context cannot be determined: proceeds with the bound context anyway", async () => {
51
- execMock.mockRejectedValue(new Error("no kubeconfig"));
48
+ test("bound while a different context is ambient: the binding wins, no refusal (#1488)", async () => {
49
+ // Ambient says staging-eks; the declared binding must be used regardless.
50
+ // Before #1488 this threw ClusterBindingMismatchError, which turned a
51
+ // healthy estate grey the moment any other project switched the context.
52
+ execMock.mockResolvedValue({ stdout: "staging-eks\n", stderr: "" });
52
53
 
53
54
  const target = await resolveClusterTarget(
54
55
  { k8s: { profiles: { prod: { context: "prod-eks" } } } },
@@ -59,23 +60,14 @@ describe("resolveClusterTarget (chant #1100)", () => {
59
60
  expect(target).toEqual({ context: "prod-eks", source: "bound" });
60
61
  });
61
62
 
62
- test("bound and ambient context mismatches: refuses loudly, naming env/expected/ambient", async () => {
63
- execMock.mockResolvedValue({ stdout: "staging-eks\n", stderr: "" });
64
-
65
- const err: unknown = await resolveClusterTarget(
66
- { k8s: { profiles: { prod: { context: "prod-eks" } } } },
67
- "prod",
68
- "k8s",
69
- ).catch((e: unknown) => e);
70
-
71
- expect(err).toBeInstanceOf(ClusterBindingMismatchError);
72
- const mismatch = err as InstanceType<typeof ClusterBindingMismatchError>;
73
- expect(mismatch.environment).toBe("prod");
74
- expect(mismatch.expectedContext).toBe("prod-eks");
75
- expect(mismatch.ambientContext).toBe("staging-eks");
76
- expect(mismatch.message).toContain('environment "prod"');
77
- expect(mismatch.message).toContain('"prod-eks"');
78
- expect(mismatch.message).toContain('"staging-eks"');
63
+ test("ClusterBindingMismatchError still constructs and names all three parts (catchers rely on it)", () => {
64
+ const err = new ClusterBindingMismatchError("prod", "prod-eks", "staging-eks");
65
+ expect(err.environment).toBe("prod");
66
+ expect(err.expectedContext).toBe("prod-eks");
67
+ expect(err.ambientContext).toBe("staging-eks");
68
+ expect(err.message).toContain('environment "prod"');
69
+ expect(err.message).toContain('"prod-eks"');
70
+ expect(err.message).toContain('"staging-eks"');
79
71
  });
80
72
 
81
73
  test("bound for a different environment than the one requested: treated as unbound for this environment", async () => {
@@ -38,11 +38,13 @@ export interface K8sConfigShape {
38
38
  }
39
39
 
40
40
  /**
41
- * Thrown when an environment declares a cluster binding but the ambient
42
- * kubectl context disagrees with it. Refusing here instead of silently
43
- * observing whichever cluster is ambient — is the fix for #1100: a
44
- * wrong-cluster read reports every declared resource as missing, which #1089
45
- * then classifies as a confident (and wrong) list of `create` actions.
41
+ * Historically thrown when an environment declared a cluster binding but the
42
+ * ambient kubectl context disagreed with it (#1100/#1155). As of #1488 the
43
+ * resolver no longer polices ambient at all a declared binding is USED, not
44
+ * checked so {@link resolveClusterTarget} never throws this anymore. The
45
+ * class stays exported because the k8s lexicon's read/write paths still catch
46
+ * it (converting a refusal into NOT-OBSERVED), and a custom connector may
47
+ * still throw it to get that classification.
46
48
  */
47
49
  export class ClusterBindingMismatchError extends Error {
48
50
  constructor(
@@ -77,8 +79,11 @@ export interface ResolvedClusterTarget {
77
79
  * `kubectl config current-context`; the k8s lexicon's typed API client
78
80
  * (chant #1074) supplies one that reads the parsed kubeconfig instead, so a
79
81
  * client that never needs the `kubectl` binary does not acquire a dependency
80
- * on it just to check the binding. Both answer the same question, so the
81
- * refusal semantics below are identical either way.
82
+ * on it just to check the binding.
83
+ *
84
+ * As of #1488 the resolver itself no longer reads ambient (a declared binding
85
+ * is used directly), so this hook only matters to callers that surface the
86
+ * ambient context in their own messages.
82
87
  */
83
88
  export type AmbientContextReader = () => Promise<string | undefined>;
84
89
 
@@ -112,13 +117,16 @@ export interface ResolveClusterTargetOptions {
112
117
  * - No binding declared: returns `{ source: "ambient" }` — unchanged
113
118
  * behavior — but logs a visible warning identifying the caller and
114
119
  * environment, so the fallback is never silent (#1100 acceptance).
115
- * - Binding declared and the ambient context agrees (or ambient can't be
116
- * determined): returns `{ context: bound, source: "bound" }`. Callers
117
- * should pass this context explicitly on every kubectl invocation rather
118
- * than relying on it also being ambient.
119
- * - Binding declared and the ambient context disagrees: throws
120
- * {@link ClusterBindingMismatchError} a loud refusal instead of quietly
121
- * reading the wrong cluster.
120
+ * - Binding declared: returns `{ context: bound, source: "bound" }`,
121
+ * regardless of what is ambient (#1488). The declared binding is the
122
+ * selection, not a check: callers pass this context explicitly on every
123
+ * read and write, so whichever cluster `kubectl` happens to be pointed at
124
+ * is irrelevant. Any k3d cluster another project creates mid-session used
125
+ * to steal the ambient context and turn a healthy estate grey with
126
+ * `read-failed` as the only explanation; now it cannot. A bound context
127
+ * that is absent from the kubeconfig fails downstream in the typed client
128
+ * with an error naming the context and the `k8s.profiles.<env>.context`
129
+ * binding — never by falling back to ambient.
122
130
  */
123
131
  export async function resolveClusterTarget(
124
132
  config: Record<string, unknown>,
@@ -141,11 +149,6 @@ export async function resolveClusterTarget(
141
149
  return { source: "ambient" };
142
150
  }
143
151
 
144
- const ambient = await (options.ambientContext ?? currentAmbientContext)();
145
- if (ambient && ambient !== bound) {
146
- throw new ClusterBindingMismatchError(environment, bound, ambient);
147
- }
148
-
149
152
  return { context: bound, source: "bound" };
150
153
  }
151
154