@intentius/chant 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,8 @@ import type { CommandContext } from "../registry.js";
2
2
  /**
3
3
  * `chant graph` — the Op dependency graph by default; `--stacks` renders the
4
4
  * cross-stack apply-ordering graph (edges, order, waves) chant computes from
5
- * cross-lexicon references.
5
+ * cross-lexicon references; `--format ir` emits the full entity-graph IR
6
+ * (lint-gated) for diagram painters and the agentic diagrammer (#493).
6
7
  */
7
8
  export declare function runGraph(ctx: CommandContext): Promise<number>;
8
9
  //# sourceMappingURL=graph.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/graph.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAGnE"}
1
+ {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/graph.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAInE"}
@@ -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,CAqIpD"}
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,CAuIpD"}
@@ -56,6 +56,8 @@ export interface ParsedArgs {
56
56
  env?: string;
57
57
  /** `chant graph --stacks` — render the cross-stack apply-ordering graph */
58
58
  stacks?: boolean;
59
+ /** `chant graph --format ir --detail <0..3>` — graph IR detail tier */
60
+ detail?: number;
59
61
  /** `chant lifecycle affected --base <ref>` — base git ref to diff against */
60
62
  base?: string;
61
63
  /** `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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"}
@@ -0,0 +1,21 @@
1
+ import type { GraphIR } from "./graph-ir.js";
2
+ /**
3
+ * Detail tiers — the diagram "detail dial". Each level is a pure IR → IR
4
+ * transform over the base graph IR (no re-discovery), so every emitter and the
5
+ * painter get them for free. See issue #494 / epic #492.
6
+ *
7
+ * - 0 STACKS — one node per lexicon; edges are cross-lexicon dependencies
8
+ * - 1 COMPOSITES — composite instances collapsed to a single node each
9
+ * - 2 DECLARABLES — every resource (the base produced by buildGraphIr)
10
+ * - 3 ATTRIBUTES — declarables plus the producer attribute on each edge
11
+ */
12
+ export type DetailLevel = 0 | 1 | 2 | 3;
13
+ export declare const DETAIL: {
14
+ readonly STACKS: 0;
15
+ readonly COMPOSITES: 1;
16
+ readonly DECLARABLES: 2;
17
+ readonly ATTRIBUTES: 3;
18
+ };
19
+ /** Apply a detail tier to the base (declarable-level) IR. */
20
+ export declare function applyDetail(ir: GraphIR, level: DetailLevel): GraphIR;
21
+ //# sourceMappingURL=graph-detail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-detail.d.ts","sourceRoot":"","sources":["../src/graph-detail.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAkB,MAAM,YAAY,CAAC;AAE1D;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAExC,eAAO,MAAM,MAAM;;;;;CAKT,CAAC;AAEX,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAYpE"}
@@ -0,0 +1,78 @@
1
+ import { type Declarable } from "./declarable.js";
2
+ /**
3
+ * Graph IR — the engine-neutral, lint-gated representation of a project's
4
+ * resolved infrastructure graph. Painters (mermaid, graphviz, custom SVG) and
5
+ * the agentic diagrammer consume this; it is a pure function of lint-clean
6
+ * source. Every node traces to the file that declared it; every edge is a real
7
+ * cross-resource reference (AttrRef).
8
+ *
9
+ * Emitted by `chant graph --format ir`. See issue #493 / epic #492.
10
+ */
11
+ /** Where in the source a node came from. Entity-level (file), not a line map. */
12
+ export interface SourceLoc {
13
+ /** Path of the declaring file, relative to the project root when possible. */
14
+ file: string;
15
+ /**
16
+ * Line number, when available. Provenance is entity-level today, so this is
17
+ * usually absent — reserved for a future source map.
18
+ */
19
+ line?: number;
20
+ }
21
+ /** A reference in an attribute projection: `<producer>.<attribute>`. */
22
+ export interface AttrRefEnvelope {
23
+ $ref: string;
24
+ }
25
+ /** One resource in the graph. */
26
+ export interface IRNode {
27
+ /** Logical name (the export name, or composite-expanded name). */
28
+ id: string;
29
+ /** Resource type, e.g. "GkeCluster". */
30
+ kind: string;
31
+ /** Lexicon the resource belongs to, e.g. "gcp". */
32
+ lexicon: string;
33
+ /**
34
+ * Name of the composite *type* that expanded this node, when it came from one
35
+ * (e.g. "CockroachDbCluster").
36
+ */
37
+ compositeParent?: string;
38
+ /**
39
+ * The composite *instance* (export name) this node belongs to — shared by every
40
+ * node from the same composite call. Detail tiers collapse on this (#494).
41
+ */
42
+ compositeInstance?: string;
43
+ /** Literal/const/ref-resolved props. References appear as `{ $ref }`. */
44
+ attrs: Record<string, unknown>;
45
+ /** Where the node was declared. */
46
+ sourceLoc?: SourceLoc;
47
+ }
48
+ /** A directed dependency: `from` references an attribute of `to`. */
49
+ export interface IREdge {
50
+ from: string;
51
+ to: string;
52
+ /** "ref" for an AttrRef-derived edge. */
53
+ kind: "ref";
54
+ /** The consumer-side property the reference flows through, when derivable. */
55
+ viaAttr?: string;
56
+ /** The producer-side attribute referenced (e.g. "id"). Added at detail T3. */
57
+ toAttr?: string;
58
+ }
59
+ /** Grouping metadata for cluster/subgraph rendering. Maps group name -> node ids. */
60
+ export interface IRGroups {
61
+ byLexicon?: Record<string, string[]>;
62
+ byComposite?: Record<string, string[]>;
63
+ /** Reserved for stack grouping (#494). */
64
+ byStack?: Record<string, string[]>;
65
+ }
66
+ /** The full graph IR for a project at the default (declarable) detail level. */
67
+ export interface GraphIR {
68
+ nodes: IRNode[];
69
+ edges: IREdge[];
70
+ groups: IRGroups;
71
+ }
72
+ /**
73
+ * Build the graph IR from resolved entities (the output of `discover`). Pure and
74
+ * deterministic: nodes and edges are sorted, so the same source yields identical
75
+ * IR. `projectPath` relativizes source-file paths for portable output.
76
+ */
77
+ export declare function buildGraphIr(entities: Map<string, Declarable>, projectPath?: string): GraphIR;
78
+ //# sourceMappingURL=graph-ir.d.ts.map
@@ -0,0 +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"}
package/dist/index.d.ts CHANGED
@@ -24,6 +24,8 @@ export * from "./discovery/graph.js";
24
24
  export * from "./discovery/index.js";
25
25
  export * from "./discovery/cache.js";
26
26
  export * from "./build.js";
27
+ export * from "./graph-ir.js";
28
+ export * from "./graph-detail.js";
27
29
  export * from "./detectLexicon.js";
28
30
  export * from "./lint/parser.js";
29
31
  export * from "./lint/rule.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,QAAQ,CAAC;AACvB,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,yBAAyB,CAAC;AACxC,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAElC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAClE,cAAc,EAAE,iBAAiB,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC5F,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,QAAQ,CAAC;AACvB,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,yBAAyB,CAAC;AACxC,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAElC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAClE,cAAc,EAAE,iBAAiB,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC5F,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
@@ -12,8 +12,15 @@
12
12
  export interface EntityProvenance {
13
13
  /** Absolute path of the source file that declared (or exported) the entity. */
14
14
  sourceFile?: string;
15
- /** The composite that expanded this entity, when it came from one. */
15
+ /** The composite (type) that expanded this entity, when it came from one. */
16
16
  composite?: string;
17
+ /**
18
+ * The composite *instance* this entity belongs to — the export name of the
19
+ * top-level composite, shared by every member it expanded to. Distinguishes
20
+ * two instances of the same composite type, which `composite` cannot. Used to
21
+ * collapse a composite to a single node at coarse diagram detail levels (#494).
22
+ */
23
+ compositeInstance?: string;
17
24
  }
18
25
  /**
19
26
  * Merge provenance onto an entity. Non-enumerable so it is invisible to
@@ -1 +1 @@
1
- {"version":3,"file":"provenance.d.ts","sourceRoot":"","sources":["../src/provenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,MAAM,WAAW,gBAAgB;IAC/B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAc1E;AAED,6DAA6D;AAC7D,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAE1E"}
1
+ {"version":3,"file":"provenance.d.ts","sourceRoot":"","sources":["../src/provenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,MAAM,WAAW,gBAAgB;IAC/B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAe1E;AAED,6DAA6D;AAC7D,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAE1E"}
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Provider-agnostic reconcile primitive.
3
+ *
4
+ * The reusable machinery behind a declarative reconcile loop, with NO knowledge
5
+ * of any specific provider (GitHub, GitLab, a cloud, …): the change-set model,
6
+ * the generic collection diff (selective-by-omission + ownership-gated deletes),
7
+ * the plan renderer, and the guardrail framework (rename resolution + a removal
8
+ * cap + a pluggable check runner).
9
+ *
10
+ * A "warden" (e.g. github-warden) builds its provider-specific resource diffing,
11
+ * live-state types, and domain guardrails on top of this. It complements
12
+ * chant's `ownership.ts` marker contract: ownership markers make a `delete`
13
+ * precise; this module decides *which* entries are creates / updates / deletes
14
+ * in the first place.
15
+ *
16
+ * Consumed as `@intentius/chant/reconcile`. Pure and deterministic: no I/O,
17
+ * no clock.
18
+ */
19
+ /** A single field-level change: what the old value was and what it will become. */
20
+ export interface FieldChange {
21
+ field: string;
22
+ before: unknown;
23
+ after: unknown;
24
+ }
25
+ /** The kind of operation this change represents. */
26
+ export type ChangeKind = "create" | "update" | "delete";
27
+ /** A single entry in the change set. */
28
+ export interface ChangeSetEntry {
29
+ kind: ChangeKind;
30
+ /** High-level resource category (e.g. "team", "member", "branch-protection"). */
31
+ resourceType: string;
32
+ /**
33
+ * Unique key identifying this resource within its type.
34
+ * - For top-level resources: a single name (team slug, member login, …).
35
+ * - For nested resources: "<parent>/<child>" (e.g. "backend/alice").
36
+ */
37
+ key: string;
38
+ /** The live value before the change (absent for creates). */
39
+ before?: unknown;
40
+ /** The desired value after the change (absent for deletes). */
41
+ after?: unknown;
42
+ /** Field-level diff, populated for `update` entries. */
43
+ fields?: FieldChange[];
44
+ }
45
+ /** The full set of changes to reconcile for one scope (e.g. one org). */
46
+ export interface ChangeSet {
47
+ /** Scope identifier this change set applies to (e.g. a GitHub org login). */
48
+ org: string;
49
+ /** All proposed changes, in stable order. */
50
+ entries: ChangeSetEntry[];
51
+ }
52
+ /** Options controlling diff behaviour. */
53
+ export interface DiffOptions {
54
+ /**
55
+ * Ownership predicate for collection entries. The diff only emits a `delete`
56
+ * for a live entry absent from desired when this returns `true`. Omitted →
57
+ * deletes are never emitted ("assume nothing is owned").
58
+ */
59
+ isOwned?: (resourceType: string, key: string) => boolean;
60
+ /**
61
+ * Reference "now" in epoch milliseconds, used by time-based diffs. Callers
62
+ * inject `Date.now()` when unset; tests pass an explicit value.
63
+ */
64
+ nowMs?: number;
65
+ }
66
+ /** Deep value equality via JSON for plain data (config/live snapshots). */
67
+ export declare function deepEqual(a: unknown, b: unknown): boolean;
68
+ /**
69
+ * Diff fields of `desired` against `live`, returning one `FieldChange` per
70
+ * differing field. When `keys` is given, only those keys are compared (and only
71
+ * when present in `desired`); otherwise every key in `desired` is compared.
72
+ * Selective-by-omission: keys absent from `desired` are never compared.
73
+ */
74
+ export declare function diffFields(desired: Record<string, unknown>, live: Record<string, unknown>, keys?: string[]): FieldChange[];
75
+ /** Parameters for {@link diffCollection}. */
76
+ export interface DiffCollectionParams<D, L> {
77
+ /** Resource type stamped on emitted entries. */
78
+ resourceType: string;
79
+ /** Prefix prepended to each entry key (e.g. "<parent>/"). Default "". */
80
+ keyPrefix?: string;
81
+ /** Desired entries, keyed by logical key. */
82
+ desired: Map<string, D>;
83
+ /** Live entries, keyed by logical key. */
84
+ live: Map<string, L>;
85
+ /** Fields that differ → an update. Return `[]` for "no change". */
86
+ compareFields: (desired: D, live: L) => FieldChange[];
87
+ /** `after` value for a create entry. Defaults to the desired value. */
88
+ createAfter?: (key: string, desired: D) => unknown;
89
+ /** `after` value for an update entry. Defaults to the desired value. */
90
+ updateAfter?: (key: string, desired: D, live: L) => unknown;
91
+ opts: DiffOptions;
92
+ out: ChangeSetEntry[];
93
+ }
94
+ /**
95
+ * The generic managed-collection diff: creates for desired-not-live, updates
96
+ * when `compareFields` reports differences, and ownership-gated deletes for
97
+ * live-not-desired. This is the selective-by-omission + ownership-gated-delete
98
+ * pattern shared by every keyed-collection diff.
99
+ */
100
+ export declare function diffCollection<D, L>(params: DiffCollectionParams<D, L>): void;
101
+ /** Count entries per change kind. */
102
+ export declare function summarizeChangeSet(cs: ChangeSet): Record<ChangeKind, number>;
103
+ /** Human-readable plan summary for dry-run output. Pure. */
104
+ export declare function renderChangeSet(cs: ChangeSet): string;
105
+ /** A single tripped guardrail with a human-readable message. */
106
+ export interface GuardrailDiagnostic {
107
+ /** Short identifier, e.g. "removalDeltaCap". */
108
+ guardrail: string;
109
+ /** Clear, actionable description of why the apply was refused. */
110
+ message: string;
111
+ }
112
+ /** Aggregated guardrail result. */
113
+ export type GuardrailResult = {
114
+ ok: true;
115
+ } | {
116
+ ok: false;
117
+ diagnostics: GuardrailDiagnostic[];
118
+ };
119
+ /** A guardrail check over a (rename-resolved) change set. Returns null when it passes. */
120
+ export type GuardrailCheck = (resolved: ChangeSet) => GuardrailDiagnostic | null;
121
+ /** Config for `removalDeltaCap`. */
122
+ export interface RemovalDeltaCapOptions {
123
+ /** Max fraction of pre-existing entries that may be deleted. Must be in (0,1]. Default 0.25. */
124
+ maxFraction?: number;
125
+ }
126
+ /**
127
+ * Resolve rename aliases. A create entry carrying a `previously` key matching a
128
+ * delete entry's key is collapsed into an update, removing the delete. Returns a
129
+ * new ChangeSet with renames resolved. Provider-agnostic — works on any entry
130
+ * whose `after.previously` is a string.
131
+ */
132
+ export declare function resolveRenames(changeSet: ChangeSet): ChangeSet;
133
+ /**
134
+ * Refuse if deletes exceed `maxFraction` of the pre-existing managed entries
135
+ * (deletes + updates; creates excluded so a flood of new entries can't dilute
136
+ * the delete fraction). Guards against a typo wiping the config in one apply.
137
+ *
138
+ * CONTRACT: pass a RENAME-RESOLVED change set (see {@link resolveRenames}).
139
+ */
140
+ export declare function removalDeltaCap(changeSet: ChangeSet, opts?: RemovalDeltaCapOptions): GuardrailDiagnostic | null;
141
+ /**
142
+ * Run a set of guardrail checks against a change set. Resolves renames ONCE,
143
+ * then runs every check on the resolved set, aggregating any diagnostics. The
144
+ * caller composes provider-specific checks (e.g. an admin floor) as closures.
145
+ */
146
+ export declare function runGuardrailChecks(changeSet: ChangeSet, checks: GuardrailCheck[]): GuardrailResult;
147
+ //# sourceMappingURL=reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../src/reconcile.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -2,19 +2,61 @@ import { resolve } from "node:path";
2
2
  import { discoverOps } from "../../op/discover";
3
3
  import { discover } from "../../discovery/index";
4
4
  import { partitionByLexicon, computeStackGraph } from "../../build";
5
+ import { buildGraphIr } from "../../graph-ir";
6
+ import { applyDetail, type DetailLevel } from "../../graph-detail";
7
+ import { lintCommand } from "../commands/lint";
5
8
  import { formatError, formatWarning, formatBold } from "../format";
6
9
  import type { CommandContext } from "../registry";
7
10
 
8
11
  /**
9
12
  * `chant graph` — the Op dependency graph by default; `--stacks` renders the
10
13
  * cross-stack apply-ordering graph (edges, order, waves) chant computes from
11
- * cross-lexicon references.
14
+ * cross-lexicon references; `--format ir` emits the full entity-graph IR
15
+ * (lint-gated) for diagram painters and the agentic diagrammer (#493).
12
16
  */
13
17
  export async function runGraph(ctx: CommandContext): Promise<number> {
18
+ if (ctx.args.format === "ir") return runGraphIr(ctx);
14
19
  if (ctx.args.stacks) return runStackGraph(ctx);
15
20
  return runOpGraph();
16
21
  }
17
22
 
23
+ /**
24
+ * `chant graph --format ir` — emit the graph IR as JSON. Lint-gated: the IR is a
25
+ * representation of valid infra, so we refuse to emit it for source that does
26
+ * not pass lint (EVL + lexicon rules). Non-zero exit on discovery errors.
27
+ */
28
+ async function runGraphIr(ctx: CommandContext): Promise<number> {
29
+ const projectPath = resolve(ctx.args.path === "." ? "." : ctx.args.path);
30
+
31
+ const level = ctx.args.detail ?? 2;
32
+ if (![0, 1, 2, 3].includes(level)) {
33
+ console.error(formatError({ message: `Invalid --detail ${level}. Expected 0, 1, 2, or 3.` }));
34
+ return 1;
35
+ }
36
+
37
+ // Gate: only emit an IR for lint-clean source.
38
+ const lint = await lintCommand({ path: ctx.args.path, format: "stylish" });
39
+ if (!lint.success) {
40
+ console.error(
41
+ formatError({
42
+ message:
43
+ "Refusing to emit graph IR: source has lint errors. Run `chant lint` and fix them first.",
44
+ }),
45
+ );
46
+ return 1;
47
+ }
48
+
49
+ const result = await discover(projectPath);
50
+ if (result.errors.length > 0) {
51
+ for (const e of result.errors) console.error(formatError({ message: e.message }));
52
+ return 1;
53
+ }
54
+
55
+ const ir = applyDetail(buildGraphIr(result.entities, projectPath), level as DetailLevel);
56
+ console.log(JSON.stringify(ir, null, 2));
57
+ return 0;
58
+ }
59
+
18
60
  async function runOpGraph(): Promise<number> {
19
61
  const { ops, errors } = await discoverOps();
20
62
  for (const err of errors) console.error(formatError({ message: err }));
package/src/cli/main.ts CHANGED
@@ -126,6 +126,8 @@ export function parseArgs(args: string[]): ParsedArgs {
126
126
  result.theme = args[++i];
127
127
  } else if (arg === "--stacks") {
128
128
  result.stacks = true;
129
+ } else if (arg === "--detail") {
130
+ result.detail = Number(args[++i]);
129
131
  } else if (arg === "--base") {
130
132
  result.base = args[++i];
131
133
  } else if (arg === "--head") {
@@ -190,7 +192,9 @@ Ops:
190
192
  run cancel <name> Cancel the active workflow run (requires --force)
191
193
  run log <name> Show run history for an Op
192
194
 
193
- graph Show Op dependency graph (--stacks for cross-stack order)
195
+ graph Show Op dependency graph (--stacks for cross-stack order,
196
+ --format ir for the lint-gated entity-graph IR;
197
+ --detail 0..3: stacks|composites|declarables|attributes)
194
198
 
195
199
  Lifecycle (alias: lc):
196
200
  lifecycle snapshot <env> Query API, save metadata to orphan branch
@@ -57,6 +57,8 @@ export interface ParsedArgs {
57
57
  env?: string;
58
58
  /** `chant graph --stacks` — render the cross-stack apply-ordering graph */
59
59
  stacks?: boolean;
60
+ /** `chant graph --format ir --detail <0..3>` — graph IR detail tier */
61
+ detail?: number;
60
62
  /** `chant lifecycle affected --base <ref>` — base git ref to diff against */
61
63
  base?: string;
62
64
  /** `chant lifecycle affected --head <ref>` — head git ref (default: working tree) */
@@ -58,7 +58,7 @@ export function collectEntities(
58
58
  "resolution",
59
59
  );
60
60
  }
61
- setProvenance(entity, { sourceFile: file });
61
+ setProvenance(entity, { sourceFile: file, compositeInstance: indexedName });
62
62
  entities.set(expandedName, entity);
63
63
  }
64
64
  }
@@ -73,7 +73,7 @@ export function collectEntities(
73
73
  "resolution",
74
74
  );
75
75
  }
76
- setProvenance(entity, { sourceFile: file });
76
+ setProvenance(entity, { sourceFile: file, compositeInstance: name });
77
77
  entities.set(expandedName, entity);
78
78
  }
