@uicast/core 0.0.1-beta.2 → 0.0.2

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 (78) hide show
  1. package/CHANGELOG.md +3 -13
  2. package/README.md +126 -1
  3. package/dist/build-elements-by-key.d.ts +13 -0
  4. package/dist/build-elements-by-key.js +39 -0
  5. package/dist/constants.d.ts +2 -0
  6. package/dist/constants.js +4 -0
  7. package/dist/create-component-definition.d.ts +26 -0
  8. package/dist/create-component-definition.js +74 -0
  9. package/dist/entry-error.d.ts +41 -31
  10. package/dist/entry-error.js +49 -20
  11. package/dist/entry-shape.d.ts +7 -0
  12. package/dist/entry-shape.js +88 -0
  13. package/dist/expr/evaluate.d.ts +4 -6
  14. package/dist/expr/evaluate.js +36 -46
  15. package/dist/expr/extract-deps.d.ts +3 -1
  16. package/dist/expr/extract-deps.js +20 -20
  17. package/dist/expr/plan-step-waves.d.ts +8 -0
  18. package/dist/expr/plan-step-waves.js +45 -0
  19. package/dist/index.d.ts +7 -8
  20. package/dist/index.js +5 -9
  21. package/dist/internal.d.ts +12 -0
  22. package/dist/internal.js +13 -0
  23. package/dist/json-schema.d.ts +58 -0
  24. package/dist/json-schema.js +73 -0
  25. package/dist/prompt/format.d.ts +5 -0
  26. package/dist/prompt/format.js +22 -0
  27. package/dist/prompt/get-common-instructions-partial-prompt.d.ts +20 -10
  28. package/dist/prompt/get-common-instructions-partial-prompt.js +11 -9
  29. package/dist/prompt/get-components-partial-prompt.d.ts +20 -28
  30. package/dist/prompt/get-components-partial-prompt.js +122 -123
  31. package/dist/prompt/get-edit-request-prompt.d.ts +16 -16
  32. package/dist/prompt/get-edit-request-prompt.js +11 -18
  33. package/dist/prompt/get-error-recovery-prompt.d.ts +25 -14
  34. package/dist/prompt/get-error-recovery-prompt.js +17 -14
  35. package/dist/prompt/get-expressions-partial-prompt.d.ts +19 -20
  36. package/dist/prompt/get-expressions-partial-prompt.js +11 -21
  37. package/dist/prompt/get-functions-partial-prompt.d.ts +16 -16
  38. package/dist/prompt/get-functions-partial-prompt.js +52 -40
  39. package/dist/prompt/get-scope-partial-prompt.d.ts +17 -17
  40. package/dist/prompt/get-scope-partial-prompt.js +21 -29
  41. package/dist/prompt/index.js +0 -2
  42. package/dist/prompt/json-schema-to-ts.d.ts +6 -0
  43. package/dist/prompt/json-schema-to-ts.js +146 -0
  44. package/dist/prompt/md/EXPRESSIONS.json +1 -1
  45. package/dist/prompt/md/INSTRUCTIONS.json +1 -1
  46. package/dist/prompt/shared-types.d.ts +5 -0
  47. package/dist/prompt/shared-types.js +82 -0
  48. package/dist/scope/create-proxy-scope.d.ts +52 -17
  49. package/dist/scope/create-proxy-scope.js +280 -86
  50. package/dist/scope/parse-scope.d.ts +2 -20
  51. package/dist/scope/parse-scope.js +10 -35
  52. package/dist/scope/parse-set-address.d.ts +16 -0
  53. package/dist/scope/parse-set-address.js +52 -0
  54. package/dist/stream-json-lines.d.ts +14 -0
  55. package/dist/stream-json-lines.js +59 -0
  56. package/dist/types.d.ts +81 -46
  57. package/dist/types.js +6 -9
  58. package/dist/url-policy.d.ts +45 -0
  59. package/dist/url-policy.js +252 -0
  60. package/package.json +18 -16
  61. package/dist/def/create-component-definition.d.ts +0 -9
  62. package/dist/def/create-component-definition.js +0 -10
  63. package/dist/expr/analyze.d.ts +0 -5
  64. package/dist/expr/analyze.js +0 -189
  65. package/dist/expr/ast-utils.d.ts +0 -4
  66. package/dist/expr/ast-utils.js +0 -23
  67. package/dist/expr/globals.d.ts +0 -3
  68. package/dist/expr/globals.js +0 -111
  69. package/dist/expr/safer-eval.d.ts +0 -21
  70. package/dist/expr/safer-eval.js +0 -170
  71. package/dist/expr/validate.d.ts +0 -7
  72. package/dist/expr/validate.js +0 -109
  73. package/dist/prompt-utils/json-schema-to-ts.d.ts +0 -73
  74. package/dist/prompt-utils/json-schema-to-ts.js +0 -199
  75. package/dist/stream/stream-json-lines.d.ts +0 -1
  76. package/dist/stream/stream-json-lines.js +0 -94
  77. package/dist/utils/utils.d.ts +0 -13
  78. package/dist/utils/utils.js +0 -58
