@intentius/chant 0.74.1 → 0.75.1

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,EAAc,MAAM,aAAa,CAAC;AAkH9D;;;;;;;;;GASG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAwCnE"}
@@ -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;AAqcD;;;;;;;;;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":"AAGA,OAAO,EAAE,KAAK,UAAU,EAAgB,MAAM,cAAc,CAAC;AAI7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD;;;;;;;;;GASG;AAEH,iFAAiF;AACjF,MAAM,WAAW,SAAS;IACxB,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,iCAAiC;AACjC,MAAM,WAAW,MAAM;IACrB,kEAAkE;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,mCAAmC;IACnC,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAChC;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qEAAqE;AACrE,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,yCAAyC;IACzC,IAAI,EAAE,KAAK,CAAC;IACZ,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAC;IACX,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sGAAsG;IACtG,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAWD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,eAAe,EAAE,CAI/E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAGlE;AAED,qFAAqF;AACrF,MAAM,WAAW,QAAQ;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACnC;;;;iFAI6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC;;;8EAG0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACnC;AAED;qFACqF;AACrF,MAAM,WAAW,QAAQ;IACvB,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;2DAG2D;AAC3D,MAAM,WAAW,QAAQ;IACvB,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,uEAAuE;IACvE,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,OAAO,CAAC;IACd,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB;yEACqE;IACrE,KAAK,EAAE,MAAM,CAAC;CACf;AAED;oEACoE;AACpE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sFAAsF;IACtF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED,gFAAgF;AAChF,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,QAAQ,CAAC;IACjB,sEAAsE;IACtE,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC;IACrB;;gFAE4E;IAC5E,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC;IACrB,gGAAgG;IAChG,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACzC;AA2MD;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EACjC,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAqGT;AAQD;4EAC4E;AAC5E,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC5C;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC9C;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACxD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,eAAe,EAAE,GAAG,OAAO,CAyEzE;AAED,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAC/C;AAED,wFAAwF;AACxF,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,eAAe,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAInG;AAiBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAgB9F;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAqEpG"}
1
+ {"version":3,"file":"graph-ir.d.ts","sourceRoot":"","sources":["../src/graph-ir.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,UAAU,EAAgB,MAAM,cAAc,CAAC;AAI7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD;;;;;;;;;GASG;AAEH,iFAAiF;AACjF,MAAM,WAAW,SAAS;IACxB,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,iCAAiC;AACjC,MAAM,WAAW,MAAM;IACrB,kEAAkE;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,mCAAmC;IACnC,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAChC;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qEAAqE;AACrE,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,yCAAyC;IACzC,IAAI,EAAE,KAAK,CAAC;IACZ,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAC;IACX,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sGAAsG;IACtG,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAWD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,eAAe,EAAE,CAI/E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAGlE;AAED,qFAAqF;AACrF,MAAM,WAAW,QAAQ;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACnC;;;;iFAI6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC;;;8EAG0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACnC;AAED;qFACqF;AACrF,MAAM,WAAW,QAAQ;IACvB,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;2DAG2D;AAC3D,MAAM,WAAW,QAAQ;IACvB,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,uEAAuE;IACvE,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,OAAO,CAAC;IACd,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB;yEACqE;IACrE,KAAK,EAAE,MAAM,CAAC;CACf;AAED;oEACoE;AACpE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sFAAsF;IACtF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED,gFAAgF;AAChF,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,QAAQ,CAAC;IACjB;;;;;;;;;;;;;OAaG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,sEAAsE;IACtE,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC;IACrB;;gFAE4E;IAC5E,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC;IACrB,gGAAgG;IAChG,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACzC;AA2MD;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EACjC,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAqGT;AAQD;4EAC4E;AAC5E,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC5C;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC9C;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACxD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,eAAe,EAAE,GAAG,OAAO,CAyEzE;AAED,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAC/C;AAED,wFAAwF;AACxF,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,eAAe,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAInG;AAiBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAgB9F;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAqEpG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.74.1",
3
+ "version": "0.75.1",
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 {
@@ -258,6 +259,66 @@ describe("runGraph", () => {
258
259
  expect(ir.nodes.find((n: { id: string }) => n.id === "deploy").attrs.phases).toEqual([]);
259
260
  });
260
261
 
262
+ // #2377 / epic #2355: "the live path predicts the account as it stands,
263
+ // drift included. The declared path predicts the file. The two are shown
264
+ // as a delta." An earlier draft of #2377 required --live, which forbade
265
+ // the declared half and so forbade the delta.
266
+ test("--traffic predicts the declared graph, without --live", async () => {
267
+ lintClean(); discovered();
268
+ const seen: Array<{ traffic: string; environment: string }> = [];
269
+ loadPluginsMock.mockResolvedValue([{
270
+ name: "gcp",
271
+ serializer: {},
272
+ predictBehaviour: (o: { traffic: string; environment: string }) => {
273
+ seen.push({ traffic: o.traffic, environment: o.environment });
274
+ return Promise.resolve(behaviourReport(
275
+ { entityNames: ["vpc"], traffic: o.traffic, edgeCoverage: { verdict: "unknown" } },
276
+ { engine: "fixture", version: "0.0.1", total: predictedRate(1, "USD") },
277
+ { vpc: {
278
+ at: { traffic: o.traffic },
279
+ cost: predictedRate(1, "USD"),
280
+ headroom: { cpu: 0.5 },
281
+ errorRate: 0,
282
+ resilience: { failure: "one zone lost", verdict: "survives" },
283
+ provenance: { engine: "fixture", version: "0.0.1", tolerance: "±20%", basis: "modeled" },
284
+ } },
285
+ {},
286
+ ));
287
+ },
288
+ }]);
289
+ resolveLexMock.mockResolvedValue(["gcp"]);
290
+
291
+ const exit = await runGraph({
292
+ args: makeArgs({ format: "ir", traffic: "100 rps, p50" }),
293
+ plugins: [], serializers: [],
294
+ });
295
+
296
+ expect(exit).toBe(0);
297
+ expect(seen[0]?.traffic).toBe("100 rps, p50");
298
+ const ir = JSON.parse(stdoutBuf.join("\n"));
299
+ expect(ir.meta?._behaviour?.engine).toBe("fixture");
300
+ expect(ir.nodes.find((n: { id: string }) => n.id === "vpc").attrs._behaviour.cost.perHour).toBe(1);
301
+ });
302
+
303
+ test("the declared path names no environment, because the file is deployed nowhere", async () => {
304
+ lintClean(); discovered();
305
+ const seen: string[] = [];
306
+ loadPluginsMock.mockResolvedValue([{
307
+ name: "gcp",
308
+ serializer: {},
309
+ predictBehaviour: (o: { environment: string }) => {
310
+ seen.push(o.environment);
311
+ return Promise.resolve(noBehaviourEngineRefusal("gcp"));
312
+ },
313
+ }]);
314
+ resolveLexMock.mockResolvedValue(["gcp"]);
315
+
316
+ await runGraph({ args: makeArgs({ format: "ir", traffic: "100 rps, p50" }), plugins: [], serializers: [] });
317
+
318
+ // Naming one would claim the declared estate was deployed there.
319
+ expect(seen[0]).toBe("");
320
+ });
321
+
261
322
  test("op-discovery errors are warnings, not a refusal", async () => {
262
323
  lintClean(); discovered();
263
324
  discoverOpsMock.mockResolvedValue({ ops: new Map(), errors: ["ops/bad.op.ts: boom"] });
@@ -626,6 +687,188 @@ describe("runGraph", () => {
626
687
  expect(out).toContain("web-vpc");
627
688
  });
628
689
 
690
+ // #2377 — the overlay carries the prediction. behold's reader takes
691
+ // `attrs._behaviour` per node and `meta._behaviour` on the graph, and
692
+ // reads the absence of both as "not looked" rather than as a refusal, so
693
+ // all three states are asserted on what actually reaches the IR.
694
+ describe("behaviour overlay (--traffic)", () => {
695
+ const predictingPlugin = (predictBehaviour: unknown) => ({
696
+ name: "aws",
697
+ serializer: {},
698
+ emulator: awsEmulatorStub,
699
+ describeResources: () => Promise.resolve({}),
700
+ predictBehaviour,
701
+ });
702
+
703
+ const liveEstate = () => {
704
+ resolveLexMock.mockResolvedValue(["aws"]);
705
+ // `--overlay` builds the declared canvas to classify against. Empty is
706
+ // enough here: what is under test is the behaviour block, and an empty
707
+ // declared graph leaves the observed nodes in place as foreign.
708
+ discoverMock.mockResolvedValue({ entities: new Map(), errors: [], sourceFiles: [] });
709
+ observeMock.mockResolvedValue({
710
+ observations: [{ lexicon: "aws", resources: {
711
+ web: { type: "AWS::EC2::Instance", status: "OK" },
712
+ role: { type: "AWS::IAM::Role", status: "OK" },
713
+ } }],
714
+ errors: [],
715
+ warnings: [],
716
+ });
717
+ };
718
+
719
+ const report = () => behaviourReport(
720
+ { entityNames: ["web", "role"], traffic: "100 rps, p50", edgeCoverage: { verdict: "unknown" } },
721
+ { engine: "fixture", version: "0.0.1", total: predictedRate(0.0416, "USD") },
722
+ { web: {
723
+ at: { traffic: "100 rps, p50" },
724
+ cost: predictedRate(0.0416, "USD"),
725
+ headroom: { cpu: 0.6 },
726
+ errorRate: 0.0005,
727
+ resilience: { failure: "one zone lost", verdict: "survives" },
728
+ provenance: { engine: "fixture", version: "0.0.1", tolerance: "±20%", basis: "modeled" },
729
+ } },
730
+ { role: { reason: "unsupported-kind", detail: "a role is a grant" } },
731
+ );
732
+
733
+ test("with an engine: the block is on its node and the meta on the graph", async () => {
734
+ liveEstate();
735
+ loadPluginsMock.mockResolvedValue([predictingPlugin(() => Promise.resolve(report()))]);
736
+
737
+ const exit = await runGraph({
738
+ args: makeArgs({ format: "ir", live: true, overlay: true, env: "prod", traffic: "100 rps, p50" }),
739
+ plugins: [], serializers: [],
740
+ });
741
+
742
+ expect(exit).toBe(0);
743
+ const ir = JSON.parse(stdoutBuf.join("\n")) as {
744
+ nodes: Array<{ id: string; attrs: Record<string, unknown> }>;
745
+ meta?: Record<string, { engine?: string; at?: { traffic?: string } }>;
746
+ };
747
+ expect(ir.meta?._behaviour?.engine).toBe("fixture");
748
+ expect(ir.meta?._behaviour?.at?.traffic).toBe("100 rps, p50");
749
+ const web = ir.nodes.find((n) => n.id === "web");
750
+ expect((web?.attrs._behaviour as { cost?: { perHour?: number } })?.cost?.perHour).toBe(0.0416);
751
+ // An entity the engine could not price carries no block rather than a
752
+ // zeroed one — "costs nothing" is a different claim from "not priced".
753
+ expect("_behaviour" in (ir.nodes.find((n) => n.id === "role")?.attrs ?? {})).toBe(false);
754
+ });
755
+
756
+ test("the traffic level reaches the engine verbatim, with the graph's own entities", async () => {
757
+ liveEstate();
758
+ const seen: Array<{ traffic: string; entityNames: string[] }> = [];
759
+ loadPluginsMock.mockResolvedValue([predictingPlugin((o: { traffic: string; entityNames: string[] }) => {
760
+ seen.push({ traffic: o.traffic, entityNames: o.entityNames });
761
+ return Promise.resolve(report());
762
+ })]);
763
+
764
+ await runGraph({
765
+ args: makeArgs({ format: "ir", live: true, overlay: true, env: "prod", traffic: "1000 rps, p99" }),
766
+ plugins: [], serializers: [],
767
+ });
768
+
769
+ expect(seen[0]?.traffic).toBe("1000 rps, p99");
770
+ expect(seen[0]?.entityNames).toContain("web");
771
+ });
772
+
773
+ test("without --traffic: nothing is asked and neither key appears", async () => {
774
+ liveEstate();
775
+ let called = false;
776
+ loadPluginsMock.mockResolvedValue([predictingPlugin(() => { called = true; return Promise.resolve(report()); })]);
777
+
778
+ const exit = await runGraph({
779
+ args: makeArgs({ format: "ir", live: true, overlay: true, env: "prod" }),
780
+ plugins: [], serializers: [],
781
+ });
782
+
783
+ expect(exit).toBe(0);
784
+ // Not a refusal: an engine was configured and reachable. behold reads
785
+ // this absence as "not looked", which is why nothing may be written.
786
+ expect(called).toBe(false);
787
+ const ir = JSON.parse(stdoutBuf.join("\n")) as {
788
+ nodes: Array<{ attrs: Record<string, unknown> }>; meta?: Record<string, unknown>;
789
+ };
790
+ expect(ir.meta?._behaviour).toBeUndefined();
791
+ for (const n of ir.nodes) expect("_behaviour" in n.attrs).toBe(false);
792
+ });
793
+
794
+ test("a refusal rides on the meta whole, and no node carries a block", async () => {
795
+ liveEstate();
796
+ loadPluginsMock.mockResolvedValue([
797
+ predictingPlugin(() => Promise.resolve(noBehaviourEngineRefusal("aws"))),
798
+ ]);
799
+
800
+ const exit = await runGraph({
801
+ args: makeArgs({ format: "ir", live: true, overlay: true, env: "prod", traffic: "100 rps, p50" }),
802
+ plugins: [], serializers: [],
803
+ });
804
+
805
+ expect(exit).toBe(0);
806
+ const ir = JSON.parse(stdoutBuf.join("\n")) as {
807
+ nodes: Array<{ attrs: Record<string, unknown> }>;
808
+ meta?: { _behaviour?: { refusal?: { reason?: string }; behaviour?: string } };
809
+ };
810
+ // behold branches on `refusal` before it looks for `engine`; the
811
+ // envelope is what makes it findable at all.
812
+ expect(ir.meta?._behaviour?.refusal?.reason).toMatch(/CHANT_BEHAVIOUR_ENGINE/);
813
+ expect(ir.meta?._behaviour?.behaviour).toBe("v1");
814
+ for (const n of ir.nodes) expect("_behaviour" in n.attrs).toBe(false);
815
+ });
816
+
817
+ test("no predicting lexicon: the graph is served unchanged, and says so once", async () => {
818
+ liveEstate();
819
+ loadPluginsMock.mockResolvedValue([
820
+ { name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
821
+ ]);
822
+
823
+ const exit = await runGraph({
824
+ args: makeArgs({ format: "ir", live: true, overlay: true, env: "prod", traffic: "100 rps, p50" }),
825
+ plugins: [], serializers: [],
826
+ });
827
+
828
+ expect(exit).toBe(0);
829
+ expect(stderrBuf.join("\n")).toContain("no installed lexicon implements predictBehaviour");
830
+ expect(JSON.parse(stdoutBuf.join("\n")).meta?._behaviour).toBeUndefined();
831
+ });
832
+
833
+ test("a thrown engine costs the prediction, never the graph", async () => {
834
+ liveEstate();
835
+ loadPluginsMock.mockResolvedValue([
836
+ predictingPlugin(() => Promise.reject(new Error("socket hang up"))),
837
+ ]);
838
+
839
+ const exit = await runGraph({
840
+ args: makeArgs({ format: "ir", live: true, overlay: true, env: "prod", traffic: "100 rps, p50" }),
841
+ plugins: [], serializers: [],
842
+ });
843
+
844
+ // The estate graph is what was asked for; a broken engine must not
845
+ // take it away.
846
+ expect(exit).toBe(0);
847
+ expect(stderrBuf.join("\n")).toContain("socket hang up");
848
+ const ir = JSON.parse(stdoutBuf.join("\n")) as { nodes: unknown[]; meta?: Record<string, unknown> };
849
+ expect(ir.nodes.length).toBeGreaterThan(0);
850
+ expect(ir.meta?._behaviour).toBeUndefined();
851
+ });
852
+
853
+ test("two predicting lexicons is refused rather than attributed to one", async () => {
854
+ liveEstate();
855
+ loadPluginsMock.mockResolvedValue([
856
+ predictingPlugin(() => Promise.resolve(report())),
857
+ { ...predictingPlugin(() => Promise.resolve(report())), name: "gcp" },
858
+ ]);
859
+
860
+ const exit = await runGraph({
861
+ args: makeArgs({ format: "ir", live: true, overlay: true, env: "prod", traffic: "100 rps, p50" }),
862
+ plugins: [], serializers: [],
863
+ });
864
+
865
+ // One `meta._behaviour` key and two engines: taking the first would
866
+ // put one engine's figures under the other's provenance.
867
+ expect(exit).toBe(1);
868
+ expect(stderrBuf.join("\n")).toContain("cannot merge two engines");
869
+ });
870
+ });
871
+
629
872
  // #1279 — `graph` had no `--at`, so anyone wanting the raw IR of a recorded
630
873
  // estate had to reach for the live endpoint. A snapshot could answer most
631
874
  // 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";
@@ -19,7 +22,8 @@ import { lintCommand } from "../commands/lint";
19
22
  import { loadPlugins, resolveProjectLexicons, collectBuildRootContributors } from "../plugins";
20
23
  import { readFileSync } from "node:fs";
21
24
  import { formatError, formatWarning, formatBold } from "../format";
22
- import type { CommandContext } from "../registry";
25
+ import type { CommandContext, ParsedArgs } from "../registry";
26
+ import type { LexiconPlugin } from "../../lexicon";
23
27
  import { computeComponentGraph, generateComponentsPipeline } from "../../components/cli-support";
24
28
  import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
25
29
  import type { BuildParamProvenance } from "../../provenance";
@@ -434,6 +438,19 @@ async function runGraphLive(
434
438
  }
435
439
  }
436
440
 
441
+ // Behaviour overlay (#2377, epic #2355): carry a prediction on the graph.
442
+ // After the drift overlay so it lands on the final node set beside `_status`,
443
+ // and before the lens so a lens filters a predicted node like any other.
444
+ {
445
+ const raw = buildResult.outputs ? [...buildResult.outputs.values()][0] : undefined;
446
+ const predicted = await predictOntoIr(ir, plugins, args, {
447
+ environment: environment ?? "",
448
+ buildOutput: raw === undefined ? "" : typeof raw === "string" ? raw : (raw as SerializerResult).primary,
449
+ });
450
+ if (predicted === REFUSED) return 1;
451
+ ir = predicted;
452
+ }
453
+
437
454
  if (args.lens) {
438
455
  try {
439
456
  ir = applyLens(ir, parseLens(args.lens, { up: args.up, down: args.down }));
@@ -654,6 +671,82 @@ async function buildPipelineProjection(
654
671
  * Lint-gated: the IR represents valid infra, so we refuse to emit for source that
655
672
  * does not pass lint. Non-zero on discovery errors or a layout-engine failure.
656
673
  */
674
+ /** Returned instead of a graph when the prediction cannot be attempted honestly. */
675
+ const REFUSED = Symbol("behaviour-refused");
676
+
677
+ /**
678
+ * Carry a prediction on `ir`, if one was asked for (#2377, epic #2355).
679
+ *
680
+ * Shared by both graph paths, because the epic wants both: "the live path
681
+ * predicts the account as it stands, drift included. The declared path
682
+ * predicts the file. The two are shown as a delta" (#2355). A delta between
683
+ * two shapes that reached the engine by different routes is the worst possible
684
+ * input to a delta, so there is one route.
685
+ *
686
+ * `--traffic` is the trigger, and it is the trigger rather than the engine
687
+ * variable because the contract forbids defaulting the level
688
+ * (../../behaviour.ts): every figure an engine returns is *at* a stated
689
+ * traffic, so inventing one would silently decide what every number on the
690
+ * graph means. Without the flag nothing is asked, neither key appears, and a
691
+ * consumer reads that absence as "not looked" rather than as a refusal.
692
+ */
693
+ async function predictOntoIr(
694
+ ir: GraphIR,
695
+ plugins: LexiconPlugin[],
696
+ args: ParsedArgs,
697
+ ctx: { environment: string; buildOutput: string },
698
+ ): Promise<GraphIR | typeof REFUSED> {
699
+ if (!args.traffic) return ir;
700
+
701
+ const predicting = plugins.filter((p) => p.predictBehaviour);
702
+ if (predicting.length === 0) {
703
+ console.error(formatWarning({
704
+ message:
705
+ "--traffic: no installed lexicon implements predictBehaviour, so nothing was predicted. The graph is unchanged.",
706
+ }));
707
+ return ir;
708
+ }
709
+ if (predicting.length > 1) {
710
+ // Two engines would mean two `meta._behaviour` values and one key. Rather
711
+ // than pick, say so: a merged prediction across engines is not defined by
712
+ // the contract, and silently taking the first would attribute one engine's
713
+ // figures to the other's provenance.
714
+ console.error(formatError({
715
+ message: `--traffic: ${predicting.length} lexicons implement predictBehaviour (${predicting.map((p) => p.name).join(", ")}) — chant cannot merge two engines' figures onto one graph.`,
716
+ hint: "Predict one lexicon at a time, or drop --traffic to graph without a prediction",
717
+ }));
718
+ return REFUSED;
719
+ }
720
+
721
+ const request = behaviourRequestFromIr(ir, {
722
+ environment: ctx.environment,
723
+ traffic: args.traffic,
724
+ buildOutput: ctx.buildOutput,
725
+ // `unknown`, for the reason the declared path claims it
726
+ // (../../op/activities/predict-behaviour.ts): reference edges resolved,
727
+ // containment absent, and no vocabulary here for "this kind is a boundary
728
+ // whose containment is missing". Claiming `complete` would be true about
729
+ // references and false about the graph; `partial` has to name its gap as
730
+ // `dangling` or `unresolvedKinds`, and this gap is neither.
731
+ edgeCoverage: { verdict: "unknown" },
732
+ ...(args.owned === undefined ? {} : { owned: args.owned }),
733
+ });
734
+
735
+ try {
736
+ // A refusal is a value, not a throw: it comes back as a result and rides on
737
+ // `meta._behaviour` whole, which is the one shape behold's reader branches
738
+ // on (../../behaviour-overlay.ts).
739
+ return applyBehaviourOverlay(ir, behaviourOverlay(await predicting[0].predictBehaviour!(request)));
740
+ } catch (err) {
741
+ // The estate graph is the thing asked for; a broken engine must not take it
742
+ // away. Say what failed and serve the graph without the block.
743
+ console.error(formatWarning({
744
+ message: `--traffic: ${predicting[0].name}'s predictBehaviour failed (${err instanceof Error ? err.message : String(err)}) — showing the graph without a prediction`,
745
+ }));
746
+ return ir;
747
+ }
748
+ }
749
+
657
750
  async function runGraphView(
658
751
  ctx: CommandContext,
659
752
  format: "ir" | "mermaid" | "dot" | "layout",
@@ -710,6 +803,24 @@ async function runGraphView(
710
803
  // Build the base IR, focus with a lens (declarable-level, most precise), then
711
804
  // apply the detail tier — so e.g. blast:<resource> works before any collapse.
712
805
  let ir: GraphIR = buildGraphIr(result.entities, projectPath);
806
+
807
+ // The declared half of the epic's delta (#2355): this path predicts the
808
+ // file, the live path predicts the account. Same helper, so the two shapes
809
+ // reach the engine by one route and stay comparable.
810
+ {
811
+ const plugins = ctx.plugins.length > 0
812
+ ? ctx.plugins
813
+ : await loadPlugins(await resolveProjectLexicons(projectPath));
814
+ const predicted = await predictOntoIr(ir, plugins as LexiconPlugin[], ctx.args, {
815
+ // No environment on the declared path: the file is not deployed anywhere
816
+ // yet, and naming one would claim it was.
817
+ environment: ctx.args.env ?? "",
818
+ buildOutput: "",
819
+ });
820
+ if (predicted === REFUSED) return 1;
821
+ ir = predicted;
822
+ }
823
+
713
824
  if (ctx.args.lens) {
714
825
  try {
715
826
  ir = applyLens(ir, parseLens(ctx.args.lens, { up: ctx.args.up, down: ctx.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,12 @@ 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
+ --traffic "<level>": ask the project's predicting
603
+ lexicon what the estate does at that traffic level, and
604
+ carry each entity's prediction on the IR. On the
605
+ declared graph it predicts the file; with --live it
606
+ predicts the account. The level reaches the engine
607
+ verbatim; without the flag nothing is asked (#2377)
598
608
 
599
609
  Lifecycle (alias: lc):
600
610
  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