@intentius/chant 0.11.0 → 0.12.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.
@@ -1 +1 @@
1
- {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/graph.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAOnE"}
1
+ {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/graph.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAOnE"}
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAKA,OAAO,EAAmC,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAe9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA6IpD"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAKA,OAAO,EAAmC,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAe9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAiJpD"}
@@ -64,6 +64,11 @@ export interface ParsedArgs {
64
64
  up?: boolean;
65
65
  /** `chant graph --lens blast:<node> --down` — include downstream dependents */
66
66
  down?: boolean;
67
+ /** `chant graph --format layout --node-sizes <json|-|@file>` — painter-measured
68
+ * node footprints `{id:{w,h}}` so the layout spaces for real card sizes (#509). */
69
+ nodeSizes?: string;
70
+ /** `chant graph --format layout --layout-engine dagre|graphviz` (default dagre). */
71
+ layoutEngine?: string;
67
72
  /** `chant lifecycle affected --base <ref>` — base git ref to diff against */
68
73
  base?: string;
69
74
  /** `chant lifecycle affected --head <ref>` — head git ref (default: working tree) */
@@ -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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"graph-ir.d.ts","sourceRoot":"","sources":["../src/graph-ir.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,UAAU,EAAgB,MAAM,cAAc,CAAC;AAK7D;;;;;;;;GAQG;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;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,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,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACpC;AAED,gFAAgF;AAChF,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,QAAQ,CAAC;CAClB;AAyID;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EACjC,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAmDT"}
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;AAK7D;;;;;;;;GAQG;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;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,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,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACpC;AAED,gFAAgF;AAChF,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,QAAQ,CAAC;CAClB;AA6ID;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EACjC,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAmDT"}
@@ -1,39 +1,87 @@
1
+ import type { GraphIR } from "./graph-ir.js";
1
2
  /**
2
3
  * Layout-position export — node coordinates a custom painter consumes (the
3
- * rackattack pattern: Graphviz lays out, the painter draws). Graphviz is used
4
- * for LAYOUT ONLY; its rendering is discarded. See issue #497 / epic #492.
4
+ * rackattack pattern: an engine lays out, the painter draws). The engine is used
5
+ * for LAYOUT ONLY; any rendering it can do is discarded. See issue #497 / epic
6
+ * #492, and #509 for the size-aware redesign.
5
7
  *
6
- * The {@link LayoutEngine} interface exists so a pure-JS engine (elkjs/dagre)
7
- * can drop in later for a zero-native-dependency path closing the install gap
8
- * so a painter can run without `dot`.
8
+ * The engine takes an **engine-neutral {@link LayoutInput}** sized nodes plus
9
+ * edges not a DOT string, so the painter's real node sizes drive spacing
10
+ * (without sizes a layout engine packs for tiny default boxes and big cards
11
+ * collide). Two engines implement it:
12
+ *
13
+ * - {@link DagreLayout} (default) — pure JS, size-aware, **no native dependency**.
14
+ * - {@link GraphvizLayout} — opt-in; shells `dot`. Honours `groups` as clusters.
15
+ *
16
+ * Coordinate convention (both engines): **y grows up, origin bottom-left**, as
17
+ * `dot -Tjson` reports. A painter flips y to read top-to-bottom.
9
18
  */
10
- /** A laid-out position in the engine's coordinate space. */
19
+ /** A laid-out position in the engine's coordinate space (y-up). */
11
20
  export interface Point {
12
21
  x: number;
13
22
  y: number;
14
23
  }
15
- /** Node positions plus the overall canvas size a painter needs. */
24
+ /** Node positions plus the overall canvas size a painter needs. Centres are
25
+ * y-up (origin bottom-left). `w`/`h` echo the footprint the engine laid out
26
+ * with, so a painter can route edges to card borders rather than centres. */
16
27
  export interface Layout {
17
28
  /** Canvas width in the engine's coordinate space. */
18
29
  width: number;
19
30
  /** Canvas height in the engine's coordinate space. */
20
31
  height: number;
21
- /** Each node's centre position, ordered by id for deterministic output. */
32
+ /** Each node's centre position (+ footprint), ordered by id for determinism. */
22
33
  nodes: Array<{
23
34
  id: string;
35
+ w?: number;
36
+ h?: number;
24
37
  } & Point>;
25
38
  }
26
- /** Turns DOT into node positions. The painter consumes the result; it never
27
- * asks the engine to paint. */
39
+ /** A node's painted footprint, in layout units (≈ px / points). */
40
+ export interface NodeSize {
41
+ w: number;
42
+ h: number;
43
+ }
44
+ /** Engine-neutral layout input: sized nodes + edges (+ optional groups). */
45
+ export interface LayoutInput {
46
+ nodes: Array<{
47
+ id: string;
48
+ } & NodeSize>;
49
+ edges: Array<{
50
+ from: string;
51
+ to: string;
52
+ }>;
53
+ /** Group name → member ids, for cluster-aware engines (graphviz subgraphs). */
54
+ groups?: Record<string, string[]>;
55
+ }
56
+ /** Turns sized nodes + edges into node positions. The painter consumes the
57
+ * result; it never asks the engine to paint. */
28
58
  export interface LayoutEngine {
29
59
  readonly name: string;
30
- layout(dot: string): Promise<Layout>;
60
+ layout(input: LayoutInput): Promise<Layout>;
61
+ }
62
+ /** Graphviz's default node box, in layout units — the fallback for a node the
63
+ * painter didn't measure, so a size-less call still lays out as it always did. */
64
+ export declare const DEFAULT_NODE_SIZE: NodeSize;
65
+ /** Build engine input from an IR and a painter-measured size map (id → {w,h}).
66
+ * Nodes absent from the map fall back to {@link DEFAULT_NODE_SIZE}. Pure. */
67
+ export declare function toLayoutInput(ir: GraphIR, sizes?: Record<string, NodeSize>): LayoutInput;
68
+ /**
69
+ * Layout via dagre — pure JS, size-aware, no native dependency. The default.
70
+ * Fed nodes/edges in id order so the result is deterministic. dagre's space is
71
+ * y-down; we flip to y-up so the {@link Layout} contract matches Graphviz.
72
+ */
73
+ export declare class DagreLayout implements LayoutEngine {
74
+ readonly name = "dagre";
75
+ layout(input: LayoutInput): Promise<Layout>;
31
76
  }
32
- /** Layout via `dot -Tjson`. Requires Graphviz (`brew install graphviz`). */
77
+ /** Layout via `dot -Tjson`. Opt-in; requires Graphviz (`brew install graphviz`).
78
+ * Lays out with real node sizes (`fixedsize`) and honours `groups` as clusters. */
33
79
  export declare class GraphvizLayout implements LayoutEngine {
34
80
  readonly name = "graphviz";
35
- layout(dot: string): Promise<Layout>;
81
+ layout(input: LayoutInput): Promise<Layout>;
36
82
  }
83
+ /** Resolve a layout engine by name. Defaults to dagre (no native dependency). */
84
+ export declare function getLayoutEngine(name?: string): LayoutEngine;
37
85
  /** Parse `dot -Tjson` output into a {@link Layout}. Pure; exported for testing. */
38
86
  export declare function parseDotJson(json: string): Layout;
39
87
  //# sourceMappingURL=graph-layout.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"graph-layout.d.ts","sourceRoot":"","sources":["../src/graph-layout.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,4DAA4D;AAC5D,MAAM,WAAW,KAAK;IACpB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,mEAAmE;AACnE,MAAM,WAAW,MAAM;IACrB,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,KAAK,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAC,CAAC;CACtC;AAED;+BAC+B;AAC/B,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACtC;AAED,4EAA4E;AAC5E,qBAAa,cAAe,YAAW,YAAY;IACjD,QAAQ,CAAC,IAAI,cAAc;IAErB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAG3C;AAsCD,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAiBjD"}
1
+ {"version":3,"file":"graph-layout.d.ts","sourceRoot":"","sources":["../src/graph-layout.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C;;;;;;;;;;;;;;;;GAgBG;AAEH,mEAAmE;AACnE,MAAM,WAAW,KAAK;IACpB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED;;6EAE6E;AAC7E,MAAM,WAAW,MAAM;IACrB,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,KAAK,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAC,CAAC;CAC9D;AAED,mEAAmE;AACnE,MAAM,WAAW,QAAQ;IACvB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,4EAA4E;AAC5E,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,QAAQ,CAAC,CAAC;IACxC,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3C,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACnC;AAED;gDACgD;AAChD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7C;AAED;kFACkF;AAClF,eAAO,MAAM,iBAAiB,EAAE,QAA2B,CAAC;AAM5D;6EAC6E;AAC7E,wBAAgB,aAAa,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAM,GAAG,WAAW,CAM5F;AAiBD;;;;GAIG;AACH,qBAAa,WAAY,YAAW,YAAY;IAC9C,QAAQ,CAAC,IAAI,WAAW;IAElB,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC;CA0BlD;AAED;mFACmF;AACnF,qBAAa,cAAe,YAAW,YAAY;IACjD,QAAQ,CAAC,IAAI,cAAc;IAErB,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC;CAGlD;AAED,iFAAiF;AACjF,wBAAgB,eAAe,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,YAAY,CAM3D;AA0ED,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqBjD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.11.0",
3
+ "version": "0.12.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",
@@ -65,6 +65,7 @@
65
65
  "prepack": "npm run build"
66
66
  },
67
67
  "dependencies": {
68
+ "@dagrejs/dagre": "^3.0.0",
68
69
  "fflate": "^0.8.2",
69
70
  "picomatch": "^4.0.3",
70
71
  "tsx": "^4.0.0",
@@ -18,14 +18,12 @@ vi.mock("../commands/lint", () => ({
18
18
  lintCommand: () => lintMock(),
19
19
  }));
20
20
 
21
- // Avoid shelling out to graphviz in tests; the format dispatch is what matters.
21
+ // Avoid running a real layout engine in tests; the format dispatch + size/engine
22
+ // plumbing is what matters here (engines have their own unit tests).
22
23
  const layoutMock = vi.fn();
23
24
  vi.mock("../../graph-layout", () => ({
24
- GraphvizLayout: class {
25
- layout() {
26
- return layoutMock();
27
- }
28
- },
25
+ toLayoutInput: (ir: { nodes: { id: string }[] }, sizes: unknown) => ({ ir, sizes }),
26
+ getLayoutEngine: (name?: string) => ({ name: name ?? "dagre", layout: (input: unknown) => layoutMock(input) }),
29
27
  }));
30
28
 
31
29
  const { runGraph } = await import("./graph");
@@ -7,8 +7,9 @@ import { applyDetail, type DetailLevel } from "../../graph-detail";
7
7
  import { applyLens, parseLens } from "../../graph-lens";
8
8
  import { toMermaid } from "../../graph-mermaid";
9
9
  import { toDot } from "../../graph-dot";
10
- import { GraphvizLayout } from "../../graph-layout";
10
+ import { getLayoutEngine, toLayoutInput, type NodeSize } from "../../graph-layout";
11
11
  import { lintCommand } from "../commands/lint";
12
+ import { readFileSync } from "node:fs";
12
13
  import { formatError, formatWarning, formatBold } from "../format";
13
14
  import type { CommandContext } from "../registry";
14
15
 
@@ -30,9 +31,11 @@ export async function runGraph(ctx: CommandContext): Promise<number> {
30
31
  /**
31
32
  * `chant graph --format ir|mermaid|dot|layout` — build the graph IR (honouring
32
33
  * `--detail`) and emit it as JSON, a Mermaid flowchart, Graphviz DOT, or node
33
- * positions from a layout engine. Lint-gated: the IR represents valid infra, so
34
- * we refuse to emit for source that does not pass lint. Non-zero on discovery
35
- * errors, or on a missing `dot` for `--format layout`.
34
+ * positions from a layout engine. `layout` takes optional painter-measured
35
+ * `--node-sizes` so spacing fits real node footprints, and defaults to the dagre
36
+ * engine (no native dependency); `--layout-engine graphviz` opts into `dot`.
37
+ * Lint-gated: the IR represents valid infra, so we refuse to emit for source that
38
+ * does not pass lint. Non-zero on discovery errors or a layout-engine failure.
36
39
  */
37
40
  async function runGraphView(
38
41
  ctx: CommandContext,
@@ -86,7 +89,9 @@ async function runGraphView(
86
89
  return 0;
87
90
  case "layout":
88
91
  try {
89
- const layout = await new GraphvizLayout().layout(toDot(ir));
92
+ const sizes = readNodeSizes(ctx.args.nodeSizes);
93
+ const engine = getLayoutEngine(ctx.args.layoutEngine);
94
+ const layout = await engine.layout(toLayoutInput(ir, sizes));
90
95
  console.log(JSON.stringify(layout, null, 2));
91
96
  return 0;
92
97
  } catch (err) {
@@ -100,6 +105,40 @@ async function runGraphView(
100
105
  }
101
106
  }
102
107
 
108
+ /**
109
+ * Resolve the `--node-sizes` value into a `{ id: {w, h} }` map. The spec is one
110
+ * of: inline JSON, `-` (read JSON from stdin, to dodge arg-length limits), or
111
+ * `@path` (read from a file). Empty/absent → no sizes (engine uses defaults).
112
+ * Throws on malformed JSON so a typo fails loudly rather than mis-laying out.
113
+ */
114
+ function readNodeSizes(spec?: string): Record<string, NodeSize> {
115
+ if (!spec) return {};
116
+ let raw: string;
117
+ if (spec === "-") raw = readFileSync(0, "utf8");
118
+ else if (spec.startsWith("@")) raw = readFileSync(spec.slice(1), "utf8");
119
+ else raw = spec;
120
+ raw = raw.trim();
121
+ if (!raw) return {};
122
+
123
+ let parsed: unknown;
124
+ try {
125
+ parsed = JSON.parse(raw);
126
+ } catch (err) {
127
+ throw new Error(`--node-sizes is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
128
+ }
129
+ if (typeof parsed !== "object" || parsed === null) {
130
+ throw new Error("--node-sizes must be a JSON object mapping node id → {w, h}");
131
+ }
132
+ const out: Record<string, NodeSize> = {};
133
+ for (const [id, val] of Object.entries(parsed as Record<string, unknown>)) {
134
+ const v = val as { w?: unknown; h?: unknown };
135
+ if (typeof v?.w === "number" && typeof v?.h === "number" && v.w > 0 && v.h > 0) {
136
+ out[id] = { w: v.w, h: v.h };
137
+ }
138
+ }
139
+ return out;
140
+ }
141
+
103
142
  async function runOpGraph(): Promise<number> {
104
143
  const { ops, errors } = await discoverOps();
105
144
  for (const err of errors) console.error(formatError({ message: err }));
package/src/cli/main.ts CHANGED
@@ -134,6 +134,10 @@ export function parseArgs(args: string[]): ParsedArgs {
134
134
  result.up = true;
135
135
  } else if (arg === "--down") {
136
136
  result.down = true;
137
+ } else if (arg === "--node-sizes") {
138
+ result.nodeSizes = args[++i];
139
+ } else if (arg === "--layout-engine") {
140
+ result.layoutEngine = args[++i];
137
141
  } else if (arg === "--base") {
138
142
  result.base = args[++i];
139
143
  } else if (arg === "--head") {
@@ -200,8 +204,10 @@ Ops:
200
204
 
201
205
  graph Show Op dependency graph (--stacks for cross-stack order,
202
206
  --format ir|mermaid|dot|layout for the lint-gated graph IR,
203
- a Mermaid flowchart, Graphviz DOT, or node positions
204
- (layout needs graphviz);
207
+ a Mermaid flowchart, Graphviz DOT, or node positions;
208
+ layout uses dagre by default (no native dep) — pass
209
+ --node-sizes <json|-|@file> for size-aware spacing,
210
+ --layout-engine graphviz to use dot instead;
205
211
  --detail 0..3: stacks|composites|declarables|attributes;
206
212
  --lens lexicon:<n>|stack:<n>|blast:<node> (--up/--down))
207
213
 
@@ -65,6 +65,11 @@ export interface ParsedArgs {
65
65
  up?: boolean;
66
66
  /** `chant graph --lens blast:<node> --down` — include downstream dependents */
67
67
  down?: boolean;
68
+ /** `chant graph --format layout --node-sizes <json|-|@file>` — painter-measured
69
+ * node footprints `{id:{w,h}}` so the layout spaces for real card sizes (#509). */
70
+ nodeSizes?: string;
71
+ /** `chant graph --format layout --layout-engine dagre|graphviz` (default dagre). */
72
+ layoutEngine?: string;
68
73
  /** `chant lifecycle affected --base <ref>` — base git ref to diff against */
69
74
  base?: string;
70
75
  /** `chant lifecycle affected --head <ref>` — head git ref (default: working tree) */
@@ -42,6 +42,30 @@ describe("buildGraphIr", () => {
42
42
  });
43
43
  });
44
44
 
45
+ test("treats a foreign AttrRef (different @intentius/chant copy) as a ref, not an opaque intrinsic (#511)", () => {
46
+ // A lexicon built against a *separate* copy of chant produces AttrRefs that
47
+ // fail `instanceof AttrRef` here but carry the global-symbol brand + shape.
48
+ // (This is the pinhole install scenario; before #511 it flattened to
49
+ // `{$intrinsic}` with no edge.)
50
+ const vpc = decl({ lexicon: "aws", entityType: "Vpc" });
51
+ const foreignRef = {
52
+ [Symbol.for("chant.intrinsic")]: true,
53
+ parent: new WeakRef(vpc),
54
+ attribute: "VpcId",
55
+ _setLogicalName() {}, // shape utils.isAttrRefLike duck-types on
56
+ getLogicalName: () => undefined, // resolves via object identity in the reverse map
57
+ };
58
+ const sg = decl({ lexicon: "aws", entityType: "SecurityGroup", props: { VpcId: foreignRef } });
59
+ const entities = new Map<string, Declarable>([
60
+ ["vpc", vpc],
61
+ ["sg", sg],
62
+ ]);
63
+
64
+ const ir = buildGraphIr(entities);
65
+ expect(ir.edges).toEqual([{ from: "sg", to: "vpc", kind: "ref", viaAttr: "VpcId" }]);
66
+ expect(ir.nodes.find((n) => n.id === "sg")!.attrs).toEqual({ VpcId: { $ref: "vpc.VpcId" } });
67
+ });
68
+
45
69
  test("excludes property-kind declarables and keeps resources", () => {
46
70
  const resource = decl({ lexicon: "k8s", entityType: "Deployment" });
47
71
  const prop = decl({ lexicon: "k8s", entityType: "Probe", kind: "property" as const });
package/src/graph-ir.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { relative, isAbsolute } from "node:path";
2
2
  import { AttrRef } from "./attrref";
3
+ import { isAttrRefLike } from "./utils";
3
4
  import { type Declarable, isDeclarable } from "./declarable";
4
5
  import { isLexiconOutput } from "./lexicon-output";
5
6
  import { getProvenance } from "./provenance";
@@ -113,7 +114,11 @@ function project(value: unknown, seen: Set<unknown>, reverse: Map<object, string
113
114
  if (t === "string" || t === "number" || t === "boolean") return value;
114
115
  if (t !== "object") return undefined; // functions, symbols, undefined
115
116
 
116
- if (value instanceof AttrRef) {
117
+ // Duck-type, not `instanceof`: a lexicon built against a different copy of
118
+ // `@intentius/chant` produces AttrRefs that fail `instanceof AttrRef` but carry
119
+ // the same shape. Without this, a real cross-resource reference falls through to
120
+ // the intrinsic branch below and is silently flattened to `{$intrinsic}` (#511).
121
+ if (isAttrRefLike(value)) {
117
122
  const to = refTarget(value, reverse);
118
123
  return { $ref: to ? `${to}.${value.attribute}` : value.attribute } satisfies AttrRefEnvelope;
119
124
  }
@@ -192,7 +197,7 @@ function collectEdges(
192
197
  const seen = new Set<unknown>();
193
198
  const visit = (value: unknown, viaAttr: string): void => {
194
199
  if (value === null || typeof value !== "object") return;
195
- if (value instanceof AttrRef) {
200
+ if (isAttrRefLike(value)) {
196
201
  const to = refTarget(value, reverse);
197
202
  if (to && to !== from && nodeIds.has(to)) {
198
203
  edges.push({ from, to, kind: "ref", viaAttr });
@@ -0,0 +1,131 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ toLayoutInput,
4
+ DagreLayout,
5
+ GraphvizLayout,
6
+ getLayoutEngine,
7
+ parseDotJson,
8
+ DEFAULT_NODE_SIZE,
9
+ type LayoutInput,
10
+ } from "./graph-layout";
11
+ import type { GraphIR } from "./graph-ir";
12
+
13
+ const ir: GraphIR = {
14
+ nodes: [
15
+ { id: "vpc", kind: "Vpc", lexicon: "aws", attrs: {} },
16
+ { id: "subnet", kind: "Subnet", lexicon: "aws", attrs: {} },
17
+ ],
18
+ edges: [{ from: "subnet", to: "vpc", kind: "ref", viaAttr: "VpcId" }],
19
+ groups: { byLexicon: { aws: ["vpc", "subnet"] } },
20
+ };
21
+
22
+ describe("toLayoutInput", () => {
23
+ it("maps IR nodes/edges and carries groups", () => {
24
+ const input = toLayoutInput(ir, { vpc: { w: 180, h: 60 }, subnet: { w: 180, h: 76 } });
25
+ expect(input.nodes).toEqual([
26
+ { id: "vpc", w: 180, h: 60 },
27
+ { id: "subnet", w: 180, h: 76 },
28
+ ]);
29
+ expect(input.edges).toEqual([{ from: "subnet", to: "vpc" }]);
30
+ expect(input.groups).toEqual({ aws: ["vpc", "subnet"] });
31
+ });
32
+
33
+ it("falls back to the default box for unmeasured nodes", () => {
34
+ const input = toLayoutInput(ir);
35
+ expect(input.nodes.every((n) => n.w === DEFAULT_NODE_SIZE.w && n.h === DEFAULT_NODE_SIZE.h)).toBe(true);
36
+ });
37
+ });
38
+
39
+ describe("getLayoutEngine", () => {
40
+ it("defaults to dagre (no native dependency)", () => {
41
+ expect(getLayoutEngine().name).toBe("dagre");
42
+ expect(getLayoutEngine("dagre").name).toBe("dagre");
43
+ });
44
+ it("returns graphviz on request", () => {
45
+ expect(getLayoutEngine("graphviz").name).toBe("graphviz");
46
+ });
47
+ it("throws on an unknown engine", () => {
48
+ expect(() => getLayoutEngine("elk")).toThrow(/unknown layout engine/i);
49
+ });
50
+ });
51
+
52
+ describe("DagreLayout", () => {
53
+ const input: LayoutInput = {
54
+ nodes: [
55
+ { id: "vpc", w: 180, h: 60 },
56
+ { id: "subnetA", w: 180, h: 76 },
57
+ { id: "subnetB", w: 180, h: 76 },
58
+ ],
59
+ edges: [
60
+ { from: "subnetA", to: "vpc" },
61
+ { from: "subnetB", to: "vpc" },
62
+ ],
63
+ };
64
+
65
+ it("lays out with no native dependency and echoes sizes", async () => {
66
+ const layout = await new DagreLayout().layout(input);
67
+ expect(layout.nodes.map((n) => n.id)).toEqual(["subnetA", "subnetB", "vpc"]);
68
+ expect(layout.width).toBeGreaterThan(0);
69
+ expect(layout.nodes.find((n) => n.id === "vpc")).toMatchObject({ w: 180, h: 60 });
70
+ });
71
+
72
+ it("spaces sized cards so none overlap", async () => {
73
+ const layout = await new DagreLayout().layout(input);
74
+ const r = layout.nodes.map((n) => ({
75
+ id: n.id,
76
+ x0: n.x - (n.w ?? 0) / 2,
77
+ x1: n.x + (n.w ?? 0) / 2,
78
+ y0: n.y - (n.h ?? 0) / 2,
79
+ y1: n.y + (n.h ?? 0) / 2,
80
+ }));
81
+ for (let i = 0; i < r.length; i++) {
82
+ for (let j = i + 1; j < r.length; j++) {
83
+ const xOverlap = Math.min(r[i].x1, r[j].x1) - Math.max(r[i].x0, r[j].x0);
84
+ const yOverlap = Math.min(r[i].y1, r[j].y1) - Math.max(r[i].y0, r[j].y0);
85
+ expect(xOverlap > 0 && yOverlap > 0).toBe(false);
86
+ }
87
+ }
88
+ });
89
+
90
+ it("emits y-up coordinates matching graphviz (edge from drawn above to)", async () => {
91
+ // Edges are subnet → vpc (consumer → producer). rankdir=TB draws the tail
92
+ // above the head, so subnets sit above vpc — same as `dot`. In y-up space
93
+ // that means subnets have the larger y. (Verified against GraphvizLayout.)
94
+ const layout = await new DagreLayout().layout(input);
95
+ const vpc = layout.nodes.find((n) => n.id === "vpc")!;
96
+ const subnetA = layout.nodes.find((n) => n.id === "subnetA")!;
97
+ expect(subnetA.y).toBeGreaterThan(vpc.y);
98
+ });
99
+
100
+ it("is deterministic", async () => {
101
+ const a = await new DagreLayout().layout(input);
102
+ const b = await new DagreLayout().layout(input);
103
+ expect(a).toEqual(b);
104
+ });
105
+ });
106
+
107
+ describe("parseDotJson", () => {
108
+ it("reads bounds, positions, and echoes node size in points", () => {
109
+ const json = JSON.stringify({
110
+ bb: "0,0,200,100",
111
+ objects: [
112
+ { name: "b", pos: "50,80", width: "2.5", height: "0.5" },
113
+ { name: "a", pos: "50,20" },
114
+ ],
115
+ });
116
+ const layout = parseDotJson(json);
117
+ expect(layout).toMatchObject({ width: 200, height: 100 });
118
+ expect(layout.nodes.map((n) => n.id)).toEqual(["a", "b"]); // sorted
119
+ expect(layout.nodes.find((n) => n.id === "b")).toMatchObject({ x: 50, y: 80, w: 180, h: 36 });
120
+ });
121
+
122
+ it("rejects zero bounds", () => {
123
+ expect(() => parseDotJson(JSON.stringify({ bb: "0,0,0,0", objects: [] }))).toThrow(/zero graph bounds/);
124
+ });
125
+ });
126
+
127
+ describe("GraphvizLayout", () => {
128
+ it("exposes the graphviz engine name", () => {
129
+ expect(new GraphvizLayout().name).toBe("graphviz");
130
+ });
131
+ });
Binary file