package/dist/types.d.ts CHANGED
@@ -1,89 +1,124 @@
1
- import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec";
2
- export type Expression = string;
3
- export type ScopePath = string;
1
+ import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@uicast/expr";
2
+ type Expression = string;
3
+ type ScopePath = string;
4
+ /**
5
+ * One value: `literal` is used as is, `expr` is an expression evaluated against the scopes.
6
+ *
7
+ * @example
8
+ * const props: ValueSource = { expr: "({ text: scopes.root.title })" };
9
+ * const fixed: ValueSource = { literal: { variant: "outline" } };
10
+ */
4
11
  export type ValueSource = {
5
12
  expr: Expression;
6
13
  } | {
7
14
  literal: unknown;
8
15
  };
16
+ /**
17
+ * A `seed` step: a value source and the field it writes.
18
+ *
19
+ * @example
20
+ * const step: ValueSourceAssignment = { set: "scopes.root.users", expr: "getUsers()" };
21
+ */
9
22
  export type ValueSourceAssignment = {
23
+ /** The field to write: `scopes.<scope>.<field>`, never deeper. */
10
24
  set: ScopePath;
11
25
  } & ValueSource;
12
- export type ConfirmableValueSourceAssignment = {
26
+ /**
27
+ * A callback step: a value source, optionally written to a field, confirmed first or debounced.
28
+ *
29
+ * @example
30
+ * const toggle: CallbackValueSourceAssignment = { set: "scopes.root.open", expr: "!currentValue" };
31
+ * const remove: CallbackValueSourceAssignment = { confirm: "Delete it?", expr: "deleteRow({ id: scopes.row.id })" };
32
+ */
33
+ export type CallbackValueSourceAssignment = {
34
+ /** The field to write: `scopes.<scope>.<field>`. Absent: the step runs for its effect only. */
35
+ set?: ScopePath;
36
+ /** A question asked before this step, e.g. `"Delete this row?"`. Declined, this step and the rest are skipped. */
13
37
  confirm?: string;
14
- } & ValueSourceAssignment;
38
+ /** This step and the rest wait for 300 ms of quiet; only the latest call runs. */
39
+ debounce?: boolean;
40
+ } & ValueSource;
15
41
  /**
16
- * The uicast entry model. One line of a uicast document is a
17
- * `ComponentEntry`. A `ComponentListEntry` is an entry that repeats: it carries
18
- * `each` (the array to iterate) plus `as` and an optional `keyBy`.
42
+ * One line of a document: the JSON for one element. List fields are optional, so a `ComponentEntry[]` holds lists
43
+ * too; `each` marks a list.
19
44
  *
20
- * There is no separate "plain entry" type and no
21
- * `ComponentEntry | ComponentListEntry` union: the list fields (`each`, `as`,
22
- * `keyBy`) live on `ComponentEntry` as optional, so one type describes every
23
- * line and a heterogeneous `ComponentEntry[]` can hold list lines directly.
24
- * `ComponentListEntry` is the same shape with `each`/`as` required — narrow
25
- * `ComponentEntry` → `ComponentListEntry` with `isComponentListEntry` (presence
26
- * of `each`).
45
+ * @example
46
+ * const entry: ComponentEntry = {
47
+ * key: "open-count", component: "Typography",
48
+ * props: { expr: "({ text: 'Open: ' + scopes.root.open })" },
49
+ * };
27
50
  */