79
79
  } else if (isLexiconOutput(value)) {
@@ -0,0 +1,79 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { applyDetail, DETAIL } from "./graph-detail";
3
+ import type { GraphIR } from "./graph-ir";
4
+
5
+ // A small graph: a gcp vpc/subnet pair plus a k8s namespace and deployment that
6
+ // came from one composite instance ("db"), with the deployment referencing the
7
+ // subnet across lexicons.
8
+ const base: GraphIR = {
9
+ nodes: [
10
+ { id: "vpc", kind: "Vpc", lexicon: "gcp", attrs: {} },
11
+ { id: "subnet", kind: "Subnet", lexicon: "gcp", attrs: { network: { $ref: "vpc.id" } } },
12
+ {
13
+ id: "dbNamespace",
14
+ kind: "Namespace",
15
+ lexicon: "k8s",
16
+ compositeParent: "DbStack",
17
+ compositeInstance: "db",
18
+ attrs: {},
19
+ },
20
+ {
21
+ id: "dbDeployment",
22
+ kind: "Deployment",
23
+ lexicon: "k8s",
24
+ compositeParent: "DbStack",
25
+ compositeInstance: "db",
26
+ attrs: { subnet: { $ref: "subnet.selfLink" } },
27
+ },
28
+ ],
29
+ edges: [
30
+ { from: "subnet", to: "vpc", kind: "ref", viaAttr: "network" },
31
+ { from: "dbDeployment", to: "subnet", kind: "ref", viaAttr: "subnet" },
32
+ ],
33
+ groups: { byLexicon: { gcp: ["subnet", "vpc"], k8s: ["dbDeployment", "dbNamespace"] } },
34
+ };
35
+
36
+ describe("applyDetail", () => {
37
+ test("T2 (declarables) is the identity", () => {
38
+ expect(applyDetail(base, DETAIL.DECLARABLES)).toBe(base);
39
+ });
40
+
41
+ test("T0 (stacks) collapses to one node per lexicon with cross-lexicon edges", () => {
42
+ const ir = applyDetail(base, DETAIL.STACKS);
43
+ expect(ir.nodes.map((n) => n.id)).toEqual(["gcp", "k8s"]);
44
+ expect(ir.nodes.every((n) => n.kind === "stack")).toBe(true);
45
+ // subnet→vpc is intra-gcp (dropped); dbDeployment→subnet is k8s→gcp (kept).
46
+ expect(ir.edges).toEqual([{ from: "k8s", to: "gcp", kind: "ref" }]);
47
+ });
48
+
49
+ test("T1 (composites) collapses a composite instance to a single node", () => {
50
+ const ir = applyDetail(base, DETAIL.COMPOSITES);
51
+ expect(ir.nodes.map((n) => n.id).sort()).toEqual(["db", "subnet", "vpc"]);
52
+ const db = ir.nodes.find((n) => n.id === "db")!;
53
+ expect(db).toMatchObject({ kind: "DbStack", lexicon: "k8s", attrs: { members: 2 } });
54
+ // The composite's external ref is preserved, remapped to the composite node.
55
+ expect(ir.edges).toContainEqual({ from: "db", to: "subnet", kind: "ref" });
56
+ // The intra-gcp edge survives with its label.
57
+ expect(ir.edges).toContainEqual({ from: "subnet", to: "vpc", kind: "ref", viaAttr: "network" });
58
+ });
59
+
60
+ test("T1 node count is between T0 and T2", () => {
61
+ const t0 = applyDetail(base, DETAIL.STACKS).nodes.length;
62
+ const t1 = applyDetail(base, DETAIL.COMPOSITES).nodes.length;
63
+ const t2 = applyDetail(base, DETAIL.DECLARABLES).nodes.length;
64
+ expect(t0).toBeLessThan(t1);
65
+ expect(t1).toBeLessThan(t2);
66
+ });
67
+
68
+ test("T3 (attributes) annotates edges with the producer attribute", () => {
69
+ const ir = applyDetail(base, DETAIL.ATTRIBUTES);
70
+ expect(ir.edges).toContainEqual({ from: "subnet", to: "vpc", kind: "ref", viaAttr: "network", toAttr: "id" });
71
+ expect(ir.edges).toContainEqual({
72
+ from: "dbDeployment",
73
+ to: "subnet",
74
+ kind: "ref",
75
+ viaAttr: "subnet",
76
+ toAttr: "selfLink",
77
+ });
78
+ });
79
+ });