@svadmin/surface 0.8.6 → 0.9.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/README.md +70 -38
- package/STYLING.md +31 -0
- package/compatibility.json +23 -2
- package/dist/agent-contract.d.ts +37 -0
- package/dist/agent-contract.js +203 -0
- package/dist/agent.d.ts +38 -7
- package/dist/agent.js +98 -59
- package/dist/binding.d.ts +2 -0
- package/dist/binding.js +7 -1
- package/dist/builtin-definitions.d.ts +6 -0
- package/dist/builtin-definitions.js +40 -0
- package/dist/builtin-schemas.d.ts +25 -0
- package/dist/builtin-schemas.js +13 -0
- package/dist/catalog.d.ts +4 -61
- package/dist/catalog.js +20 -44
- package/dist/components/MetricWidget.svelte +12 -8
- package/dist/components/ResourceFormBody.svelte +97 -0
- package/dist/components/ResourceFormBody.svelte.d.ts +13 -0
- package/dist/components/ResourceFormWidget.svelte +19 -0
- package/dist/components/ResourceFormWidget.svelte.d.ts +4 -0
- package/dist/components/ResourceTableWidget.svelte +14 -12
- package/dist/components/SurfaceEditPreview.svelte +107 -0
- package/dist/components/SurfaceEditPreview.svelte.d.ts +20 -0
- package/dist/components/SurfaceRenderer.svelte +120 -163
- package/dist/components/SurfaceRenderer.svelte.d.ts +4 -0
- package/dist/components/SurfaceWorkflowProvider.svelte +22 -0
- package/dist/components/SurfaceWorkflowProvider.svelte.d.ts +17 -0
- package/dist/components/SvarGridWidget.svelte +47 -0
- package/dist/components/SvarGridWidget.svelte.d.ts +4 -0
- package/dist/components/SvarSurfaceProvider.svelte +12 -0
- package/dist/components/SvarSurfaceProvider.svelte.d.ts +11 -0
- package/dist/edits.d.ts +57 -0
- package/dist/edits.js +166 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +5 -1
- package/dist/interactive.d.ts +6 -0
- package/dist/interactive.js +15 -0
- package/dist/json.js +1 -2
- package/dist/openui.d.ts +64 -0
- package/dist/openui.js +168 -0
- package/dist/schema.d.ts +108 -0
- package/dist/schema.js +73 -0
- package/dist/server-sqlite.d.ts +1 -0
- package/dist/server-sqlite.js +2 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +4 -0
- package/dist/source-cache.d.ts +30 -0
- package/dist/source-cache.js +135 -0
- package/dist/styled-system/css/conditions.js +36 -0
- package/dist/styled-system/css/css.d.ts +22 -0
- package/dist/styled-system/css/css.js +45 -0
- package/dist/styled-system/css/cva.d.ts +6 -0
- package/dist/styled-system/css/cva.js +87 -0
- package/dist/styled-system/css/cx.d.ts +5 -0
- package/dist/styled-system/css/cx.js +15 -0
- package/dist/styled-system/css/index.d.ts +5 -0
- package/dist/styled-system/css/index.js +4 -0
- package/dist/styled-system/css/sva.d.ts +4 -0
- package/dist/styled-system/css/sva.js +46 -0
- package/dist/styled-system/design-contract.d.ts +13 -0
- package/dist/styled-system/design-contract.js +11 -0
- package/dist/styled-system/helpers.js +316 -0
- package/dist/styled-system/patterns/aspect-ratio.d.ts +20 -0
- package/dist/styled-system/patterns/aspect-ratio.js +38 -0
- package/dist/styled-system/patterns/bleed.d.ts +21 -0
- package/dist/styled-system/patterns/bleed.js +24 -0
- package/dist/styled-system/patterns/box.d.ts +20 -0
- package/dist/styled-system/patterns/box.js +15 -0
- package/dist/styled-system/patterns/center.d.ts +20 -0
- package/dist/styled-system/patterns/center.js +21 -0
- package/dist/styled-system/patterns/circle.d.ts +20 -0
- package/dist/styled-system/patterns/circle.js +25 -0
- package/dist/styled-system/patterns/container.d.ts +20 -0
- package/dist/styled-system/patterns/container.js +21 -0
- package/dist/styled-system/patterns/cq.d.ts +21 -0
- package/dist/styled-system/patterns/cq.js +21 -0
- package/dist/styled-system/patterns/divider.d.ts +22 -0
- package/dist/styled-system/patterns/divider.js +25 -0
- package/dist/styled-system/patterns/flex.d.ts +26 -0
- package/dist/styled-system/patterns/flex.js +26 -0
- package/dist/styled-system/patterns/float.d.ts +23 -0
- package/dist/styled-system/patterns/float.js +52 -0
- package/dist/styled-system/patterns/grid-item.d.ts +25 -0
- package/dist/styled-system/patterns/grid-item.js +25 -0
- package/dist/styled-system/patterns/grid.d.ts +24 -0
- package/dist/styled-system/patterns/grid.js +27 -0
- package/dist/styled-system/patterns/hstack.d.ts +21 -0
- package/dist/styled-system/patterns/hstack.js +24 -0
- package/dist/styled-system/patterns/index.d.ts +21 -0
- package/dist/styled-system/patterns/index.js +20 -0
- package/dist/styled-system/patterns/link-overlay.d.ts +20 -0
- package/dist/styled-system/patterns/link-overlay.js +24 -0
- package/dist/styled-system/patterns/spacer.d.ts +20 -0
- package/dist/styled-system/patterns/spacer.js +25 -0
- package/dist/styled-system/patterns/square.d.ts +20 -0
- package/dist/styled-system/patterns/square.js +24 -0
- package/dist/styled-system/patterns/stack.d.ts +23 -0
- package/dist/styled-system/patterns/stack.js +24 -0
- package/dist/styled-system/patterns/visually-hidden.d.ts +20 -0
- package/dist/styled-system/patterns/visually-hidden.js +18 -0
- package/dist/styled-system/patterns/vstack.d.ts +21 -0
- package/dist/styled-system/patterns/vstack.js +24 -0
- package/dist/styled-system/patterns/wrap.d.ts +24 -0
- package/dist/styled-system/patterns/wrap.js +25 -0
- package/dist/styled-system/recipes/content-header.d.ts +31 -0
- package/dist/styled-system/recipes/content-header.js +65 -0
- package/dist/styled-system/recipes/content-page.d.ts +34 -0
- package/dist/styled-system/recipes/content-page.js +43 -0
- package/dist/styled-system/recipes/create-recipe.js +82 -0
- package/dist/styled-system/recipes/index.d.ts +17 -0
- package/dist/styled-system/recipes/index.js +16 -0
- package/dist/styled-system/recipes/metric-block.d.ts +34 -0
- package/dist/styled-system/recipes/metric-block.js +76 -0
- package/dist/styled-system/recipes/product-list.d.ts +31 -0
- package/dist/styled-system/recipes/product-list.js +73 -0
- package/dist/styled-system/recipes/product-section.d.ts +31 -0
- package/dist/styled-system/recipes/product-section.js +49 -0
- package/dist/styled-system/recipes/product-settings-row.d.ts +34 -0
- package/dist/styled-system/recipes/product-settings-row.js +58 -0
- package/dist/styled-system/recipes/product-settings.d.ts +31 -0
- package/dist/styled-system/recipes/product-settings.js +57 -0
- package/dist/styled-system/recipes/product-status.d.ts +34 -0
- package/dist/styled-system/recipes/product-status.js +45 -0
- package/dist/styled-system/recipes/product-toolbar.d.ts +31 -0
- package/dist/styled-system/recipes/product-toolbar.js +41 -0
- package/dist/styled-system/recipes/product-workspace.d.ts +34 -0
- package/dist/styled-system/recipes/product-workspace.js +58 -0
- package/dist/styled-system/recipes/surface-metric.d.ts +38 -0
- package/dist/styled-system/recipes/surface-metric.js +63 -0
- package/dist/styled-system/recipes/surface-table.d.ts +34 -0
- package/dist/styled-system/recipes/surface-table.js +62 -0
- package/dist/styled-system/recipes/ui-badge.d.ts +31 -0
- package/dist/styled-system/recipes/ui-badge.js +38 -0
- package/dist/styled-system/recipes/ui-button.d.ts +32 -0
- package/dist/styled-system/recipes/ui-button.js +43 -0
- package/dist/styled-system/recipes/ui-input.d.ts +31 -0
- package/dist/styled-system/recipes/ui-input.js +49 -0
- package/dist/styled-system/recipes/ui-textarea.d.ts +31 -0
- package/dist/styled-system/recipes/ui-textarea.js +24 -0
- package/dist/styled-system/styles.css +1257 -0
- package/dist/styled-system/tokens/index.d.ts +9 -0
- package/dist/styled-system/tokens/index.js +268 -0
- package/dist/styled-system/tokens/tokens.d.ts +36 -0
- package/dist/styled-system/types/composition.d.ts +227 -0
- package/dist/styled-system/types/conditions.d.ts +236 -0
- package/dist/styled-system/types/csstype.d.ts +22570 -0
- package/dist/styled-system/types/index.d.ts +6 -0
- package/dist/styled-system/types/parts.d.ts +8 -0
- package/dist/styled-system/types/pattern.d.ts +78 -0
- package/dist/styled-system/types/prop-type.d.ts +223 -0
- package/dist/styled-system/types/recipe.d.ts +181 -0
- package/dist/styled-system/types/selectors.d.ts +59 -0
- package/dist/styled-system/types/static-css.d.ts +56 -0
- package/dist/styled-system/types/style-props.d.ts +8088 -0
- package/dist/styled-system/types/system-types.d.ts +151 -0
- package/dist/styles/editor.css +124 -0
- package/dist/styles/editor.generated.d.ts +8 -0
- package/dist/styles/editor.generated.js +9 -0
- package/dist/styles.css +1262 -0
- package/dist/svar-catalog.d.ts +3 -0
- package/dist/svar-catalog.js +7 -0
- package/dist/svar-context.d.ts +9 -0
- package/dist/svar-context.js +1 -0
- package/dist/svar-schema.d.ts +21 -0
- package/dist/svar-schema.js +36 -0
- package/dist/svar.d.ts +4 -0
- package/dist/svar.js +3 -0
- package/dist/svelte.d.ts +4 -2
- package/dist/svelte.js +2 -1
- package/dist/types.d.ts +5 -1
- package/dist/validation.js +2 -102
- package/dist/wire.d.ts +13 -0
- package/dist/wire.js +30 -0
- package/dist/workflows/action-contracts.d.ts +7 -0
- package/dist/workflows/action-contracts.js +54 -0
- package/dist/workflows/catalog.d.ts +20 -0
- package/dist/workflows/catalog.js +50 -0
- package/dist/workflows/client.d.ts +45 -0
- package/dist/workflows/client.js +125 -0
- package/dist/workflows/context.d.ts +9 -0
- package/dist/workflows/context.js +5 -0
- package/dist/workflows/openui-guard.d.ts +19 -0
- package/dist/workflows/openui-guard.js +209 -0
- package/dist/workflows/service.d.ts +58 -0
- package/dist/workflows/service.js +296 -0
- package/dist/workflows/sqlite-store.d.ts +20 -0
- package/dist/workflows/sqlite-store.js +91 -0
- package/dist/workflows/types.d.ts +103 -0
- package/dist/workflows/types.js +8 -0
- package/dist/workflows.d.ts +5 -0
- package/dist/workflows.js +4 -0
- package/package.json +53 -9
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext } from 'svelte';
|
|
3
|
+
import SvarDataGrid from '@svadmin/ui/components/SvarDataGrid.svelte';
|
|
4
|
+
import type { SurfaceWidgetRendererProps } from '../catalog.js';
|
|
5
|
+
import { SVAR_SURFACE_CONTEXT, type SvarSurfaceContext } from '../svar-context.js';
|
|
6
|
+
import { decodeSvarGridProps } from '../svar-schema.js';
|
|
7
|
+
import { asRecordArray } from '../widget-data.js';
|
|
8
|
+
import { resolveSurfaceMessages } from '../localization.js';
|
|
9
|
+
|
|
10
|
+
let { widgetId, props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
|
|
11
|
+
const host = getContext<SvarSurfaceContext | undefined>(SVAR_SURFACE_CONTEXT);
|
|
12
|
+
const labels = $derived(resolveSurfaceMessages(locale, messages));
|
|
13
|
+
const config = $derived(decodeSvarGridProps(props));
|
|
14
|
+
const records = $derived(data.status === 'ready' ? asRecordArray(data.value) : undefined);
|
|
15
|
+
const columns = $derived(config.columns.map(column => ({
|
|
16
|
+
key: column.field, label: column.label,
|
|
17
|
+
...(column.width === undefined ? {} : { width: column.width }),
|
|
18
|
+
...(column.format === undefined ? {} : { format: column.format }),
|
|
19
|
+
})));
|
|
20
|
+
const Grid = $derived(host?.Grid);
|
|
21
|
+
const Theme = $derived(host?.Theme);
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<section aria-labelledby="svar-surface-{widgetId}" class="surface-data-grid">
|
|
25
|
+
<h3 id="svar-surface-{widgetId}">{config.title}</h3>
|
|
26
|
+
{#if !Grid || !Theme}
|
|
27
|
+
<p role="alert">{locale.startsWith('zh') ? '宿主尚未配置 SVAR 引擎' : 'The host has not configured the SVAR engine'}</p>
|
|
28
|
+
{:else if data.status === 'error'}
|
|
29
|
+
<p role="alert">{data.error.message}</p>
|
|
30
|
+
{:else if data.status === 'unbound' || (records && !records.ok)}
|
|
31
|
+
<p role="alert">{labels.tableInvalidData}</p>
|
|
32
|
+
{:else}
|
|
33
|
+
<SvarDataGrid {Grid} {Theme} {columns} items={records?.ok ? records.value : []}
|
|
34
|
+
primaryKey={config.rowKey ?? 'id'} height={config.height ?? 360} density={config.density ?? 'comfortable'}
|
|
35
|
+
freezeLeft={config.freezeLeft ?? 0} freezeRight={config.freezeRight ?? 0}
|
|
36
|
+
loading={data.status === 'loading'} label={config.title} {locale}
|
|
37
|
+
scopeKey={`${host?.scopeKey ?? 0}:${widgetId}:${'sourceId' in data ? data.sourceId : ''}`}
|
|
38
|
+
loadingLabel={labels.tableLoading} emptyLabel={config.emptyLabel ?? labels.tableNoRecords}
|
|
39
|
+
selectable={false} />
|
|
40
|
+
{/if}
|
|
41
|
+
</section>
|
|
42
|
+
|
|
43
|
+
<style>
|
|
44
|
+
.surface-data-grid { min-width: 0; padding: 1rem; background: var(--card, Canvas); color: var(--foreground, CanvasText); border: 1px solid var(--border, GrayText); border-radius: var(--radius, 8px); }
|
|
45
|
+
h3 { margin: 0 0 0.75rem; font-size: 1rem; }
|
|
46
|
+
[role='alert'] { color: var(--destructive, CanvasText); }
|
|
47
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { setContext, type Snippet } from 'svelte';
|
|
3
|
+
import { SVAR_SURFACE_CONTEXT, type SvarSurfaceContext } from '../svar-context.js';
|
|
4
|
+
let { Grid, Theme, scopeKey = 0, children }: {
|
|
5
|
+
Grid: SvarSurfaceContext['Grid']; Theme: SvarSurfaceContext['Theme']; scopeKey?: string | number; children: Snippet;
|
|
6
|
+
} = $props();
|
|
7
|
+
setContext<SvarSurfaceContext>(SVAR_SURFACE_CONTEXT, {
|
|
8
|
+
get Grid() { return Grid; }, get Theme() { return Theme; }, get scopeKey() { return scopeKey; },
|
|
9
|
+
});
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
{@render children()}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import { type SvarSurfaceContext } from '../svar-context.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
Grid: SvarSurfaceContext['Grid'];
|
|
5
|
+
Theme: SvarSurfaceContext['Theme'];
|
|
6
|
+
scopeKey?: string | number;
|
|
7
|
+
children: Snippet;
|
|
8
|
+
};
|
|
9
|
+
declare const SvarSurfaceProvider: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
|
+
type SvarSurfaceProvider = ReturnType<typeof SvarSurfaceProvider>;
|
|
11
|
+
export default SvarSurfaceProvider;
|
package/dist/edits.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { SurfaceAgentMessage } from './agent.js';
|
|
2
|
+
import type { JsonObject, SurfaceCatalog, SurfaceDataSource, SurfaceGridLayout, SurfacePolicy, SurfaceSpec, SurfaceValidationIssue, SurfaceWidget } from './types.js';
|
|
3
|
+
export declare const SURFACE_EDIT_SCHEMA_VERSION: "surface-edit/v1";
|
|
4
|
+
export declare const SURFACE_EDIT_LIMITS: {
|
|
5
|
+
readonly maxOperations: 64;
|
|
6
|
+
};
|
|
7
|
+
export type SurfaceEditOperation = {
|
|
8
|
+
readonly op: 'set-title';
|
|
9
|
+
readonly value: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly op: 'set-layout';
|
|
12
|
+
readonly value: SurfaceGridLayout;
|
|
13
|
+
} | {
|
|
14
|
+
readonly op: 'upsert-widget';
|
|
15
|
+
readonly widget: SurfaceWidget;
|
|
16
|
+
} | {
|
|
17
|
+
readonly op: 'remove-widget';
|
|
18
|
+
readonly id: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly op: 'upsert-source';
|
|
21
|
+
readonly source: SurfaceDataSource;
|
|
22
|
+
} | {
|
|
23
|
+
readonly op: 'remove-source';
|
|
24
|
+
readonly id: string;
|
|
25
|
+
} | {
|
|
26
|
+
readonly op: 'reorder-widgets';
|
|
27
|
+
readonly ids: readonly string[];
|
|
28
|
+
};
|
|
29
|
+
export interface SurfaceEditProposal {
|
|
30
|
+
readonly schemaVersion: typeof SURFACE_EDIT_SCHEMA_VERSION;
|
|
31
|
+
readonly catalogVersion: string;
|
|
32
|
+
readonly surfaceId: string;
|
|
33
|
+
readonly baseRevision: number;
|
|
34
|
+
readonly operations: readonly SurfaceEditOperation[];
|
|
35
|
+
}
|
|
36
|
+
export interface SurfaceRevision {
|
|
37
|
+
readonly revision: number;
|
|
38
|
+
readonly spec: SurfaceSpec;
|
|
39
|
+
}
|
|
40
|
+
export interface SurfaceEditIssue extends Omit<SurfaceValidationIssue, 'code'> {
|
|
41
|
+
readonly code: SurfaceValidationIssue['code'] | 'revision_conflict' | 'invalid_edit';
|
|
42
|
+
}
|
|
43
|
+
export type SurfaceRevisionResult = {
|
|
44
|
+
readonly ok: true;
|
|
45
|
+
readonly value: SurfaceRevision;
|
|
46
|
+
} | {
|
|
47
|
+
readonly ok: false;
|
|
48
|
+
readonly issues: readonly SurfaceEditIssue[];
|
|
49
|
+
};
|
|
50
|
+
export declare function createSurfaceRevision(spec: unknown, catalog: SurfaceCatalog, policy: SurfacePolicy, revision?: number): SurfaceRevisionResult;
|
|
51
|
+
/**
|
|
52
|
+
* 只生成下一版候选快照,不保存、不渲染、不访问 DataProvider。
|
|
53
|
+
* 服务端保存时仍必须对 revision 做原子 compare-and-swap,并重新授权。
|
|
54
|
+
*/
|
|
55
|
+
export declare function applySurfaceEditProposal(current: SurfaceRevision, input: unknown, catalog: SurfaceCatalog, policy: SurfacePolicy): SurfaceRevisionResult;
|
|
56
|
+
export declare function createSurfaceEditSchema(): JsonObject;
|
|
57
|
+
export declare function buildSurfaceEditMessages(request: string, current: SurfaceRevision, catalog: SurfaceCatalog, policy: SurfacePolicy): readonly SurfaceAgentMessage[];
|
package/dist/edits.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { TypeCompiler } from '@sinclair/typebox/compiler';
|
|
3
|
+
import { createSurfaceCatalogManifest, SURFACE_AGENT_LIMITS, surfaceSchemaToJson } from './agent-contract.js';
|
|
4
|
+
import { surfaceIdSchema, surfaceLayoutSchema, surfaceListSourceSchema, surfaceOneSourceSchema, surfaceSpecSchema, surfaceWidgetSchema, } from './schema.js';
|
|
5
|
+
import { SURFACE_LIMITS } from './types.js';
|
|
6
|
+
import { validateSurfaceSpec } from './validation.js';
|
|
7
|
+
import { parseSurfaceJson } from './wire.js';
|
|
8
|
+
export const SURFACE_EDIT_SCHEMA_VERSION = 'surface-edit/v1';
|
|
9
|
+
export const SURFACE_EDIT_LIMITS = { maxOperations: 64 };
|
|
10
|
+
const revisionSchema = Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
|
|
11
|
+
const editSchema = Type.Object({
|
|
12
|
+
schemaVersion: Type.Literal(SURFACE_EDIT_SCHEMA_VERSION),
|
|
13
|
+
catalogVersion: Type.String({ minLength: 1 }),
|
|
14
|
+
surfaceId: surfaceIdSchema,
|
|
15
|
+
baseRevision: revisionSchema,
|
|
16
|
+
operations: Type.Array(Type.Union([
|
|
17
|
+
Type.Object({ op: Type.Literal('set-title'), value: Type.String({ minLength: 1, maxLength: SURFACE_LIMITS.maxTitleLength }) }, { additionalProperties: false }),
|
|
18
|
+
Type.Object({ op: Type.Literal('set-layout'), value: surfaceLayoutSchema }, { additionalProperties: false }),
|
|
19
|
+
Type.Object({ op: Type.Literal('upsert-widget'), widget: surfaceWidgetSchema }, { additionalProperties: false }),
|
|
20
|
+
Type.Object({ op: Type.Literal('remove-widget'), id: surfaceIdSchema }, { additionalProperties: false }),
|
|
21
|
+
Type.Object({ op: Type.Literal('upsert-source'), source: Type.Union([surfaceListSourceSchema, surfaceOneSourceSchema]) }, { additionalProperties: false }),
|
|
22
|
+
Type.Object({ op: Type.Literal('remove-source'), id: surfaceIdSchema }, { additionalProperties: false }),
|
|
23
|
+
Type.Object({ op: Type.Literal('reorder-widgets'), ids: Type.Array(surfaceIdSchema, { maxItems: SURFACE_LIMITS.maxWidgets, uniqueItems: true }) }, { additionalProperties: false }),
|
|
24
|
+
]), { minItems: 1, maxItems: SURFACE_EDIT_LIMITS.maxOperations }),
|
|
25
|
+
}, { additionalProperties: false });
|
|
26
|
+
const compiledEdit = TypeCompiler.Compile(editSchema);
|
|
27
|
+
const compiledRevision = TypeCompiler.Compile(Type.Object({ revision: revisionSchema, spec: surfaceSpecSchema }, { additionalProperties: false }));
|
|
28
|
+
function editError(code, path, message) {
|
|
29
|
+
return { ok: false, issues: [{ code, path, message }] };
|
|
30
|
+
}
|
|
31
|
+
// 输入已通过 JSON 检查。新快照独立于调用方对象,也不会冻结调用方原有状态。
|
|
32
|
+
function immutableCopy(input) {
|
|
33
|
+
const copy = JSON.parse(JSON.stringify(input));
|
|
34
|
+
const pending = [copy];
|
|
35
|
+
while (pending.length > 0) {
|
|
36
|
+
const current = pending.pop();
|
|
37
|
+
if (current !== null && typeof current === 'object') {
|
|
38
|
+
pending.push(...Object.values(current));
|
|
39
|
+
Object.freeze(current);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return copy;
|
|
43
|
+
}
|
|
44
|
+
export function createSurfaceRevision(spec, catalog, policy, revision = 0) {
|
|
45
|
+
if (!Number.isSafeInteger(revision) || revision < 0)
|
|
46
|
+
return editError('invalid_edit', '/revision', 'Revision must be a non-negative safe integer');
|
|
47
|
+
const parsed = parseSurfaceJson(spec);
|
|
48
|
+
if (!parsed.ok)
|
|
49
|
+
return parsed;
|
|
50
|
+
const result = validateSurfaceSpec(parsed.value, catalog, policy);
|
|
51
|
+
if (!result.ok)
|
|
52
|
+
return result;
|
|
53
|
+
return { ok: true, value: immutableCopy({ revision, spec: result.value }) };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 只生成下一版候选快照,不保存、不渲染、不访问 DataProvider。
|
|
57
|
+
* 服务端保存时仍必须对 revision 做原子 compare-and-swap,并重新授权。
|
|
58
|
+
*/
|
|
59
|
+
export function applySurfaceEditProposal(current, input, catalog, policy) {
|
|
60
|
+
const parsedCurrent = parseSurfaceJson(current);
|
|
61
|
+
if (!parsedCurrent.ok)
|
|
62
|
+
return parsedCurrent;
|
|
63
|
+
if (!compiledRevision.Check(parsedCurrent.value))
|
|
64
|
+
return editError('invalid_edit', '', 'Invalid current surface revision');
|
|
65
|
+
const original = validateSurfaceSpec(parsedCurrent.value.spec, catalog, policy);
|
|
66
|
+
if (!original.ok)
|
|
67
|
+
return original;
|
|
68
|
+
const parsed = parseSurfaceJson(input);
|
|
69
|
+
if (!parsed.ok)
|
|
70
|
+
return parsed;
|
|
71
|
+
if (!compiledEdit.Check(parsed.value))
|
|
72
|
+
return editError('invalid_edit', '', 'Edit does not match the surface-edit/v1 schema');
|
|
73
|
+
const edit = parsed.value;
|
|
74
|
+
if (edit.catalogVersion !== catalog.version || edit.catalogVersion !== original.value.catalogVersion) {
|
|
75
|
+
return editError('catalog_version_mismatch', '/catalogVersion', 'Edit catalog version does not match the active catalog');
|
|
76
|
+
}
|
|
77
|
+
if (edit.surfaceId !== original.value.surfaceId)
|
|
78
|
+
return editError('invalid_edit', '/surfaceId', 'Edit targets a different surface');
|
|
79
|
+
if (edit.baseRevision !== parsedCurrent.value.revision)
|
|
80
|
+
return editError('revision_conflict', '/baseRevision', 'Edit is based on a stale surface revision');
|
|
81
|
+
if (edit.baseRevision === Number.MAX_SAFE_INTEGER)
|
|
82
|
+
return editError('invalid_edit', '/baseRevision', 'Revision counter is exhausted');
|
|
83
|
+
let title = original.value.title;
|
|
84
|
+
let layout = original.value.layout;
|
|
85
|
+
const sources = [...original.value.dataSources];
|
|
86
|
+
let widgets = [...original.value.widgets];
|
|
87
|
+
for (const [index, operation] of edit.operations.entries()) {
|
|
88
|
+
const path = `/operations/${index}`;
|
|
89
|
+
switch (operation.op) {
|
|
90
|
+
case 'set-title':
|
|
91
|
+
title = operation.value;
|
|
92
|
+
break;
|
|
93
|
+
case 'set-layout':
|
|
94
|
+
layout = operation.value;
|
|
95
|
+
break;
|
|
96
|
+
case 'upsert-widget': {
|
|
97
|
+
const target = widgets.findIndex((widget) => widget.id === operation.widget.id);
|
|
98
|
+
if (target === -1)
|
|
99
|
+
widgets.push(operation.widget);
|
|
100
|
+
else
|
|
101
|
+
widgets[target] = operation.widget;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case 'remove-widget': {
|
|
105
|
+
const target = widgets.findIndex((widget) => widget.id === operation.id);
|
|
106
|
+
if (target === -1)
|
|
107
|
+
return editError('invalid_edit', path, 'Cannot remove an unknown widget');
|
|
108
|
+
widgets.splice(target, 1);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case 'upsert-source': {
|
|
112
|
+
const target = sources.findIndex((source) => source.id === operation.source.id);
|
|
113
|
+
if (target === -1)
|
|
114
|
+
sources.push(operation.source);
|
|
115
|
+
else
|
|
116
|
+
sources[target] = operation.source;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case 'remove-source': {
|
|
120
|
+
const target = sources.findIndex((source) => source.id === operation.id);
|
|
121
|
+
if (target === -1)
|
|
122
|
+
return editError('invalid_edit', path, 'Cannot remove an unknown source');
|
|
123
|
+
sources.splice(target, 1);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case 'reorder-widgets': {
|
|
127
|
+
if (operation.ids.length !== widgets.length || new Set(operation.ids).size !== widgets.length) {
|
|
128
|
+
return editError('invalid_edit', path, 'Reordering must list every widget exactly once');
|
|
129
|
+
}
|
|
130
|
+
const ordered = [];
|
|
131
|
+
for (const id of operation.ids) {
|
|
132
|
+
const widget = widgets.find((candidate) => candidate.id === id);
|
|
133
|
+
if (!widget)
|
|
134
|
+
return editError('invalid_edit', path, 'Cannot reorder an unknown widget');
|
|
135
|
+
ordered.push(widget);
|
|
136
|
+
}
|
|
137
|
+
widgets = ordered;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// 整个事务合并后再验证关联;任何失败都不改变上一版快照。
|
|
143
|
+
const result = validateSurfaceSpec({ ...original.value, title, layout, dataSources: sources, widgets }, catalog, policy);
|
|
144
|
+
if (!result.ok)
|
|
145
|
+
return result;
|
|
146
|
+
const bounded = parseSurfaceJson(result.value);
|
|
147
|
+
if (!bounded.ok)
|
|
148
|
+
return bounded;
|
|
149
|
+
return { ok: true, value: immutableCopy({ revision: edit.baseRevision + 1, spec: result.value }) };
|
|
150
|
+
}
|
|
151
|
+
export function createSurfaceEditSchema() {
|
|
152
|
+
return surfaceSchemaToJson(editSchema);
|
|
153
|
+
}
|
|
154
|
+
export function buildSurfaceEditMessages(request, current, catalog, policy) {
|
|
155
|
+
if (typeof request !== 'string' || !request.trim() || request.length > SURFACE_AGENT_LIMITS.maxRequestCharacters) {
|
|
156
|
+
throw new Error('Surface edit request must be non-empty and within the request character limit');
|
|
157
|
+
}
|
|
158
|
+
const parsed = parseSurfaceJson(current);
|
|
159
|
+
if (!parsed.ok || !compiledRevision.Check(parsed.value) || !validateSurfaceSpec(parsed.value.spec, catalog, policy).ok) {
|
|
160
|
+
throw new Error('Cannot generate edits for an invalid current revision');
|
|
161
|
+
}
|
|
162
|
+
const content = `Return one JSON surface-edit/v1 proposal, not code, markdown, or a full replacement surface. Preserve IDs and all unrequested widgets, sources, filters, and layout. Use the current surfaceId, catalogVersion and baseRevision exactly. Only the listed operations and catalog widgets are allowed. Do not generate CSS, JavaScript, SQL, URLs, event handlers, or mutations. Edits are proposals requiring whole-document validation and host approval, never permission grants.\nCatalog: ${JSON.stringify(createSurfaceCatalogManifest(catalog))}\nPolicy: ${JSON.stringify(policy)}\nEdit JSON Schema: ${JSON.stringify(createSurfaceEditSchema())}\nCurrent revision: ${JSON.stringify(parsed.value)}`;
|
|
163
|
+
if (content.length > SURFACE_AGENT_LIMITS.maxContractCharacters)
|
|
164
|
+
throw new Error('Surface edit context exceeds the contract character limit');
|
|
165
|
+
return [{ role: 'system', content }, { role: 'user', content: request }];
|
|
166
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,11 @@ export { SURFACE_LIMITS, SURFACE_SCHEMA_VERSION } from './types.js';
|
|
|
2
2
|
export type { JsonObject, JsonPrimitive, JsonValue, ResourceListDataSource, ResourceListSource, ResourceOneDataSource, ResourceOneSource, SurfaceBinding, SurfaceCatalog, SurfaceCatalogDataKind, SurfaceDataError, SurfaceDataProvider, SurfaceDataSource, SurfaceFilter, SurfaceGridLayout, SurfaceGridSpan, SurfacePolicy, SurfaceResourcePolicy, SurfaceSort, SurfaceSpec, SurfaceValidationCode, SurfaceValidationIssue, SurfaceValidationResult, SurfaceWidget, SurfaceWidgetDataState, SurfaceWidgetDefinition, } from './types.js';
|
|
3
3
|
export { validateSurfaceSpec } from './validation.js';
|
|
4
4
|
export type { SurfaceMessages } from './localization.js';
|
|
5
|
-
export { SURFACE_AGENT_SCHEMA_VERSION, buildSurfaceAgentPrompt, parseSurfaceAgentProposal, } from './agent.js';
|
|
6
|
-
export type { SurfaceAgentProposal, SurfaceAgentValidationResult, } from './agent.js';
|
|
5
|
+
export { SURFACE_AGENT_SCHEMA_VERSION, buildSurfaceAgentPrompt, buildSurfaceAgentMessages, parseSurfaceAgentProposal, parseSurfaceAgentResponse, createSurfaceAgentStream, } from './agent.js';
|
|
6
|
+
export type { SurfaceAgentProposal, SurfaceAgentProposalV2, SurfaceAgentCannotFulfill, SurfaceAgentResponse, SurfaceAgentResponseResult, SurfaceAgentMessage, SurfaceAgentStream, SurfaceAgentValidationResult, } from './agent.js';
|
|
7
|
+
export { SURFACE_AGENT_LIMITS, SURFACE_AGENT_RESPONSE_SCHEMA_VERSION, SURFACE_CATALOG_SCHEMA_VERSION, createSurfaceCatalogManifest, createSurfaceAgentResponseSchema, selectSurfaceCatalog, } from './agent-contract.js';
|
|
8
|
+
export type { SurfaceCatalogManifest } from './agent-contract.js';
|
|
9
|
+
export { SURFACE_EDIT_SCHEMA_VERSION, SURFACE_EDIT_LIMITS, createSurfaceRevision, applySurfaceEditProposal, createSurfaceEditSchema, buildSurfaceEditMessages, } from './edits.js';
|
|
10
|
+
export type { SurfaceEditOperation, SurfaceEditProposal, SurfaceEditIssue, SurfaceRevision, SurfaceRevisionResult, } from './edits.js';
|
|
11
|
+
export { svarGridPropsSchema, svarGridDefinition } from './svar-schema.js';
|
|
12
|
+
export type { SvarGridProps } from './svar-schema.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export { SURFACE_LIMITS, SURFACE_SCHEMA_VERSION } from './types.js';
|
|
2
2
|
export { validateSurfaceSpec } from './validation.js';
|
|
3
|
-
export { SURFACE_AGENT_SCHEMA_VERSION, buildSurfaceAgentPrompt, parseSurfaceAgentProposal, } from './agent.js';
|
|
3
|
+
export { SURFACE_AGENT_SCHEMA_VERSION, buildSurfaceAgentPrompt, buildSurfaceAgentMessages, parseSurfaceAgentProposal, parseSurfaceAgentResponse, createSurfaceAgentStream, } from './agent.js';
|
|
4
|
+
export { SURFACE_AGENT_LIMITS, SURFACE_AGENT_RESPONSE_SCHEMA_VERSION, SURFACE_CATALOG_SCHEMA_VERSION, createSurfaceCatalogManifest, createSurfaceAgentResponseSchema, selectSurfaceCatalog, } from './agent-contract.js';
|
|
5
|
+
export { SURFACE_EDIT_SCHEMA_VERSION, SURFACE_EDIT_LIMITS, createSurfaceRevision, applySurfaceEditProposal, createSurfaceEditSchema, buildSurfaceEditMessages, } from './edits.js';
|
|
6
|
+
// 只导出 JSON schema,不将可选 SVAR 渲染器加载到协议入口。
|
|
7
|
+
export { svarGridPropsSchema, svarGridDefinition } from './svar-schema.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type SurfaceRenderCatalog } from './catalog.js';
|
|
2
|
+
import type { SurfaceActionDescriptor } from './workflows/types.js';
|
|
3
|
+
export { default as SurfaceWorkflowProvider } from './components/SurfaceWorkflowProvider.svelte';
|
|
4
|
+
export { withSurfaceAppearance } from './workflows/catalog.js';
|
|
5
|
+
export type { SurfaceWorkflowTransport } from './workflows/client.js';
|
|
6
|
+
export declare function createInteractiveSurfaceCatalog(actions: readonly SurfaceActionDescriptor[], base?: SurfaceRenderCatalog): SurfaceRenderCatalog;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineSurfaceCatalog, styledSurfaceCatalog } from './catalog.js';
|
|
2
|
+
import { createInteractiveSurfaceDefinitions } from './workflows/catalog.js';
|
|
3
|
+
import ResourceFormWidget from './components/ResourceFormWidget.svelte';
|
|
4
|
+
export { default as SurfaceWorkflowProvider } from './components/SurfaceWorkflowProvider.svelte';
|
|
5
|
+
export { withSurfaceAppearance } from './workflows/catalog.js';
|
|
6
|
+
export function createInteractiveSurfaceCatalog(actions, base = styledSurfaceCatalog) {
|
|
7
|
+
const definitions = createInteractiveSurfaceDefinitions(actions, base);
|
|
8
|
+
const components = new Map(base.widgets.map((widget) => [widget.type, widget.component]));
|
|
9
|
+
return defineSurfaceCatalog({ ...definitions, widgets: definitions.widgets.map((widget) => {
|
|
10
|
+
const component = widget.type === 'resource-form' ? ResourceFormWidget : components.get(widget.type);
|
|
11
|
+
if (!component)
|
|
12
|
+
throw new Error(`Missing renderer for ${widget.type}`);
|
|
13
|
+
return { ...widget, component };
|
|
14
|
+
}) });
|
|
15
|
+
}
|
package/dist/json.js
CHANGED
|
@@ -99,8 +99,7 @@ export function jsonValueIssue(input) {
|
|
|
99
99
|
}
|
|
100
100
|
entries.push([key, descriptor.value]);
|
|
101
101
|
}
|
|
102
|
-
for (
|
|
103
|
-
const [key, entry] = entries[index];
|
|
102
|
+
for (const [key, entry] of entries.reverse()) {
|
|
104
103
|
pending.push({ kind: 'visit', candidate: entry, path: [...path, key], depth: depth + 1 });
|
|
105
104
|
}
|
|
106
105
|
}
|
package/dist/openui.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { SurfaceCatalog, SurfacePolicy, SurfaceSpec } from './types.js';
|
|
2
|
+
import { SurfaceOpenUIError, OPENUI_LIMITS } from './workflows/openui-guard.js';
|
|
3
|
+
export { SurfaceOpenUIError, OPENUI_LIMITS };
|
|
4
|
+
/** Structural subset of @openuidev/lang-core's public types. The actual parser
|
|
5
|
+
* factory is injected by the trusted host, avoiding a React/Zod/runtime dependency
|
|
6
|
+
* in the existing Surface root entry. Tested integration must use the real factory.
|
|
7
|
+
*/
|
|
8
|
+
export interface SurfaceOpenUILibrarySchema {
|
|
9
|
+
$defs: Record<string, {
|
|
10
|
+
properties: Record<string, unknown>;
|
|
11
|
+
required: string[];
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
export interface SurfaceOpenUIElement {
|
|
15
|
+
type: 'element';
|
|
16
|
+
typeName: string;
|
|
17
|
+
props: Record<string, unknown>;
|
|
18
|
+
partial: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface SurfaceOpenUIParseResult {
|
|
21
|
+
root: SurfaceOpenUIElement | null;
|
|
22
|
+
meta: {
|
|
23
|
+
incomplete: boolean;
|
|
24
|
+
unresolved: string[];
|
|
25
|
+
orphaned: string[];
|
|
26
|
+
errors: readonly unknown[];
|
|
27
|
+
};
|
|
28
|
+
stateDeclarations?: Record<string, unknown>;
|
|
29
|
+
queryStatements?: readonly unknown[];
|
|
30
|
+
mutationStatements?: readonly unknown[];
|
|
31
|
+
}
|
|
32
|
+
export type SurfaceOpenUIParserFactory = (schema: SurfaceOpenUILibrarySchema, rootName?: string) => {
|
|
33
|
+
push(chunk: string): SurfaceOpenUIParseResult;
|
|
34
|
+
getResult(): SurfaceOpenUIParseResult;
|
|
35
|
+
};
|
|
36
|
+
export type SurfaceOpenUIResult = {
|
|
37
|
+
readonly ok: true;
|
|
38
|
+
readonly complete: boolean;
|
|
39
|
+
readonly preview: SurfaceSpec | null;
|
|
40
|
+
readonly unresolved: readonly string[];
|
|
41
|
+
} | {
|
|
42
|
+
readonly ok: false;
|
|
43
|
+
readonly preview: null;
|
|
44
|
+
readonly error: {
|
|
45
|
+
readonly code: string;
|
|
46
|
+
readonly message: string;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
export declare function createSurfaceOpenUISchema(catalog: SurfaceCatalog): SurfaceOpenUILibrarySchema;
|
|
50
|
+
export declare function createSurfaceOpenUIStream(options: {
|
|
51
|
+
readonly catalog: SurfaceCatalog;
|
|
52
|
+
readonly policy: SurfacePolicy;
|
|
53
|
+
readonly createStreamingParser: SurfaceOpenUIParserFactory;
|
|
54
|
+
}): {
|
|
55
|
+
push(chunk: string): SurfaceOpenUIResult;
|
|
56
|
+
finish(): SurfaceOpenUIResult;
|
|
57
|
+
};
|
|
58
|
+
export declare function buildSurfaceOpenUIMessages(request: string, catalog: SurfaceCatalog, policy: SurfacePolicy): ({
|
|
59
|
+
role: "system";
|
|
60
|
+
content: string;
|
|
61
|
+
} | {
|
|
62
|
+
role: "user";
|
|
63
|
+
content: string;
|
|
64
|
+
})[];
|
package/dist/openui.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { SURFACE_AGENT_LIMITS, createSurfaceCatalogManifest } from './agent-contract.js';
|
|
2
|
+
import { validateSurfaceSpec } from './validation.js';
|
|
3
|
+
import { isJsonValue } from './json.js';
|
|
4
|
+
import { createOpenUIStatementGuard, SurfaceOpenUIError, OPENUI_LIMITS } from './workflows/openui-guard.js';
|
|
5
|
+
export { SurfaceOpenUIError, OPENUI_LIMITS };
|
|
6
|
+
function componentNames(catalog) {
|
|
7
|
+
const names = new Map();
|
|
8
|
+
for (const widget of catalog.widgets) {
|
|
9
|
+
const name = widget.type.split(/[^A-Za-z0-9]+/u).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('');
|
|
10
|
+
if (!/^[A-Z][A-Za-z0-9]*$/u.test(name) || ['Surface', 'Source'].includes(name) || names.has(name)) {
|
|
11
|
+
throw new Error(`Surface widget cannot be mapped to a unique OpenUI name: ${widget.type}`);
|
|
12
|
+
}
|
|
13
|
+
names.set(name, widget.type);
|
|
14
|
+
}
|
|
15
|
+
return names;
|
|
16
|
+
}
|
|
17
|
+
export function createSurfaceOpenUISchema(catalog) {
|
|
18
|
+
// Validates the same host-owned catalog used by prompts and the renderer.
|
|
19
|
+
createSurfaceCatalogManifest(catalog);
|
|
20
|
+
const names = componentNames(catalog);
|
|
21
|
+
const defs = {
|
|
22
|
+
Surface: { properties: {
|
|
23
|
+
surfaceId: { type: 'string' }, title: { type: 'string' },
|
|
24
|
+
sources: { type: 'array' }, widgets: { type: 'array' }, gap: { type: 'string' },
|
|
25
|
+
}, required: ['surfaceId', 'title', 'sources', 'widgets'] },
|
|
26
|
+
Source: { properties: { value: { type: 'object' } }, required: ['value'] },
|
|
27
|
+
};
|
|
28
|
+
for (const [name, type] of names) {
|
|
29
|
+
const definition = catalog.widgets.find((widget) => widget.type === type);
|
|
30
|
+
if (!definition)
|
|
31
|
+
throw new Error(`Missing component definition: ${type}`);
|
|
32
|
+
defs[name] = { properties: {
|
|
33
|
+
id: { type: 'string' }, props: JSON.parse(JSON.stringify(definition.propsSchema)),
|
|
34
|
+
binding: { anyOf: [{ type: 'object' }, { type: 'null' }] },
|
|
35
|
+
span: { type: 'number' },
|
|
36
|
+
}, required: ['id', 'props'] };
|
|
37
|
+
}
|
|
38
|
+
return { $defs: defs };
|
|
39
|
+
}
|
|
40
|
+
function element(value) {
|
|
41
|
+
return !!value && typeof value === 'object' && !Array.isArray(value)
|
|
42
|
+
&& value.type === 'element'
|
|
43
|
+
&& typeof value.typeName === 'string'
|
|
44
|
+
&& !!value.props && typeof value.props === 'object';
|
|
45
|
+
}
|
|
46
|
+
export function createSurfaceOpenUIStream(options) {
|
|
47
|
+
const { catalog, policy } = options;
|
|
48
|
+
const schema = createSurfaceOpenUISchema(catalog);
|
|
49
|
+
const names = componentNames(catalog);
|
|
50
|
+
const parser = options.createStreamingParser(schema, 'Surface');
|
|
51
|
+
const guard = createOpenUIStatementGuard(Object.keys(schema.$defs));
|
|
52
|
+
let terminal;
|
|
53
|
+
let latestPreview = { ok: true, complete: false, preview: null, unresolved: [] };
|
|
54
|
+
function normalize(final) {
|
|
55
|
+
const parsed = parser.getResult();
|
|
56
|
+
if ((parsed.queryStatements?.length ?? 0) || (parsed.mutationStatements?.length ?? 0)
|
|
57
|
+
|| Object.keys(parsed.stateDeclarations ?? {}).length) {
|
|
58
|
+
throw new SurfaceOpenUIError('unsupported', 'OpenUI state, Query and Mutation execution is not allowed');
|
|
59
|
+
}
|
|
60
|
+
if (parsed.meta.errors.length)
|
|
61
|
+
throw new SurfaceOpenUIError('syntax', 'OpenUI component arguments failed validation');
|
|
62
|
+
if (final && (parsed.meta.incomplete || parsed.meta.unresolved.length || parsed.meta.orphaned.length)) {
|
|
63
|
+
throw new SurfaceOpenUIError('syntax', 'Incomplete, unresolved or unreachable declarations remain');
|
|
64
|
+
}
|
|
65
|
+
if (!parsed.root) {
|
|
66
|
+
if (final)
|
|
67
|
+
throw new SurfaceOpenUIError('syntax', 'A Surface root is required');
|
|
68
|
+
return { ok: true, complete: false, preview: null, unresolved: parsed.meta.unresolved };
|
|
69
|
+
}
|
|
70
|
+
const root = parsed.root;
|
|
71
|
+
if (root.typeName !== 'Surface' || root.partial)
|
|
72
|
+
throw new SurfaceOpenUIError('syntax', 'Expected a complete Surface declaration');
|
|
73
|
+
const { surfaceId, title, sources, widgets, gap } = root.props;
|
|
74
|
+
if (!Array.isArray(sources) || !Array.isArray(widgets))
|
|
75
|
+
throw new SurfaceOpenUIError('syntax', 'Surface sources and widgets must be arrays');
|
|
76
|
+
const dataSources = [];
|
|
77
|
+
for (const source of sources) {
|
|
78
|
+
if (source === null && !final)
|
|
79
|
+
continue;
|
|
80
|
+
if (!element(source) || source.typeName !== 'Source' || source.partial || !isJsonValue(source.props['value'])) {
|
|
81
|
+
throw new SurfaceOpenUIError('syntax', 'Invalid Source node');
|
|
82
|
+
}
|
|
83
|
+
dataSources.push(source.props['value']);
|
|
84
|
+
}
|
|
85
|
+
const normalized = [];
|
|
86
|
+
for (const widget of widgets) {
|
|
87
|
+
if (widget === null && !final)
|
|
88
|
+
continue;
|
|
89
|
+
if (!element(widget) || widget.partial || !names.has(widget.typeName))
|
|
90
|
+
throw new SurfaceOpenUIError('syntax', 'Unregistered widget node');
|
|
91
|
+
const { id, props, binding, span } = widget.props;
|
|
92
|
+
if (!isJsonValue(props))
|
|
93
|
+
throw new SurfaceOpenUIError('syntax', 'Widget props must be literal JSON');
|
|
94
|
+
// A forward reference is never replaced with invented data or defaults.
|
|
95
|
+
// Exclude widgets depending on unresolved sources from structural previews.
|
|
96
|
+
if (!final && binding && typeof binding === 'object'
|
|
97
|
+
&& !dataSources.some((source) => source.id === binding.sourceId))
|
|
98
|
+
continue;
|
|
99
|
+
normalized.push({ id, type: names.get(widget.typeName), props,
|
|
100
|
+
...(binding == null ? {} : { binding }),
|
|
101
|
+
...(span == null ? {} : { placement: { columnSpan: span } }),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const candidate = { schemaVersion: 'surface/v1', catalogVersion: catalog.version,
|
|
105
|
+
surfaceId, title, layout: { type: 'grid', columns: 12, ...(gap == null ? {} : { gap }) }, dataSources, widgets: normalized };
|
|
106
|
+
const result = validateSurfaceSpec(candidate, catalog, policy);
|
|
107
|
+
if (!result.ok)
|
|
108
|
+
throw new SurfaceOpenUIError('invalid_surface', result.issues.map((issue) => `${issue.code}:${issue.path}`).join(', '));
|
|
109
|
+
return { ok: true, complete: final, preview: result.value, unresolved: [...parsed.meta.unresolved] };
|
|
110
|
+
}
|
|
111
|
+
function fail(error) {
|
|
112
|
+
terminal = { ok: false, preview: null, error: {
|
|
113
|
+
code: error instanceof SurfaceOpenUIError ? error.code : 'syntax',
|
|
114
|
+
message: error instanceof SurfaceOpenUIError ? error.message : 'OpenUI parser failed',
|
|
115
|
+
} };
|
|
116
|
+
return terminal;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
push(chunk) {
|
|
120
|
+
if (terminal)
|
|
121
|
+
return { ok: false, preview: null, error: { code: 'closed', message: 'Stream is closed' } };
|
|
122
|
+
try {
|
|
123
|
+
const statements = guard.push(chunk);
|
|
124
|
+
// Token-by-token transport must not repeatedly materialize an unchanged tree.
|
|
125
|
+
if (!statements.length)
|
|
126
|
+
return latestPreview;
|
|
127
|
+
for (const statement of statements)
|
|
128
|
+
parser.push(statement);
|
|
129
|
+
latestPreview = normalize(false);
|
|
130
|
+
return latestPreview;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return fail(error);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
finish() {
|
|
137
|
+
if (terminal)
|
|
138
|
+
return terminal;
|
|
139
|
+
try {
|
|
140
|
+
for (const statement of guard.finish())
|
|
141
|
+
parser.push(statement);
|
|
142
|
+
terminal = normalize(true);
|
|
143
|
+
return terminal;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
return fail(error);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export function buildSurfaceOpenUIMessages(request, catalog, policy) {
|
|
152
|
+
if (typeof request !== 'string' || !request.trim() || request.length > 8000)
|
|
153
|
+
throw new Error('Invalid Surface request');
|
|
154
|
+
const schema = createSurfaceOpenUISchema(catalog);
|
|
155
|
+
const content = [
|
|
156
|
+
'Return only static OpenUI Lang declarations. Use root = Surface(surfaceId, title, sources, widgets, gap).',
|
|
157
|
+
'Use Source(resourceSourceJSON). Each catalog component uses (id, props, bindingOrNull, columnSpan).',
|
|
158
|
+
'Use named component declarations, never inline calls or reusable literal declarations. Forward references are allowed only once in the Surface root. Use quoted JSON object keys and double-quoted strings.',
|
|
159
|
+
'Never emit Markdown, expressions, state, Query, Mutation, Action, arbitrary code, CSS, URLs or tool calls.',
|
|
160
|
+
'Form field schemas and submission labels belong to registered host actions, not model output. Never claim a write has executed.',
|
|
161
|
+
`Library: ${JSON.stringify(schema)}`,
|
|
162
|
+
`Catalog: ${JSON.stringify(createSurfaceCatalogManifest(catalog))}`,
|
|
163
|
+
`Resource policy: ${JSON.stringify(policy)}`,
|
|
164
|
+
].join('\n');
|
|
165
|
+
if (content.length > SURFACE_AGENT_LIMITS.maxContractCharacters)
|
|
166
|
+
throw new Error('Select a smaller Surface catalog');
|
|
167
|
+
return [{ role: 'system', content }, { role: 'user', content: request }];
|
|
168
|
+
}
|