@intentius/chant 0.74.1 → 0.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/chant CHANGED
@@ -11,4 +11,17 @@ done
11
11
  SCRIPT_DIR="$(cd "$(dirname "$SELF")" && pwd)"
12
12
  MAIN_TS="$SCRIPT_DIR/../src/cli/main.ts"
13
13
 
14
+ # tsx is our own dependency, so register its hooks in this node process rather
15
+ # than shelling `npx tsx`, which resolves a bin and then spawns a second node.
16
+ # Worth ~30% of startup (#2481). Nested install first, then hoisted; npx stays
17
+ # as the fallback for any layout neither guess covers.
18
+ for CANDIDATE in \
19
+ "$SCRIPT_DIR/../node_modules/tsx/dist/loader.mjs" \
20
+ "$SCRIPT_DIR/../../../tsx/dist/loader.mjs"
21
+ do
22
+ if [ -f "$CANDIDATE" ]; then
23
+ exec node --import "file://$CANDIDATE" "$MAIN_TS" "$@"
24
+ fi
25
+ done
26
+
14
27
  exec npx tsx "$MAIN_TS" "$@"
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The seam between a graph read and a prediction (#2377, epic #2355).
3
+ *
4
+ * `./behaviour-overlay.ts` projects a {@link BehaviourResult} onto the two
5
+ * channels the IR carries it on. This module is the other half: it builds the
6
+ * request from the graph the read already holds, and puts the projection back
7
+ * onto that graph. Both halves are pure, and neither calls an engine — the
8
+ * plugin call itself lives in `./cli/handlers/graph.ts`, which is the only
9
+ * place that has plugins to call.
10
+ *
11
+ * ## The request is the graph, not a second read
12
+ *
13
+ * The epic's rule is that the engine never sees credentials, and the way this
14
+ * path keeps that true is by never reading anything: `chant graph --live`
15
+ * already observed the estate, and {@link behaviourRequestFromIr} reshapes
16
+ * exactly those nodes and edges into the contract's options. There is no
17
+ * provider call here and nothing to leak, because there is no second read to
18
+ * leak from. `screenBehaviourRequest` (`./behaviour-predict.ts`) still screens
19
+ * what this produces; a node whose observed `attrs` picked up a secret is
20
+ * caught there, on the one path every prediction goes through.
21
+ *
22
+ * ## Why `attrs` are the props verbatim
23
+ *
24
+ * An IR node's `attrs` are what the read resolved — literals, and `{ $ref }`
25
+ * for a reference. The contract's `props` is the same thing, so this passes
26
+ * them across unchanged rather than filtering to a "useful" subset. Filtering
27
+ * would be chant deciding which properties can affect a price, and chant does
28
+ * not price anything; an engine that needs a field chant thought irrelevant
29
+ * would get a silently wrong figure with nothing to say so.
30
+ */
31
+ import { type BehaviourOverlay } from "./behaviour-overlay.js";
32
+ import type { GraphIR } from "./graph-ir.js";
33
+ import type { PredictBehaviourOptions } from "./behaviour.js";
34
+ /**
35
+ * What the caller knows and the graph does not.
36
+ *
37
+ * `edgeCoverage` is here rather than derived because this module cannot
38
+ * honestly derive it. Whether the edges in hand are the whole graph depends on
39
+ * which anchoring produced them and which lexicons contributed a reference
40
+ * catalog, and both facts live with the caller.
41
+ */
42
+ export interface BehaviourRequestContext extends Pick<PredictBehaviourOptions, "environment" | "traffic" | "buildOutput" | "edgeCoverage"> {
43
+ stack?: string;
44
+ region?: string;
45
+ owned?: boolean;
46
+ }
47
+ /**
48
+ * Reshape an already-read graph into `predictBehaviour`'s options.
49
+ *
50
+ * Entity identity is the node's `id` and nothing derived from it. That is the
51
+ * same key the drift overlay writes `_status` under and the same key
52
+ * {@link applyBehaviourOverlay} reads back, so in a multi-stack project where
53
+ * ids are `${stack}::${logicalId}`, the block lands on the node it was
54
+ * predicted for without this module knowing that qualification exists.
55
+ */
56
+ export declare function behaviourRequestFromIr(ir: GraphIR, ctx: BehaviourRequestContext): PredictBehaviourOptions;
57
+ /**
58
+ * Put a projection back onto the graph: each entity's block on its node's
59
+ * `attrs`, the graph-level half on `ir.meta`.
60
+ *
61
+ * A node with no entry keeps the `attrs` object it already had, untouched —
62
+ * an unpriced entity carries no `_behaviour` key rather than an empty or
63
+ * zeroed one, because zero cost and "not priced" are different claims and a
64
+ * renderer cannot tell them apart after the fact.
65
+ *
66
+ * Returns a new graph and mutates nothing: the caller's `ir` is shared with
67
+ * the lens pipeline downstream, and a prediction is not allowed to be the
68
+ * reason a later pass sees different nodes.
69
+ */
70
+ export declare function applyBehaviourOverlay(ir: GraphIR, overlay: BehaviourOverlay): GraphIR;
71
+ //# sourceMappingURL=behaviour-graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"behaviour-graph.d.ts","sourceRoot":"","sources":["../src/behaviour-graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACpF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAE3D;;;;;;;GAOG;AACH,MAAM,WAAW,uBACf,SAAQ,IAAI,CAAC,uBAAuB,EAAE,aAAa,GAAG,SAAS,GAAG,aAAa,GAAG,cAAc,CAAC;IACjG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,uBAAuB,GAAG,uBAAuB,CAiBzG;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAUrF"}
@@ -1 +1 @@
1
- {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/graph.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAiHlD;;;;;;;;;GASG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAwCnE"}
1
+ {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/graph.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAiHlD;;;;;;;;;GASG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAkDnE"}
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAMA,OAAO,EAAkB,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AA0F9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA0WpD;AA+bD;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,EAAE,UAAU,EAmGvC,CAAC;AAqIF;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB5E"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAMA,OAAO,EAAkB,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AA0F9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA8WpD;AAocD;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,EAAE,UAAU,EAmGvC,CAAC;AAqIF;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB5E"}
@@ -158,6 +158,17 @@ export interface ParsedArgs {
158
158
  * cross-substrate topology) and joins live status per node; `live` keeps the
159
159
  * provisioned graph's reconstructed edges (the pre-#821 behaviour). */
160
160
  overlayAnchor?: "source" | "live";
161
+ /** `chant graph --live --overlay --traffic "<level>"` (#2377) — predict the
162
+ * overlaid estate's behaviour at this traffic level and carry the result on
163
+ * the IR: each entity's block on `attrs._behaviour`, the report's meta (or a
164
+ * whole refusal report) on `meta._behaviour`.
165
+ *
166
+ * The flag is the trigger, and it has no default. The level is passed to the
167
+ * engine verbatim — chant does not parse it, default it or convert it
168
+ * (`../behaviour.ts`) — so without the flag there is no traffic level to
169
+ * predict *at*, nothing is asked, and neither key appears. A consumer reads
170
+ * that absence as "not looked", never as a refusal. */
171
+ traffic?: string;
161
172
  /** `chant list --components` / `chant graph --components` — surface discovered
162
173
  * `Component` declarations (#560) instead of/alongside lexicon resources. */
163
174
  components?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/cli/registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/cli/registry.ts"],"names":[],"mappings":"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"}
@@ -258,6 +258,21 @@ export interface GraphIR {
258
258
  nodes: IRNode[];
259
259
  edges: IREdge[];
260
260
  groups: IRGroups;
261
+ /**
262
+ * Graph-level facts that are about the whole read rather than any one node.
263
+ *
264
+ * Deliberately an open bag. A fact lands here when it has no node to sit on
265
+ * — `_behaviour` (#2377) carries the engine, its version, the traffic level
266
+ * the figures are *at*, and the estate's totals, or a whole refusal report
267
+ * when there was no engine to ask. None of that is a property of a single
268
+ * entity, and putting the refusal on nodes would mean writing it N times or
269
+ * dropping it, and dropping it is the one outcome the contract forbids
270
+ * (`./behaviour-overlay.ts`).
271
+ *
272
+ * Absent unless something filled it, and a consumer reads absence as "not
273
+ * looked" rather than as a negative finding.
274
+ */
275
+ meta?: Record<string, unknown>;
261
276
  /** Outputs this stack publishes for other stacks to import (#513). */
262
277
  exports?: IRExport[];
263
278
  /** Parameters this stack imports from other stacks. A viewer matches an import's
@@ -1 +1 @@
1
- {"version":3,"file":"graph-ir.d.ts","sourceRoot":"","sources":["../src/graph-ir.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"graph-ir.d.ts","sourceRoot":"","sources":["../src/graph-ir.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.74.1",
3
+ "version": "0.75.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",
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The graph seam (#2377): a request built from the graph in hand, and a
3
+ * projection put back onto it.
4
+ *
5
+ * The three states #2377 names are asserted on the values that reach the IR,
6
+ * because that IR is behold's whole input: a block per predicted node and the
7
+ * meta, nothing at all when no prediction was asked for, and the refusal on
8
+ * the meta with no node carrying a block.
9
+ */
10
+
11
+ import { describe, expect, it } from "vitest";
12
+ import {
13
+ behaviourReport,
14
+ noBehaviourEngineRefusal,
15
+ predictedRate,
16
+ type PredictedBehaviour,
17
+ } from "./behaviour";
18
+ import { BEHAVIOUR_OVERLAY_ATTR, behaviourOverlay } from "./behaviour-overlay";
19
+ import { applyBehaviourOverlay, behaviourRequestFromIr } from "./behaviour-graph";
20
+ import type { GraphIR, IRNode } from "./graph-ir";
21
+
22
+ const node = (id: string, kind: string, attrs: Record<string, unknown> = {}): IRNode => ({
23
+ id,
24
+ kind,
25
+ lexicon: "aws",
26
+ attrs,
27
+ });
28
+
29
+ const ir = (nodes: IRNode[], edges: GraphIR["edges"] = []): GraphIR => ({
30
+ nodes,
31
+ edges,
32
+ groups: { stacks: [], composites: [] } as unknown as GraphIR["groups"],
33
+ });
34
+
35
+ const block: PredictedBehaviour = {
36
+ at: { traffic: "100 rps, p50" },
37
+ cost: predictedRate(0.0416, "USD"),
38
+ headroom: { cpu: 0.6 },
39
+ errorRate: 0.0005,
40
+ resilience: { failure: "one zone lost", verdict: "survives" },
41
+ provenance: { engine: "fixture", version: "0.0.1", tolerance: "±20%", basis: "modeled" },
42
+ };
43
+
44
+ const ctx = {
45
+ environment: "prod",
46
+ traffic: "100 rps, p50",
47
+ buildOutput: "",
48
+ edgeCoverage: { verdict: "unknown" } as const,
49
+ };
50
+
51
+ describe("behaviourRequestFromIr", () => {
52
+ it("names every node and carries its kind and props verbatim", () => {
53
+ const graph = ir([node("web", "AWS::EC2::Instance", { instanceType: "t3.small" }), node("role", "AWS::IAM::Role")]);
54
+ const req = behaviourRequestFromIr(graph, ctx);
55
+
56
+ expect(req.entityNames).toEqual(["web", "role"]);
57
+ expect(req.entities.get("web")).toEqual({
58
+ entityType: "AWS::EC2::Instance",
59
+ props: { instanceType: "t3.small" },
60
+ });
61
+ expect(req.traffic).toBe("100 rps, p50");
62
+ expect(req.environment).toBe("prod");
63
+ });
64
+
65
+ it("passes the graph's own edges through, because the prediction is about paths", () => {
66
+ const edges = [{ from: "web", to: "db", kind: "ref" }] as unknown as GraphIR["edges"];
67
+ const req = behaviourRequestFromIr(ir([node("web", "X"), node("db", "Y")], edges), ctx);
68
+ expect(req.edges).toBe(edges);
69
+ });
70
+
71
+ it("does not filter props down to what chant thinks matters", () => {
72
+ // chant prices nothing, so it cannot know which field moves a figure. A
73
+ // filtered request would hand the engine a silently incomplete entity.
74
+ const attrs = { instanceType: "t3.small", tags: { team: "core" }, somethingChantHasNeverHeardOf: 7 };
75
+ const req = behaviourRequestFromIr(ir([node("web", "AWS::EC2::Instance", attrs)]), ctx);
76
+ expect(req.entities.get("web")?.props).toEqual(attrs);
77
+ });
78
+
79
+ it("omits stack, region and owned rather than sending undefined for them", () => {
80
+ const req = behaviourRequestFromIr(ir([node("web", "X")]), ctx);
81
+ expect("stack" in req).toBe(false);
82
+ expect("region" in req).toBe(false);
83
+ expect("owned" in req).toBe(false);
84
+ });
85
+ });
86
+
87
+ describe("applyBehaviourOverlay", () => {
88
+ const report = behaviourReport(
89
+ { entityNames: ["web", "role"], traffic: "100 rps, p50", edgeCoverage: { verdict: "unknown" } },
90
+ { engine: "fixture", version: "0.0.1", total: predictedRate(0.0416, "USD") },
91
+ { web: block },
92
+ { role: { reason: "unsupported-kind", detail: "a role is a grant" } },
93
+ );
94
+
95
+ it("puts a predicted entity's block on its own node, and the meta on the graph", () => {
96
+ const out = applyBehaviourOverlay(ir([node("web", "X"), node("role", "Y")]), behaviourOverlay(report));
97
+
98
+ expect(out.nodes[0].attrs[BEHAVIOUR_OVERLAY_ATTR]).toBe(block);
99
+ const meta = out.meta?.[BEHAVIOUR_OVERLAY_ATTR] as Record<string, unknown>;
100
+ expect(meta.engine).toBe("fixture");
101
+ expect(meta.at).toEqual({ traffic: "100 rps, p50" });
102
+ });
103
+
104
+ it("leaves an unpredicted node with no _behaviour key at all", () => {
105
+ const out = applyBehaviourOverlay(ir([node("web", "X"), node("role", "Y")]), behaviourOverlay(report));
106
+ // Not a zeroed block: "costs nothing" and "was not priced" are different
107
+ // claims, and a renderer cannot tell them apart after the fact.
108
+ expect(BEHAVIOUR_OVERLAY_ATTR in out.nodes[1].attrs).toBe(false);
109
+ });
110
+
111
+ it("a refusal rides on the meta whole, and no node carries a block", () => {
112
+ const out = applyBehaviourOverlay(
113
+ ir([node("web", "X"), node("role", "Y")]),
114
+ behaviourOverlay(noBehaviourEngineRefusal("terraform")),
115
+ );
116
+
117
+ const meta = out.meta?.[BEHAVIOUR_OVERLAY_ATTR] as Record<string, unknown>;
118
+ // behold's reader branches on `refusal` before it looks for `engine`.
119
+ expect(meta.refusal).toBeDefined();
120
+ expect(meta.behaviour).toBe("v1");
121
+ for (const n of out.nodes) expect(BEHAVIOUR_OVERLAY_ATTR in n.attrs).toBe(false);
122
+ });
123
+
124
+ it("keeps the drift overlay's attrs on the node beside the new block", () => {
125
+ // `_behaviour` joins `_status` rather than replacing the attrs object;
126
+ // losing the drift paint to gain a prediction would be a regression a
127
+ // renderer shows as every node going uncoloured.
128
+ const graph = ir([node("web", "X", { _status: "good", instanceType: "t3.small" })]);
129
+ const out = applyBehaviourOverlay(graph, behaviourOverlay(report));
130
+
131
+ expect(out.nodes[0].attrs._status).toBe("good");
132
+ expect(out.nodes[0].attrs.instanceType).toBe("t3.small");
133
+ expect(out.nodes[0].attrs[BEHAVIOUR_OVERLAY_ATTR]).toBe(block);
134
+ });
135
+
136
+ it("does not mutate the graph it was given", () => {
137
+ // The caller's `ir` goes on to the lens pipeline; a prediction must not be
138
+ // the reason a later pass sees different nodes.
139
+ const graph = ir([node("web", "X", { _status: "good" })]);
140
+ const before = JSON.stringify(graph);
141
+ applyBehaviourOverlay(graph, behaviourOverlay(report));
142
+ expect(JSON.stringify(graph)).toBe(before);
143
+ });
144
+
145
+ it("keeps meta keys another pass already put on the graph", () => {
146
+ const graph = { ...ir([node("web", "X")]), meta: { _somethingElse: 1 } };
147
+ const out = applyBehaviourOverlay(graph, behaviourOverlay(report));
148
+ expect(out.meta?._somethingElse).toBe(1);
149
+ expect(out.meta?.[BEHAVIOUR_OVERLAY_ATTR]).toBeDefined();
150
+ });
151
+ });
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The seam between a graph read and a prediction (#2377, epic #2355).
3
+ *
4
+ * `./behaviour-overlay.ts` projects a {@link BehaviourResult} onto the two
5
+ * channels the IR carries it on. This module is the other half: it builds the
6
+ * request from the graph the read already holds, and puts the projection back
7
+ * onto that graph. Both halves are pure, and neither calls an engine — the
8
+ * plugin call itself lives in `./cli/handlers/graph.ts`, which is the only
9
+ * place that has plugins to call.
10
+ *
11
+ * ## The request is the graph, not a second read
12
+ *
13
+ * The epic's rule is that the engine never sees credentials, and the way this
14
+ * path keeps that true is by never reading anything: `chant graph --live`
15
+ * already observed the estate, and {@link behaviourRequestFromIr} reshapes
16
+ * exactly those nodes and edges into the contract's options. There is no
17
+ * provider call here and nothing to leak, because there is no second read to
18
+ * leak from. `screenBehaviourRequest` (`./behaviour-predict.ts`) still screens
19
+ * what this produces; a node whose observed `attrs` picked up a secret is
20
+ * caught there, on the one path every prediction goes through.
21
+ *
22
+ * ## Why `attrs` are the props verbatim
23
+ *
24
+ * An IR node's `attrs` are what the read resolved — literals, and `{ $ref }`
25
+ * for a reference. The contract's `props` is the same thing, so this passes
26
+ * them across unchanged rather than filtering to a "useful" subset. Filtering
27
+ * would be chant deciding which properties can affect a price, and chant does
28
+ * not price anything; an engine that needs a field chant thought irrelevant
29
+ * would get a silently wrong figure with nothing to say so.
30
+ */
31
+
32
+ import { BEHAVIOUR_OVERLAY_ATTR, type BehaviourOverlay } from "./behaviour-overlay";
33
+ import type { GraphIR } from "./graph-ir";
34
+ import type { PredictBehaviourOptions } from "./behaviour";
35
+
36
+ /**
37
+ * What the caller knows and the graph does not.
38
+ *
39
+ * `edgeCoverage` is here rather than derived because this module cannot
40
+ * honestly derive it. Whether the edges in hand are the whole graph depends on
41
+ * which anchoring produced them and which lexicons contributed a reference
42
+ * catalog, and both facts live with the caller.
43
+ */
44
+ export interface BehaviourRequestContext
45
+ extends Pick<PredictBehaviourOptions, "environment" | "traffic" | "buildOutput" | "edgeCoverage"> {
46
+ stack?: string;
47
+ region?: string;
48
+ owned?: boolean;
49
+ }
50
+
51
+ /**
52
+ * Reshape an already-read graph into `predictBehaviour`'s options.
53
+ *
54
+ * Entity identity is the node's `id` and nothing derived from it. That is the
55
+ * same key the drift overlay writes `_status` under and the same key
56
+ * {@link applyBehaviourOverlay} reads back, so in a multi-stack project where
57
+ * ids are `${stack}::${logicalId}`, the block lands on the node it was
58
+ * predicted for without this module knowing that qualification exists.
59
+ */
60
+ export function behaviourRequestFromIr(ir: GraphIR, ctx: BehaviourRequestContext): PredictBehaviourOptions {
61
+ const entities = new Map<string, { entityType: string; props: Record<string, unknown> }>();
62
+ for (const node of ir.nodes) {
63
+ entities.set(node.id, { entityType: node.kind, props: node.attrs });
64
+ }
65
+ return {
66
+ environment: ctx.environment,
67
+ buildOutput: ctx.buildOutput,
68
+ entityNames: [...entities.keys()],
69
+ entities,
70
+ edges: ir.edges,
71
+ edgeCoverage: ctx.edgeCoverage,
72
+ traffic: ctx.traffic,
73
+ ...(ctx.stack === undefined ? {} : { stack: ctx.stack }),
74
+ ...(ctx.region === undefined ? {} : { region: ctx.region }),
75
+ ...(ctx.owned === undefined ? {} : { owned: ctx.owned }),
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Put a projection back onto the graph: each entity's block on its node's
81
+ * `attrs`, the graph-level half on `ir.meta`.
82
+ *
83
+ * A node with no entry keeps the `attrs` object it already had, untouched —
84
+ * an unpriced entity carries no `_behaviour` key rather than an empty or
85
+ * zeroed one, because zero cost and "not priced" are different claims and a
86
+ * renderer cannot tell them apart after the fact.
87
+ *
88
+ * Returns a new graph and mutates nothing: the caller's `ir` is shared with
89
+ * the lens pipeline downstream, and a prediction is not allowed to be the
90
+ * reason a later pass sees different nodes.
91
+ */
92
+ export function applyBehaviourOverlay(ir: GraphIR, overlay: BehaviourOverlay): GraphIR {
93
+ const nodes = ir.nodes.map((node) => {
94
+ const block = overlay.attrs[node.id];
95
+ return block === undefined ? node : { ...node, attrs: { ...node.attrs, ...block } };
96
+ });
97
+ return {
98
+ ...ir,
99
+ nodes,
100
+ meta: { ...(ir.meta ?? {}), [BEHAVIOUR_OVERLAY_ATTR]: overlay.meta[BEHAVIOUR_OVERLAY_ATTR] },
101
+ };
102
+ }
@@ -110,6 +110,7 @@ vi.mock("../../build", async () => {
110
110
  });
111
111
 
112
112
  const { runGraph } = await import("./graph");
113
+ const { behaviourReport, noBehaviourEngineRefusal, predictedRate } = await import("../../behaviour");
113
114
 
114
115
  function makeArgs(overrides: Partial<ParsedArgs> = {}): ParsedArgs {
115
116
  return {
@@ -626,6 +627,200 @@ describe("runGraph", () => {
626
627
  expect(out).toContain("web-vpc");
627
628
  });
628
629
 
630
+ // #2377 — the overlay carries the prediction. behold's reader takes
631
+ // `attrs._behaviour` per node and `meta._behaviour` on the graph, and
632
+ // reads the absence of both as "not looked" rather than as a refusal, so
633
+ // all three states are asserted on what actually reaches the IR.
634
+ describe("behaviour overlay (--traffic)", () => {
635
+ const predictingPlugin = (predictBehaviour: unknown) => ({
636
+ name: "aws",
637
+ serializer: {},
638
+ emulator: awsEmulatorStub,
639
+ describeResources: () => Promise.resolve({}),
640
+ predictBehaviour,
641
+ });
642
+
643
+ const liveEstate = () => {
644
+ resolveLexMock.mockResolvedValue(["aws"]);
645
+ // `--overlay` builds the declared canvas to classify against. Empty is
646
+ // enough here: what is under test is the behaviour block, and an empty
647
+ // declared graph leaves the observed nodes in place as foreign.
648
+ discoverMock.mockResolvedValue({ entities: new Map(), errors: [], sourceFiles: [] });
649
+ observeMock.mockResolvedValue({
650
+ observations: [{ lexicon: "aws", resources: {
651
+ web: { type: "AWS::EC2::Instance", status: "OK" },
652
+ role: { type: "AWS::IAM::Role", status: "OK" },
653
+ } }],
654
+ errors: [],
655
+ warnings: [],
656
+ });
657
+ };
658
+
659
+ const report = () => behaviourReport(
660
+ { entityNames: ["web", "role"], traffic: "100 rps, p50", edgeCoverage: { verdict: "unknown" } },
661
+ { engine: "fixture", version: "0.0.1", total: predictedRate(0.0416, "USD") },
662
+ { web: {
663
+ at: { traffic: "100 rps, p50" },
664
+ cost: predictedRate(0.0416, "USD"),
665
+ headroom: { cpu: 0.6 },
666
+ errorRate: 0.0005,
667
+ resilience: { failure: "one zone lost", verdict: "survives" },
668
+ provenance: { engine: "fixture", version: "0.0.1", tolerance: "±20%", basis: "modeled" },
669
+ } },
670
+ { role: { reason: "unsupported-kind", detail: "a role is a grant" } },
671
+ );
672
+
673
+ test("with an engine: the block is on its node and the meta on the graph", async () => {
674
+ liveEstate();
675
+ loadPluginsMock.mockResolvedValue([predictingPlugin(() => Promise.resolve(report()))]);
676
+
677
+ const exit = await runGraph({
678
+ args: makeArgs({ format: "ir", live: true, overlay: true, env: "prod", traffic: "100 rps, p50" }),
679
+ plugins: [], serializers: [],
680
+ });
681
+
682
+ expect(exit).toBe(0);
683
+ const ir = JSON.parse(stdoutBuf.join("\n")) as {
684
+ nodes: Array<{ id: string; attrs: Record<string, unknown> }>;
685
+ meta?: Record<string, { engine?: string; at?: { traffic?: string } }>;
686
+ };
687
+ expect(ir.meta?._behaviour?.engine).toBe("fixture");
688
+ expect(ir.meta?._behaviour?.at?.traffic).toBe("100 rps, p50");
689
+ const web = ir.nodes.find((n) => n.id === "web");
690
+ expect((web?.attrs._behaviour as { cost?: { perHour?: number } })?.cost?.perHour).toBe(0.0416);
691
+ // An entity the engine could not price carries no block rather than a
692
+ // zeroed one — "costs nothing" is a different claim from "not priced".
693
+ expect("_behaviour" in (ir.nodes.find((n) => n.id === "role")?.attrs ?? {})).toBe(false);
694
+ });
695
+
696
+ test("the traffic level reaches the engine verbatim, with the graph's own entities", async () => {
697
+ liveEstate();
698
+ const seen: Array<{ traffic: string; entityNames: string[] }> = [];
699
+ loadPluginsMock.mockResolvedValue([predictingPlugin((o: { traffic: string; entityNames: string[] }) => {
700
+ seen.push({ traffic: o.traffic, entityNames: o.entityNames });
701
+ return Promise.resolve(report());
702
+ })]);
703
+
704
+ await runGraph({
705
+ args: makeArgs({ format: "ir", live: true, overlay: true, env: "prod", traffic: "1000 rps, p99" }),
706
+ plugins: [], serializers: [],
707
+ });
708
+
709
+ expect(seen[0]?.traffic).toBe("1000 rps, p99");
710
+ expect(seen[0]?.entityNames).toContain("web");
711
+ });
712
+
713
+ test("without --traffic: nothing is asked and neither key appears", async () => {
714
+ liveEstate();
715
+ let called = false;
716
+ loadPluginsMock.mockResolvedValue([predictingPlugin(() => { called = true; return Promise.resolve(report()); })]);
717
+
718
+ const exit = await runGraph({
719
+ args: makeArgs({ format: "ir", live: true, overlay: true, env: "prod" }),
720
+ plugins: [], serializers: [],
721
+ });
722
+
723
+ expect(exit).toBe(0);
724
+ // Not a refusal: an engine was configured and reachable. behold reads
725
+ // this absence as "not looked", which is why nothing may be written.
726
+ expect(called).toBe(false);
727
+ const ir = JSON.parse(stdoutBuf.join("\n")) as {
728
+ nodes: Array<{ attrs: Record<string, unknown> }>; meta?: Record<string, unknown>;
729
+ };
730
+ expect(ir.meta?._behaviour).toBeUndefined();
731
+ for (const n of ir.nodes) expect("_behaviour" in n.attrs).toBe(false);
732
+ });
733
+
734
+ test("a refusal rides on the meta whole, and no node carries a block", async () => {
735
+ liveEstate();
736
+ loadPluginsMock.mockResolvedValue([
737
+ predictingPlugin(() => Promise.resolve(noBehaviourEngineRefusal("aws"))),
738
+ ]);
739
+
740
+ const exit = await runGraph({
741
+ args: makeArgs({ format: "ir", live: true, overlay: true, env: "prod", traffic: "100 rps, p50" }),
742
+ plugins: [], serializers: [],
743
+ });
744
+
745
+ expect(exit).toBe(0);
746
+ const ir = JSON.parse(stdoutBuf.join("\n")) as {
747
+ nodes: Array<{ attrs: Record<string, unknown> }>;
748
+ meta?: { _behaviour?: { refusal?: { reason?: string }; behaviour?: string } };
749
+ };
750
+ // behold branches on `refusal` before it looks for `engine`; the
751
+ // envelope is what makes it findable at all.
752
+ expect(ir.meta?._behaviour?.refusal?.reason).toMatch(/CHANT_BEHAVIOUR_ENGINE/);
753
+ expect(ir.meta?._behaviour?.behaviour).toBe("v1");
754
+ for (const n of ir.nodes) expect("_behaviour" in n.attrs).toBe(false);
755
+ });
756
+
757
+ test("no predicting lexicon: the graph is served unchanged, and says so once", async () => {
758
+ liveEstate();
759
+ loadPluginsMock.mockResolvedValue([
760
+ { name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
761
+ ]);
762
+
763
+ const exit = await runGraph({
764
+ args: makeArgs({ format: "ir", live: true, overlay: true, env: "prod", traffic: "100 rps, p50" }),
765
+ plugins: [], serializers: [],
766
+ });
767
+
768
+ expect(exit).toBe(0);
769
+ expect(stderrBuf.join("\n")).toContain("no installed lexicon implements predictBehaviour");
770
+ expect(JSON.parse(stdoutBuf.join("\n")).meta?._behaviour).toBeUndefined();
771
+ });
772
+
773
+ test("a thrown engine costs the prediction, never the graph", async () => {
774
+ liveEstate();
775
+ loadPluginsMock.mockResolvedValue([
776
+ predictingPlugin(() => Promise.reject(new Error("socket hang up"))),
777
+ ]);
778
+
779
+ const exit = await runGraph({
780
+ args: makeArgs({ format: "ir", live: true, overlay: true, env: "prod", traffic: "100 rps, p50" }),
781
+ plugins: [], serializers: [],
782
+ });
783
+
784
+ // The estate graph is what was asked for; a broken engine must not
785
+ // take it away.
786
+ expect(exit).toBe(0);
787
+ expect(stderrBuf.join("\n")).toContain("socket hang up");
788
+ const ir = JSON.parse(stdoutBuf.join("\n")) as { nodes: unknown[]; meta?: Record<string, unknown> };
789
+ expect(ir.nodes.length).toBeGreaterThan(0);
790
+ expect(ir.meta?._behaviour).toBeUndefined();
791
+ });
792
+
793
+ test("--traffic without --live is refused rather than silently ignored", async () => {
794
+ // Accepted-and-ignored would read as "predicted, and everything came
795
+ // back empty", which is the opposite of what happened.
796
+ const exit = await runGraph({
797
+ args: makeArgs({ format: "ir", traffic: "100 rps, p50" }),
798
+ plugins: [], serializers: [],
799
+ });
800
+
801
+ expect(exit).toBe(1);
802
+ expect(stderrBuf.join("\n")).toContain("--traffic needs --live");
803
+ });
804
+
805
+ test("two predicting lexicons is refused rather than attributed to one", async () => {
806
+ liveEstate();
807
+ loadPluginsMock.mockResolvedValue([
808
+ predictingPlugin(() => Promise.resolve(report())),
809
+ { ...predictingPlugin(() => Promise.resolve(report())), name: "gcp" },
810
+ ]);
811
+
812
+ const exit = await runGraph({
813
+ args: makeArgs({ format: "ir", live: true, overlay: true, env: "prod", traffic: "100 rps, p50" }),
814
+ plugins: [], serializers: [],
815
+ });
816
+
817
+ // One `meta._behaviour` key and two engines: taking the first would
818
+ // put one engine's figures under the other's provenance.
819
+ expect(exit).toBe(1);
820
+ expect(stderrBuf.join("\n")).toContain("cannot merge two engines");
821
+ });
822
+ });
823
+
629
824
  // #1279 — `graph` had no `--at`, so anyone wanting the raw IR of a recorded
630
825
  // estate had to reach for the live endpoint. A snapshot could answer most
631
826
  // questions and never all of them.
@@ -3,6 +3,9 @@ import { discoverOps } from "../../op/discover";
3
3
  import { discover } from "../../discovery/index";
4
4
  import { partitionByLexicon, computeStackGraph, build, mergeBuildRootEntities } from "../../build";
5
5
  import { buildGraphIr, buildLiveGraphIr, collectUnobserved, overlayGraphs, sourceOverlayGraphs, type GraphIR, type IRPipeline, type LiveObservation } from "../../graph-ir";
6
+ import { behaviourOverlay } from "../../behaviour-overlay";
7
+ import type { SerializerResult } from "../../serializer";
8
+ import { applyBehaviourOverlay, behaviourRequestFromIr } from "../../behaviour-graph";
6
9
  import { buildDeclaredPerStack } from "../../graph-declared";
7
10
  import { mergeProjectOps } from "../../graph-ops";
8
11
  import { reconstructEdges, mergeCatalogs, containmentGroups, type ReferenceCatalog, type ContainmentPair } from "../../graph-refs";
@@ -157,6 +160,16 @@ export async function runGraph(ctx: CommandContext): Promise<number> {
157
160
  }));
158
161
  return 1;
159
162
  }
163
+ // A prediction is about an estate, and only the live path has one. Without
164
+ // `--live` the flag would be accepted and do nothing at all, which reads as
165
+ // "predicted, and everything came back empty" rather than "never ran".
166
+ if (ctx.args.traffic && !ctx.args.live) {
167
+ console.error(formatError({
168
+ message: "--traffic needs --live: a prediction is made about the observed estate, and there is nothing to predict on the declared graph alone.",
169
+ hint: 'chant graph --format ir --live --env <name> --overlay --traffic "100 rps, p50"',
170
+ }));
171
+ return 1;
172
+ }
160
173
  // `--at` graphs a recorded observation instead of reading the estate (#1279).
161
174
  // Two different observations with no rule for which wins, so not both.
162
175
  if (ctx.args.at && ctx.args.live) {
@@ -434,6 +447,71 @@ async function runGraphLive(
434
447
  }
435
448
  }
436
449
 
450
+ // Behaviour overlay (#2377, epic #2355): carry a prediction on the graph.
451
+ //
452
+ // `--traffic` is the trigger, and it is the trigger because the contract
453
+ // forbids defaulting the level (../../behaviour.ts): every figure an engine
454
+ // returns is *at* a stated traffic, so inventing one here would silently
455
+ // decide what every number on the graph means. No flag: nothing is asked,
456
+ // neither key appears, and a consumer reads that absence as "not looked".
457
+ //
458
+ // Placed after the drift overlay so the prediction lands on the final node
459
+ // set — the canvas is settled by here under either anchoring — and before
460
+ // the lens below, so a lens filters predicted nodes like any other.
461
+ if (args.traffic) {
462
+ const predicting = plugins.filter((p) => p.predictBehaviour);
463
+ if (predicting.length === 0) {
464
+ console.error(formatWarning({
465
+ message:
466
+ "--traffic: no installed lexicon implements predictBehaviour, so nothing was predicted. The graph is unchanged.",
467
+ }));
468
+ } else if (predicting.length > 1) {
469
+ // Two engines would mean two `meta._behaviour` values and one key. Rather
470
+ // than pick, say so: a merged prediction across engines is not defined by
471
+ // the contract, and silently taking the first would attribute one
472
+ // engine's figures to the other's provenance.
473
+ console.error(formatError({
474
+ message: `--traffic: ${predicting.length} lexicons implement predictBehaviour (${predicting.map((p) => p.name).join(", ")}) — chant cannot merge two engines' figures onto one graph.`,
475
+ hint: "Predict one lexicon at a time, or drop --traffic to graph without a prediction",
476
+ }));
477
+ return 1;
478
+ } else {
479
+ // The first output the build produced, or empty. Nothing on this path
480
+ // reads it — the mirror is kept so the four observation methods take the
481
+ // same shape (../../op/activities/predict-behaviour.ts) — so a build
482
+ // result without outputs costs an empty string, never the graph.
483
+ const raw = buildResult.outputs ? [...buildResult.outputs.values()][0] : undefined;
484
+ const buildOutput = raw === undefined ? "" : typeof raw === "string" ? raw : (raw as SerializerResult).primary;
485
+ const request = behaviourRequestFromIr(ir, {
486
+ environment: environment ?? "",
487
+ traffic: args.traffic,
488
+ buildOutput,
489
+ // `unknown`, for the reason the declared path claims it
490
+ // (../../op/activities/predict-behaviour.ts): reference edges resolved,
491
+ // containment absent, and no vocabulary here for "this kind is a
492
+ // boundary whose containment is missing". Claiming `complete` would be
493
+ // true about references and false about the graph; `partial` has to
494
+ // name its gap as `dangling` or `unresolvedKinds`, and this gap is
495
+ // neither.
496
+ edgeCoverage: { verdict: "unknown" },
497
+ ...(args.owned === undefined ? {} : { owned: args.owned }),
498
+ });
499
+ try {
500
+ // A refusal is a value, not a throw: it comes back as a result and
501
+ // rides on `meta._behaviour` whole, which is the one shape behold's
502
+ // reader branches on (../../behaviour-overlay.ts).
503
+ const result = await predicting[0].predictBehaviour!(request);
504
+ ir = applyBehaviourOverlay(ir, behaviourOverlay(result));
505
+ } catch (err) {
506
+ // The estate graph is the thing asked for; a broken engine must not
507
+ // take it away. Say what failed and serve the graph without the block.
508
+ console.error(formatWarning({
509
+ message: `--traffic: ${predicting[0].name}'s predictBehaviour failed (${err instanceof Error ? err.message : String(err)}) — showing the graph without a prediction`,
510
+ }));
511
+ }
512
+ }
513
+ }
514
+
437
515
  if (args.lens) {
438
516
  try {
439
517
  ir = applyLens(ir, parseLens(args.lens, { up: args.up, down: args.down }));
@@ -4,6 +4,16 @@ import { parseArgs, waitForStreamDrain } from "./main";
4
4
  import { resolveCommand, type CommandDef, type ParsedArgs } from "./registry";
5
5
 
6
6
  describe("parseArgs", () => {
7
+ // #2377 — the level reaches the engine verbatim, so parsing must not
8
+ // normalise it, and an empty one must not read as "predict at nothing".
9
+ test("takes --traffic as an opaque level, and refuses an empty one", () => {
10
+ expect(parseArgs(["graph", "--live", "--overlay"]).traffic).toBeUndefined();
11
+ expect(parseArgs(["graph", "--live", "--overlay", "--traffic", "100 rps, p50"]).traffic).toBe("100 rps, p50");
12
+ // Spacing, casing and punctuation are the engine's to interpret.
13
+ expect(parseArgs(["graph", "--traffic", "1000rps p99"]).traffic).toBe("1000rps p99");
14
+ expect(() => parseArgs(["graph", "--traffic"])).toThrow(/--traffic needs a level/);
15
+ });
16
+
7
17
  test("--fold and --no-fold set the tri-state fold option (#1134)", () => {
8
18
  expect(parseArgs(["build", "src"]).fold).toBeUndefined();
9
19
  expect(parseArgs(["build", "src", "--fold"]).fold).toBe(true);
package/src/cli/main.ts CHANGED
@@ -195,6 +195,10 @@ export function parseArgs(args: string[]): ParsedArgs {
195
195
  result.betweenA = args[++i];
196
196
  result.betweenB = args[++i];
197
197
  if (!result.betweenA || !result.betweenB) throw new Error("--between needs two snapshot refs: --between <refA> <refB>");
198
+ } else if (arg === "--traffic") {
199
+ const v = args[++i];
200
+ if (!v) throw new Error('--traffic needs a level, passed to the engine verbatim: --traffic "100 rps, p50"');
201
+ result.traffic = v;
198
202
  } else if (arg === "--overlay-anchor") {
199
203
  const v = args[++i];
200
204
  if (v !== "source" && v !== "live") throw new Error(`--overlay-anchor must be 'source' or 'live', got '${v}'`);
@@ -595,6 +599,11 @@ Ops:
595
599
  add the CI/pipeline projection (stages/jobs/needs) to
596
600
  the component-graph IR, from the same generator
597
601
  'build --components --generate' uses (#989)
602
+ --live --overlay --traffic "<level>": ask the project's
603
+ predicting lexicon what the overlaid estate does at that
604
+ traffic level, and carry each entity's prediction on the
605
+ IR. The level is passed to the engine verbatim; without
606
+ the flag nothing is asked and nothing is carried (#2377)
598
607
 
599
608
  Lifecycle (alias: lc):
600
609
  lifecycle snapshot <env> Query API, save metadata to orphan branch
@@ -159,6 +159,17 @@ export interface ParsedArgs {
159
159
  * cross-substrate topology) and joins live status per node; `live` keeps the
160
160
  * provisioned graph's reconstructed edges (the pre-#821 behaviour). */
161
161
  overlayAnchor?: "source" | "live";
162
+ /** `chant graph --live --overlay --traffic "<level>"` (#2377) — predict the
163
+ * overlaid estate's behaviour at this traffic level and carry the result on
164
+ * the IR: each entity's block on `attrs._behaviour`, the report's meta (or a
165
+ * whole refusal report) on `meta._behaviour`.
166
+ *
167
+ * The flag is the trigger, and it has no default. The level is passed to the
168
+ * engine verbatim — chant does not parse it, default it or convert it
169
+ * (`../behaviour.ts`) — so without the flag there is no traffic level to
170
+ * predict *at*, nothing is asked, and neither key appears. A consumer reads
171
+ * that absence as "not looked", never as a refusal. */
172
+ traffic?: string;
162
173
  /** `chant list --components` / `chant graph --components` — surface discovered
163
174
  * `Component` declarations (#560) instead of/alongside lexicon resources. */
164
175
  components?: boolean;
package/src/graph-ir.ts CHANGED
@@ -295,6 +295,21 @@ export interface GraphIR {
295
295
  nodes: IRNode[];
296
296
  edges: IREdge[];
297
297
  groups: IRGroups;
298
+ /**
299
+ * Graph-level facts that are about the whole read rather than any one node.
300
+ *
301
+ * Deliberately an open bag. A fact lands here when it has no node to sit on
302
+ * — `_behaviour` (#2377) carries the engine, its version, the traffic level
303
+ * the figures are *at*, and the estate's totals, or a whole refusal report
304
+ * when there was no engine to ask. None of that is a property of a single
305
+ * entity, and putting the refusal on nodes would mean writing it N times or
306
+ * dropping it, and dropping it is the one outcome the contract forbids
307
+ * (`./behaviour-overlay.ts`).
308
+ *
309
+ * Absent unless something filled it, and a consumer reads absence as "not
310
+ * looked" rather than as a negative finding.
311
+ */
312
+ meta?: Record<string, unknown>;
298
313
  /** Outputs this stack publishes for other stacks to import (#513). */
299
314
  exports?: IRExport[];
300
315
  /** Parameters this stack imports from other stacks. A viewer matches an import's