28
51
  export interface ComponentEntry {
52
+ /** Unique id. Parents name children by it; re-emitting it replaces the element and its subtree. */
29
53
  key: string;
54
+ /** The component name, matched exactly against the catalog. */
30
55
  component: string;
56
+ /** The props object, e.g. `{ expr: "({ text: scopes.root.title })" }`. Reactive. */
31
57
  props?: ValueSource;
58
+ /** Steps that set up state once, at mount. Each writes its field only while it is still undefined. */
32
59
  seed?: ValueSourceAssignment[];
60
+ /** An expression; truthy hides the element. Reactive. */
33
61
  hidden?: Expression;
34
- callbacks?: Record<string, ConfirmableValueSourceAssignment[]>;
62
+ /** An expression; truthy renders the element as busy. Reactive, like `hidden`. */
63
+ busy?: Expression;
64
+ /** Steps per event, e.g. `{ onClick: [{ set: "scopes.root.open", expr: "!currentValue" }] }`. */
65
+ callbacks?: Record<string, CallbackValueSourceAssignment[]>;
66
+ /** Child keys, in render order. */
35
67
  children?: string[];
68
+ /** An expression returning the array to repeat over; it makes the entry a list. */
36
69
  each?: Expression;
70
+ /** Names each item's two scopes: `scopes.<as>` is the item, `scopes.$<as>` its row. */
37
71
  as?: string;
38
- keyBy?: (string & {}) | "_index" | "_item";
72
+ /** The item field that gives the row its `id`. Absent, or missing on an item: the index. */
73
+ keyBy?: string;
39
74
  }
40
75
  /**
41
- * A list entry repeats — the same shape as `ComponentEntry` with the iteration
42
- * fields required. `each` is the structural marker (its presence is what makes
43
- * an entry a list); `as` names the per-item scope.
76
+ * An entry that repeats once per item of `each`.
77
+ *
78
+ * @example
79
+ * const rows: ComponentListEntry = { key: "task-row", component: "TableRow", each: "scopes.root.tasks", as: "task" };
44
80
  */
45
81
  export interface ComponentListEntry extends ComponentEntry {
82
+ /** An expression returning the array to repeat over. */
46
83
  each: Expression;
84
+ /** Names each item's two scopes: `scopes.<as>` is the item, `scopes.$<as>` its row. */
47
85
  as: string;
48
86
  }
49
- /**
50
- * Narrow a `ComponentEntry` to a `ComponentListEntry`. List-ness is the
51
- * presence of `each` — there is no `kind` discriminator, exactly like root-ness
52
- * (derived from an entry being referenced by no other entry's `children`).
53
- */
54
87
  export declare function isComponentListEntry(entry: ComponentEntry): entry is ComponentListEntry;
55
88
  /**
56
- * Narrow an untrusted parsed value to a `ComponentEntry` — the shape guard for
57
- * model-emitted lines (`streamJsonLines` output, fence bodies, stored JSONL).
89
+ * Whether parsed JSON is an entry: string `key` and `component`, and `children`, if present, an array. The renderer
90
+ * checks the other fields at mount.
91
+ *
92
+ * @example
93
+ * const entries = parsedLines.filter(isComponentEntry);
58
94
  */
59
95
  export declare function isComponentEntry(value: unknown): value is ComponentEntry;
60
96
  /**
61
- * An interface that combines StandardJSONSchema and StandardSchema.
62
- * */
97
+ * A schema that validates at run time (Standard Schema) and converts to JSON Schema for the prompt (Standard JSON
98
+ * Schema). Zod 4.2+, Valibot and ArkType schemas fit.
99
+ *
100
+ * @example
101
+ * const props: CombinedSpec = z.strictObject({ text: z.string() });
102
+ */
63
103
  export type CombinedSpec<Input = unknown, Output = Input> = StandardSchemaV1<Input, Output> & StandardJSONSchemaV1<Input, Output>;
64
104
  /**
65
- * The partner module the LLM reads for a component: a props spec plus optional
66
- * callback specs (its `description` + `props` schema are serialized into the
67
- * prompt). Parameterized over the concrete `props` / `callbacks` specs so an
68
- * implementation can recover their exact types — `createComponentImplementation`
69
- * infers `TProps` / `TCallbacks` straight off the `def` it's handed to type its
70
- * `render` callback. Both params default to the widened base, so a bare
71
- * `ComponentDefinition` is the heterogeneous form used in collections
72
- * (`ComponentDefinition[]`, the `allDefinitions` registry, the prompt
73
- * serializer). The `createComponentDefinition` factory (in
74
- * `def/create-component-definition.ts`) is the value-side constructor for this.
105
+ * What the model reads about a component: its name, description, props and events. Make one with
106
+ * `createComponentDefinition`.
107
+ *
108
+ * @example
109
+ * const definitions: ComponentDefinition[] = [...defs, BadgeDef];
110
+ * getComponentsPartialPrompt({ definitions });
75
111
  */
