@auto-engineer/model-diff 1.12.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.
Files changed (57) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-test.log +14 -0
  3. package/.turbo/turbo-type-check.log +4 -0
  4. package/CHANGELOG.md +17 -0
  5. package/LICENSE +10 -0
  6. package/dist/src/change-detector.d.ts +17 -0
  7. package/dist/src/change-detector.d.ts.map +1 -0
  8. package/dist/src/change-detector.js +37 -0
  9. package/dist/src/change-detector.js.map +1 -0
  10. package/dist/src/commands/detect-changes.d.ts +8 -0
  11. package/dist/src/commands/detect-changes.d.ts.map +1 -0
  12. package/dist/src/commands/detect-changes.js +72 -0
  13. package/dist/src/commands/detect-changes.js.map +1 -0
  14. package/dist/src/fingerprint.d.ts +19 -0
  15. package/dist/src/fingerprint.d.ts.map +1 -0
  16. package/dist/src/fingerprint.js +38 -0
  17. package/dist/src/fingerprint.js.map +1 -0
  18. package/dist/src/generation-state.d.ts +11 -0
  19. package/dist/src/generation-state.d.ts.map +1 -0
  20. package/dist/src/generation-state.js +33 -0
  21. package/dist/src/generation-state.js.map +1 -0
  22. package/dist/src/index.d.ts +12 -0
  23. package/dist/src/index.d.ts.map +1 -0
  24. package/dist/src/index.js +4 -0
  25. package/dist/src/index.js.map +1 -0
  26. package/dist/src/model-dependencies.d.ts +26 -0
  27. package/dist/src/model-dependencies.d.ts.map +1 -0
  28. package/dist/src/model-dependencies.js +130 -0
  29. package/dist/src/model-dependencies.js.map +1 -0
  30. package/dist/src/stable-stringify.d.ts +2 -0
  31. package/dist/src/stable-stringify.d.ts.map +1 -0
  32. package/dist/src/stable-stringify.js +19 -0
  33. package/dist/src/stable-stringify.js.map +1 -0
  34. package/dist/src/utils.d.ts +2 -0
  35. package/dist/src/utils.d.ts.map +1 -0
  36. package/dist/src/utils.js +7 -0
  37. package/dist/src/utils.js.map +1 -0
  38. package/dist/tsconfig.tsbuildinfo +1 -0
  39. package/ketchup-plan.md +19 -0
  40. package/package.json +32 -0
  41. package/src/change-detector.specs.ts +138 -0
  42. package/src/change-detector.ts +65 -0
  43. package/src/commands/detect-changes.specs.ts +190 -0
  44. package/src/commands/detect-changes.ts +112 -0
  45. package/src/fingerprint.specs.ts +213 -0
  46. package/src/fingerprint.ts +59 -0
  47. package/src/generation-state.specs.ts +89 -0
  48. package/src/generation-state.ts +46 -0
  49. package/src/index.ts +7 -0
  50. package/src/model-dependencies.specs.ts +455 -0
  51. package/src/model-dependencies.ts +136 -0
  52. package/src/stable-stringify.specs.ts +45 -0
  53. package/src/stable-stringify.ts +19 -0
  54. package/src/utils.specs.ts +24 -0
  55. package/src/utils.ts +6 -0
  56. package/tsconfig.json +10 -0
  57. package/vitest.config.ts +21 -0
