@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.
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +5 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-layout.d.ts +61 -13
- package/dist/graph-layout.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/cli/handlers/graph.test.ts +4 -6
- package/src/cli/handlers/graph.ts +44 -5
- package/src/cli/main.ts +8 -2
- package/src/cli/registry.ts +5 -0
- package/src/graph-ir.test.ts +24 -0
- package/src/graph-ir.ts +7 -2
- package/src/graph-layout.test.ts +131 -0
- package/src/graph-layout.ts +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/graph.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/cli/main.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/cli/registry.d.ts
CHANGED
|
@@ -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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"}
|
package/dist/graph-ir.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-ir.d.ts","sourceRoot":"","sources":["../src/graph-ir.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/graph-layout.d.ts
CHANGED
|
@@ -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:
|
|
4
|
-
* for LAYOUT ONLY;
|
|
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
27
|
-
|
|
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(
|
|
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`.
|
|
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(
|
|
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":"
|
|
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.
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
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 {
|
|
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.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
package/src/cli/registry.ts
CHANGED
|
@@ -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) */
|
package/src/graph-ir.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
+
});
|
package/src/graph-layout.ts
CHANGED
|
Binary file
|