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 @@
+
+
+
+
+
+
+
+
source = value} />
+ {status}
+ {#if diagnostics.length > 0}
+
+ {#each diagnostics as diagnostic}
+ -
+ {diagnostic.kind}
+ {diagnostic.message}
+ bytes {diagnostic.span.start_byte}–{diagnostic.span.end_byte}
+
+ {/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}
+
+
+