76
112
  export type ComponentDefinition<TProps extends CombinedSpec = CombinedSpec, TCallbacks extends Record<string, CombinedSpec> = Record<string, CombinedSpec>> = {
113
+ /** What an entry's `component` names, e.g. `"Badge"`. Must be unique: the prompt builder and `<RendererProvider>` throw on a repeat. */
77
114
  name: string;
115
+ /** When to use the component, printed word for word in the prompt. */
78
116
  description: string;
117
+ /** The props schema: the prompt prints it, and the renderer parses props through it before `render`. */
79
118
  props: TProps;
119
+ /** Event name → payload schema, e.g. `{ onChange: z.object({ value: z.string() }), onClear: z.null() }`. */
80
120
  callbacks?: TCallbacks;
81
- /**
82
- * Host-only component. Registered in the renderer registry so entries
83
- * referencing it mount correctly, but filtered out of the LLM-facing
84
- * prompt list in `getComponentsPartialPrompt`. Use for internal infrastructure
85
- * (e.g. RootFragment — the synthetic wrapper used by `Renderer`'s `init`
86
- * prop machinery) that the LLM should never emit.
87
- */
121
+ /** Kept out of the prompt; an entry that names it still renders. */
88
122
  hidden?: boolean;
89
123
  };
124
+ export {};
package/dist/types.js CHANGED
@@ -1,14 +1,12 @@
1
- /**
2
- * Narrow a `ComponentEntry` to a `ComponentListEntry`. List-ness is the
3
- * presence of `each` — there is no `kind` discriminator, exactly like root-ness
4
- * (derived from an entry being referenced by no other entry's `children`).
5
- */
6
1
  export function isComponentListEntry(entry) {
7
2
  return entry.each !== undefined;
8
3
  }
9
4
  /**
10
- * Narrow an untrusted parsed value to a `ComponentEntry` — the shape guard for
11
- * model-emitted lines (`streamJsonLines` output, fence bodies, stored JSONL).
5
+ * Whether parsed JSON is an entry: string `key` and `component`, and `children`, if present, an array. The renderer
6
+ * checks the other fields at mount.
7
+ *
8
+ * @example
9
+ * const entries = parsedLines.filter(isComponentEntry);
12
10
  */
