@intentius/chant 0.1.13 → 0.1.15
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/package.json +1 -1
- package/src/build.ts +18 -2
- package/src/cli/commands/build.ts +9 -1
- package/src/cli/commands/import-live.test.ts +126 -0
- package/src/cli/commands/import.ts +152 -2
- package/src/cli/commands/migrate.ts +2 -2
- package/src/cli/handlers/{state.test.ts → lifecycle.test.ts} +37 -37
- package/src/cli/handlers/{state.ts → lifecycle.ts} +148 -36
- package/src/cli/handlers/misc.ts +31 -2
- package/src/cli/handlers/run.test.ts +98 -0
- package/src/cli/handlers/run.ts +123 -0
- package/src/cli/main.test.ts +14 -0
- package/src/cli/main.ts +38 -12
- package/src/cli/mcp/{state-tools.ts → lifecycle-tools.ts} +9 -9
- package/src/cli/mcp/op-tools.ts +2 -2
- package/src/cli/mcp/resource-handlers.ts +1 -1
- package/src/cli/mcp/server.test.ts +2 -2
- package/src/cli/mcp/server.ts +1 -1
- package/src/cli/registry.ts +21 -2
- package/src/codegen/fetch.test.ts +103 -2
- package/src/codegen/fetch.ts +62 -10
- package/src/config.ts +31 -0
- package/src/detectLexicon.test.ts +2 -2
- package/src/index.ts +2 -2
- package/src/lexicon-export.test.ts +92 -0
- package/src/lexicon.ts +88 -9
- package/src/lifecycle/change-set.test.ts +151 -0
- package/src/lifecycle/change-set.ts +172 -0
- package/src/{state → lifecycle}/git.test.ts +15 -15
- package/src/{state → lifecycle}/git.ts +14 -14
- package/src/{state → lifecycle}/index.ts +2 -0
- package/src/{state → lifecycle}/snapshot.test.ts +5 -5
- package/src/{state → lifecycle}/snapshot.ts +9 -9
- package/src/{state → lifecycle}/types.ts +1 -1
- package/src/op/activity-registry.test.ts +59 -0
- package/src/op/activity-registry.ts +91 -0
- package/src/op/builders.ts +3 -3
- package/src/op/index.ts +6 -1
- package/src/op/local-executor.test.ts +247 -0
- package/src/op/local-executor.ts +300 -0
- package/src/op/local-output.test.ts +54 -0
- package/src/op/local-output.ts +63 -0
- package/src/op/op.test.ts +4 -4
- package/src/op/types.ts +1 -1
- package/src/ownership.test.ts +109 -0
- package/src/ownership.ts +142 -0
- package/src/serializer.ts +19 -1
- package/src/toml-parse.ts +3 -3
- /package/src/{state → lifecycle}/digest.test.ts +0 -0
- /package/src/{state → lifecycle}/digest.ts +0 -0
- /package/src/{state → lifecycle}/live-diff.test.ts +0 -0
- /package/src/{state → lifecycle}/live-diff.ts +0 -0
package/src/op/types.ts
CHANGED
|
@@ -52,7 +52,7 @@ export interface ActivityStep {
|
|
|
52
52
|
* The serializer captures the awaited result into a temporary, then emits
|
|
53
53
|
* `upsertSearchAttributes({ <name>: [String(<from-path>)] })` immediately
|
|
54
54
|
* after. Useful for filtering runs by outcome (e.g. `Drift: "true"/"false"`
|
|
55
|
-
* from a
|
|
55
|
+
* from a lifecycleDiff activity).
|
|
56
56
|
*
|
|
57
57
|
* `from` is a dot-path into the return value (e.g. `"drifted"` for
|
|
58
58
|
* `{ drifted: boolean }`); when omitted, the whole return value is
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
ownershipEntries,
|
|
4
|
+
ownershipKeys,
|
|
5
|
+
hasOwnershipMarker,
|
|
6
|
+
readOwnership,
|
|
7
|
+
classifyOwnership,
|
|
8
|
+
tagArrayToMap,
|
|
9
|
+
OWNERSHIP_MANAGED_BY_VALUE,
|
|
10
|
+
} from "./ownership";
|
|
11
|
+
import { resolveOwnershipMarker } from "./config";
|
|
12
|
+
|
|
13
|
+
describe("ownershipEntries (#119)", () => {
|
|
14
|
+
test("label channel carries managed-by + stack + env", () => {
|
|
15
|
+
const e = ownershipEntries("label", { stack: "billing", env: "prod" });
|
|
16
|
+
expect(e["app.kubernetes.io/managed-by"]).toBe("chant");
|
|
17
|
+
expect(e["chant.intentius.io/stack"]).toBe("billing");
|
|
18
|
+
expect(e["chant.intentius.io/env"]).toBe("prod");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("aws-tag channel uses colon keys", () => {
|
|
22
|
+
const e = ownershipEntries("aws-tag", { stack: "billing" });
|
|
23
|
+
expect(e["chant:managed-by"]).toBe("chant");
|
|
24
|
+
expect(e["chant:stack"]).toBe("billing");
|
|
25
|
+
expect(e["chant:env"]).toBeUndefined(); // env omitted when not set
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("azure-tag channel uses hyphen keys (no slash, which Azure forbids)", () => {
|
|
29
|
+
const e = ownershipEntries("azure-tag", { stack: "billing", env: "stg" });
|
|
30
|
+
expect(e["chant-managed-by"]).toBe("chant");
|
|
31
|
+
expect(e["chant-stack"]).toBe("billing");
|
|
32
|
+
expect(e["chant-env"]).toBe("stg");
|
|
33
|
+
expect(Object.keys(e).some((k) => k.includes("/"))).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("carries stack identity, not just managed=true", () => {
|
|
37
|
+
const a = ownershipEntries("label", { stack: "stack-a" });
|
|
38
|
+
const b = ownershipEntries("label", { stack: "stack-b" });
|
|
39
|
+
expect(a[ownershipKeys("label").stack]).not.toBe(b[ownershipKeys("label").stack]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("hasOwnershipMarker / readOwnership", () => {
|
|
44
|
+
test("detects chant's marker even when co-stamped with other tools", () => {
|
|
45
|
+
const tags = {
|
|
46
|
+
"chant:managed-by": OWNERSHIP_MANAGED_BY_VALUE,
|
|
47
|
+
"chant:stack": "billing",
|
|
48
|
+
"team": "payments", // foreign co-stamp
|
|
49
|
+
};
|
|
50
|
+
expect(hasOwnershipMarker(tags, "aws-tag")).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("absent marker → not owned", () => {
|
|
54
|
+
expect(hasOwnershipMarker({ team: "payments" }, "aws-tag")).toBe(false);
|
|
55
|
+
expect(hasOwnershipMarker(undefined, "label")).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("readOwnership recovers stack/env from a marked resource", () => {
|
|
59
|
+
const labels = ownershipEntries("label", { stack: "billing", env: "prod" });
|
|
60
|
+
expect(readOwnership(labels, "label")).toEqual({ stack: "billing", env: "prod" });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("readOwnership returns undefined when unmarked", () => {
|
|
64
|
+
expect(readOwnership({ foo: "bar" }, "label")).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("classifyOwnership / tagArrayToMap (#120)", () => {
|
|
69
|
+
test("marker present → owned, absent → foreign", () => {
|
|
70
|
+
expect(classifyOwnership({ "chant:managed-by": "chant" }, "aws-tag")).toBe("owned");
|
|
71
|
+
expect(classifyOwnership({ team: "x" }, "aws-tag")).toBe("foreign");
|
|
72
|
+
expect(classifyOwnership(undefined, "label")).toBe("foreign");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("tagArrayToMap converts CloudFormation {Key,Value} tags", () => {
|
|
76
|
+
const map = tagArrayToMap([
|
|
77
|
+
{ Key: "chant:managed-by", Value: "chant" },
|
|
78
|
+
{ Key: "chant:stack", Value: "billing" },
|
|
79
|
+
]);
|
|
80
|
+
expect(map["chant:managed-by"]).toBe("chant");
|
|
81
|
+
expect(classifyOwnership(map, "aws-tag")).toBe("owned");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("tagArrayToMap tolerates undefined / malformed entries", () => {
|
|
85
|
+
expect(tagArrayToMap(undefined)).toEqual({});
|
|
86
|
+
expect(tagArrayToMap([{ Value: "no-key" }])).toEqual({});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("resolveOwnershipMarker (config opt-in)", () => {
|
|
91
|
+
test("enabled when stack is set", () => {
|
|
92
|
+
expect(resolveOwnershipMarker({ ownership: { stack: "billing", env: "prod" } })).toEqual({
|
|
93
|
+
stack: "billing",
|
|
94
|
+
env: "prod",
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("off when no ownership config", () => {
|
|
99
|
+
expect(resolveOwnershipMarker({})).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("off when stack missing", () => {
|
|
103
|
+
expect(resolveOwnershipMarker({ ownership: { env: "prod" } })).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("off when explicitly disabled", () => {
|
|
107
|
+
expect(resolveOwnershipMarker({ ownership: { stack: "billing", enabled: false } })).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
});
|
package/src/ownership.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership marker contract.
|
|
3
|
+
*
|
|
4
|
+
* chant stamps managed resources with a provider-native marker at synthesis
|
|
5
|
+
* time. This is what later lets `delete` be precise without an authoritative
|
|
6
|
+
* state file — the ownership record lives on the cloud resource, not in a file
|
|
7
|
+
* chant has to host or lock. The marker is standard tags/labels, so walk-away
|
|
8
|
+
* cost stays zero: nothing proprietary lands in the output.
|
|
9
|
+
*
|
|
10
|
+
* The marker carries stack identity, not just `managed=true`, so one stack
|
|
11
|
+
* never mistakes another stack's resources for its own. Ownership means
|
|
12
|
+
* "carries chant's marker", not "carries only chant's marker" — co-stamping
|
|
13
|
+
* with other tools is fine.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The value of the managed-by marker for every channel. */
|
|
17
|
+
export const OWNERSHIP_MANAGED_BY_VALUE = "chant";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Stack (and optional environment) identity stamped onto a resource. Computed
|
|
21
|
+
* from project config and threaded into each serializer.
|
|
22
|
+
*/
|
|
23
|
+
export interface OwnershipMarker {
|
|
24
|
+
/** Distinguishes one chant stack's resources from another's. */
|
|
25
|
+
stack: string;
|
|
26
|
+
/** Optional environment identity. */
|
|
27
|
+
env?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The native metadata channel a target stamps into. Tag-key syntax differs:
|
|
32
|
+
* Kubernetes/GCP labels allow a `prefix/name` form; AWS tag keys allow `:`;
|
|
33
|
+
* Azure tag keys forbid `/`, so they use a hyphenated form.
|
|
34
|
+
*/
|
|
35
|
+
export type OwnershipChannel = "label" | "aws-tag" | "azure-tag";
|
|
36
|
+
|
|
37
|
+
interface ChannelKeys {
|
|
38
|
+
readonly managedBy: string;
|
|
39
|
+
readonly stack: string;
|
|
40
|
+
readonly env: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const CHANNEL_KEYS: Record<OwnershipChannel, ChannelKeys> = {
|
|
44
|
+
label: {
|
|
45
|
+
managedBy: "app.kubernetes.io/managed-by",
|
|
46
|
+
stack: "chant.intentius.io/stack",
|
|
47
|
+
env: "chant.intentius.io/env",
|
|
48
|
+
},
|
|
49
|
+
"aws-tag": {
|
|
50
|
+
managedBy: "chant:managed-by",
|
|
51
|
+
stack: "chant:stack",
|
|
52
|
+
env: "chant:env",
|
|
53
|
+
},
|
|
54
|
+
"azure-tag": {
|
|
55
|
+
managedBy: "chant-managed-by",
|
|
56
|
+
stack: "chant-stack",
|
|
57
|
+
env: "chant-env",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** The marker key names used in a given channel. */
|
|
62
|
+
export function ownershipKeys(channel: OwnershipChannel): ChannelKeys {
|
|
63
|
+
return CHANNEL_KEYS[channel];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The key/value entries to stamp for a channel: the managed-by marker, the
|
|
68
|
+
* stack identity, and the env identity when present.
|
|
69
|
+
*/
|
|
70
|
+
export function ownershipEntries(
|
|
71
|
+
channel: OwnershipChannel,
|
|
72
|
+
marker: OwnershipMarker,
|
|
73
|
+
): Record<string, string> {
|
|
74
|
+
const keys = CHANNEL_KEYS[channel];
|
|
75
|
+
const entries: Record<string, string> = {
|
|
76
|
+
[keys.managedBy]: OWNERSHIP_MANAGED_BY_VALUE,
|
|
77
|
+
[keys.stack]: marker.stack,
|
|
78
|
+
};
|
|
79
|
+
if (marker.env) entries[keys.env] = marker.env;
|
|
80
|
+
return entries;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Ownership test: does this resource carry chant's managed-by marker? Other
|
|
85
|
+
* tools may co-stamp; only the managed-by marker is required to count as owned.
|
|
86
|
+
*/
|
|
87
|
+
export function hasOwnershipMarker(
|
|
88
|
+
tagsOrLabels: Record<string, unknown> | undefined,
|
|
89
|
+
channel: OwnershipChannel,
|
|
90
|
+
): boolean {
|
|
91
|
+
if (!tagsOrLabels) return false;
|
|
92
|
+
return tagsOrLabels[CHANNEL_KEYS[channel].managedBy] === OWNERSHIP_MANAGED_BY_VALUE;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The two live-side ownership verdicts a marker query can produce.
|
|
97
|
+
*
|
|
98
|
+
* - `owned` — carries chant's marker; an undeclared owned resource is a safe
|
|
99
|
+
* delete candidate.
|
|
100
|
+
* - `foreign` — no marker; can be adopted but never auto-deleted.
|
|
101
|
+
*
|
|
102
|
+
* The third verdict, `unknown`, is reserved for when no marker channel was
|
|
103
|
+
* queried at all (see the `Ownership` type in the change set).
|
|
104
|
+
*/
|
|
105
|
+
export function classifyOwnership(
|
|
106
|
+
tagsOrLabels: Record<string, unknown> | undefined,
|
|
107
|
+
channel: OwnershipChannel,
|
|
108
|
+
): "owned" | "foreign" {
|
|
109
|
+
return hasOwnershipMarker(tagsOrLabels, channel) ? "owned" : "foreign";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Convert a tag array of `{Key, Value}` (AWS/CloudFormation form) into the
|
|
114
|
+
* key→value map the ownership helpers expect.
|
|
115
|
+
*/
|
|
116
|
+
export function tagArrayToMap(
|
|
117
|
+
tags: ReadonlyArray<{ Key?: string; Value?: unknown }> | undefined,
|
|
118
|
+
): Record<string, unknown> {
|
|
119
|
+
const out: Record<string, unknown> = {};
|
|
120
|
+
for (const t of tags ?? []) {
|
|
121
|
+
if (typeof t.Key === "string") out[t.Key] = t.Value;
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Read the stack/env identity from a marked resource's tags/labels. Returns
|
|
128
|
+
* undefined when the managed-by marker is absent.
|
|
129
|
+
*/
|
|
130
|
+
export function readOwnership(
|
|
131
|
+
tagsOrLabels: Record<string, unknown> | undefined,
|
|
132
|
+
channel: OwnershipChannel,
|
|
133
|
+
): OwnershipMarker | undefined {
|
|
134
|
+
if (!hasOwnershipMarker(tagsOrLabels, channel)) return undefined;
|
|
135
|
+
const keys = CHANNEL_KEYS[channel];
|
|
136
|
+
const stack = tagsOrLabels![keys.stack];
|
|
137
|
+
const env = tagsOrLabels![keys.env];
|
|
138
|
+
return {
|
|
139
|
+
stack: typeof stack === "string" ? stack : "",
|
|
140
|
+
env: typeof env === "string" ? env : undefined,
|
|
141
|
+
};
|
|
142
|
+
}
|
package/src/serializer.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import type { Declarable } from "./declarable";
|
|
2
2
|
import type { LexiconOutput } from "./lexicon-output";
|
|
3
|
+
import type { OwnershipMarker } from "./ownership";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build-time context passed to a serializer. Optional — serializers that don't
|
|
7
|
+
* need it ignore the parameter.
|
|
8
|
+
*/
|
|
9
|
+
export interface SerializeContext {
|
|
10
|
+
/**
|
|
11
|
+
* When set, the serializer stamps this ownership marker into the target's
|
|
12
|
+
* native metadata channel (AWS/Azure tags, K8s/GCP labels).
|
|
13
|
+
*/
|
|
14
|
+
ownership?: OwnershipMarker;
|
|
15
|
+
}
|
|
3
16
|
|
|
4
17
|
/**
|
|
5
18
|
* Result of serialization that may include additional files (e.g. nested stack templates).
|
|
@@ -29,8 +42,13 @@ export interface Serializer {
|
|
|
29
42
|
* Serializes the entities to a string representation
|
|
30
43
|
* @param entities - Map of entity name to Declarable entity
|
|
31
44
|
* @param outputs - Optional array of LexiconOutputs produced by this lexicon
|
|
45
|
+
* @param context - Optional build-time context (e.g. ownership marker)
|
|
32
46
|
*/
|
|
33
|
-
serialize(
|
|
47
|
+
serialize(
|
|
48
|
+
entities: Map<string, Declarable>,
|
|
49
|
+
outputs?: LexiconOutput[],
|
|
50
|
+
context?: SerializeContext,
|
|
51
|
+
): string | SerializerResult;
|
|
34
52
|
|
|
35
53
|
/**
|
|
36
54
|
* Serialize a cross-lexicon reference to a foreign output.
|
package/src/toml-parse.ts
CHANGED
|
@@ -10,8 +10,8 @@ import { unescapeString, stripInlineComment } from "./toml-utils";
|
|
|
10
10
|
/**
|
|
11
11
|
* Parse a TOML document string into a plain object.
|
|
12
12
|
*
|
|
13
|
-
* Uses a built-in parser that handles the TOML subset used by
|
|
14
|
-
* configuration files.
|
|
13
|
+
* Uses a built-in parser that handles the common TOML subset used by tool
|
|
14
|
+
* configuration files (tables, dotted keys, inline tables, and arrays).
|
|
15
15
|
*/
|
|
16
16
|
export function parseTOML(content: string): Record<string, unknown> {
|
|
17
17
|
const result: Record<string, unknown> = {};
|
|
@@ -106,7 +106,7 @@ function findEquals(line: string): number {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
|
-
* Parse a dotted key like `
|
|
109
|
+
* Parse a dotted key like `tool.placeholders.name` into path segments.
|
|
110
110
|
*/
|
|
111
111
|
function parseDottedKey(raw: string): string[] {
|
|
112
112
|
const parts: string[] = [];
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|