@intentius/chant 0.0.18 → 0.0.24

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.
Files changed (87) hide show
  1. package/bin/chant +4 -1
  2. package/package.json +20 -1
  3. package/src/build.test.ts +4 -2
  4. package/src/build.ts +3 -0
  5. package/src/builder.test.ts +3 -0
  6. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -3
  7. package/src/cli/commands/build.ts +5 -12
  8. package/src/cli/commands/diff.test.ts +2 -1
  9. package/src/cli/commands/diff.ts +2 -1
  10. package/src/cli/commands/init-lexicon/templates/codegen.ts +188 -0
  11. package/src/cli/commands/init-lexicon/templates/docs.ts +81 -0
  12. package/src/cli/commands/init-lexicon/templates/examples.ts +35 -0
  13. package/src/cli/commands/init-lexicon/templates/lint.ts +30 -0
  14. package/src/cli/commands/init-lexicon/templates/lsp.ts +39 -0
  15. package/src/cli/commands/init-lexicon/templates/plugin.ts +110 -0
  16. package/src/cli/commands/init-lexicon/templates/project.ts +182 -0
  17. package/src/cli/commands/init-lexicon/templates/spec.ts +57 -0
  18. package/src/cli/commands/init-lexicon/templates/tests.ts +70 -0
  19. package/src/cli/commands/init-lexicon.test.ts +0 -9
  20. package/src/cli/commands/init-lexicon.ts +12 -868
  21. package/src/cli/commands/init.ts +2 -20
  22. package/src/cli/conflict-check.test.ts +43 -0
  23. package/src/cli/handlers/build.ts +3 -3
  24. package/src/cli/handlers/lint.ts +2 -2
  25. package/src/cli/handlers/spell.ts +396 -0
  26. package/src/cli/handlers/state.ts +230 -0
  27. package/src/cli/lsp/server.test.ts +4 -0
  28. package/src/cli/main.ts +37 -3
  29. package/src/cli/mcp/resource-handlers.ts +227 -0
  30. package/src/cli/mcp/server.test.ts +13 -9
  31. package/src/cli/mcp/server.ts +24 -199
  32. package/src/cli/mcp/state-tools.ts +138 -0
  33. package/src/cli/mcp/tools/build.ts +2 -1
  34. package/src/cli/mcp/types.ts +45 -0
  35. package/src/cli/plugins.ts +1 -1
  36. package/src/cli/reporters/stylish.test.ts +2 -2
  37. package/src/cli/reporters/stylish.ts +1 -1
  38. package/src/codegen/docs-file-markers.ts +69 -0
  39. package/src/codegen/docs-rule-scanning.ts +159 -0
  40. package/src/codegen/docs-sections.ts +159 -0
  41. package/src/codegen/docs-sidebar.ts +56 -0
  42. package/src/codegen/docs-types.ts +79 -0
  43. package/src/codegen/docs.ts +9 -495
  44. package/src/composite.test.ts +76 -1
  45. package/src/composite.ts +37 -0
  46. package/src/config.ts +4 -0
  47. package/src/declarable.test.ts +2 -1
  48. package/src/declarable.ts +1 -1
  49. package/src/discovery/collect.test.ts +34 -0
  50. package/src/discovery/collect.ts +12 -0
  51. package/src/discovery/graph.test.ts +40 -0
  52. package/src/discovery/import.test.ts +5 -5
  53. package/src/discovery/resolve.test.ts +20 -0
  54. package/src/discovery/resolve.ts +2 -2
  55. package/src/index.ts +2 -0
  56. package/src/lexicon-plugin-helpers.ts +130 -0
  57. package/src/lexicon.ts +24 -0
  58. package/src/lint/rule-options.test.ts +3 -3
  59. package/src/lint/rule-registry.test.ts +1 -1
  60. package/src/lint/rules/composite-scope.ts +1 -1
  61. package/src/serializer-walker.ts +2 -1
  62. package/src/spell/discovery.ts +183 -0
  63. package/src/spell/index.ts +3 -0
  64. package/src/spell/prompt.ts +133 -0
  65. package/src/spell/types.ts +89 -0
  66. package/src/state/digest.ts +88 -0
  67. package/src/state/git.ts +317 -0
  68. package/src/state/index.ts +4 -0
  69. package/src/state/snapshot.ts +179 -0
  70. package/src/state/types.ts +59 -0
  71. package/src/toml-emit.ts +182 -0
  72. package/src/toml-parse.ts +370 -0
  73. package/src/toml-utils.ts +60 -0
  74. package/src/toml.ts +5 -602
  75. package/src/types.ts +2 -1
  76. package/src/utils.test.ts +16 -3
  77. package/src/utils.ts +31 -1
  78. package/src/validation.test.ts +11 -0
  79. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/getting-started.mdx +0 -6
  80. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/lint-rules.mdx +0 -6
  81. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/serialization.mdx +0 -6
  82. package/src/cli/commands/__fixtures__/init-lexicon-output/src/actions/.gitkeep +0 -0
  83. package/src/cli/commands/__fixtures__/init-lexicon-output/src/composites/.gitkeep +0 -0
  84. package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +0 -11
  85. package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +0 -10
  86. package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +0 -10
  87. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/post-synth/.gitkeep +0 -0
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Git plumbing operations for the chant/state orphan branch.
3
+ *
4
+ * All operations use git plumbing commands — no checkout, no branch switching,
5
+ * no working tree changes.
6
+ */
7
+ import { getRuntime } from "../runtime-adapter";
8
+
9
+ const STATE_BRANCH = "chant/state";
10
+
11
+ /**
12
+ * Write a state snapshot JSON to the orphan branch.
13
+ *
14
+ * Pipeline: hash-object → mktree → commit-tree → update-ref
15
+ */
16
+ export async function writeSnapshot(
17
+ environment: string,
18
+ lexicon: string,
19
+ json: string,
20
+ opts?: { cwd?: string },
21
+ ): Promise<string> {
22
+ const rt = getRuntime();
23
+ const cwd = opts?.cwd;
24
+
25
+ // 1. Write blob
26
+ const hashResult = await rt.spawn(
27
+ ["git", "hash-object", "-w", "--stdin"],
28
+ { cwd },
29
+ );
30
+ // hash-object reads from stdin, but spawn doesn't support piping directly.
31
+ // Use a shell pipeline instead.
32
+ const blobResult = await rt.spawn(
33
+ ["sh", "-c", `echo '${json.replace(/'/g, "'\\''")}' | git hash-object -w --stdin`],
34
+ { cwd },
35
+ );
36
+ if (blobResult.exitCode !== 0) {
37
+ throw new Error(`git hash-object failed: ${blobResult.stderr}`);
38
+ }
39
+ const blobSha = blobResult.stdout.trim();
40
+
41
+ // 2. Read existing tree (if branch exists) to preserve other env/lexicon entries
42
+ const existingTree = await readTree(cwd);
43
+
44
+ // 3. Build new tree entries
45
+ const path = `${environment}/${lexicon}.json`;
46
+ const entries = mergeTreeEntry(existingTree, path, blobSha);
47
+ const treeInput = entries.map((e) => `${e.mode} ${e.type} ${e.sha}\t${e.name}`).join("\n");
48
+
49
+ // mktree needs a nested tree structure. Build env subtree first, then root tree.
50
+ // Build env subtree
51
+ const envEntries = entries
52
+ .filter((e) => e.env === environment)
53
+ .map((e) => `${e.mode} ${e.type} ${e.sha}\t${e.name}`)
54
+ .join("\n");
55
+
56
+ const envTreeResult = await rt.spawn(
57
+ ["sh", "-c", `printf '%s\\n' ${shellQuoteLines(envEntries)} | git mktree`],
58
+ { cwd },
59
+ );
60
+ if (envTreeResult.exitCode !== 0) {
61
+ throw new Error(`git mktree (env) failed: ${envTreeResult.stderr}`);
62
+ }
63
+ const envTreeSha = envTreeResult.stdout.trim();
64
+
65
+ // Build root tree: collect env subtrees
66
+ const rootEntries: string[] = [];
67
+ const envsSeen = new Set<string>();
68
+ for (const e of entries) {
69
+ if (!envsSeen.has(e.env)) {
70
+ envsSeen.add(e.env);
71
+ if (e.env === environment) {
72
+ rootEntries.push(`040000 tree ${envTreeSha}\t${environment}`);
73
+ } else {
74
+ rootEntries.push(`040000 tree ${e.envTreeSha!}\t${e.env}`);
75
+ }
76
+ }
77
+ }
78
+
79
+ const rootTreeResult = await rt.spawn(
80
+ ["sh", "-c", `printf '%s\\n' ${shellQuoteLines(rootEntries.join("\n"))} | git mktree`],
81
+ { cwd },
82
+ );
83
+ if (rootTreeResult.exitCode !== 0) {
84
+ throw new Error(`git mktree (root) failed: ${rootTreeResult.stderr}`);
85
+ }
86
+ const rootTreeSha = rootTreeResult.stdout.trim();
87
+
88
+ // 4. Create commit
89
+ const parentRef = await getStateBranchTip(cwd);
90
+ const parentArgs = parentRef ? ["-p", parentRef] : [];
91
+ const commitResult = await rt.spawn(
92
+ ["git", "commit-tree", ...parentArgs, "-m", "State snapshot", rootTreeSha],
93
+ { cwd },
94
+ );
95
+ if (commitResult.exitCode !== 0) {
96
+ throw new Error(`git commit-tree failed: ${commitResult.stderr}`);
97
+ }
98
+ const commitSha = commitResult.stdout.trim();
99
+
100
+ // 5. Update ref
101
+ const updateResult = await rt.spawn(
102
+ ["git", "update-ref", `refs/heads/${STATE_BRANCH}`, commitSha],
103
+ { cwd },
104
+ );
105
+ if (updateResult.exitCode !== 0) {
106
+ throw new Error(`git update-ref failed: ${updateResult.stderr}`);
107
+ }
108
+
109
+ return commitSha;
110
+ }
111
+
112
+ /**
113
+ * Read a snapshot from the orphan branch.
114
+ */
115
+ export async function readSnapshot(
116
+ environment: string,
117
+ lexicon: string,
118
+ opts?: { cwd?: string },
119
+ ): Promise<string | null> {
120
+ const rt = getRuntime();
121
+ const result = await rt.spawn(
122
+ ["git", "show", `${STATE_BRANCH}:${environment}/${lexicon}.json`],
123
+ { cwd: opts?.cwd },
124
+ );
125
+ if (result.exitCode !== 0) return null;
126
+ return result.stdout;
127
+ }
128
+
129
+ /**
130
+ * Read all snapshots for an environment (all lexicons).
131
+ */
132
+ export async function readEnvironmentSnapshots(
133
+ environment: string,
134
+ opts?: { cwd?: string },
135
+ ): Promise<Map<string, string>> {
136
+ const rt = getRuntime();
137
+ const snapshots = new Map<string, string>();
138
+
139
+ // List files in the environment directory
140
+ const lsResult = await rt.spawn(
141
+ ["git", "ls-tree", "--name-only", `${STATE_BRANCH}:${environment}/`],
142
+ { cwd: opts?.cwd },
143
+ );
144
+ if (lsResult.exitCode !== 0) return snapshots;
145
+
146
+ const files = lsResult.stdout.trim().split("\n").filter(Boolean);
147
+ for (const file of files) {
148
+ if (file.endsWith(".json")) {
149
+ const lexicon = file.replace(/\.json$/, "");
150
+ const content = await readSnapshot(environment, lexicon, opts);
151
+ if (content) snapshots.set(lexicon, content);
152
+ }
153
+ }
154
+
155
+ return snapshots;
156
+ }
157
+
158
+ /**
159
+ * List snapshot history from the orphan branch.
160
+ */
161
+ export async function listSnapshots(
162
+ opts?: { cwd?: string; environment?: string },
163
+ ): Promise<Array<{ commit: string; date: string; message: string }>> {
164
+ const rt = getRuntime();
165
+ const result = await rt.spawn(
166
+ ["git", "log", "--format=%H %aI %s", STATE_BRANCH],
167
+ { cwd: opts?.cwd },
168
+ );
169
+ if (result.exitCode !== 0) return [];
170
+
171
+ return result.stdout
172
+ .trim()
173
+ .split("\n")
174
+ .filter(Boolean)
175
+ .map((line) => {
176
+ const [commit, date, ...rest] = line.split(" ");
177
+ return { commit, date, message: rest.join(" ") };
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Push the state branch to remote.
183
+ */
184
+ export async function pushState(opts?: { cwd?: string }): Promise<boolean> {
185
+ const rt = getRuntime();
186
+ // Check if remote exists
187
+ const remoteResult = await rt.spawn(["git", "remote"], { cwd: opts?.cwd });
188
+ if (remoteResult.exitCode !== 0 || !remoteResult.stdout.trim()) return false;
189
+
190
+ const remote = remoteResult.stdout.trim().split("\n")[0];
191
+ const pushResult = await rt.spawn(
192
+ ["git", "push", remote, `${STATE_BRANCH}:${STATE_BRANCH}`],
193
+ { cwd: opts?.cwd },
194
+ );
195
+ return pushResult.exitCode === 0;
196
+ }
197
+
198
+ /**
199
+ * Fetch the state branch from remote.
200
+ */
201
+ export async function fetchState(opts?: { cwd?: string }): Promise<boolean> {
202
+ const rt = getRuntime();
203
+ const remoteResult = await rt.spawn(["git", "remote"], { cwd: opts?.cwd });
204
+ if (remoteResult.exitCode !== 0 || !remoteResult.stdout.trim()) return false;
205
+
206
+ const remote = remoteResult.stdout.trim().split("\n")[0];
207
+ const fetchResult = await rt.spawn(
208
+ ["git", "fetch", remote, `${STATE_BRANCH}:${STATE_BRANCH}`],
209
+ { cwd: opts?.cwd },
210
+ );
211
+ return fetchResult.exitCode === 0;
212
+ }
213
+
214
+ /**
215
+ * Get the current HEAD commit SHA of the main working branch.
216
+ */
217
+ export async function getHeadCommit(opts?: { cwd?: string }): Promise<string> {
218
+ const rt = getRuntime();
219
+ const result = await rt.spawn(["git", "rev-parse", "HEAD"], { cwd: opts?.cwd });
220
+ if (result.exitCode !== 0) {
221
+ throw new Error(`git rev-parse HEAD failed: ${result.stderr}`);
222
+ }
223
+ return result.stdout.trim();
224
+ }
225
+
226
+ // ── Internal helpers ────────────────────────────────────────────
227
+
228
+ interface TreeEntry {
229
+ mode: string;
230
+ type: string;
231
+ sha: string;
232
+ name: string;
233
+ env: string;
234
+ envTreeSha?: string;
235
+ }
236
+
237
+ async function getStateBranchTip(cwd?: string): Promise<string | null> {
238
+ const rt = getRuntime();
239
+ const result = await rt.spawn(
240
+ ["git", "rev-parse", "--verify", `refs/heads/${STATE_BRANCH}`],
241
+ { cwd },
242
+ );
243
+ if (result.exitCode !== 0) return null;
244
+ return result.stdout.trim();
245
+ }
246
+
247
+ async function readTree(cwd?: string): Promise<TreeEntry[]> {
248
+ const rt = getRuntime();
249
+ const tip = await getStateBranchTip(cwd);
250
+ if (!tip) return [];
251
+
252
+ // List root tree to get env directories
253
+ const rootResult = await rt.spawn(
254
+ ["git", "ls-tree", STATE_BRANCH],
255
+ { cwd },
256
+ );
257
+ if (rootResult.exitCode !== 0) return [];
258
+
259
+ const entries: TreeEntry[] = [];
260
+ const lines = rootResult.stdout.trim().split("\n").filter(Boolean);
261
+
262
+ for (const line of lines) {
263
+ // Format: mode type sha\tname
264
+ const match = line.match(/^(\d+)\s+(\w+)\s+([0-9a-f]+)\t(.+)$/);
265
+ if (!match) continue;
266
+ const [, mode, type, sha, name] = match;
267
+
268
+ if (type === "tree") {
269
+ // This is an env directory — list its contents
270
+ const envResult = await rt.spawn(
271
+ ["git", "ls-tree", `${STATE_BRANCH}:${name}/`],
272
+ { cwd },
273
+ );
274
+ if (envResult.exitCode !== 0) continue;
275
+
276
+ const envLines = envResult.stdout.trim().split("\n").filter(Boolean);
277
+ for (const envLine of envLines) {
278
+ const envMatch = envLine.match(/^(\d+)\s+(\w+)\s+([0-9a-f]+)\t(.+)$/);
279
+ if (!envMatch) continue;
280
+ entries.push({
281
+ mode: envMatch[1],
282
+ type: envMatch[2],
283
+ sha: envMatch[3],
284
+ name: envMatch[4],
285
+ env: name,
286
+ envTreeSha: sha,
287
+ });
288
+ }
289
+ }
290
+ }
291
+
292
+ return entries;
293
+ }
294
+
295
+ function mergeTreeEntry(
296
+ existing: TreeEntry[],
297
+ path: string,
298
+ blobSha: string,
299
+ ): TreeEntry[] {
300
+ const [env, filename] = path.split("/");
301
+ const entries = existing.filter(
302
+ (e) => !(e.env === env && e.name === filename),
303
+ );
304
+ entries.push({
305
+ mode: "100644",
306
+ type: "blob",
307
+ sha: blobSha,
308
+ name: filename,
309
+ env,
310
+ });
311
+ return entries;
312
+ }
313
+
314
+ function shellQuoteLines(input: string): string {
315
+ // Escape for printf in shell
316
+ return `'${input.replace(/'/g, "'\\''")}'`;
317
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./types";
2
+ export * from "./git";
3
+ export * from "./digest";
4
+ export * from "./snapshot";
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Snapshot orchestration: queries plugins for deployed resource metadata,
3
+ * assembles StateSnapshots, computes build digests, and writes to git.
4
+ */
5
+ import type { LexiconPlugin, ResourceMetadata } from "../lexicon";
6
+ import type { BuildResult } from "../build";
7
+ import type { SerializerResult } from "../serializer";
8
+ import type { StateSnapshot } from "./types";
9
+ import { computeBuildDigest } from "./digest";
10
+ import { writeSnapshot, getHeadCommit, pushState } from "./git";
11
+ import { sortedJsonReplacer } from "../utils";
12
+
13
+ /** Patterns in attribute names that suggest sensitive data. */
14
+ const SENSITIVE_PATTERNS = [
15
+ /password/i,
16
+ /secret/i,
17
+ /token/i,
18
+ /private.?key/i,
19
+ /credential/i,
20
+ /connection.?string/i,
21
+ ];
22
+
23
+ /**
24
+ * Check for potential sensitive data in resource attributes and return warnings.
25
+ */
26
+ function checkSensitiveData(
27
+ resources: Record<string, ResourceMetadata>,
28
+ ): string[] {
29
+ const warnings: string[] = [];
30
+ for (const [name, meta] of Object.entries(resources)) {
31
+ if (!meta.attributes) continue;
32
+ for (const attrName of Object.keys(meta.attributes)) {
33
+ if (SENSITIVE_PATTERNS.some((p) => p.test(attrName))) {
34
+ warnings.push(
35
+ `Potential sensitive data in ${name}.attributes.${attrName} — ensure it is scrubbed`,
36
+ );
37
+ }
38
+ }
39
+ }
40
+ return warnings;
41
+ }
42
+
43
+ /**
44
+ * Validate ResourceMetadata entries — resources must have at least type and status.
45
+ * Returns { valid, dropped, warnings }.
46
+ */
47
+ function validateResources(
48
+ resources: Record<string, ResourceMetadata>,
49
+ ): {
50
+ valid: Record<string, ResourceMetadata>;
51
+ dropped: string[];
52
+ warnings: string[];
53
+ } {
54
+ const valid: Record<string, ResourceMetadata> = {};
55
+ const dropped: string[] = [];
56
+ const warnings: string[] = [];
57
+
58
+ for (const [name, meta] of Object.entries(resources)) {
59
+ if (!meta.type || !meta.status) {
60
+ dropped.push(name);
61
+ warnings.push(`Dropped ${name}: missing type or status`);
62
+ continue;
63
+ }
64
+ valid[name] = meta;
65
+ }
66
+
67
+ // Check for sensitive data in valid resources
68
+ warnings.push(...checkSensitiveData(valid));
69
+
70
+ return { valid, dropped, warnings };
71
+ }
72
+
73
+ export interface TakeSnapshotResult {
74
+ snapshots: StateSnapshot[];
75
+ commit: string;
76
+ warnings: string[];
77
+ errors: string[];
78
+ }
79
+
80
+ /**
81
+ * Take state snapshots for all plugins that implement describeResources.
82
+ */
83
+ export async function takeSnapshot(
84
+ environment: string,
85
+ plugins: LexiconPlugin[],
86
+ buildResult: BuildResult,
87
+ opts?: { cwd?: string },
88
+ ): Promise<TakeSnapshotResult> {
89
+ const warnings: string[] = [];
90
+ const errors: string[] = [];
91
+ const snapshots: StateSnapshot[] = [];
92
+
93
+ const headCommit = await getHeadCommit(opts);
94
+ const timestamp = new Date().toISOString();
95
+ const digest = computeBuildDigest(buildResult);
96
+
97
+ for (const plugin of plugins) {
98
+ if (!plugin.describeResources) continue;
99
+
100
+ // Get serialized build output for this lexicon
101
+ const rawOutput = buildResult.outputs.get(plugin.name);
102
+ const buildOutput =
103
+ rawOutput === undefined
104
+ ? ""
105
+ : typeof rawOutput === "string"
106
+ ? rawOutput
107
+ : (rawOutput as SerializerResult).primary;
108
+
109
+ // Get entity names for this lexicon
110
+ const entityNames: string[] = [];
111
+ for (const [name, entity] of buildResult.entities) {
112
+ if (entity.lexicon === plugin.name) {
113
+ entityNames.push(name);
114
+ }
115
+ }
116
+
117
+ try {
118
+ const resources = await plugin.describeResources({
119
+ environment,
120
+ buildOutput,
121
+ entityNames,
122
+ });
123
+
124
+ const { valid, dropped, warnings: validationWarnings } =
125
+ validateResources(resources);
126
+ warnings.push(...validationWarnings);
127
+
128
+ if (dropped.length > 0) {
129
+ warnings.push(
130
+ `${plugin.name}: dropped ${dropped.length} invalid resource(s)`,
131
+ );
132
+ }
133
+
134
+ if (Object.keys(valid).length === 0) {
135
+ errors.push(`${plugin.name}: no valid resources returned`);
136
+ continue;
137
+ }
138
+
139
+ const snapshot: StateSnapshot = {
140
+ lexicon: plugin.name,
141
+ environment,
142
+ commit: headCommit,
143
+ timestamp,
144
+ resources: valid,
145
+ digest,
146
+ };
147
+
148
+ snapshots.push(snapshot);
149
+ } catch (err) {
150
+ errors.push(
151
+ `${plugin.name}: ${err instanceof Error ? err.message : String(err)}`,
152
+ );
153
+ }
154
+ }
155
+
156
+ // Write all successful snapshots to git
157
+ let commitSha = "";
158
+ for (const snapshot of snapshots) {
159
+ const json = JSON.stringify(snapshot, sortedJsonReplacer, 2);
160
+ commitSha = await writeSnapshot(
161
+ snapshot.environment,
162
+ snapshot.lexicon,
163
+ json,
164
+ opts,
165
+ );
166
+ }
167
+
168
+ // Push to remote
169
+ if (snapshots.length > 0) {
170
+ await pushState(opts);
171
+ }
172
+
173
+ return {
174
+ snapshots,
175
+ commit: commitSha,
176
+ warnings,
177
+ errors,
178
+ };
179
+ }
@@ -0,0 +1,59 @@
1
+ import type { ResourceMetadata } from "../lexicon";
2
+
3
+ export type { ResourceMetadata } from "../lexicon";
4
+
5
+ /**
6
+ * State snapshot for a single lexicon in an environment.
7
+ */
8
+ export interface StateSnapshot {
9
+ lexicon: string;
10
+ environment: string;
11
+ /** Main branch commit this corresponds to */
12
+ commit: string;
13
+ /** ISO timestamp when the snapshot was taken */
14
+ timestamp: string;
15
+ /** Resource metadata keyed by logical name */
16
+ resources: Record<string, ResourceMetadata>;
17
+ /** Build digest at snapshot time — what was declared when this snapshot was taken */
18
+ digest?: BuildDigest;
19
+ }
20
+
21
+ /**
22
+ * Digest of a single resource declaration.
23
+ */
24
+ export interface ResourceDigest {
25
+ /** Entity type (e.g. AWS::S3::Bucket) */
26
+ type: string;
27
+ /** Which lexicon owns this resource */
28
+ lexicon: string;
29
+ /** Hash of deterministically-serialized declaration props */
30
+ propsHash: string;
31
+ }
32
+
33
+ /**
34
+ * Digest of the entire build at a point in time.
35
+ */
36
+ export interface BuildDigest {
37
+ /** Per-resource digest keyed by logical name */
38
+ resources: Record<string, ResourceDigest>;
39
+ /** Resource-level dependency graph */
40
+ dependencies: Record<string, string[]>;
41
+ /** Cross-lexicon output bridges from BuildManifest */
42
+ outputs: Record<string, { source: string; entity: string; attribute: string }>;
43
+ /** Lexicon-level deploy order */
44
+ deployOrder: string[];
45
+ }
46
+
47
+ /**
48
+ * Result of comparing two build digests.
49
+ */
50
+ export interface DigestDiff {
51
+ /** Resources in current build but not in previous digest */
52
+ added: string[];
53
+ /** Resources in previous digest but not in current build */
54
+ removed: string[];
55
+ /** Resources where propsHash differs */
56
+ changed: string[];
57
+ /** Resources where propsHash matches */
58
+ unchanged: string[];
59
+ }