diff --git a/web/workspace/deno.lock b/web/workspace/deno.lock index fbd75248..21b562fc 100644 --- a/web/workspace/deno.lock +++ b/web/workspace/deno.lock @@ -1,6 +1,8 @@ { "version": "5", "specifiers": { + "jsr:@std/assert@*": "1.0.19", + "jsr:@std/internal@^1.0.12": "1.0.14", "npm:@codemirror/state@6.5.2": "6.5.2", "npm:@codemirror/view@6.38.8": "6.38.8", "npm:@fontsource/ibm-plex-mono@5.3.0": "5.3.0", @@ -20,6 +22,17 @@ "npm:typescript@5.9.3": "5.9.3", "npm:vite@7.2.7": "7.2.7" }, + "jsr": { + "@std/assert@1.0.19": { + "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/internal@1.0.14": { + "integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7" + } + }, "npm": { "@codemirror/language@6.12.4": { "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==", diff --git a/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte b/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte new file mode 100644 index 00000000..aab09791 --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte @@ -0,0 +1,240 @@ + + +
+ + +
+
+
+ Virtual path + {selectedPath || "Select or create a source"} +
+
+ + + + + +
+
+ source = value} /> +

{status}

+ {#if diagnostics.length > 0} +
    + {#each diagnostics as diagnostic} +
  1. + {diagnostic.kind} + {diagnostic.message} + bytes {diagnostic.span.start_byte}–{diagnostic.span.end_byte} +
  2. + {/each} +
+ {/if} +
+
diff --git a/web/workspace/src/lib/workspace/config-source/api.test.ts b/web/workspace/src/lib/workspace/config-source/api.test.ts new file mode 100644 index 00000000..e072e44d --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/api.test.ts @@ -0,0 +1,60 @@ +/// + +import { assert, assertEquals } from "jsr:@std/assert"; +import { + commitConfigTree, + fetchConfigEntry, + fetchConfigTree, + previewConfigTree, +} from "./api.ts"; + +function response(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" }, + }); +} + +Deno.test("config source API stays workspace-scoped and separates preview from commit", async () => { + const calls: Array<{ url: string; init?: RequestInit }> = []; + const fetcher = ((input: string | URL | Request, init?: RequestInit) => { + calls.push({ url: String(input), init }); + return Promise.resolve(response({ ok: true })); + }) as typeof fetch; + + await fetchConfigTree("w/one", fetcher); + await fetchConfigEntry("w/one", "profiles/main.dcdl", fetcher); + await previewConfigTree("w/one", { changes: [], entrypoints: [] }, fetcher); + await commitConfigTree("w/one", { + base_revision: 4, + base_digest: "sha256:base", + changes: [], + entrypoints: [], + }, fetcher); + + assertEquals(calls.map((call) => call.url), [ + "/api/w/w%2Fone/config/source-tree", + "/api/w/w%2Fone/config/source-tree/entries/profiles%2Fmain.dcdl", + "/api/w/w%2Fone/config/source-tree/preview", + "/api/w/w%2Fone/config/source-tree/commit", + ]); + assertEquals(calls[2].init?.method, "POST"); + assertEquals(calls[3].init?.method, "POST"); + assert( + String(calls[3].init?.body).includes('"base_digest":"sha256:base"'), + ); +}); + +Deno.test("config source API surfaces failed evaluation instead of treating it as a draft write", async () => { + const fetcher = (() => + Promise.resolve( + new Response("structured diagnostics", { status: 422 }), + )) as typeof fetch; + let message = ""; + try { + await previewConfigTree("w", { changes: [], entrypoints: [] }, fetcher); + } catch (error) { + message = String(error); + } + assert(message.includes("structured diagnostics")); +}); diff --git a/web/workspace/src/lib/workspace/config-source/api.ts b/web/workspace/src/lib/workspace/config-source/api.ts new file mode 100644 index 00000000..40722e87 --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/api.ts @@ -0,0 +1,70 @@ +import type { + ConfigCommitRequest, + ConfigEntry, + EvaluatedConfigCandidate, + WorkspaceConfigTreeResponse, +} from "./types.ts"; + +function sourceTreeUrl(workspaceId: string): string { + return `/api/w/${encodeURIComponent(workspaceId)}/config/source-tree`; +} + +async function readJson(response: Response): Promise { + if (!response.ok) { + const body = await response.text(); + throw new Error(body || `${response.status} ${response.statusText}`); + } + return await response.json() as T; +} + +export async function fetchConfigTree( + workspaceId: string, + fetcher: typeof fetch = fetch, +): Promise { + return await readJson( + await fetcher(sourceTreeUrl(workspaceId), { + headers: { accept: "application/json" }, + }), + ); +} + +export async function fetchConfigEntry( + workspaceId: string, + path: string, + fetcher: typeof fetch = fetch, +): Promise { + return await readJson( + await fetcher( + `${sourceTreeUrl(workspaceId)}/entries/${encodeURIComponent(path)}`, + { headers: { accept: "application/json" } }, + ), + ); +} + +export async function previewConfigTree( + workspaceId: string, + request: Omit, + fetcher: typeof fetch = fetch, +): Promise { + return await readJson( + await fetcher(`${sourceTreeUrl(workspaceId)}/preview`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(request), + }), + ); +} + +export async function commitConfigTree( + workspaceId: string, + request: ConfigCommitRequest, + fetcher: typeof fetch = fetch, +): Promise { + return await readJson( + await fetcher(`${sourceTreeUrl(workspaceId)}/commit`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(request), + }), + ); +} diff --git a/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm.d.ts b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm.d.ts new file mode 100644 index 00000000..88d3b53b --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm.d.ts @@ -0,0 +1,50 @@ +/* tslint:disable */ +/* eslint-disable */ + +export function analyze_snapshot(snapshot: any, entrypoint: string, source_override?: string | null): any; + +export function evaluate_snapshot(snapshot: any, contract: any): any; + +export function formatSource(source: string): string; + +export function format_source(source: string): string; + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly analyze_snapshot: (a: any, b: number, c: number, d: number, e: number) => [number, number, number]; + readonly evaluate_snapshot: (a: any, b: any) => [number, number, number]; + readonly format_source: (a: number, b: number) => [number, number, number, number]; + readonly formatSource: (a: number, b: number) => [number, number]; + readonly __wbindgen_malloc: (a: number, b: number) => number; + readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; + readonly __wbindgen_exn_store: (a: number) => void; + readonly __externref_table_alloc: () => number; + readonly __wbindgen_externrefs: WebAssembly.Table; + readonly __externref_table_dealloc: (a: number) => void; + readonly __wbindgen_free: (a: number, b: number, c: number) => void; + readonly __wbindgen_start: () => void; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; + +/** + * Instantiates the given `module`, which can either be bytes or + * a precompiled `WebAssembly.Module`. + * + * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. + * + * @returns {InitOutput} + */ +export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; + +/** + * If `module_or_path` is {RequestInfo} or {URL}, makes a request and + * for everything else, calls `WebAssembly.instantiate` directly. + * + * @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. + * + * @returns {Promise} + */ +export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; diff --git a/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm.js b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm.js new file mode 100644 index 00000000..0af013fc --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm.js @@ -0,0 +1,582 @@ +/* @ts-self-types="./config_source_wasm.d.ts" */ + +/** + * @param {any} snapshot + * @param {string} entrypoint + * @param {string | null} [source_override] + * @returns {any} + */ +export function analyze_snapshot(snapshot, entrypoint, source_override) { + const ptr0 = passStringToWasm0(entrypoint, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + var ptr1 = isLikeNone(source_override) ? 0 : passStringToWasm0(source_override, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + const ret = wasm.analyze_snapshot(snapshot, ptr0, len0, ptr1, len1); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + return takeFromExternrefTable0(ret[0]); +} + +/** + * @param {any} snapshot + * @param {any} contract + * @returns {any} + */ +export function evaluate_snapshot(snapshot, contract) { + const ret = wasm.evaluate_snapshot(snapshot, contract); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + return takeFromExternrefTable0(ret[0]); +} + +/** + * @param {string} source + * @returns {string} + */ +export function formatSource(source) { + let deferred2_0; + let deferred2_1; + try { + const ptr0 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.formatSource(ptr0, len0); + deferred2_0 = ret[0]; + deferred2_1 = ret[1]; + return getStringFromWasm0(ret[0], ret[1]); + } finally { + wasm.__wbindgen_free(deferred2_0, deferred2_1, 1); + } +} + +/** + * @param {string} source + * @returns {string} + */ +export function format_source(source) { + let deferred3_0; + let deferred3_1; + try { + const ptr0 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.format_source(ptr0, len0); + var ptr2 = ret[0]; + var len2 = ret[1]; + if (ret[3]) { + ptr2 = 0; len2 = 0; + throw takeFromExternrefTable0(ret[2]); + } + deferred3_0 = ptr2; + deferred3_1 = len2; + return getStringFromWasm0(ptr2, len2); + } finally { + wasm.__wbindgen_free(deferred3_0, deferred3_1, 1); + } +} + +function __wbg_get_imports() { + const import0 = { + __proto__: null, + __wbg_Error_2e59b1b37a9a34c3: function(arg0, arg1) { + const ret = Error(getStringFromWasm0(arg0, arg1)); + return ret; + }, + __wbg_Number_e6ffdb596c888833: function(arg0) { + const ret = Number(arg0); + return ret; + }, + __wbg_String_8564e559799eccda: function(arg0, arg1) { + const ret = String(arg1); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg___wbindgen_bigint_get_as_i64_2c5082002e4826e2: function(arg0, arg1) { + const v = arg1; + const ret = typeof(v) === 'bigint' ? v : undefined; + getDataViewMemory0().setBigInt64(arg0 + 8 * 1, isLikeNone(ret) ? BigInt(0) : ret, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); + }, + __wbg___wbindgen_boolean_get_a86c216575a75c30: function(arg0) { + const v = arg0; + const ret = typeof(v) === 'boolean' ? v : undefined; + return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0; + }, + __wbg___wbindgen_debug_string_dd5d2d07ce9e6c57: function(arg0, arg1) { + const ret = debugString(arg1); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg___wbindgen_in_4bd7a57e54337366: function(arg0, arg1) { + const ret = arg0 in arg1; + return ret; + }, + __wbg___wbindgen_is_bigint_6c98f7e945dacdde: function(arg0) { + const ret = typeof(arg0) === 'bigint'; + return ret; + }, + __wbg___wbindgen_is_function_49868bde5eb1e745: function(arg0) { + const ret = typeof(arg0) === 'function'; + return ret; + }, + __wbg___wbindgen_is_object_40c5a80572e8f9d3: function(arg0) { + const val = arg0; + const ret = typeof(val) === 'object' && val !== null; + return ret; + }, + __wbg___wbindgen_is_string_b29b5c5a8065ba1a: function(arg0) { + const ret = typeof(arg0) === 'string'; + return ret; + }, + __wbg___wbindgen_is_undefined_c0cca72b82b86f4d: function(arg0) { + const ret = arg0 === undefined; + return ret; + }, + __wbg___wbindgen_jsval_eq_7d430e744a913d26: function(arg0, arg1) { + const ret = arg0 === arg1; + return ret; + }, + __wbg___wbindgen_jsval_loose_eq_3a72ae764d46d944: function(arg0, arg1) { + const ret = arg0 == arg1; + return ret; + }, + __wbg___wbindgen_number_get_7579aab02a8a620c: function(arg0, arg1) { + const obj = arg1; + const ret = typeof(obj) === 'number' ? obj : undefined; + getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); + }, + __wbg___wbindgen_string_get_914df97fcfa788f2: function(arg0, arg1) { + const obj = arg1; + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg___wbindgen_throw_81fc77679af83bc6: function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }, + __wbg_call_7f2987183bb62793: function() { return handleError(function (arg0, arg1) { + const ret = arg0.call(arg1); + return ret; + }, arguments); }, + __wbg_done_547d467e97529006: function(arg0) { + const ret = arg0.done; + return ret; + }, + __wbg_entries_616b1a459b85be0b: function(arg0) { + const ret = Object.entries(arg0); + return ret; + }, + __wbg_get_4848e350b40afc16: function(arg0, arg1) { + const ret = arg0[arg1 >>> 0]; + return ret; + }, + __wbg_get_ed0642c4b9d31ddf: function() { return handleError(function (arg0, arg1) { + const ret = Reflect.get(arg0, arg1); + return ret; + }, arguments); }, + __wbg_get_unchecked_7d7babe32e9e6a54: function(arg0, arg1) { + const ret = arg0[arg1 >>> 0]; + return ret; + }, + __wbg_get_with_ref_key_6412cf3094599694: function(arg0, arg1) { + const ret = arg0[arg1]; + return ret; + }, + __wbg_instanceof_ArrayBuffer_ff7c1337a5e3b33a: function(arg0) { + let result; + try { + result = arg0 instanceof ArrayBuffer; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_instanceof_Uint8Array_4b8da683deb25d72: function(arg0) { + let result; + try { + result = arg0 instanceof Uint8Array; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_isArray_db61795ad004c139: function(arg0) { + const ret = Array.isArray(arg0); + return ret; + }, + __wbg_isSafeInteger_ea83862ba994770c: function(arg0) { + const ret = Number.isSafeInteger(arg0); + return ret; + }, + __wbg_iterator_de403ef31815a3e6: function() { + const ret = Symbol.iterator; + return ret; + }, + __wbg_length_0c32cb8543c8e4c8: function(arg0) { + const ret = arg0.length; + return ret; + }, + __wbg_length_6e821edde497a532: function(arg0) { + const ret = arg0.length; + return ret; + }, + __wbg_new_4f9fafbb3909af72: function() { + const ret = new Object(); + return ret; + }, + __wbg_new_99cabae501c0a8a0: function() { + const ret = new Map(); + return ret; + }, + __wbg_new_a560378ea1240b14: function(arg0) { + const ret = new Uint8Array(arg0); + return ret; + }, + __wbg_new_f3c9df4f38f3f798: function() { + const ret = new Array(); + return ret; + }, + __wbg_next_01132ed6134b8ef5: function(arg0) { + const ret = arg0.next; + return ret; + }, + __wbg_next_b3713ec761a9dbfd: function() { return handleError(function (arg0) { + const ret = arg0.next(); + return ret; + }, arguments); }, + __wbg_prototypesetcall_3e05eb9545565046: function(arg0, arg1, arg2) { + Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2); + }, + __wbg_set_08463b1df38a7e29: function(arg0, arg1, arg2) { + const ret = arg0.set(arg1, arg2); + return ret; + }, + __wbg_set_6be42768c690e380: function(arg0, arg1, arg2) { + arg0[arg1] = arg2; + }, + __wbg_set_6c60b2e8ad0e9383: function(arg0, arg1, arg2) { + arg0[arg1 >>> 0] = arg2; + }, + __wbg_value_7f6052747ccf940f: function(arg0) { + const ret = arg0.value; + return ret; + }, + __wbindgen_cast_0000000000000001: function(arg0) { + // Cast intrinsic for `F64 -> Externref`. + const ret = arg0; + return ret; + }, + __wbindgen_cast_0000000000000002: function(arg0) { + // Cast intrinsic for `I64 -> Externref`. + const ret = arg0; + return ret; + }, + __wbindgen_cast_0000000000000003: function(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return ret; + }, + __wbindgen_cast_0000000000000004: function(arg0) { + // Cast intrinsic for `U64 -> Externref`. + const ret = BigInt.asUintN(64, arg0); + return ret; + }, + __wbindgen_init_externref_table: function() { + const table = wasm.__wbindgen_externrefs; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + }, + }; + return { + __proto__: null, + "./config_source_wasm_bg.js": import0, + }; +} + +function addToExternrefTable0(obj) { + const idx = wasm.__externref_table_alloc(); + wasm.__wbindgen_externrefs.set(idx, obj); + return idx; +} + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches && builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +function getArrayU8FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); +} + +let cachedDataViewMemory0 = null; +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return decodeText(ptr, len); +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + const idx = addToExternrefTable0(e); + wasm.__wbindgen_exn_store(idx); + } +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_externrefs.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText(ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +const cachedTextEncoder = new TextEncoder(); + +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + }; +} + +let WASM_VECTOR_LEN = 0; + +let wasmModule, wasm; +function __wbg_finalize_init(instance, module) { + wasm = instance.exports; + wasmModule = module; + cachedDataViewMemory0 = null; + cachedUint8ArrayMemory0 = null; + wasm.__wbindgen_start(); + return wasm; +} + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + } catch (e) { + const validResponse = module.ok && expectedResponseType(module.type); + + if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { throw e; } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + } else { + return instance; + } + } + + function expectedResponseType(type) { + switch (type) { + case 'basic': case 'cors': case 'default': return true; + } + return false; + } +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (module !== undefined) { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + const instance = new WebAssembly.Instance(module, imports); + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (module_or_path !== undefined) { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (module_or_path === undefined) { + module_or_path = new URL('config_source_wasm_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync, __wbg_init as default }; diff --git a/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm new file mode 100644 index 00000000..5e6dcadd Binary files /dev/null and b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm differ diff --git a/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm.d.ts b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm.d.ts new file mode 100644 index 00000000..ee4a225b --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm.d.ts @@ -0,0 +1,15 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const analyze_snapshot: (a: any, b: number, c: number, d: number, e: number) => [number, number, number]; +export const evaluate_snapshot: (a: any, b: any) => [number, number, number]; +export const format_source: (a: number, b: number) => [number, number, number, number]; +export const formatSource: (a: number, b: number) => [number, number]; +export const __wbindgen_malloc: (a: number, b: number) => number; +export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; +export const __wbindgen_exn_store: (a: number) => void; +export const __externref_table_alloc: () => number; +export const __wbindgen_externrefs: WebAssembly.Table; +export const __externref_table_dealloc: (a: number) => void; +export const __wbindgen_free: (a: number, b: number, c: number) => void; +export const __wbindgen_start: () => void; diff --git a/web/workspace/src/lib/workspace/config-source/toolchain.ts b/web/workspace/src/lib/workspace/config-source/toolchain.ts new file mode 100644 index 00000000..b0f98fa9 --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/toolchain.ts @@ -0,0 +1,76 @@ +import type { + ConfigDiagnostic, + ConfigTreeSnapshot, + ToolchainContract, +} from "./types.ts"; +import type { + ConfigSourceWorkerRequest, + ConfigSourceWorkerResponse, +} from "./toolchain.worker.ts"; + +type ConfigSourceWorkerCommand = + | Omit, "id"> + | Omit, "id"> + | Omit, "id">; + +export class ConfigSourceToolchain { + #worker: Worker; + #nextId = 1; + #pending = new Map< + number, + { resolve: (value: unknown) => void; reject: (reason: unknown) => void } + >(); + + constructor( + worker = new Worker(new URL("./toolchain.worker.ts", import.meta.url), { + type: "module", + }), + ) { + this.#worker = worker; + worker.addEventListener( + "message", + (event: MessageEvent) => { + const pending = this.#pending.get(event.data.id); + if (!pending) return; + this.#pending.delete(event.data.id); + if (event.data.ok) pending.resolve(event.data.result); + else pending.reject(event.data.error); + }, + ); + } + + analyze( + snapshot: ConfigTreeSnapshot, + path: string, + source?: string, + ): Promise { + return this.#request({ kind: "analyze", snapshot, path, source }); + } + + evaluate(snapshot: ConfigTreeSnapshot, contract: ToolchainContract) { + return this.#request({ kind: "evaluate", snapshot, contract }); + } + + format(source: string): Promise { + return this.#request({ kind: "format", source }); + } + + close(): void { + this.#worker.terminate(); + for (const pending of this.#pending.values()) { + pending.reject(new Error("config source toolchain was closed")); + } + this.#pending.clear(); + } + + #request(request: ConfigSourceWorkerCommand): Promise { + const id = this.#nextId++; + return new Promise((resolve, reject) => { + this.#pending.set(id, { + resolve: (value) => resolve(value as T), + reject, + }); + this.#worker.postMessage({ ...request, id }); + }); + } +} diff --git a/web/workspace/src/lib/workspace/config-source/toolchain.worker.ts b/web/workspace/src/lib/workspace/config-source/toolchain.worker.ts new file mode 100644 index 00000000..aec5bc4f --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/toolchain.worker.ts @@ -0,0 +1,69 @@ +import init, { + analyze_snapshot, + evaluate_snapshot, + format_source, +} from "./generated/config_source_wasm.js"; +import type { + ConfigDiagnostic, + ConfigTreeSnapshot, + ToolchainContract, +} from "./types.ts"; + +export type ConfigSourceWorkerRequest = + | { + id: number; + kind: "analyze"; + snapshot: ConfigTreeSnapshot; + path: string; + source?: string; + } + | { + id: number; + kind: "evaluate"; + snapshot: ConfigTreeSnapshot; + contract: ToolchainContract; + } + | { id: number; kind: "format"; source: string }; + +export type ConfigSourceWorkerResponse = + | { id: number; ok: true; result: unknown } + | { id: number; ok: false; error: unknown }; + +const ready = init(); + +self.onmessage = async ( + event: MessageEvent, +): Promise => { + const request = event.data; + try { + await ready; + let result: unknown; + switch (request.kind) { + case "analyze": + result = analyze_snapshot( + request.snapshot, + request.path, + request.source, + ) as ConfigDiagnostic[]; + break; + case "evaluate": + result = evaluate_snapshot(request.snapshot, request.contract); + break; + case "format": + result = format_source(request.source); + break; + } + self.postMessage({ id: request.id, ok: true, result }); + } catch (error) { + self.postMessage({ + id: request.id, + ok: false, + error: normalizeError(error), + }); + } +}; + +function normalizeError(error: unknown): unknown { + if (error instanceof Error) return error.message; + return error; +} diff --git a/web/workspace/src/lib/workspace/config-source/types.ts b/web/workspace/src/lib/workspace/config-source/types.ts new file mode 100644 index 00000000..7f910c8e --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/types.ts @@ -0,0 +1,92 @@ +export type VirtualPath = string; + +export type ConfigContentType = "decodal" | "text"; + +export interface ConfigEntry { + path: VirtualPath; + content_type: ConfigContentType; + content: string; + content_digest: string; +} + +export interface ConfigTreeSnapshot { + revision: number; + digest: string; + entries: Record; +} + +export type ConfigTreeChange = + | { + kind: "create"; + path: VirtualPath; + content_type: ConfigContentType; + content: string; + } + | { + kind: "update"; + path: VirtualPath; + expected_digest: string; + content: string; + } + | { + kind: "rename"; + from: VirtualPath; + to: VirtualPath; + expected_digest: string; + } + | { + kind: "delete"; + path: VirtualPath; + expected_digest: string; + }; + +export interface ToolchainContract { + contract_version: number; + decodal_version: string; + schema_version: number; + entrypoints: VirtualPath[]; + import_policy_version: number; + fingerprint: string; +} + +export interface ConfigDiagnostic { + path: VirtualPath; + revision: number; + tree_digest: string; + kind: string; + span: { start_byte: number; end_byte: number }; + message: string; + labels: Array<{ + span: { start_byte: number; end_byte: number }; + message: string; + }>; + notes: string[]; +} + +export interface WorkspaceConfigTreeResponse { + snapshot: ConfigTreeSnapshot; + contract: ToolchainContract; + projection_digest: string; +} + +export interface EvaluatedConfigCandidate { + base_revision: number; + base_digest: string; + snapshot: ConfigTreeSnapshot; + contract: ToolchainContract; + evaluation: { + projections: Array<{ + entrypoint: VirtualPath; + data_json: unknown; + projection_digest: string; + }>; + projection_digest: string; + }; +} + +export interface ConfigCommitRequest { + base_revision: number; + base_digest: string; + changes: ConfigTreeChange[]; + entrypoints: VirtualPath[]; +} diff --git a/web/workspace/src/lib/workspace/settings/model.ts b/web/workspace/src/lib/workspace/settings/model.ts index 768f31fc..9aab0873 100644 --- a/web/workspace/src/lib/workspace/settings/model.ts +++ b/web/workspace/src/lib/workspace/settings/model.ts @@ -7,6 +7,7 @@ export type Diagnostic = { export type SettingsSectionId = | "runtime-connections" | "runtime-inventory" + | "configuration-sources" | "profile-sources" | "backend-config" | "workspace-identity"; @@ -98,6 +99,18 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [ "Console routes may still target a Runtime handle directly, but Runtime discovery belongs under Settings.", ], }, + { + id: "configuration-sources", + label: "Configuration Sources", + status: "editable", + summary: + "Edit the Server-owned virtual Decodal source tree through one native/WASM toolchain contract.", + bullets: [ + "Virtual paths and imports resolve inside the committed Workspace tree, never from browser or Server host paths.", + "Browser analysis is advisory; Server evaluation is required before an atomic revision commit.", + "Profile, Skill, Prompt, and Plugin consumers remain on their existing authorities until their follow-up cutovers.", + ], + }, { id: "profile-sources", label: "Profile Sources", @@ -160,6 +173,8 @@ export function settingsSectionHref(id: SettingsSectionId): string { return `${SETTINGS_ROUTE}/runtime-connections`; case "runtime-inventory": return `${SETTINGS_ROUTE}/runtimes`; + case "configuration-sources": + return `${SETTINGS_ROUTE}/configuration`; case "profile-sources": return `${SETTINGS_ROUTE}/profiles`; case "workspace-identity": diff --git a/web/workspace/src/lib/workspace/styles/settings.css b/web/workspace/src/lib/workspace/styles/settings.css index 9dec29b5..a2b9568d 100644 --- a/web/workspace/src/lib/workspace/styles/settings.css +++ b/web/workspace/src/lib/workspace/styles/settings.css @@ -515,6 +515,157 @@ font-weight: 800; text-transform: uppercase; } + .config-source-shell { + display: grid; + grid-template-columns: minmax(12rem, 18rem) minmax(0, 1fr); + min-height: 40rem; + overflow: hidden; + border: 1px solid var(--line); + border-radius: var(--radius-panel); + background: var(--bg-raised); + } + .config-source-tree { + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + border-right: 1px solid var(--line); + background: var(--bg-subtle); + } + .config-source-tree__header, + .config-source-workbench__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + padding: var(--space-3); + border-bottom: 1px solid var(--line); + } + .config-source-tree nav { + display: grid; + align-content: start; + overflow: auto; + padding: var(--space-2); + } + .config-source-tree nav button { + border: 0; + border-radius: 0.45rem; + background: transparent; + color: var(--text-muted); + padding: 0.45rem 0.55rem; + font: inherit; + font-family: var(--font-mono); + text-align: left; + cursor: pointer; + } + .config-source-tree nav button.active, + .config-source-tree nav button:hover { + background: var(--interactive-hover); + color: var(--text-strong); + } + .config-source-create { + display: grid; + gap: var(--space-2); + padding: var(--space-3); + border-top: 1px solid var(--line); + } + .config-source-create label, + .config-source-workbench__header span { + color: var(--text-muted); + font-size: 0.75rem; + } + .config-source-create input { + min-width: 0; + border: 1px solid var(--line); + border-radius: 0.45rem; + background: var(--bg-raised); + color: var(--text-strong); + padding: 0.45rem; + font: inherit; + font-family: var(--font-mono); + } + .config-source-create button, + .config-source-actions button { + border: 1px solid var(--line); + border-radius: 0.45rem; + background: var(--bg-subtle); + color: var(--text-strong); + padding: 0.4rem 0.6rem; + cursor: pointer; + } + .config-source-actions button.primary { + border-color: transparent; + background: var(--accent); + color: var(--bg); + } + .config-source-actions button.danger { + color: var(--danger); + } + .config-source-actions button:disabled { + cursor: not-allowed; + opacity: 0.45; + } + .config-source-workbench { + display: grid; + grid-template-rows: auto minmax(20rem, 1fr) auto auto; + min-width: 0; + } + .config-source-workbench__header > div:first-child { + display: grid; + min-width: 0; + } + .config-source-workbench__header strong { + overflow: hidden; + font-family: var(--font-mono); + text-overflow: ellipsis; + white-space: nowrap; + } + .config-source-actions { + display: flex; + flex-wrap: wrap; + gap: var(--space-2); + } + .config-source-status { + margin: 0; + padding: var(--space-2) var(--space-3); + border-top: 1px solid var(--line); + color: var(--text-muted); + font-size: 0.8rem; + } + .config-source-diagnostics { + display: grid; + gap: var(--space-2); + max-height: 12rem; + overflow: auto; + margin: 0; + padding: var(--space-3); + border-top: 1px solid var(--line); + } + .config-source-diagnostics li { + display: grid; + grid-template-columns: auto 1fr auto; + gap: var(--space-2); + color: var(--text-muted); + } + .config-source-diagnostics strong { + color: var(--danger); + } + @media (max-width: 48rem) { + .config-source-shell { + grid-template-columns: 1fr; + } + .config-source-tree { + grid-template-rows: auto auto auto; + border-right: 0; + border-bottom: 1px solid var(--line); + } + .config-source-tree nav { + max-height: 12rem; + } + .config-source-workbench__header { + align-items: stretch; + flex-direction: column; + } + } + .status-message.error { color: var(--danger); } diff --git a/web/workspace/src/routes/w/[workspaceId]/settings/configuration/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/settings/configuration/+page.svelte new file mode 100644 index 00000000..8928e527 --- /dev/null +++ b/web/workspace/src/routes/w/[workspaceId]/settings/configuration/+page.svelte @@ -0,0 +1,27 @@ + + + + Configuration | {workspaceName} + + +
+ + + +