13
11
  export function isComponentEntry(value) {
14
12
  if (!value || typeof value !== "object")
@@ -16,7 +14,6 @@ export function isComponentEntry(value) {
16
14
  const entry = value;
17
15
  return (typeof entry.key === "string" &&
18
16
  typeof entry.component === "string" &&
19
- // The least-trusted input path: `children` as a string (a plausible LLM
20
- // slip) would be iterated char-by-char downstream into phantom child slots.
17
+ // A string `children` (a plausible LLM slip) would be iterated char by char downstream.
21
18
  (entry.children === undefined || Array.isArray(entry.children)));
22
19
  }
@@ -0,0 +1,45 @@
1
+ import { type JSONSchema } from "./json-schema.js";
2
+ /**
3
+ * Which URLs a prop declared as a URL (`z.url()`) may hold. An object changes the default rules field by field; a
4
+ * predicate replaces them.
5
+ *
6
+ * @example
7
+ * const urlPolicy: UrlPolicy = { hosts: ["cdn.example.com", "*.imgix.net"] };
8
+ *
9
+ * @example
10
+ * const urlPolicy: UrlPolicy = (url) => url.startsWith("https://cdn.example.com/");
11
+ */
12
+ export type UrlPolicy = {
13
+ /** Relative URLs: `/a`, `a/b`, `?q=1`, `#x`. Default `true`. */
14
+ allowRelative?: boolean;
15
+ /** Absolute URLs on the page origin. Default `true`. */
16
+ allowSameOrigin?: boolean;
17
+ /** Extra http/https hosts: an exact match, or `*.example.com` for subdomains (not the apex, so list both). */
18
+ hosts?: readonly string[];
19
+ /** `data:` raster images. Default `true`. SVG is never allowed: it can carry script. */
20
+ allowDataImages?: boolean;
21
+ /** The page origin, for where there is no `location` (SSR); without either, only `hosts` matches an absolute URL. */
22
+ origin?: string;
23
+ } | ((url: string) => boolean);
24
+ type UrlCheck = {
25
+ ok: true;
26
+ } | {
27
+ ok: false;
28
+ reason: string;
29
+ };
30
+ export declare const resolveUrlPolicy: (policy?: Exclude<UrlPolicy, (url: string) => boolean>) => {
31
+ allowRelative: boolean;
32
+ allowSameOrigin: boolean;
33
+ hosts: readonly string[];
34
+ allowDataImages: boolean;
35
+ origin: string | undefined;
36
+ };
37
+ export declare const checkUrl: (value: string, policy?: UrlPolicy) => UrlCheck;
38
+ type UrlViolation = {
39
+ path: string;
40
+ url: string;
41
+ reason: string;
42
+ };
43
+ export declare const findUrlViolations: (schema: JSONSchema | undefined, value: unknown, policy?: UrlPolicy) => UrlViolation[];
44
+ export declare const schemaHasUrlFormat: (schema: JSONSchema | undefined) => boolean;
45
+ export {};
@@ -0,0 +1,252 @@
1
+ import { isSchemaObject, resolvePointer } from "./json-schema.js";
2
+ // The defaults, in one place: the check applies them and the prompt describes them.
3
+ export const resolveUrlPolicy = (policy = {}) => {
4
+ const { allowRelative = true, allowSameOrigin = true, hosts = [], allowDataImages = true, origin } = policy;
5
+ return { allowRelative, allowSameOrigin, hosts, allowDataImages, origin };
6
+ };
7
+ const OK = { ok: true };
8
+ // Never fetch on their own.
9
+ const INERT_SCHEMES = new Set(["mailto", "tel", "sms", "blob"]);
10
+ // No `svg+xml`: SVG can carry script.
11
+ const DATA_IMAGE_RE = /^data:image\/(png|jpe?g|gif|webp|avif|bmp|x-icon|vnd\.microsoft\.icon)[;,]/i;
12
+ const SCHEME_RE = /^([a-z][a-z0-9+.-]*):/i;
13
+ // For http(s) the URL parser reads `\` as `/`, so a value with no scheme names a host exactly when it starts with two.
14
+ const NAMES_HOST_RE = /^[\\/]{2}/;
15
+ // Supplies only the scheme: a value that names a host keeps its own.
16
+ const NETWORK_PATH_BASE = "https://base.invalid/";
17
+ // Strip what the URL parser strips, or `"java\nscript:"` reads as relative here and `javascript:` in the DOM.
18
+ const normalize = (raw) => {
19
+ const url = raw.replace(/[\t\n\r]/g, "");
20
+ // A loop, not a regex: a trailing `[\0- ]+$` takes quadratic time on a long run of spaces.
21
+ let start = 0;
22
+ let end = url.length;
23
+ while (start < end && url.charCodeAt(start) <= 0x20)
24
+ start++;
25
+ while (end > start && url.charCodeAt(end - 1) <= 0x20)
26
+ end--;
27
+ return url.slice(start, end);
28
+ };
29
+ const parseUrl = (url, base) => {
30
+ try {
31
+ return new URL(url, base);
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ };
37
+ const hostAllowed = (host, hosts) => {
38
+ const lower = host.toLowerCase();
39
+ return hosts.some((entry) => {
40
+ const pattern = entry.toLowerCase();
41
+ if (pattern.startsWith("*.")) {
42
+ return lower.endsWith(pattern.slice(1)) && lower !== pattern.slice(2);
43
+ }
44
+ return lower === pattern;
45
+ });
46
+ };
47
+ // Parsed once per string: an explicit origin with a path or a trailing slash still names its origin.
48
+ const explicitOrigins = new Map();
49
+ const currentOrigin = (explicit) => {
50
+ if (!explicit)
51
+ return typeof location !== "undefined" ? location.origin : null;
52
+ let origin = explicitOrigins.get(explicit);
53
+ if (origin === undefined) {
54
+ origin = parseUrl(explicit)?.origin ?? null;
55
+ explicitOrigins.set(explicit, origin);
56
+ }
57
+ return origin;
58
+ };
59
+ const checkHost = (url, allowSameOrigin, explicitOrigin, hosts) => {
60
+ if (allowSameOrigin && url.origin === currentOrigin(explicitOrigin))
61
+ return OK;
62
+ if (hosts.length && hostAllowed(url.hostname, hosts))
63
+ return OK;
64
+ return {
65
+ ok: false,
66
+ reason: `"${url.hostname}" is not an allowed host. Add it to the renderer's ` +
67
+ `urlPolicy ({ hosts: ["${url.hostname}"] }) if this destination is trusted`,
68
+ };
69
+ };
70
+ // An empty string passes: nothing to fetch.
71
+ export const checkUrl = (value, policy) => {
72
+ const url = normalize(value);
73
+ if (!url)
74
+ return OK;
75
+ if (typeof policy === "function") {
76
+ return policy(url) ? OK : { ok: false, reason: "rejected by the host's urlPolicy predicate" };
77
+ }
78
+ const { allowRelative, allowSameOrigin, hosts, allowDataImages, origin: explicitOrigin } = resolveUrlPolicy(policy);
79
+ const scheme = SCHEME_RE.exec(url)?.[1]?.toLowerCase() ?? null;
80
+ if (scheme === null) {
81
+ if (NAMES_HOST_RE.test(url)) {
82
+ const resolved = parseUrl(url, NETWORK_PATH_BASE);
83
+ if (!resolved)
84
+ return { ok: false, reason: "not a parseable URL" };
85
+ return checkHost(resolved, allowSameOrigin, explicitOrigin, hosts);
86
+ }
87
+ return allowRelative ? OK : { ok: false, reason: "relative URLs are not allowed by this urlPolicy" };
88
+ }
89
+ if (scheme === "data") {
90
+ if (allowDataImages && DATA_IMAGE_RE.test(url))
91
+ return OK;
92
+ return {
93
+ ok: false,
94
+ reason: DATA_IMAGE_RE.test(url)
95
+ ? "data: URLs are not allowed by this urlPolicy"
96
+ : 'only "data:image/<raster>" URLs are allowed (svg+xml can carry script)',
97
+ };
98
+ }
99
+ if (INERT_SCHEMES.has(scheme))
100
+ return OK;
101
+ if (scheme !== "http" && scheme !== "https") {
102
+ return { ok: false, reason: `the "${scheme}:" scheme is not allowed` };
103
+ }
104
+ const parsed = parseUrl(url);
105
+ if (!parsed)
106
+ return { ok: false, reason: "not a parseable absolute URL" };
107
+ return checkHost(parsed, allowSameOrigin, explicitOrigin, hosts);
108
+ };
109
+ const URL_FORMATS = new Set(["uri", "url", "uri-reference", "iri", "iri-reference"]);
110
+ const hasUrlFormat = (node) => {
111
+ const format = Object.hasOwn(node, "format") ? Reflect.get(node, "format") : undefined;
112
+ return typeof format === "string" && URL_FORMATS.has(format);
113
+ };
114
+ const indexes = new WeakMap();
115
+ const indexOf = (root) => {
116
+ let index = indexes.get(root);
117
+ if (!index) {
118
+ index = { root, reach: new Map(), applicable: new Map() };
119
+ indexes.set(root, index);
120
+ }
121
+ return index;
122
+ };
123
+ // Whether a URL format sits at or below `start`, through any keyword and any `$ref` chain. A stack, so depth sets no limit.
124
+ const reachesUrlFormat = (index, start) => {
125
+ if (typeof start !== "object" || start === null)
126
+ return false;
127
+ const cached = index.reach.get(start);
128
+ if (cached !== undefined)
129
+ return cached;
130
+ const seen = new Set();
131
+ const stack = [start];
132
+ let found = false;
133
+ while (!found && stack.length > 0) {
134
+ const node = stack.pop();
135
+ if (typeof node !== "object" || node === null || seen.has(node))
136
+ continue;
137
+ seen.add(node);
138
+ found = hasUrlFormat(node);
139
+ for (const [key, child] of Object.entries(node)) {
140
+ stack.push(key === "$ref" && typeof child === "string" ? resolvePointer(child, index.root) : child);
141
+ }
142
+ }
143
+ index.reach.set(start, found);
144
+ return found;
145
+ };
146
+ // The nodes that hold for one value: `start` and every applicator under it that keeps the value.
147
+ // All but `not` (a URL format under it forbids a URL); one that holds for only some values counts for all.
148
+ const applicableNodes = (index, start) => {
149
+ const cached = index.applicable.get(start);
150
+ if (cached)
151
+ return cached;
152
+ const out = new Set();
153
+ const stack = [start];
154
+ while (stack.length > 0) {
155
+ const node = stack.pop();
156
+ if (!isSchemaObject(node) || out.has(node) || !reachesUrlFormat(index, node))
157
+ continue;
158
+ out.add(node);
159
+ if (typeof node.$ref === "string")
160
+ stack.push(resolvePointer(node.$ref, index.root));
161
+ stack.push(...(node.allOf ?? []), ...(node.anyOf ?? []), ...(node.oneOf ?? []));
162
+ stack.push(node.if, node.then, node.else, ...Object.values(node.dependentSchemas ?? {}));
163
+ }
164
+ const nodes = [...out];
165
+ index.applicable.set(start, nodes);
166
+ return nodes;
167
+ };
168
+ const pathOf = (visit) => {
169
+ let path = "";
170
+ for (let at = visit; at?.parent; at = at.parent) {
171
+ const step = typeof at.key === "number" ? `[${at.key}]` : `.${at.key}`;
172
+ path = step + path;
173
+ }
174
+ return path.startsWith(".") ? path.slice(1) : path;
175
+ };
176
+ // A subschema with no URL format below it is never walked, so a URL-free table costs nothing.
177
+ const pushVisit = (stack, index, node, value, parent, key) => {
178
+ if (isSchemaObject(node) && reachesUrlFormat(index, node))
179
+ stack.push({ node, value, parent, key });
180
+ };
181
+ // Only declared URL props are checked: a URL-looking `text` prop is content.
182
+ export const findUrlViolations = (schema, value, policy) => {
183
+ if (!schema)
184
+ return [];
185
+ const index = indexOf(schema);
186
+ if (!reachesUrlFormat(index, schema))
187
+ return [];
188
+ const out = [];
189
+ const flagged = new Set();
190
+ // Per object, the nodes already applied: a shared or cyclic value is walked once per node.
191
+ const walked = new WeakMap();
192
+ // Children are pushed last to first, so values are checked, and violations reported, in document order.
193
+ const stack = [{ node: schema, value, parent: null, key: "" }];
194
+ for (let visit = stack.pop(); visit; visit = stack.pop()) {
195
+ const val = visit.value;
196
+ const nodes = applicableNodes(index, visit.node);
197
+ if (typeof val === "string") {
198
+ if (!nodes.some(hasUrlFormat))
199
+ continue;
200
+ const result = checkUrl(val, policy);
201
+ if (result.ok)
202
+ continue;
203
+ const path = pathOf(visit);
204
+ if (!flagged.has(path)) {
205
+ flagged.add(path);
206
+ out.push({ path, url: val, reason: result.reason });
207
+ }
208
+ continue;
209
+ }
210
+ if (typeof val !== "object" || val === null)
211
+ continue;
212
+ let done = walked.get(val);
213
+ if (!done) {
214
+ done = new Set();
215
+ walked.set(val, done);
216
+ }
217
+ for (let n = nodes.length - 1; n >= 0; n--) {
218
+ const node = nodes[n];
219
+ if (done.has(node))
220
+ continue;
221
+ done.add(node);
222
+ if (Array.isArray(val)) {
223
+ const tuple = node.prefixItems ?? [];
224
+ for (let i = val.length - 1; i >= 0; i--) {
225
+ pushVisit(stack, index, node.unevaluatedItems, val[i], visit, i);
226
+ pushVisit(stack, index, node.contains, val[i], visit, i);
227
+ pushVisit(stack, index, i < tuple.length ? tuple[i] : node.items, val[i], visit, i);
228
+ }
229
+ continue;
230
+ }
231
+ const declared = node.properties ?? {};
232
+ // Patterns are not matched: each one applies to every key.
233
+ const patterns = Object.values(node.patternProperties ?? {});
234
+ const entries = Object.entries(val);
235
+ for (let e = entries.length - 1; e >= 0; e--) {
236
+ const [key, child] = entries[e];
237
+ if (Object.hasOwn(declared, key)) {
238
+ pushVisit(stack, index, declared[key], child, visit, key);
239
+ }
240
+ else {
241
+ pushVisit(stack, index, node.unevaluatedProperties, child, visit, key);
242
+ pushVisit(stack, index, node.additionalProperties, child, visit, key);
243
+ }
244
+ for (const sub of patterns)
245
+ pushVisit(stack, index, sub, child, visit, key);
246
+ pushVisit(stack, index, node.propertyNames, key, visit, key);
247
+ }
248
+ }
249
+ }
250
+ return out;
251
+ };
252
+ export const schemaHasUrlFormat = (schema) => schema !== undefined && reachesUrlFormat(indexOf(schema), schema);
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@uicast/core",
3
- "version": "0.0.1-beta.2",
4
- "description": "LLM-generated UIs from a stream of JSONLines — the framework-agnostic uicast engine: entry format, sandboxed expressions, reactive scopes, and prompt builders.",
3
+ "version": "0.0.2",
4
+ "description": "The framework-agnostic engine of uicast, the expression-driven generative UI framework: entry format, guarded expressions (allow-list, not a sandbox), reactive scopes, and prompt builders.",
5
5
  "license": "MIT",
6
+ "author": "Andrey Gubanov (https://github.com/finom)",
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "git+https://github.com/finom/uicast.git",
9
10
  "directory": "packages/core"
10
11
  },
11
- "homepage": "https://github.com/finom/uicast#readme",
12
+ "homepage": "https://uicast.dev",
12
13
  "keywords": [
13
14
  "generative-ui",
14
15
  "llm",
15
- "jsonlines",
16
16
  "streaming",
17
17
  "ui",
18
18
  "agent",
@@ -24,6 +24,10 @@
24
24
  "types": "./dist/index.d.ts",
25
25
  "default": "./dist/index.js"
26
26
  },
27
+ "./internal": {
28
+ "types": "./dist/internal.d.ts",
29
+ "default": "./dist/internal.js"
30
+ },
27
31
  "./prompt": {
28
32
  "types": "./dist/prompt/index.d.ts",
29
33
  "default": "./dist/prompt/index.js"
@@ -35,25 +39,23 @@
35
39
  "CHANGELOG.md"
36
40
  ],
37
41
  "scripts": {
38
- "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
39
- "prepublishOnly": "npm run build",
42
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
43
+ "prepublishOnly": "node -e \"if (!process.env.CI) throw new Error('Publish from CI: PACKAGE=<dir> npm run patch or minor tags a release, and the tag publishes it')\" && npm run build",
40
44
  "test": "vitest run",
41
45
  "test:watch": "vitest",
42
46
  "test:coverage": "vitest run --coverage",
43
- "typecheck": "tsc --noEmit",
44
- "npm-publish": "if [ -z \"$NPM_TAG\" ]; then echo 'Error: NPM_TAG is not set'; exit 1; fi; npm publish --tag=$NPM_TAG"
45
- },
46
- "dependencies": {
47
- "@standard-schema/spec": "^1.1.0",
48
- "acorn": "^8.16.0",
49
- "standard-tool": "^0.1.0"
47
+ "typecheck": "tsc --noEmit"
50
48
  },
51
49
  "devDependencies": {
52
- "@vitest/coverage-v8": "^4.1.0",
53
- "vitest": "^4.1.0",
54
- "zod": "~4.3.6"
50
+ "@vitest/coverage-v8": "^5.0.2",
51
+ "standard-tool": "^0.1.0",
52
+ "vitest": "^5.0.1",
53
+ "zod": "~4.6.5"
55
54
  },
56
55
  "publishConfig": {
57
56
  "access": "public"
57
+ },
58
+ "peerDependencies": {
59
+ "@uicast/expr": "^0.0.2"
58
60
  }
59
61
  }
