@intentius/chant 0.112.0 → 0.113.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.
@@ -1 +1 @@
1
- {"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../../src/lifecycle/digest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EAAE,WAAW,EAAkB,UAAU,EAAE,MAAM,SAAS,CAAC;AAIvE;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAGhD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,WAAW,GAAG,WAAW,CA4BxE;AAED;;GAEG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,WAAW,GAAG,SAAS,GAChC,UAAU,CAiCZ"}
1
+ {"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../../src/lifecycle/digest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EAAE,WAAW,EAAkB,UAAU,EAAE,MAAM,SAAS,CAAC;AAKvE;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAGhD;AA0BD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,WAAW,GAAG,WAAW,CA4BxE;AAED;;GAEG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,WAAW,GAAG,SAAS,GAChC,UAAU,CAiCZ"}
@@ -23,8 +23,8 @@ export interface LifecycleDiffResult {
23
23
  exitCode: number;
24
24
  /**
25
25
  * True when the diff output contains any drift indicators
26
- * (MISSING / ORPHAN / DRIFTED / DISAPPEARED section headers from
27
- * `chant lifecycle diff --live`).
26
+ * (MISSING / ORPHAN / DRIFTED / DISAPPEARED / PROPERTY DRIFT section
27
+ * headers from `chant lifecycle diff --live`).
28
28
  */
29
29
  drifted: boolean;
30
30
  /**
@@ -55,13 +55,15 @@ export interface LifecycleDiffResult {
55
55
  * approval would be stale the moment it was written.
56
56
  */
57
57
  export declare function normalizeDiffChangeSet(output: string): string[];
58
+ /** True when a `chant lifecycle diff` render carries any of {@link DRIFT_HEADERS}' sections. */
59
+ export declare function detectDrift(output: string): boolean;
58
60
  /**
59
61
  * Run `chant lifecycle diff <env>` and return the output + structured drift
60
62
  * flag. Read-only; intended for use inside watch/observation Ops.
61
63
  * Uses fastIdempotent profile.
62
64
  *
63
65
  * The `drifted` field is computed by scanning the output for any of the
64
- * MISSING / ORPHAN / DRIFTED / DISAPPEARED section headers documented in
66
+ * MISSING / ORPHAN / DRIFTED / DISAPPEARED / PROPERTY DRIFT section headers documented in
65
67
  * cli/state.mdx. Pair with `outcomeAttribute: { name: "Drift", from: "drifted" }`
66
68
  * on a WatchOp activity step to surface drift as the run's `Drift` outcome.
67
69
  */
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/op/activities/lifecycle.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,qBAAqB;IACpC,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAIxG;AAED,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;;;;;OASG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAM/D;AAkCD;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,iBAAiB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAa/G"}
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/op/activities/lifecycle.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,qBAAqB;IACpC,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAIxG;AAED,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;;;;;OASG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAM/D;AAmBD,gGAAgG;AAChG,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;AAgBD;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,iBAAiB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAa/G"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.112.0",
3
+ "version": "0.113.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,6 +2,8 @@ import { describe, test, expect } from "vitest";
2
2
  import { computeBuildDigest, diffDigests, hashProps } from "./digest";
3
3
  import type { BuildResult } from "../build";
4
4
  import type { BuildDigest } from "./types";
5
+ import { AttrRef } from "../attrref";
6
+ import { LOGICAL_NAME_SYMBOL } from "../utils";
5
7
 
6
8
  function makeBuildResult(entitiesByLexicon: Record<string, Array<{ name: string; type: string; props: unknown }>>): BuildResult {
7
9
  const entities = new Map();
@@ -37,6 +39,30 @@ describe("hashProps", () => {
37
39
  test("produces different hashes for different props", () => {
38
40
  expect(hashProps({ a: 1 })).not.toBe(hashProps({ a: 2 }));
39
41
  });
42
+
43
+ // #3642: a sql index's `columns` holds AttrRefs the build never names, and
44
+ // their toJSON throws, so a snapshot of the project failed.
45
+ describe("an AttrRef the build did not name", () => {
46
+ const parent = { [LOGICAL_NAME_SYMBOL]: "orders" };
47
+
48
+ test("hashes instead of throwing, as the envelope named by its parent", () => {
49
+ const nested = { columns: [new AttrRef(parent, "status")] };
50
+ expect(() => JSON.stringify(nested)).toThrow(/logical name not set/);
51
+ const named = new AttrRef({}, "status");
52
+ named._setLogicalName("orders");
53
+ expect(hashProps(nested)).toBe(hashProps({ columns: [named] }));
54
+ });
55
+
56
+ test("tells two attributes apart", () => {
57
+ expect(hashProps({ c: [new AttrRef(parent, "status")] })).not.toBe(hashProps({ c: [new AttrRef(parent, "amount")] }));
58
+ });
59
+
60
+ test("hashes a named AttrRef as it always has", () => {
61
+ const ref = new AttrRef({}, "arn");
62
+ ref._setLogicalName("bucket");
63
+ expect(hashProps({ a: ref })).toBe(hashProps({ a: { __attrRef: { entity: "bucket", attribute: "arn" } } }));
64
+ });
65
+ });
40
66
  });
41
67
 
42
68
  describe("computeBuildDigest", () => {
@@ -9,6 +9,7 @@ import type { Declarable } from "../declarable";
9
9
  import type { BuildDigest, ResourceDigest, DigestDiff } from "./types";
10
10
  import { canonicalJson } from "../effect-receipt";
11
11
  import { contentDigest } from "../content-digest";
12
+ import { isAttrRefLike, LOGICAL_NAME_SYMBOL } from "../utils";
12
13
 
13
14
  /**
14
15
  * Hash an entity's props deterministically: bare hex SHA-256 over their
@@ -18,10 +19,34 @@ import { contentDigest } from "../content-digest";
18
19
  * hashes rather than throwing.
19
20
  */
20
21
  export function hashProps(props: unknown): string {
21
- const plain: unknown = JSON.parse(JSON.stringify(props) ?? "null");
22
+ const plain: unknown = JSON.parse(JSON.stringify(namedRefs(props)) ?? "null");
22
23
  return contentDigest(canonicalJson(plain)).slice("sha256:".length);
23
24
  }
24
25
 
26
+ /**
27
+ * `props` with each AttrRef that has no logical name of its own written as
28
+ * the envelope its `toJSON` gives, named by its parent's logical name. The
29
+ * build names only an entity's top-level AttrRefs; one held in a record,
30
+ * such as a sql table's `columns`, is never named, and `toJSON` throws on
31
+ * it (chant#3642). Plain objects and arrays are walked; anything else is
32
+ * left for `JSON.stringify`, so a named AttrRef hashes as it always has.
33
+ */
34
+ function namedRefs(value: unknown): unknown {
35
+ if (isAttrRefLike(value)) {
36
+ if (value.getLogicalName()) return value;
37
+ const parent = value.parent.deref() as Record<symbol, unknown> | undefined;
38
+ const entity = parent?.[LOGICAL_NAME_SYMBOL];
39
+ return { __attrRef: { entity: typeof entity === "string" ? entity : null, attribute: value.attribute } };
40
+ }
41
+ if (Array.isArray(value)) return value.map(namedRefs);
42
+ if (value !== null && typeof value === "object") {
43
+ const proto = Object.getPrototypeOf(value);
44
+ if (proto !== Object.prototype && proto !== null) return value;
45
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, namedRefs(v)]));
46
+ }
47
+ return value;
48
+ }
49
+
25
50
  /**
26
51
  * Compute a full build digest from a BuildResult.
27
52
  */
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { detectDrift } from "./lifecycle";
3
+
4
+ const BOLD = "\u001b[1m";
5
+ const RESET = "\u001b[0m";
6
+
7
+ // Renders as `chant lifecycle diff <env> --live` printed them against the sql
8
+ // emulator (#3642), bold headers included: the activity reads them off a
9
+ // child process, colour and all.
10
+ const HEAD = `${BOLD}sql${RESET} — environment: watch\n`;
11
+ const CLEAN = `${HEAD}0 missing, 0 orphan, 0 disappeared, 0 newly observed, 0 drifted, 3 unchanged\n${"-".repeat(80)}\n\u001b[32mNo drift detected across 1 lexicon(s)${RESET}`;
12
+ const PROPERTY = `${HEAD}0 missing, 0 orphan, 0 disappeared, 0 newly observed, 0 drifted, 3 unchanged\n${"-".repeat(80)}\n\n${BOLD}sql (properties)${RESET}\n1 property drift across 1 resource(s), 0 accepted, 2 unchanged\n${"-".repeat(80)}\n${BOLD}\nPROPERTY DRIFT (declared vs live; baseline shown where one exists):${RESET}\n - events (ClickHouse::Table)\n ttl: ts + INTERVAL 180 DAY → ts + toIntervalDay(30) [from: authored]`;
13
+ const MISSING = `${HEAD}1 missing, 0 orphan, 0 disappeared, 0 newly observed, 0 drifted, 2 unchanged\n${"-".repeat(80)}\n${BOLD}\nMISSING (declared, provider reports not in cloud):${RESET}\n - byKind [queried http://127.0.0.1:8123 yodel_3642.by_kind]`;
14
+
15
+ describe("detectDrift", () => {
16
+ test("a clean render is not drift", () => {
17
+ expect(detectDrift(CLEAN)).toBe(false);
18
+ });
19
+
20
+ test("an owned object's property changed out of band is drift (PROPERTY DRIFT)", () => {
21
+ expect(detectDrift(PROPERTY)).toBe(true);
22
+ });
23
+
24
+ test("a declared object gone from the server is drift (MISSING)", () => {
25
+ expect(detectDrift(MISSING)).toBe(true);
26
+ });
27
+
28
+ test("the counts line alone, lower case, is not drift", () => {
29
+ expect(detectDrift("1 property drift across 1 resource(s)")).toBe(false);
30
+ });
31
+ });
@@ -36,8 +36,8 @@ export interface LifecycleDiffResult {
36
36
  exitCode: number;
37
37
  /**
38
38
  * True when the diff output contains any drift indicators
39
- * (MISSING / ORPHAN / DRIFTED / DISAPPEARED section headers from
40
- * `chant lifecycle diff --live`).
39
+ * (MISSING / ORPHAN / DRIFTED / DISAPPEARED / PROPERTY DRIFT section
40
+ * headers from `chant lifecycle diff --live`).
41
41
  */
42
42
  drifted: boolean;
43
43
  /**
@@ -85,12 +85,16 @@ const DRIFT_HEADERS = [
85
85
  "ORPHAN",
86
86
  "DISAPPEARED",
87
87
  "DRIFTED",
88
+ // An owned resource whose live properties differ from its declaration (the
89
+ // deep read): a sql table's TTL changed out of band, say (#3642).
90
+ "PROPERTY DRIFT",
88
91
  "ARTIFACTS ADDED",
89
92
  "ARTIFACTS REMOVED",
90
93
  "ARTIFACTS CHANGED",
91
94
  ];
92
95
 
93
- function detectDrift(output: string): boolean {
96
+ /** True when a `chant lifecycle diff` render carries any of {@link DRIFT_HEADERS}' sections. */
97
+ export function detectDrift(output: string): boolean {
94
98
  return DRIFT_HEADERS.some((h) => output.includes(`${h} (`) || output.includes(`\n${h}`));
95
99
  }
96
100
 
@@ -114,7 +118,7 @@ function lifecycleDiffDigest(args: LifecycleDiffArgs, output: string): string {
114
118
  * Uses fastIdempotent profile.
115
119
  *
116
120
  * The `drifted` field is computed by scanning the output for any of the
117
- * MISSING / ORPHAN / DRIFTED / DISAPPEARED section headers documented in
121
+ * MISSING / ORPHAN / DRIFTED / DISAPPEARED / PROPERTY DRIFT section headers documented in
118
122
  * cli/state.mdx. Pair with `outcomeAttribute: { name: "Drift", from: "drifted" }`
119
123
  * on a WatchOp activity step to surface drift as the run's `Drift` outcome.
120
124
  */