duet-kit 0.1.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.
package/dist/duet.d.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * duet-kit - Main Entry Point
3
+ *
4
+ * Single factory that creates everything: store + LLM bridge + schema access.
5
+ * Follows Zustand conventions - returns a hook with static properties.
6
+ */
7
+ import { type StoreApi, type UseBoundStore } from 'zustand';
8
+ import { z } from 'zod';
9
+ import type { SchemaFields, FieldDef, InferData, EditResult, LLMBridge } from './types';
10
+ import { DuetSchema } from './schema';
11
+ interface DuetState<T extends SchemaFields> {
12
+ data: InferData<T>;
13
+ set: <K extends keyof T>(field: K, value: z.infer<T[K]['schema']>) => boolean;
14
+ setMany: (updates: Partial<InferData<T>>) => boolean;
15
+ reset: () => void;
16
+ }
17
+ export interface DuetHook<T extends SchemaFields> extends UseBoundStore<StoreApi<DuetState<T>>> {
18
+ llm: LLMBridge<T>;
19
+ schema: DuetSchema<T>;
20
+ }
21
+ export interface DuetOptions {
22
+ /** Optional localStorage key. Omit for in-memory only. For backend sync, use store.subscribe() */
23
+ persist?: string;
24
+ /** Transform the default context string before returning from getContext() */
25
+ transformContext?: (context: string) => string;
26
+ /** Transform the default function schema before returning from getFunctionSchema() */
27
+ transformFunctionSchema?: (schema: object) => object;
28
+ }
29
+ /**
30
+ * Create a Duet - a Zustand store with LLM bridge and schema access.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * const useTripStore = createDuet('TripBudget', {
35
+ * destination: field(z.string(), 'Destination', 'Tokyo'),
36
+ * budget: field(z.number().min(0), 'Budget', 5000),
37
+ * }, { persist: 'trip-data' })
38
+ *
39
+ * // React
40
+ * const { data, set } = useTripStore()
41
+ *
42
+ * // LLM (JSON Patch)
43
+ * useTripStore.llm.applyJSON('[{"op":"replace","path":"/budget","value":10000}]')
44
+ * ```
45
+ */
46
+ export declare function createDuet<T extends SchemaFields>(name: string, fields: T, options?: DuetOptions): DuetHook<T>;
47
+ /**
48
+ * Helper to define a field with Zod schema, label, and default value.
49
+ */
50
+ export declare function field<T>(schema: z.ZodType<T>, label: string, defaultValue: T): FieldDef<T>;
51
+ /**
52
+ * Helper to check if edit was successful
53
+ */
54
+ export declare function isSuccess(result: EditResult): result is {
55
+ success: true;
56
+ applied: number;
57
+ };
58
+ /**
59
+ * Get human-readable message from result
60
+ */
61
+ export declare function getResultMessage(result: EditResult): string;
62
+ export {};
63
+ //# sourceMappingURL=duet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duet.d.ts","sourceRoot":"","sources":["../src/duet.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAU,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAEpE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAe,UAAU,EAAE,SAAS,EAAgB,MAAM,SAAS,CAAC;AACnH,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAGtC,UAAU,SAAS,CAAC,CAAC,SAAS,YAAY;IACxC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IACnB,GAAG,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,KAAK,OAAO,CAAC;IAC9E,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC;IACrD,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAGD,MAAM,WAAW,QAAQ,CAAC,CAAC,SAAS,YAAY,CAAE,SAAQ,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7F,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IAClB,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,kGAAkG;IAClG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/C,sFAAsF;IACtF,uBAAuB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;CACtD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,YAAY,EAC/C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,CAAC,EACT,OAAO,GAAE,WAAgB,GACxB,QAAQ,CAAC,CAAC,CAAC,CAmPb;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,CAAC,EACrB,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EACpB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,CAAC,GACd,QAAQ,CAAC,CAAC,CAAC,CAEb;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,IAAI;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAE1F;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAK3D"}
package/dist/duet.js ADDED
@@ -0,0 +1,259 @@
1
+ /**
2
+ * duet-kit - Main Entry Point
3
+ *
4
+ * Single factory that creates everything: store + LLM bridge + schema access.
5
+ * Follows Zustand conventions - returns a hook with static properties.
6
+ */
7
+ import { create } from 'zustand';
8
+ import { persist } from 'zustand/middleware';
9
+ import { DuetSchema } from './schema';
10
+ /**
11
+ * Create a Duet - a Zustand store with LLM bridge and schema access.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const useTripStore = createDuet('TripBudget', {
16
+ * destination: field(z.string(), 'Destination', 'Tokyo'),
17
+ * budget: field(z.number().min(0), 'Budget', 5000),
18
+ * }, { persist: 'trip-data' })
19
+ *
20
+ * // React
21
+ * const { data, set } = useTripStore()
22
+ *
23
+ * // LLM (JSON Patch)
24
+ * useTripStore.llm.applyJSON('[{"op":"replace","path":"/budget","value":10000}]')
25
+ * ```
26
+ */
27
+ export function createDuet(name, fields, options = {}) {
28
+ // Create schema
29
+ const schema = new DuetSchema(name, fields);
30
+ const initialData = schema.getDefaults();
31
+ // Store creator function
32
+ const storeCreator = (set) => ({
33
+ data: initialData,
34
+ set: (field, value) => {
35
+ const result = schema.validate(field, value);
36
+ if (!result.success) {
37
+ console.warn(`Validation failed for ${String(field)}:`, result.error.message);
38
+ return false;
39
+ }
40
+ set((state) => ({
41
+ data: { ...state.data, [field]: result.data },
42
+ }));
43
+ return true;
44
+ },
45
+ setMany: (updates) => {
46
+ const validated = {};
47
+ for (const [key, value] of Object.entries(updates)) {
48
+ if (key in schema.fields) {
49
+ const result = schema.validate(key, value);
50
+ if (!result.success) {
51
+ console.warn(`Validation failed for ${key}:`, result.error.message);
52
+ return false;
53
+ }
54
+ validated[key] = result.data;
55
+ }
56
+ }
57
+ set((state) => ({
58
+ data: { ...state.data, ...validated },
59
+ }));
60
+ return true;
61
+ },
62
+ reset: () => {
63
+ set(() => ({ data: schema.getDefaults() }));
64
+ },
65
+ });
66
+ // Create the Zustand store
67
+ const useStore = options.persist
68
+ ? create()(persist(storeCreator, {
69
+ name: options.persist,
70
+ partialize: (state) => ({ data: state.data }),
71
+ }))
72
+ : create()(storeCreator);
73
+ // History log for audit trail
74
+ const patchHistory = [];
75
+ let historyId = 0;
76
+ // Create LLM bridge (JSON Patch format)
77
+ const llmBridge = {
78
+ applyPatch(patch, source = 'llm') {
79
+ const currentData = { ...useStore.getState().data };
80
+ for (const op of patch) {
81
+ // Parse JSON Pointer path: /field or /parent/child
82
+ const pathParts = op.path.replace(/^\//, '').split('/');
83
+ const rootField = pathParts[0];
84
+ if (!(rootField in schema.fields)) {
85
+ const result = { success: false, error: `Unknown field: ${rootField}` };
86
+ patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result });
87
+ return result;
88
+ }
89
+ if (op.op === 'remove') {
90
+ if (pathParts.length === 1) {
91
+ currentData[rootField] = schema.getDefaults()[rootField];
92
+ }
93
+ else {
94
+ let target = currentData[rootField];
95
+ for (let i = 1; i < pathParts.length - 1; i++) {
96
+ target = target[pathParts[i]];
97
+ }
98
+ delete target[pathParts[pathParts.length - 1]];
99
+ }
100
+ continue;
101
+ }
102
+ if (op.op === 'replace' || op.op === 'add') {
103
+ if (pathParts.length === 1) {
104
+ const result = schema.validate(rootField, op.value);
105
+ if (!result.success) {
106
+ const editResult = {
107
+ success: false,
108
+ error: `Invalid value for ${rootField}: ${result.error.errors[0]?.message}`
109
+ };
110
+ patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result: editResult });
111
+ return editResult;
112
+ }
113
+ currentData[rootField] = result.data;
114
+ }
115
+ else {
116
+ const newValue = JSON.parse(JSON.stringify(currentData[rootField]));
117
+ let target = newValue;
118
+ for (let i = 1; i < pathParts.length - 1; i++) {
119
+ if (!(pathParts[i] in target)) {
120
+ target[pathParts[i]] = {};
121
+ }
122
+ target = target[pathParts[i]];
123
+ }
124
+ target[pathParts[pathParts.length - 1]] = op.value;
125
+ const result = schema.validate(rootField, newValue);
126
+ if (!result.success) {
127
+ const editResult = {
128
+ success: false,
129
+ error: `Invalid value for ${op.path}: ${result.error.errors[0]?.message}`
130
+ };
131
+ patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result: editResult });
132
+ return editResult;
133
+ }
134
+ currentData[rootField] = result.data;
135
+ }
136
+ }
137
+ }
138
+ const committed = useStore.getState().setMany(currentData);
139
+ if (!committed) {
140
+ const result = { success: false, error: 'Failed to commit changes to store' };
141
+ patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result });
142
+ return result;
143
+ }
144
+ const result = { success: true, applied: patch.length };
145
+ patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result });
146
+ return result;
147
+ },
148
+ applyJSON(json, source = 'llm') {
149
+ try {
150
+ const parsed = JSON.parse(json);
151
+ if (Array.isArray(parsed)) {
152
+ return this.applyPatch(parsed, source);
153
+ }
154
+ if (parsed.patch && Array.isArray(parsed.patch)) {
155
+ return this.applyPatch(parsed.patch, source);
156
+ }
157
+ return { success: false, error: 'Expected JSON Patch array or { patch: [...] } format' };
158
+ }
159
+ catch (e) {
160
+ return { success: false, error: `JSON parse error: ${e.message}` };
161
+ }
162
+ },
163
+ history() {
164
+ return [...patchHistory];
165
+ },
166
+ clearHistory() {
167
+ patchHistory.length = 0;
168
+ historyId = 0;
169
+ },
170
+ getContext() {
171
+ const data = useStore.getState().data;
172
+ let context = schema.getDescription();
173
+ context += '\nCurrent Values:\n';
174
+ for (const [id, field] of Object.entries(schema.fields)) {
175
+ const value = data[id];
176
+ context += ` ${id}: ${JSON.stringify(value)} (${field.label})\n`;
177
+ }
178
+ context += `
179
+ To edit fields, respond with a JSON Patch array (RFC 6902):
180
+ [{ "op": "replace", "path": "/fieldName", "value": newValue }]
181
+
182
+ Examples:
183
+ - Single edit: [{ "op": "replace", "path": "/budget", "value": 5000 }]
184
+ - Multiple: [{ "op": "replace", "path": "/budget", "value": 5000 }, { "op": "replace", "path": "/days", "value": 14 }]`;
185
+ return options.transformContext ? options.transformContext(context) : context;
186
+ },
187
+ getCompactContext() {
188
+ const data = useStore.getState().data;
189
+ const values = Object.entries(data)
190
+ .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
191
+ .join(', ');
192
+ return `${schema.name}: {${values}}\nJSON Patch: [{"op":"replace","path":"/field","value":x}]`;
193
+ },
194
+ getFunctionSchema() {
195
+ const fnSchema = {
196
+ name: `patch_${schema.name.toLowerCase().replace(/\s+/g, '_')}`,
197
+ description: `Apply JSON Patch operations to ${schema.name}. Uses RFC 6902 format.`,
198
+ parameters: {
199
+ type: 'object',
200
+ properties: {
201
+ patch: {
202
+ type: 'array',
203
+ description: 'JSON Patch operations (RFC 6902)',
204
+ items: {
205
+ type: 'object',
206
+ properties: {
207
+ op: {
208
+ type: 'string',
209
+ enum: ['replace', 'add', 'remove'],
210
+ description: 'Operation type',
211
+ },
212
+ path: {
213
+ type: 'string',
214
+ description: `JSON Pointer to field. Valid paths: ${schema.getFieldIds().map(f => `/${f}`).join(', ')}`,
215
+ },
216
+ value: {
217
+ description: 'New value (required for replace/add)',
218
+ },
219
+ },
220
+ required: ['op', 'path'],
221
+ },
222
+ },
223
+ },
224
+ required: ['patch'],
225
+ },
226
+ };
227
+ return options.transformFunctionSchema ? options.transformFunctionSchema(fnSchema) : fnSchema;
228
+ },
229
+ getCurrentValues() {
230
+ return useStore.getState().data;
231
+ },
232
+ };
233
+ // Attach llm and schema to the hook
234
+ const hook = useStore;
235
+ hook.llm = llmBridge;
236
+ hook.schema = schema;
237
+ return hook;
238
+ }
239
+ /**
240
+ * Helper to define a field with Zod schema, label, and default value.
241
+ */
242
+ export function field(schema, label, defaultValue) {
243
+ return { schema, label, default: defaultValue };
244
+ }
245
+ /**
246
+ * Helper to check if edit was successful
247
+ */
248
+ export function isSuccess(result) {
249
+ return result.success;
250
+ }
251
+ /**
252
+ * Get human-readable message from result
253
+ */
254
+ export function getResultMessage(result) {
255
+ if (result.success) {
256
+ return `Successfully applied ${result.applied} operation(s)`;
257
+ }
258
+ return result.error;
259
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * duet-kit - Shared state for humans and LLMs
3
+ *
4
+ * A lightweight library for building UIs where both humans and AI
5
+ * can edit the same validated state through a shared schema.
6
+ *
7
+ * Built on Zustand (state) + Zod (validation) + JSON Patch (RFC 6902).
8
+ *
9
+ * @packageDocumentation
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { createDuet, field, z } from 'duet-kit'
14
+ *
15
+ * const useTripStore = createDuet('TripBudget', {
16
+ * destination: field(z.string(), 'Destination', 'Tokyo'),
17
+ * budget: field(z.number().min(0).max(100000), 'Budget', 5000),
18
+ * }, { persist: 'trip-data' })
19
+ *
20
+ * // React
21
+ * const { data, set } = useTripStore()
22
+ *
23
+ * // LLM (JSON Patch format)
24
+ * useTripStore.llm.applyJSON('[{"op":"replace","path":"/budget","value":10000}]')
25
+ * ```
26
+ */
27
+ export { createDuet, field, isSuccess, getResultMessage, type DuetHook, type DuetOptions } from './duet';
28
+ export { attachLLM } from './attach';
29
+ export type { SchemaFields, FieldDef, InferData, JsonPatchOp, EditResult, HistoryEntry, LLMBridge, } from './types';
30
+ export { z } from 'zod';
31
+ export { createSchema, DuetSchema } from './schema';
32
+ export { createStore, type DuetStore, type StoreOptions } from './store';
33
+ export { createLLMBridge } from './llm';
34
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAC;AAGzG,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAGrC,YAAY,EACV,YAAY,EACZ,QAAQ,EACR,SAAS,EACT,WAAW,EACX,UAAU,EACV,YAAY,EACZ,SAAS,GACV,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * duet-kit - Shared state for humans and LLMs
3
+ *
4
+ * A lightweight library for building UIs where both humans and AI
5
+ * can edit the same validated state through a shared schema.
6
+ *
7
+ * Built on Zustand (state) + Zod (validation) + JSON Patch (RFC 6902).
8
+ *
9
+ * @packageDocumentation
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { createDuet, field, z } from 'duet-kit'
14
+ *
15
+ * const useTripStore = createDuet('TripBudget', {
16
+ * destination: field(z.string(), 'Destination', 'Tokyo'),
17
+ * budget: field(z.number().min(0).max(100000), 'Budget', 5000),
18
+ * }, { persist: 'trip-data' })
19
+ *
20
+ * // React
21
+ * const { data, set } = useTripStore()
22
+ *
23
+ * // LLM (JSON Patch format)
24
+ * useTripStore.llm.applyJSON('[{"op":"replace","path":"/budget","value":10000}]')
25
+ * ```
26
+ */
27
+ // Main API
28
+ export { createDuet, field, isSuccess, getResultMessage } from './duet';
29
+ // Drop-in for existing Zustand + Zod codebases
30
+ export { attachLLM } from './attach';
31
+ // Re-export Zod for convenience
32
+ export { z } from 'zod';
33
+ // Advanced: individual building blocks (createDuet combines these)
34
+ export { createSchema, DuetSchema } from './schema';
35
+ export { createStore } from './store';
36
+ export { createLLMBridge } from './llm';
package/dist/llm.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * duet-kit - LLM Utilities
3
+ *
4
+ * Context generation and JSON Patch application for LLM integration.
5
+ * Uses RFC 6902 JSON Patch format.
6
+ */
7
+ import type { SchemaFields, EditResult, LLMBridge } from './types';
8
+ import type { DuetSchema } from './schema';
9
+ import type { DuetStore } from './store';
10
+ /**
11
+ * Create an LLM bridge for a schema + store
12
+ */
13
+ export declare function createLLMBridge<T extends SchemaFields>(schema: DuetSchema<T>, store: DuetStore<T>): LLMBridge<T>;
14
+ /**
15
+ * Helper to check if edit was successful
16
+ */
17
+ export declare function isSuccess(result: EditResult): result is {
18
+ success: true;
19
+ applied: number;
20
+ };
21
+ /**
22
+ * Get human-readable message from result
23
+ */
24
+ export declare function getResultMessage(result: EditResult): string;
25
+ //# sourceMappingURL=llm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm.d.ts","sourceRoot":"","sources":["../src/llm.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAA0B,UAAU,EAAE,SAAS,EAAgB,MAAM,SAAS,CAAC;AACzG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,YAAY,EACpD,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,EACrB,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,GAClB,SAAS,CAAC,CAAC,CAAC,CAqKd;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,IAAI;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAE1F;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAK3D"}
package/dist/llm.js ADDED
@@ -0,0 +1,170 @@
1
+ /**
2
+ * duet-kit - LLM Utilities
3
+ *
4
+ * Context generation and JSON Patch application for LLM integration.
5
+ * Uses RFC 6902 JSON Patch format.
6
+ */
7
+ /**
8
+ * Create an LLM bridge for a schema + store
9
+ */
10
+ export function createLLMBridge(schema, store) {
11
+ // History log for audit trail
12
+ const patchHistory = [];
13
+ let historyId = 0;
14
+ return {
15
+ /**
16
+ * Apply JSON Patch operations (RFC 6902)
17
+ */
18
+ applyPatch(patch, source = 'llm') {
19
+ const updates = {};
20
+ for (const op of patch) {
21
+ const fieldName = op.path.replace(/^\//, '').split('/')[0];
22
+ if (!(fieldName in schema.fields)) {
23
+ const result = { success: false, error: `Unknown field: ${fieldName}` };
24
+ patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result });
25
+ return result;
26
+ }
27
+ if (op.op === 'remove') {
28
+ const defaultValue = schema.getDefaults()[fieldName];
29
+ updates[fieldName] = defaultValue;
30
+ continue;
31
+ }
32
+ if (op.op === 'replace' || op.op === 'add') {
33
+ const result = schema.validate(fieldName, op.value);
34
+ if (!result.success) {
35
+ const editResult = {
36
+ success: false,
37
+ error: `Invalid value for ${fieldName}: ${result.error.errors[0]?.message}`
38
+ };
39
+ patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result: editResult });
40
+ return editResult;
41
+ }
42
+ updates[fieldName] = result.data;
43
+ }
44
+ }
45
+ store.getState().setMany(updates);
46
+ const result = { success: true, applied: patch.length };
47
+ patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result });
48
+ return result;
49
+ },
50
+ /**
51
+ * Apply edits from JSON string (LLM output)
52
+ */
53
+ applyJSON(json, source = 'llm') {
54
+ try {
55
+ const parsed = JSON.parse(json);
56
+ if (Array.isArray(parsed)) {
57
+ return this.applyPatch(parsed, source);
58
+ }
59
+ if (parsed.patch && Array.isArray(parsed.patch)) {
60
+ return this.applyPatch(parsed.patch, source);
61
+ }
62
+ return { success: false, error: 'Expected JSON Patch array or { patch: [...] } format' };
63
+ }
64
+ catch (e) {
65
+ return { success: false, error: `JSON parse error: ${e.message}` };
66
+ }
67
+ },
68
+ /**
69
+ * Generate context for LLM prompt
70
+ */
71
+ getContext() {
72
+ const data = store.getState().data;
73
+ let context = schema.getDescription();
74
+ context += '\nCurrent Values:\n';
75
+ for (const [id, field] of Object.entries(schema.fields)) {
76
+ const value = data[id];
77
+ context += ` ${id}: ${JSON.stringify(value)} (${field.label})\n`;
78
+ }
79
+ context += `
80
+ To edit fields, respond with a JSON Patch array (RFC 6902):
81
+ [{ "op": "replace", "path": "/fieldName", "value": newValue }]
82
+
83
+ Examples:
84
+ - Single edit: [{ "op": "replace", "path": "/budget", "value": 5000 }]
85
+ - Multiple: [{ "op": "replace", "path": "/budget", "value": 5000 }, { "op": "replace", "path": "/days", "value": 14 }]`;
86
+ return context;
87
+ },
88
+ /**
89
+ * Compact context for constrained prompts
90
+ */
91
+ getCompactContext() {
92
+ const data = store.getState().data;
93
+ const values = Object.entries(data)
94
+ .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
95
+ .join(', ');
96
+ return `${schema.name}: {${values}}\nJSON Patch: [{"op":"replace","path":"/field","value":x}]`;
97
+ },
98
+ /**
99
+ * Generate OpenAI/Anthropic function calling schema
100
+ */
101
+ getFunctionSchema() {
102
+ return {
103
+ name: `patch_${schema.name.toLowerCase().replace(/\s+/g, '_')}`,
104
+ description: `Apply JSON Patch operations to ${schema.name}. Uses RFC 6902 format.`,
105
+ parameters: {
106
+ type: 'object',
107
+ properties: {
108
+ patch: {
109
+ type: 'array',
110
+ description: 'JSON Patch operations (RFC 6902)',
111
+ items: {
112
+ type: 'object',
113
+ properties: {
114
+ op: {
115
+ type: 'string',
116
+ enum: ['replace', 'add', 'remove'],
117
+ description: 'Operation type',
118
+ },
119
+ path: {
120
+ type: 'string',
121
+ description: `JSON Pointer to field. Valid paths: ${schema.getFieldIds().map(f => `/${f}`).join(', ')}`,
122
+ },
123
+ value: {
124
+ description: 'New value (required for replace/add)',
125
+ },
126
+ },
127
+ required: ['op', 'path'],
128
+ },
129
+ },
130
+ },
131
+ required: ['patch'],
132
+ },
133
+ };
134
+ },
135
+ /**
136
+ * Get current values
137
+ */
138
+ getCurrentValues() {
139
+ return store.getState().data;
140
+ },
141
+ /**
142
+ * Get patch history for audit trail
143
+ */
144
+ history() {
145
+ return [...patchHistory];
146
+ },
147
+ /**
148
+ * Clear patch history
149
+ */
150
+ clearHistory() {
151
+ patchHistory.length = 0;
152
+ historyId = 0;
153
+ },
154
+ };
155
+ }
156
+ /**
157
+ * Helper to check if edit was successful
158
+ */
159
+ export function isSuccess(result) {
160
+ return result.success;
161
+ }
162
+ /**
163
+ * Get human-readable message from result
164
+ */
165
+ export function getResultMessage(result) {
166
+ if (result.success) {
167
+ return `Successfully applied ${result.applied} operation(s)`;
168
+ }
169
+ return result.error;
170
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * duet-kit - Schema
3
+ *
4
+ * Thin wrapper around Zod that adds metadata for UI labels and LLM context.
5
+ */
6
+ import { z } from 'zod';
7
+ import type { SchemaFields, FieldDef, InferData } from './types';
8
+ export declare class DuetSchema<T extends SchemaFields> {
9
+ readonly name: string;
10
+ readonly fields: T;
11
+ constructor(name: string, fields: T);
12
+ getDefaults(): InferData<T>;
13
+ validate<K extends keyof T>(field: K, value: unknown): z.SafeParseReturnType<unknown, z.infer<T[K]['schema']>>;
14
+ validateAll(data: Partial<InferData<T>>): {
15
+ success: boolean;
16
+ errors: Record<string, string>;
17
+ };
18
+ getField(fieldId: string): FieldDef | undefined;
19
+ getFieldIds(): string[];
20
+ getDescription(): string;
21
+ toJSONSchema(): object;
22
+ private getZodTypeDescription;
23
+ private zodToJSONSchema;
24
+ }
25
+ export declare function createSchema<T extends SchemaFields>(name: string, fields: T): DuetSchema<T>;
26
+ export declare function field<T>(schema: z.ZodType<T>, label: string, defaultValue: T): FieldDef<T>;
27
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAGjE,qBAAa,UAAU,CAAC,CAAC,SAAS,YAAY;aAE1B,IAAI,EAAE,MAAM;aACZ,MAAM,EAAE,CAAC;gBADT,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,CAAC;IAI3B,WAAW,IAAI,SAAS,CAAC,CAAC,CAAC;IAS3B,QAAQ,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,CAAC,CAAC,mBAAmB,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;IAK9G,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE;IAc9F,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAK/C,WAAW,IAAI,MAAM,EAAE;IAKvB,cAAc,IAAI,MAAM;IAYxB,YAAY,IAAI,MAAM;IActB,OAAO,CAAC,qBAAqB;IAS7B,OAAO,CAAC,eAAe;CAwBxB;AAGD,wBAAgB,YAAY,CAAC,CAAC,SAAS,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAE3F;AAGD,wBAAgB,KAAK,CAAC,CAAC,EACrB,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EACpB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,CAAC,GACd,QAAQ,CAAC,CAAC,CAAC,CAEb"}