@@ -1,9 +0,0 @@
1
- import type { CombinedSpec, ComponentDefinition } from "../types.js";
2
- /**
3
- * Value-side constructor for a {@link ComponentDefinition} (the type lives in
4
- * `types.ts`). Infers the concrete `props` / `callbacks` specs at the call site
5
- * and returns the def unchanged — its purpose is the inference + a single typed
6
- * authoring shape; `createComponentImplementation` later reads those inferred
7
- * types off the returned def.
8
- */
9
- export declare const createComponentDefinition: <TProps extends CombinedSpec, TCallbacks extends Record<string, CombinedSpec> = Record<string, never>>({ name, description, props, callbacks, hidden, }: ComponentDefinition<TProps, TCallbacks>) => ComponentDefinition<TProps, TCallbacks>;
@@ -1,10 +0,0 @@
1
- /**
2
- * Value-side constructor for a {@link ComponentDefinition} (the type lives in
3
- * `types.ts`). Infers the concrete `props` / `callbacks` specs at the call site
4
- * and returns the def unchanged — its purpose is the inference + a single typed
5
- * authoring shape; `createComponentImplementation` later reads those inferred
6
- * types off the returned def.
7
- */
8
- export const createComponentDefinition = ({ name, description, props, callbacks, hidden, }) => {
9
- return { name, description, props, callbacks, hidden };
10
- };
@@ -1,5 +0,0 @@
1
- import type * as acorn from "acorn";
2
- /** Does the expression `await` anything? */
3
- export declare function containsAwait(node: acorn.AnyNode | null | undefined): boolean;
4
- export declare function extractScopeReads(ast: acorn.AnyNode): string[];
5
- export declare function extractFreeIdentifiers(ast: acorn.AnyNode): string[];