@@ -0,0 +1,112 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+ import { type Command, defineCommandHandler, type Event } from '@auto-engineer/message-bus';
4
+ import type { Model } from '@auto-engineer/narrative';
5
+ import type { ChangeSet } from '../change-detector';
6
+ import { computeChangeSet } from '../change-detector';
7
+ import { computeAllFingerprints, computeAllSnapshots } from '../fingerprint';
8
+ import type { GenerationState } from '../generation-state';
9
+ import { loadGenerationState } from '../generation-state';
10
+ import { computeSharedTypesHash } from '../model-dependencies';
11
+
12
+ type DetectChangesCommand = Command<
13
+ 'DetectChanges',
14
+ {
15
+ modelPath: string;
16
+ destination: string;
17
+ force?: boolean;
18
+ }
19
+ >;
20
+
21
+ type ChangesDetectedEvent = Event<
22
+ 'ChangesDetected',
23
+ {
24
+ modelPath: string;
25
+ destination: string;
26
+ changeSet: ChangeSet;
27
+ isFirstRun: boolean;
28
+ newState: GenerationState;
29
+ }
30
+ >;
31
+
32
+ type ChangeDetectionFailedEvent = Event<
33
+ 'ChangeDetectionFailed',
34
+ {
35
+ modelPath: string;
36
+ destination: string;
37
+ error: string;
38
+ }
39
+ >;
40
+
41
+ type DetectChangesEvents = ChangesDetectedEvent | ChangeDetectionFailedEvent;
42
+
43
+ export const commandHandler = defineCommandHandler({
44
+ name: 'DetectChanges',
45
+ displayName: 'Detect Changes',
46
+ alias: 'detect:changes',
47
+ description: 'Detect model-level changes between pipeline runs',
48
+ category: 'generate',
49
+ icon: 'diff',
50
+ events: [
51
+ { name: 'ChangesDetected', displayName: 'Changes Detected' },
52
+ { name: 'ChangeDetectionFailed', displayName: 'Change Detection Failed' },
53
+ ],
54
+ fields: {
55
+ modelPath: { description: 'Path to the JSON model file', required: true },
56
+ destination: { description: 'Project root directory', required: true },
57
+ force: { description: 'Force full regeneration (treat as first run)' },
58
+ },
59
+ examples: ['$ auto detect:changes --model-path=.context/schema.json --destination=.'],
60
+ handle: async (command: Command): Promise<DetectChangesEvents> => {
61
+ const typed = command as DetectChangesCommand;
62
+ const { modelPath, destination, force } = typed.data;
63
+
64
+ try {
65
+ const absModel = resolve(modelPath);
66
+ const absDest = resolve(destination);
67
+
68
+ const content = await readFile(absModel, 'utf8');
69
+ const model = JSON.parse(content) as Model;
70
+
71
+ const previousState = force ? null : await loadGenerationState(absDest);
72
+ const isFirstRun = previousState === null;
73
+
74
+ const snapshots = computeAllSnapshots(model);
75
+ const fingerprints = computeAllFingerprints(snapshots);
76
+ const sharedTypesHash = computeSharedTypesHash(model.messages);
77
+ const changeSet = computeChangeSet(previousState, fingerprints, snapshots, sharedTypesHash);
78
+
79
+ const newState: GenerationState = {
80
+ version: 1,
81
+ timestamp: new Date().toISOString(),
82
+ sliceFingerprints: fingerprints,
83
+ sliceSnapshots: snapshots,
84
+ sharedTypesHash,
85
+ };
86
+
87
+ const contextDir = join(absDest, '.context');
88
+ await mkdir(contextDir, { recursive: true });
89
+ await writeFile(join(contextDir, '.change-set.json'), JSON.stringify(changeSet, null, 2), 'utf8');
90
+
91
+ return {
92
+ type: 'ChangesDetected',
93
+ data: { modelPath, destination, changeSet, isFirstRun, newState },
94
+ timestamp: new Date(),
95
+ requestId: typed.requestId,
96
+ correlationId: typed.correlationId,
97
+ };
98
+ } catch (error) {
99
+ return {
100
+ type: 'ChangeDetectionFailed',
101
+ data: {
102
+ modelPath,
103
+ destination,
104
+ error: error instanceof Error ? error.message : 'Unknown error',
105
+ },
106
+ timestamp: new Date(),
107
+ requestId: typed.requestId,
108
+ correlationId: typed.correlationId,
109
+ };
110
+ }
111
+ },
112
+ });
@@ -0,0 +1,213 @@
1
+ import type { Model } from '@auto-engineer/narrative';
2
+ import { describe, expect, it } from 'vitest';
3
+ import {
4
+ buildSliceSnapshot,
5
+ computeAllFingerprints,
6
+ computeAllSnapshots,
7
+ computeFingerprintFromSnapshot,
8
+ } from './fingerprint';
9
+
10
+ const makeSpec = (steps: Array<{ keyword: 'Given' | 'When' | 'Then' | 'And'; text: string }>) => [
11
+ {
12
+ type: 'gherkin' as const,
13
+ feature: 'test',
14
+ rules: [{ name: 'r1', examples: [{ name: 'e1', steps }] }],
15
+ },
16
+ ];
17
+
18
+ const baseModel: Model = {
19
+ variant: 'specs',
20
+ narratives: [
21
+ {
22
+ name: 'TodoManagement',
23
+ slices: [
24
+ {
25
+ name: 'AddTodo',
26
+ type: 'command' as const,
27
+ client: { specs: [] },
28
+ server: {
29
+ description: 'Add a todo item',
30
+ specs: makeSpec([
31
+ { keyword: 'When', text: 'AddTodo' },
32
+ { keyword: 'Then', text: 'TodoAdded' },
33
+ ]),
34
+ },
35
+ },
36
+ {
37
+ name: 'GetTodos',
38
+ type: 'query' as const,
39
+ client: { specs: [] },
40
+ server: {
41
+ description: 'Get all todos',
42
+ specs: makeSpec([
43
+ { keyword: 'Given', text: 'TodoAdded' },
44
+ { keyword: 'When', text: 'GetTodos' },
45
+ { keyword: 'Then', text: 'TodoList' },
46
+ ]),
47
+ },
48
+ },
49
+ ],
50
+ },
51
+ ],
52
+ messages: [
53
+ { name: 'AddTodo', type: 'command' as const, fields: [{ name: 'title', type: 'string', required: true }] },
54
+ {
55
+ name: 'TodoAdded',
56
+ type: 'event' as const,
57
+ fields: [
58
+ { name: 'id', type: 'string', required: true },
59
+ { name: 'title', type: 'string', required: true },
60
+ ],
61
+ },
62
+ { name: 'GetTodos', type: 'query' as const, fields: [] },
63
+ { name: 'TodoList', type: 'state' as const, fields: [{ name: 'items', type: 'array', required: true }] },
64
+ ],
65
+ modules: [],
66
+ };
67
+
68
+ describe('buildSliceSnapshot', () => {
69
+ it('assembles snapshot with referenced messages sorted by name', () => {
70
+ const slice = baseModel.narratives[0].slices[0];
71
+ const snapshot = buildSliceSnapshot(slice, 'TodoManagement', baseModel);
72
+
73
+ expect(snapshot).toEqual({
74
+ slice,
75
+ flowName: 'TodoManagement',
76
+ referencedMessages: [
77
+ { name: 'AddTodo', type: 'command', fields: [{ name: 'title', type: 'string', required: true }] },
78
+ {
79
+ name: 'TodoAdded',
80
+ type: 'event',
81
+ fields: [
82
+ { name: 'id', type: 'string', required: true },
83
+ { name: 'title', type: 'string', required: true },
84
+ ],
85
+ },
86
+ ],
87
+ eventSources: {},
88
+ commandSources: {},
89
+ referencedIntegrations: undefined,
90
+ });
91
+ });
92
+
93
+ it('includes event sources for query slices', () => {
94
+ const slice = baseModel.narratives[0].slices[1];
95
+ const snapshot = buildSliceSnapshot(slice, 'TodoManagement', baseModel);
96
+
97
+ expect(snapshot.eventSources).toEqual({
98
+ TodoAdded: { flowName: 'TodoManagement', sliceName: 'AddTodo' },
99
+ });
100
+ });
101
+
102
+ it('handles experience slices with empty dependencies', () => {
103
+ const model: Model = {
104
+ ...baseModel,
105
+ narratives: [
106
+ {
107
+ name: 'UI',
108
+ slices: [{ name: 'ViewTodos', type: 'experience' as const, client: { specs: [] } }],
109
+ },
110
+ ],
111
+ };
112
+ const snapshot = buildSliceSnapshot(model.narratives[0].slices[0], 'UI', model);
113
+
114
+ expect(snapshot).toEqual({
115
+ slice: model.narratives[0].slices[0],
116
+ flowName: 'UI',
117
+ referencedMessages: [],
118
+ eventSources: {},
119
+ commandSources: {},
120
+ referencedIntegrations: undefined,
121
+ });
122
+ });
123
+
124
+ it('includes referenced integrations when slice uses via', () => {
125
+ const model: Model = {
126
+ ...baseModel,
127
+ integrations: [
128
+ { name: 'MailChimp', source: '@auto-engineer/mailchimp' },
129
+ { name: 'Twilio', source: '@auto-engineer/twilio' },
130
+ ],
131
+ narratives: [
132
+ {
133
+ name: 'Notifications',
134
+ slices: [
135
+ {
136
+ name: 'NotifyUser',
137
+ type: 'react' as const,
138
+ via: ['MailChimp'],
139
+ server: {
140
+ specs: makeSpec([
141
+ { keyword: 'Given', text: 'TodoAdded' },
142
+ { keyword: 'When', text: 'TodoAdded' },
143
+ { keyword: 'Then', text: 'SendEmail' },
144
+ ]),
145
+ },
146
+ },
147
+ ],
148
+ },
149
+ ...baseModel.narratives,
150
+ ],
151
+ };
152
+ const snapshot = buildSliceSnapshot(model.narratives[0].slices[0], 'Notifications', model);
153
+ expect(snapshot.referencedIntegrations).toEqual([{ name: 'MailChimp', source: '@auto-engineer/mailchimp' }]);
154
+ });
155
+ });
156
+
157
+ describe('computeFingerprintFromSnapshot', () => {
158
+ it('returns a SHA-256 hex string', () => {
159
+ const snapshot = buildSliceSnapshot(baseModel.narratives[0].slices[0], 'TodoManagement', baseModel);
160
+ const fingerprint = computeFingerprintFromSnapshot(snapshot);
161
+ expect(fingerprint).toMatch(/^[a-f0-9]{64}$/);
162
+ });
163
+
164
+ it('produces different fingerprints for different snapshots', () => {
165
+ const snap1 = buildSliceSnapshot(baseModel.narratives[0].slices[0], 'TodoManagement', baseModel);
166
+ const snap2 = buildSliceSnapshot(baseModel.narratives[0].slices[1], 'TodoManagement', baseModel);
167
+ expect(computeFingerprintFromSnapshot(snap1)).not.toBe(computeFingerprintFromSnapshot(snap2));
168
+ });
169
+
170
+ it('produces same fingerprint for identical snapshots', () => {
171
+ const snap1 = buildSliceSnapshot(baseModel.narratives[0].slices[0], 'TodoManagement', baseModel);
172
+ const snap2 = buildSliceSnapshot(baseModel.narratives[0].slices[0], 'TodoManagement', baseModel);
173
+ expect(computeFingerprintFromSnapshot(snap1)).toBe(computeFingerprintFromSnapshot(snap2));
174
+ });
175
+ });
176
+
177
+ describe('computeAllSnapshots', () => {
178
+ it('creates snapshots keyed by kebab-case sliceId', () => {
179
+ const snapshots = computeAllSnapshots(baseModel);
180
+ expect(Object.keys(snapshots)).toEqual(['todo-management/add-todo', 'todo-management/get-todos']);
181
+ });
182
+
183
+ it('includes experience slices', () => {
184
+ const model: Model = {
185
+ ...baseModel,
186
+ narratives: [
187
+ ...baseModel.narratives,
188
+ { name: 'UI', slices: [{ name: 'ViewTodos', type: 'experience' as const, client: { specs: [] } }] },
189
+ ],
190
+ };
191
+ const snapshots = computeAllSnapshots(model);
192
+ const expSlice = model.narratives[1].slices[0];
193
+ expect(snapshots['ui/view-todos']).toEqual({
194
+ slice: expSlice,
195
+ flowName: 'UI',
196
+ referencedMessages: [],
197
+ eventSources: {},
198
+ commandSources: {},
199
+ referencedIntegrations: undefined,
200
+ });
201
+ });
202
+ });
203
+
204
+ describe('computeAllFingerprints', () => {
205
+ it('returns fingerprints for all snapshots', () => {
206
+ const snapshots = computeAllSnapshots(baseModel);
207
+ const fingerprints = computeAllFingerprints(snapshots);
208
+ expect(Object.keys(fingerprints)).toEqual(['todo-management/add-todo', 'todo-management/get-todos']);
209
+ for (const fp of Object.values(fingerprints)) {
210
+ expect(fp).toMatch(/^[a-f0-9]{64}$/);
211
+ }
212
+ });
213
+ });
@@ -0,0 +1,59 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type { Message, Model, Slice } from '@auto-engineer/narrative';
3
+ import {
4
+ getCommandSourceMap,
5
+ getEventSourceMap,
6
+ getReferencedIntegrations,
7
+ getReferencedMessageNames,
8
+ } from './model-dependencies';
9
+ import { stableStringify } from './stable-stringify';
10
+ import { toKebabCase } from './utils';
11
+
12
+ type SourceLocation = { flowName: string; sliceName: string };
13
+
14
+ export type SliceSnapshot = {
15
+ slice: Slice;
16
+ flowName: string;
17
+ referencedMessages: Message[];
18
+ eventSources: Record<string, SourceLocation>;
19
+ commandSources: Record<string, SourceLocation>;
20
+ referencedIntegrations?: Model['integrations'];
21
+ };
22
+
23
+ export function buildSliceSnapshot(slice: Slice, flowName: string, model: Model): SliceSnapshot {
24
+ const messageNames = getReferencedMessageNames(slice);
25
+ const referencedMessages = messageNames
26
+ .map((name) => model.messages.find((m) => m.name === name))
27
+ .filter((m): m is Message => m !== undefined)
28
+ .sort((a, b) => a.name.localeCompare(b.name));
29
+ const eventSources = getEventSourceMap(slice, model.narratives);
30
+ const commandSources = getCommandSourceMap(slice, model.narratives);
31
+ const referencedIntegrations = getReferencedIntegrations(slice, model.integrations);
32
+
33
+ return { slice, flowName, referencedMessages, eventSources, commandSources, referencedIntegrations };
34
+ }
35
+
36
+ export function computeFingerprintFromSnapshot(snapshot: SliceSnapshot): string {
37
+ const hash = createHash('sha256');
38
+ hash.update(stableStringify(snapshot));
39
+ return hash.digest('hex');
40
+ }
41
+
42
+ export function computeAllSnapshots(model: Model): Record<string, SliceSnapshot> {
43
+ const snapshots: Record<string, SliceSnapshot> = {};
44
+ for (const narrative of model.narratives) {
45
+ for (const slice of narrative.slices) {
46
+ const sliceId = `${toKebabCase(narrative.name)}/${toKebabCase(slice.name)}`;
47
+ snapshots[sliceId] = buildSliceSnapshot(slice, narrative.name, model);
48
+ }
49
+ }
50
+ return snapshots;
51
+ }
52
+
53
+ export function computeAllFingerprints(snapshots: Record<string, SliceSnapshot>): Record<string, string> {
54
+ const fingerprints: Record<string, string> = {};
55
+ for (const [sliceId, snapshot] of Object.entries(snapshots)) {
56
+ fingerprints[sliceId] = computeFingerprintFromSnapshot(snapshot);
57
+ }
58
+ return fingerprints;
59
+ }
@@ -0,0 +1,89 @@
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import type { SliceSnapshot } from './fingerprint';
6
+ import type { GenerationState } from './generation-state';
7
+ import { loadGenerationState, saveGenerationState } from './generation-state';
8
+
9
+ const stubSnapshot: SliceSnapshot = {
10
+ slice: {
11
+ name: 'AddTodo',
12
+ type: 'command',
13
+ client: { specs: [] },
14
+ server: { description: '', specs: [] },
15
+ },
16
+ flowName: 'Todo',
17
+ referencedMessages: [],
18
+ eventSources: {},
19
+ commandSources: {},
20
+ referencedIntegrations: undefined,
21
+ };
22
+
23
+ describe('generation state persistence', () => {
24
+ let testDir: string;
25
+
26
+ beforeEach(async () => {
27
+ testDir = join(tmpdir(), `model-diff-test-${Date.now()}`);
28
+ await mkdir(testDir, { recursive: true });
29
+ });
30
+
31
+ afterEach(async () => {
32
+ await rm(testDir, { recursive: true, force: true });
33
+ });
34
+
35
+ const validState: GenerationState = {
36
+ version: 1,
37
+ timestamp: '2024-01-01T00:00:00.000Z',
38
+ sliceFingerprints: { 'todo/add-todo': 'abc123' },
39
+ sliceSnapshots: { 'todo/add-todo': stubSnapshot },
40
+ sharedTypesHash: 'hash123',
41
+ };
42
+
43
+ describe('saveGenerationState', () => {
44
+ it('writes state to .context/.generation-state.json', async () => {
45
+ await saveGenerationState(testDir, validState);
46
+
47
+ const content = await readFile(join(testDir, '.context', '.generation-state.json'), 'utf8');
48
+ const parsed = JSON.parse(content);
49
+ expect(parsed).toEqual(validState);
50
+ });
51
+
52
+ it('creates .context directory if it does not exist', async () => {
53
+ await saveGenerationState(testDir, validState);
54
+
55
+ const content = await readFile(join(testDir, '.context', '.generation-state.json'), 'utf8');
56
+ expect(JSON.parse(content)).toEqual(validState);
57
+ });
58
+ });
59
+
60
+ describe('loadGenerationState', () => {
61
+ it('loads previously saved state', async () => {
62
+ await saveGenerationState(testDir, validState);
63
+ const loaded = await loadGenerationState(testDir);
64
+ expect(loaded).toEqual(validState);
65
+ });
66
+
67
+ it('returns null when file does not exist', async () => {
68
+ const loaded = await loadGenerationState(testDir);
69
+ expect(loaded).toBeNull();
70
+ });
71
+
72
+ it('returns null when file has wrong version', async () => {
73
+ const badState = { ...validState, version: 999 };
74
+ await mkdir(join(testDir, '.context'), { recursive: true });
75
+ await writeFile(join(testDir, '.context', '.generation-state.json'), JSON.stringify(badState), 'utf8');
76
+
77
+ const loaded = await loadGenerationState(testDir);
78
+ expect(loaded).toBeNull();
79
+ });
80
+
81
+ it('returns null when file contains invalid JSON', async () => {
82
+ await mkdir(join(testDir, '.context'), { recursive: true });
83
+ await writeFile(join(testDir, '.context', '.generation-state.json'), 'not json', 'utf8');
84
+
85
+ const loaded = await loadGenerationState(testDir);
86
+ expect(loaded).toBeNull();
87
+ });
88
+ });
89
+ });
@@ -0,0 +1,46 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import type { SliceSnapshot } from './fingerprint';
4
+
5
+ export type GenerationState = {
6
+ version: 1;
7
+ timestamp: string;
8
+ sliceFingerprints: Record<string, string>;
9
+ sliceSnapshots: Record<string, SliceSnapshot>;
10
+ sharedTypesHash: string;
11
+ };
12
+
13
+ const STATE_VERSION = 1;
14
+
15
+ function statePath(destination: string): string {
16
+ return join(destination, '.context', '.generation-state.json');
17
+ }
18
+
19
+ function isGenerationState(value: Record<string, unknown>): value is GenerationState {
20
+ return (
21
+ value.version === STATE_VERSION &&
22
+ typeof value.timestamp === 'string' &&
23
+ typeof value.sliceFingerprints === 'object' &&
24
+ value.sliceFingerprints !== null &&
25
+ typeof value.sliceSnapshots === 'object' &&
26
+ value.sliceSnapshots !== null &&
27
+ typeof value.sharedTypesHash === 'string'
28
+ );
29
+ }
30
+
31
+ export async function loadGenerationState(destination: string): Promise<GenerationState | null> {
32
+ try {
33
+ const content = await readFile(statePath(destination), 'utf8');
34
+ const parsed = JSON.parse(content) as Record<string, unknown>;
35
+ if (!isGenerationState(parsed)) return null;
36
+ return parsed;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ export async function saveGenerationState(destination: string, state: GenerationState): Promise<void> {
43
+ const filePath = statePath(destination);
44
+ await mkdir(dirname(filePath), { recursive: true });
45
+ await writeFile(filePath, JSON.stringify(state, null, 2), 'utf8');
46
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { commandHandler as detectChangesHandler } from './commands/detect-changes';
2
+
3
+ export const COMMANDS = [detectChangesHandler];
4
+ export type { ChangeSet, SliceDelta } from './change-detector';
5
+ export type { SliceSnapshot } from './fingerprint';
6
+ export type { GenerationState } from './generation-state';
7
+ export { saveGenerationState } from './generation-state';