@uicast/core 0.0.1-beta.1
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/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/package.json +52 -0
- package/src/def/create-component-definition.ts +21 -0
- package/src/entry-error.ts +104 -0
- package/src/expr/analyze.ts +190 -0
- package/src/expr/ast-utils.ts +28 -0
- package/src/expr/evaluate.ts +74 -0
- package/src/expr/extract-deps.ts +32 -0
- package/src/expr/globals.ts +118 -0
- package/src/expr/safer-eval.ts +233 -0
- package/src/expr/validate.ts +149 -0
- package/src/index.ts +28 -0
- package/src/prompt/get-common-instructions-partial-prompt.ts +20 -0
- package/src/prompt/get-components-partial-prompt.ts +153 -0
- package/src/prompt/get-edit-request-prompt.ts +37 -0
- package/src/prompt/get-error-recovery-prompt.ts +37 -0
- package/src/prompt/get-expressions-partial-prompt.ts +33 -0
- package/src/prompt/get-functions-partial-prompt.ts +62 -0
- package/src/prompt/get-scope-partial-prompt.ts +57 -0
- package/src/prompt/index.ts +32 -0
- package/src/prompt/md/EXPRESSIONS.json +1 -0
- package/src/prompt/md/EXPRESSIONS.md +29 -0
- package/src/prompt/md/INSTRUCTIONS.json +1 -0
- package/src/prompt/md/INSTRUCTIONS.md +172 -0
- package/src/prompt-utils/json-schema-to-ts.ts +225 -0
- package/src/scope/create-proxy-scope.ts +131 -0
- package/src/scope/parse-scope.ts +36 -0
- package/src/stream/stream-json-lines.ts +56 -0
- package/src/types.ts +124 -0
- package/src/utils/utils.ts +64 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
type JSONSchemaType =
|
|
2
|
+
| "object"
|
|
3
|
+
| "array"
|
|
4
|
+
| "string"
|
|
5
|
+
| "number"
|
|
6
|
+
| "boolean"
|
|
7
|
+
| "null"
|
|
8
|
+
| "integer";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* JSON Schema shape this module reads — a recursive structural type covering
|
|
12
|
+
* the draft-07 / 2020-12 keywords a Standard-Schema `toJSONSchema()` can emit
|
|
13
|
+
* (primitives, composition, objects, arrays/tuples, `$ref` + `$defs`, and the
|
|
14
|
+
* refinement keywords that constrain a value without changing its TypeScript
|
|
15
|
+
* type). Every sub-schema slot is another `JSONSchema`.
|
|
16
|
+
*
|
|
17
|
+
* `JSONSchemaToTs` still takes `unknown` and narrows at runtime, because a
|
|
18
|
+
* schema value can also be a boolean (`true`/`false`) — and, in legacy
|
|
19
|
+
* draft-07, an `items` array — which this object type intentionally doesn't
|
|
20
|
+
* model. The function reads back through itself, so the recursion stays loose.
|
|
21
|
+
*/
|
|
22
|
+
export interface JSONSchema {
|
|
23
|
+
$schema?:
|
|
24
|
+
| "https://json-schema.org/draft/2020-12/schema"
|
|
25
|
+
| "http://json-schema.org/draft-07/schema#";
|
|
26
|
+
type?: JSONSchemaType | JSONSchemaType[];
|
|
27
|
+
format?: string;
|
|
28
|
+
pattern?: string;
|
|
29
|
+
$ref?: string;
|
|
30
|
+
items?: boolean | JSONSchema;
|
|
31
|
+
prefixItems?: JSONSchema[];
|
|
32
|
+
additionalItems?: boolean | JSONSchema;
|
|
33
|
+
enum?: unknown[];
|
|
34
|
+
minimum?: number;
|
|
35
|
+
maximum?: number;
|
|
36
|
+
exclusiveMinimum?: number;
|
|
37
|
+
exclusiveMaximum?: number;
|
|
38
|
+
minItems?: number;
|
|
39
|
+
maxItems?: number;
|
|
40
|
+
title?: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
properties?: { [key: string]: JSONSchema };
|
|
43
|
+
required?: string[];
|
|
44
|
+
examples?: unknown[];
|
|
45
|
+
not?: JSONSchema;
|
|
46
|
+
// support both $defs and definitions
|
|
47
|
+
$defs?: { [key: string]: JSONSchema };
|
|
48
|
+
definitions?: { [key: string]: JSONSchema };
|
|
49
|
+
additionalProperties?: boolean | JSONSchema;
|
|
50
|
+
anyOf?: JSONSchema[];
|
|
51
|
+
oneOf?: JSONSchema[];
|
|
52
|
+
allOf?: JSONSchema[];
|
|
53
|
+
// older schema
|
|
54
|
+
const?: unknown;
|
|
55
|
+
example?: unknown;
|
|
56
|
+
// binary
|
|
57
|
+
contentEncoding?: string;
|
|
58
|
+
contentMediaType?: string;
|
|
59
|
+
minLength?: number;
|
|
60
|
+
maxLength?: number;
|
|
61
|
+
// explicit TypeScript-type override for code generation
|
|
62
|
+
"x-tsType"?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Convert a JSON Schema to a compact TypeScript type string for the prompt.
|
|
67
|
+
*
|
|
68
|
+
* Pass the whole schema document (with any `$defs` / `definitions`): local
|
|
69
|
+
* `$ref`s resolve against it, and a recursive schema terminates — the cycle's
|
|
70
|
+
* back-edge renders as `unknown` while everything above it stays fully typed.
|
|
71
|
+
*/
|
|
72
|
+
export function JSONSchemaToTs(jsonSchema: unknown): string {
|
|
73
|
+
return toTs(jsonSchema, jsonSchema, new Set());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Recursive worker. `root` is the document the first call was handed (the
|
|
78
|
+
* resolution base for `$ref`); `seen` is the set of refs currently being
|
|
79
|
+
* expanded on this path, so a cycle short-circuits to `unknown` instead of
|
|
80
|
+
* recursing forever.
|
|
81
|
+
*/
|
|
82
|
+
function toTs(jsonSchema: unknown, root: unknown, seen: Set<string>): string {
|
|
83
|
+
if (jsonSchema === true) return "unknown";
|
|
84
|
+
if (jsonSchema === false) return "never";
|
|
85
|
+
if (jsonSchema === null || jsonSchema === undefined) return "unknown";
|
|
86
|
+
if (typeof jsonSchema !== "object") return "unknown";
|
|
87
|
+
const schema = jsonSchema as JSONSchema;
|
|
88
|
+
|
|
89
|
+
if (typeof schema.$ref === "string") {
|
|
90
|
+
if (seen.has(schema.$ref)) return "unknown"; // cycle back-edge
|
|
91
|
+
const target = resolveRef(schema.$ref, root);
|
|
92
|
+
if (target === undefined) return "unknown";
|
|
93
|
+
seen.add(schema.$ref);
|
|
94
|
+
const resolved = toTs(target, root, seen);
|
|
95
|
+
seen.delete(schema.$ref);
|
|
96
|
+
return resolved;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if ("const" in schema) return JSON.stringify(schema.const);
|
|
100
|
+
|
|
101
|
+
if (schema.enum) {
|
|
102
|
+
return schema.enum.map((v) => JSON.stringify(v)).join(" | ") || "never";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (schema.allOf) {
|
|
106
|
+
const parts = schema.allOf.map((s) => toTs(s, root, seen));
|
|
107
|
+
return parts.length ? `(${parts.join(" & ")})` : "unknown";
|
|
108
|
+
}
|
|
109
|
+
if (schema.anyOf) {
|
|
110
|
+
const parts = schema.anyOf.map((s) => toTs(s, root, seen));
|
|
111
|
+
return parts.length ? `(${parts.join(" | ")})` : "never";
|
|
112
|
+
}
|
|
113
|
+
if (schema.oneOf) {
|
|
114
|
+
const parts = schema.oneOf.map((s) => toTs(s, root, seen));
|
|
115
|
+
return parts.length ? `(${parts.join(" | ")})` : "never";
|
|
116
|
+
}
|
|
117
|
+
if (schema.not) return "unknown";
|
|
118
|
+
|
|
119
|
+
if (Array.isArray(schema.type)) {
|
|
120
|
+
const types = schema.type.map((t) =>
|
|
121
|
+
toTs({ ...schema, type: t }, root, seen),
|
|
122
|
+
);
|
|
123
|
+
return types.length ? `(${types.join(" | ")})` : "unknown";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const type = schema.type;
|
|
127
|
+
|
|
128
|
+
if (type === "string") return "string";
|
|
129
|
+
if (type === "number" || type === "integer") return "number";
|
|
130
|
+
if (type === "boolean") return "boolean";
|
|
131
|
+
if (type === "null") return "null";
|
|
132
|
+
|
|
133
|
+
if (
|
|
134
|
+
type === "object" ||
|
|
135
|
+
schema.properties ||
|
|
136
|
+
schema.additionalProperties !== undefined
|
|
137
|
+
) {
|
|
138
|
+
const props = schema.properties || {};
|
|
139
|
+
const required = schema.required || [];
|
|
140
|
+
|
|
141
|
+
const propStrings = Object.entries(props).map(([key, value]) => {
|
|
142
|
+
const isRequired = required.includes(key);
|
|
143
|
+
const safeName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)
|
|
144
|
+
? key
|
|
145
|
+
: JSON.stringify(key);
|
|
146
|
+
return `${safeName}${isRequired ? "" : "?"}: ${toTs(value, root, seen)}`;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
let additionalType: string | null = null;
|
|
150
|
+
if (schema.additionalProperties === true) {
|
|
151
|
+
additionalType = "unknown";
|
|
152
|
+
} else if (
|
|
153
|
+
schema.additionalProperties &&
|
|
154
|
+
typeof schema.additionalProperties === "object"
|
|
155
|
+
) {
|
|
156
|
+
additionalType = toTs(schema.additionalProperties, root, seen);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (propStrings.length === 0 && additionalType) {
|
|
160
|
+
return `{ [key: string]: ${additionalType} }`;
|
|
161
|
+
}
|
|
162
|
+
if (propStrings.length === 0 && !additionalType) {
|
|
163
|
+
return schema.additionalProperties === false
|
|
164
|
+
? "{}"
|
|
165
|
+
: "{ [key: string]: unknown }";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let result = `{ ${propStrings.join("; ")} }`;
|
|
169
|
+
if (additionalType) {
|
|
170
|
+
result = `(${result} & { [key: string]: ${additionalType} })`;
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (type === "array" || schema.items || schema.prefixItems) {
|
|
176
|
+
if (schema.prefixItems) {
|
|
177
|
+
const tupleTypes = schema.prefixItems.map((s) => toTs(s, root, seen));
|
|
178
|
+
if (schema.items === false) return `[${tupleTypes.join(", ")}]`;
|
|
179
|
+
const restType = schema.items ? toTs(schema.items, root, seen) : "unknown";
|
|
180
|
+
return `[${tupleTypes.join(", ")}, ...${restType}[]]`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (schema.items !== undefined && schema.items !== null) {
|
|
184
|
+
if (Array.isArray(schema.items)) {
|
|
185
|
+
const tupleTypes = schema.items.map((s) => toTs(s, root, seen));
|
|
186
|
+
if (schema.additionalItems === false) {
|
|
187
|
+
return `[${tupleTypes.join(", ")}]`;
|
|
188
|
+
}
|
|
189
|
+
const restType = schema.additionalItems
|
|
190
|
+
? toTs(schema.additionalItems, root, seen)
|
|
191
|
+
: "unknown";
|
|
192
|
+
return `[${tupleTypes.join(", ")}, ...${restType}[]]`;
|
|
193
|
+
}
|
|
194
|
+
return `${toTs(schema.items, root, seen)}[]`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return "unknown[]";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (schema.properties) {
|
|
201
|
+
return toTs({ ...schema, type: "object" }, root, seen);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return "unknown";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Resolve a local JSON Pointer ref (`#/$defs/Foo`, `#/definitions/Bar`)
|
|
209
|
+
* against the document root. Returns the target sub-schema, or `undefined`
|
|
210
|
+
* for a non-local ref (`https://…`, bare `#`) or a path that doesn't exist —
|
|
211
|
+
* the caller renders those as `unknown`.
|
|
212
|
+
*/
|
|
213
|
+
function resolveRef(ref: string, root: unknown): unknown {
|
|
214
|
+
if (!ref.startsWith("#/")) return undefined;
|
|
215
|
+
const segments = ref
|
|
216
|
+
.slice(2)
|
|
217
|
+
.split("/")
|
|
218
|
+
.map((seg) => seg.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
219
|
+
let node: unknown = root;
|
|
220
|
+
for (const seg of segments) {
|
|
221
|
+
if (node === null || typeof node !== "object") return undefined;
|
|
222
|
+
node = (node as Record<string, unknown>)[seg];
|
|
223
|
+
}
|
|
224
|
+
return node;
|
|
225
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
type EventHandler<T = unknown> = (payload: T) => void;
|
|
2
|
+
|
|
3
|
+
interface ChangePayload<T = unknown> {
|
|
4
|
+
path: string;
|
|
5
|
+
value: T;
|
|
6
|
+
oldValue: T;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Emitter {
|
|
10
|
+
on<T = unknown>(type: string, handler: EventHandler<T>): () => void;
|
|
11
|
+
off<T = unknown>(type: string, handler: EventHandler<T>): void;
|
|
12
|
+
emit<T = unknown>(type: string, payload: T): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createEmitter(): Emitter {
|
|
16
|
+
const events = new Map<string, Set<EventHandler<any>>>();
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
on(type, handler) {
|
|
20
|
+
const handlers = events.get(type);
|
|
21
|
+
if (handlers) handlers.add(handler);
|
|
22
|
+
else events.set(type, new Set([handler]));
|
|
23
|
+
return () => events.get(type)?.delete(handler);
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
off(type, handler) {
|
|
27
|
+
events.get(type)?.delete(handler);
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
emit(type, payload) {
|
|
31
|
+
events.get(type)?.forEach((fn) => {
|
|
32
|
+
fn(payload);
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type ReactiveProxy<T extends object = object> = T & {
|
|
39
|
+
$emitter: Emitter;
|
|
40
|
+
$set: (path: string, value: unknown, options?: { default?: boolean }) => void;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function createProxyScope<T extends object>(
|
|
44
|
+
target: T = {} as T,
|
|
45
|
+
): ReactiveProxy<T> {
|
|
46
|
+
const emitter = createEmitter();
|
|
47
|
+
const proxyCache = new WeakMap<object, object>();
|
|
48
|
+
|
|
49
|
+
function set(
|
|
50
|
+
path: string,
|
|
51
|
+
value: unknown,
|
|
52
|
+
options?: { default?: boolean },
|
|
53
|
+
): void {
|
|
54
|
+
const keys = path.split(".");
|
|
55
|
+
let current: any = proxy;
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
58
|
+
const key = keys[i];
|
|
59
|
+
if (current[key] === undefined || current[key] === null) {
|
|
60
|
+
current[key] = {};
|
|
61
|
+
}
|
|
62
|
+
current = current[key];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const lastKey = keys[keys.length - 1];
|
|
66
|
+
// `default: true` → init-if-absent (first-writer-wins); never clobber an
|
|
67
|
+
// existing value. Used by `seed` so two components seeding the same
|
|
68
|
+
// path don't overwrite each other, and user edits survive remounts.
|
|
69
|
+
if (options?.default && current[lastKey] !== undefined) return;
|
|
70
|
+
current[lastKey] = value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function wrap<U>(obj: U, path: PropertyKey[] = []): U {
|
|
74
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
75
|
+
if (proxyCache.has(obj as object))
|
|
76
|
+
return proxyCache.get(obj as object) as U;
|
|
77
|
+
|
|
78
|
+
const proxy = new Proxy(obj as object, {
|
|
79
|
+
get(target, prop, receiver) {
|
|
80
|
+
if (path.length === 0) {
|
|
81
|
+
if (prop === "$emitter") return emitter;
|
|
82
|
+
if (prop === "$set") return set;
|
|
83
|
+
}
|
|
84
|
+
const value = Reflect.get(target, prop, receiver);
|
|
85
|
+
|
|
86
|
+
// Proxy invariant: for non-configurable, non-writable properties
|
|
87
|
+
// the trap MUST return the exact target value.
|
|
88
|
+
if (value !== null && typeof value === "object") {
|
|
89
|
+
const desc = Object.getOwnPropertyDescriptor(target, prop);
|
|
90
|
+
if (desc && !desc.configurable && !desc.writable) {
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return wrap(value, path.concat(prop));
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
set(target, prop, value, receiver) {
|
|
99
|
+
const oldValue = (target as Record<PropertyKey, unknown>)[prop];
|
|
100
|
+
const result = Reflect.set(target, prop, value, receiver);
|
|
101
|
+
|
|
102
|
+
if (oldValue !== value) {
|
|
103
|
+
const fullPath = path.concat(prop).join(".");
|
|
104
|
+
emitter.emit<ChangePayload>(fullPath, {
|
|
105
|
+
path: fullPath,
|
|
106
|
+
value,
|
|
107
|
+
oldValue,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return result;
|
|
112
|
+
},
|
|
113
|
+
}) as U;
|
|
114
|
+
|
|
115
|
+
proxyCache.set(obj as object, proxy as object);
|
|
116
|
+
return proxy;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const proxy = wrap(target) as ReactiveProxy<T>;
|
|
120
|
+
|
|
121
|
+
return proxy;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export {
|
|
125
|
+
createProxyScope,
|
|
126
|
+
createEmitter,
|
|
127
|
+
type Emitter,
|
|
128
|
+
type ChangePayload,
|
|
129
|
+
type EventHandler,
|
|
130
|
+
type ReactiveProxy,
|
|
131
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split a scope key into `[scopeName, leafPath]`.
|
|
3
|
+
*
|
|
4
|
+
* Strips the leading namespace token (default `"scopes"`) if present, then
|
|
5
|
+
* splits on the first dot: the head is the named scope, the tail is the path
|
|
6
|
+
* *within* that scope.
|
|
7
|
+
*
|
|
8
|
+
* parseScope("scopes.root.user.name") → ["root", "user.name"]
|
|
9
|
+
* parseScope("root.count") → ["root", "count"] (prefix optional)
|
|
10
|
+
*
|
|
11
|
+
* `prefix` (default `"scopes"`) is the leading namespace identifier the dotted
|
|
12
|
+
* key is written against; it builds the `scopesPrefix` (`prefix + "."`) that
|
|
13
|
+
* gets stripped. Pass a different value when keys are written against another
|
|
14
|
+
* root identifier.
|
|
15
|
+
*
|
|
16
|
+
* This is the scope-layer counterpart to `createProxyScope`: `extractDeps`
|
|
17
|
+
* produces `scopes.X.Y` read strings, and the renderer hands each to
|
|
18
|
+
* `parseScope(...)` to route an `$emitter.on(targetPath, …)` subscription.
|
|
19
|
+
*/
|
|
20
|
+
export const parseScope = (key: string, prefix = "scopes") => {
|
|
21
|
+
// Strip the "<prefix>." namespace token if present.
|
|
22
|
+
const scopesPrefix = `${prefix}.`;
|
|
23
|
+
const normalizedKey = key.startsWith(scopesPrefix)
|
|
24
|
+
? key.slice(scopesPrefix.length)
|
|
25
|
+
: key;
|
|
26
|
+
|
|
27
|
+
const dotIndex = normalizedKey.indexOf(".");
|
|
28
|
+
if (dotIndex === -1) {
|
|
29
|
+
throw new Error(`Invalid scope key: ${key}`);
|
|
30
|
+
} else {
|
|
31
|
+
return [
|
|
32
|
+
normalizedKey.slice(0, dotIndex),
|
|
33
|
+
normalizedKey.slice(dotIndex + 1),
|
|
34
|
+
] as [string, string];
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Yields every parseable JSON value from a byte or string stream, skipping
|
|
2
|
+
// blank/non-JSON lines (code fences, model prose, partial lines). Narrow the
|
|
3
|
+
// element type via <T>; shape-checking is the caller's concern.
|
|
4
|
+
export async function* streamJsonLines<T = unknown>(
|
|
5
|
+
source: ReadableStream<Uint8Array | string> | AsyncIterable<string | Uint8Array>,
|
|
6
|
+
): AsyncGenerator<T> {
|
|
7
|
+
const decoder = new TextDecoder();
|
|
8
|
+
let buffer = "";
|
|
9
|
+
|
|
10
|
+
for await (const chunk of toAsyncIterable(source)) {
|
|
11
|
+
buffer += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
|
|
12
|
+
|
|
13
|
+
let nl = buffer.indexOf("\n");
|
|
14
|
+
while (nl !== -1) {
|
|
15
|
+
yield* parseLine<T>(buffer.slice(0, nl));
|
|
16
|
+
buffer = buffer.slice(nl + 1);
|
|
17
|
+
nl = buffer.indexOf("\n");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
yield* parseLine<T>(buffer + decoder.decode());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function* parseLine<T>(raw: string): Generator<T> {
|
|
25
|
+
const line = raw.trim();
|
|
26
|
+
if (!line) return;
|
|
27
|
+
try {
|
|
28
|
+
yield JSON.parse(line) as T;
|
|
29
|
+
} catch {
|
|
30
|
+
// non-JSON noise (fences, prose, partial) — drop it
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function toAsyncIterable(
|
|
35
|
+
source: ReadableStream<Uint8Array | string> | AsyncIterable<string | Uint8Array>,
|
|
36
|
+
): AsyncIterable<string | Uint8Array> {
|
|
37
|
+
if (Symbol.asyncIterator in source) {
|
|
38
|
+
return source as AsyncIterable<string | Uint8Array>;
|
|
39
|
+
}
|
|
40
|
+
// Browser ReadableStream isn't async-iterable — pull it through a reader.
|
|
41
|
+
const stream = source as ReadableStream<Uint8Array | string>;
|
|
42
|
+
return {
|
|
43
|
+
async *[Symbol.asyncIterator]() {
|
|
44
|
+
const reader = stream.getReader();
|
|
45
|
+
try {
|
|
46
|
+
for (;;) {
|
|
47
|
+
const { done, value } = await reader.read();
|
|
48
|
+
if (done) return;
|
|
49
|
+
if (value !== undefined) yield value;
|
|
50
|
+
}
|
|
51
|
+
} finally {
|
|
52
|
+
reader.releaseLock();
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
StandardJSONSchemaV1,
|
|
3
|
+
StandardSchemaV1,
|
|
4
|
+
} from "@standard-schema/spec";
|
|
5
|
+
|
|
6
|
+
// A bare JavaScript expression string, always evaluated as code (no literal
|
|
7
|
+
// form). Used by `hidden` and `each`.
|
|
8
|
+
export type Expression = string;
|
|
9
|
+
|
|
10
|
+
// A dotted path into the reactive `scopes` store — the write target of an
|
|
11
|
+
// assignment, e.g. `"scopes.root.users"`. Logically just a string; the alias
|
|
12
|
+
// documents intent (mirrors `Expression`). Used by the `set` field below.
|
|
13
|
+
export type ScopePath = string;
|
|
14
|
+
|
|
15
|
+
// A value: `literal` is any JSON value used as-is (no evaluation); `expr` is
|
|
16
|
+
// evaluated as code.
|
|
17
|
+
export type ValueSource = { expr: Expression } | { literal: unknown };
|
|
18
|
+
export type ValueSourceAssignment = { set: ScopePath } & ValueSource;
|
|
19
|
+
export type ConfirmableValueSourceAssignment = {
|
|
20
|
+
confirm?: string;
|
|
21
|
+
} & ValueSourceAssignment;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The uicast entry model. One line of a uicast document is a
|
|
25
|
+
* `ComponentEntry`. A `ComponentListEntry` is an entry that repeats: it carries
|
|
26
|
+
* `each` (the array to iterate) plus `as` and an optional `keyBy`.
|
|
27
|
+
*
|
|
28
|
+
* There is no separate "plain entry" type and no
|
|
29
|
+
* `ComponentEntry | ComponentListEntry` union: the list fields (`each`, `as`,
|
|
30
|
+
* `keyBy`) live on `ComponentEntry` as optional, so one type describes every
|
|
31
|
+
* line and a heterogeneous `ComponentEntry[]` can hold list lines directly.
|
|
32
|
+
* `ComponentListEntry` is the same shape with `each`/`as` required — narrow
|
|
33
|
+
* `ComponentEntry` → `ComponentListEntry` with `isComponentListEntry` (presence
|
|
34
|
+
* of `each`).
|
|
35
|
+
*/
|
|
36
|
+
export interface ComponentEntry {
|
|
37
|
+
key: string;
|
|
38
|
+
component: string;
|
|
39
|
+
props?: ValueSource;
|
|
40
|
+
seed?: ValueSourceAssignment[];
|
|
41
|
+
hidden?: Expression;
|
|
42
|
+
callbacks?: Record<string, ConfirmableValueSourceAssignment[]>;
|
|
43
|
+
children?: string[];
|
|
44
|
+
// List fields — present only when the entry repeats (`each` is the marker).
|
|
45
|
+
// Optional here so a heterogeneous `ComponentEntry[]` can hold list lines
|
|
46
|
+
// directly; `ComponentListEntry` below is the same shape with `each`/`as`
|
|
47
|
+
// required.
|
|
48
|
+
each?: Expression;
|
|
49
|
+
as?: string;
|
|
50
|
+
keyBy?: (string & {}) | "_index" | "_item";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A list entry repeats — the same shape as `ComponentEntry` with the iteration
|
|
55
|
+
* fields required. `each` is the structural marker (its presence is what makes
|
|
56
|
+
* an entry a list); `as` names the per-item scope.
|
|
57
|
+
*/
|
|
58
|
+
export interface ComponentListEntry extends ComponentEntry {
|
|
59
|
+
each: Expression;
|
|
60
|
+
as: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Narrow a `ComponentEntry` to a `ComponentListEntry`. List-ness is the
|
|
65
|
+
* presence of `each` — there is no `kind` discriminator, exactly like root-ness
|
|
66
|
+
* (derived from an entry being referenced by no other entry's `children`).
|
|
67
|
+
*/
|
|
68
|
+
export function isComponentListEntry(
|
|
69
|
+
entry: ComponentEntry,
|
|
70
|
+
): entry is ComponentListEntry {
|
|
71
|
+
return entry.each !== undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Narrow an untrusted parsed value to a `ComponentEntry` — the shape guard for
|
|
76
|
+
* model-emitted lines (`streamJsonLines` output, fence bodies, stored JSONL).
|
|
77
|
+
*/
|
|
78
|
+
export function isComponentEntry(value: unknown): value is ComponentEntry {
|
|
79
|
+
if (!value || typeof value !== "object") return false;
|
|
80
|
+
const entry = value as ComponentEntry;
|
|
81
|
+
return (
|
|
82
|
+
typeof entry.key === "string" &&
|
|
83
|
+
typeof entry.component === "string" &&
|
|
84
|
+
// The least-trusted input path: `children` as a string (a plausible LLM
|
|
85
|
+
// slip) would be iterated char-by-char downstream into phantom child slots.
|
|
86
|
+
(entry.children === undefined || Array.isArray(entry.children))
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* An interface that combines StandardJSONSchema and StandardSchema.
|
|
92
|
+
* */
|
|
93
|
+
export type CombinedSpec<Input = unknown, Output = Input> = StandardSchemaV1<Input, Output> &
|
|
94
|
+
StandardJSONSchemaV1<Input, Output>;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The partner module the LLM reads for a component: a props spec plus optional
|
|
98
|
+
* callback specs (its `description` + `props` schema are serialized into the
|
|
99
|
+
* prompt). Parameterized over the concrete `props` / `callbacks` specs so an
|
|
100
|
+
* implementation can recover their exact types — `createComponentImplementation`
|
|
101
|
+
* infers `TProps` / `TCallbacks` straight off the `def` it's handed to type its
|
|
102
|
+
* `render` callback. Both params default to the widened base, so a bare
|
|
103
|
+
* `ComponentDefinition` is the heterogeneous form used in collections
|
|
104
|
+
* (`ComponentDefinition[]`, the `allDefinitions` registry, the prompt
|
|
105
|
+
* serializer). The `createComponentDefinition` factory (in
|
|
106
|
+
* `def/create-component-definition.ts`) is the value-side constructor for this.
|
|
107
|
+
*/
|
|
108
|
+
export type ComponentDefinition<
|
|
109
|
+
TProps extends CombinedSpec = CombinedSpec,
|
|
110
|
+
TCallbacks extends Record<string, CombinedSpec> = Record<string, CombinedSpec>,
|
|
111
|
+
> = {
|
|
112
|
+
name: string;
|
|
113
|
+
description: string;
|
|
114
|
+
props: TProps;
|
|
115
|
+
callbacks?: TCallbacks;
|
|
116
|
+
/**
|
|
117
|
+
* Host-only component. Registered in the renderer registry so entries
|
|
118
|
+
* referencing it mount correctly, but filtered out of the LLM-facing
|
|
119
|
+
* prompt list in `getComponentsPartialPrompt`. Use for internal infrastructure
|
|
120
|
+
* (e.g. RootFragment — the synthetic wrapper used by `Renderer`'s `init`
|
|
121
|
+
* prop machinery) that the LLM should never emit.
|
|
122
|
+
*/
|
|
123
|
+
hidden?: boolean;
|
|
124
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ComponentEntry } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Collect all descendant IDs of a given entry (not including the entry itself)
|
|
5
|
+
* by walking its children array recursively.
|
|
6
|
+
*/
|
|
7
|
+
function collectDescendantIds(
|
|
8
|
+
id: string,
|
|
9
|
+
map: Record<string, ComponentEntry>,
|
|
10
|
+
): Set<string> {
|
|
11
|
+
const result = new Set<string>();
|
|
12
|
+
const stack = [id];
|
|
13
|
+
while (stack.length > 0) {
|
|
14
|
+
const current = stack.pop()!;
|
|
15
|
+
const entry = map[current];
|
|
16
|
+
if (entry?.children) {
|
|
17
|
+
for (const childId of entry.children) {
|
|
18
|
+
if (!result.has(childId)) {
|
|
19
|
+
result.add(childId);
|
|
20
|
+
stack.push(childId);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build an elements-by-key map from the NDJSON lines array.
|
|
30
|
+
*
|
|
31
|
+
* When an entry with a duplicate key is encountered (i.e. the LLM re-emits a
|
|
32
|
+
* entry to correct a mistake), the old subtree is removed before inserting
|
|
33
|
+
* the replacement — except old nodes the new `children` array still
|
|
34
|
+
* references (directly or through a kept child), which survive with their
|
|
35
|
+
* own subtrees. This lets the LLM restructure a parent by re-emitting just
|
|
36
|
+
* that entry, keeping existing children by reference, without regenerating
|
|
37
|
+
* the entire tree.
|
|
38
|
+
*/
|
|
39
|
+
export function buildElementsById(
|
|
40
|
+
lines: ComponentEntry[],
|
|
41
|
+
): Record<string, ComponentEntry> {
|
|
42
|
+
const map: Record<string, ComponentEntry> = {};
|
|
43
|
+
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
if (map[line.key]) {
|
|
46
|
+
const oldDescendants = collectDescendantIds(line.key, map);
|
|
47
|
+
const kept = new Set<string>();
|
|
48
|
+
const stack = [...(line.children ?? [])];
|
|
49
|
+
while (stack.length > 0) {
|
|
50
|
+
const id = stack.pop()!;
|
|
51
|
+
if (kept.has(id) || !oldDescendants.has(id)) continue;
|
|
52
|
+
kept.add(id);
|
|
53
|
+
const entry = map[id];
|
|
54
|
+
if (entry?.children) stack.push(...entry.children);
|
|
55
|
+
}
|
|
56
|
+
for (const descId of oldDescendants) {
|
|
57
|
+
if (!kept.has(descId)) delete map[descId];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
map[line.key] = line;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return map;
|
|
64
|
+
}
|