From 4cf34375a81f5936a21e96bb6958313affd98d82 Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 15 Aug 2026 02:06:46 +0900 Subject: [PATCH] config: commit without preview --- crates/workspace-server/src/config_source.rs | 132 ++-------------- crates/workspace-server/src/server.rs | 22 +-- .../config-source/ConfigSourceEditor.svelte | 144 +++++++----------- .../src/lib/workspace/config-source/api.ts | 16 -- .../generated/types/ConfigCommitRequest.ts | 2 +- .../generated/types/ConfigPreviewRequest.ts | 5 - .../types/EvaluatedConfigCandidate.ts | 6 - .../lib/workspace/config-source/toolchain.ts | 5 - .../config-source/toolchain.worker.ts | 5 - .../src/lib/workspace/config-source/types.ts | 2 - web/workspace/test/config-source/api.test.ts | 18 +-- .../test/config-source/editor-state.test.ts | 40 ++++- 12 files changed, 109 insertions(+), 288 deletions(-) delete mode 100644 web/workspace/src/lib/workspace/config-source/generated/types/ConfigPreviewRequest.ts delete mode 100644 web/workspace/src/lib/workspace/config-source/generated/types/EvaluatedConfigCandidate.ts diff --git a/crates/workspace-server/src/config_source.rs b/crates/workspace-server/src/config_source.rs index 092248b1..c3d1281b 100644 --- a/crates/workspace-server/src/config_source.rs +++ b/crates/workspace-server/src/config_source.rs @@ -108,10 +108,8 @@ pub struct WorkspaceConfigState { pub projection_digest: String, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)] -#[ts(export)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct EvaluatedConfigCandidate { - #[ts(type = "number")] pub base_revision: u64, pub base_digest: String, pub snapshot: ConfigTreeSnapshot, @@ -127,14 +125,6 @@ pub struct ConfigCommitRequest { pub base_digest: String, pub changes: Vec, pub entrypoints: Vec, - pub toolchain_fingerprint: String, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)] -#[ts(export)] -pub struct ConfigPreviewRequest { - pub changes: Vec, - pub entrypoints: Vec, } impl SqliteWorkspaceStore { @@ -245,13 +235,6 @@ impl SqliteWorkspaceStore { current.snapshot.revision ))); } - let expected_contract = main_config_contract_with_schema(schema_bundle.clone()); - if expected_contract.fingerprint != request.toolchain_fingerprint { - return Err(config_conflict(format!( - "toolchain fingerprint mismatch; current fingerprint is {}", - expected_contract.fingerprint - ))); - } evaluate_candidate(current, &request.changes, schema_bundle) } @@ -267,31 +250,6 @@ impl SqliteWorkspaceStore { ) } - pub fn preview_workspace_config_with_schema( - &self, - workspace_id: &str, - request: &ConfigPreviewRequest, - schema_bundle: WorkspaceConfigSchemaBundle, - ) -> Result { - validate_entrypoint_request(&request.entrypoints)?; - let current = self - .load_workspace_config(workspace_id)? - .ok_or_else(config_not_materialized)?; - evaluate_candidate(current, &request.changes, schema_bundle) - } - - pub fn preview_workspace_config( - &self, - workspace_id: &str, - request: &ConfigPreviewRequest, - ) -> Result { - self.preview_workspace_config_with_schema( - workspace_id, - request, - WorkspaceConfigSchemaBundle::empty(), - ) - } - pub fn commit_evaluated_workspace_config( &self, workspace_id: &str, @@ -943,7 +901,6 @@ mod tests { base_digest: current.snapshot.digest.clone(), changes, entrypoints: vec![path(MAIN_CONFIG_ENTRYPOINT)], - toolchain_fingerprint: current.contract.fingerprint.clone(), } } @@ -991,7 +948,6 @@ mod tests { content: "{ web = {}; }".to_string(), }], entrypoints: vec![path(MAIN_CONFIG_ENTRYPOINT)], - toolchain_fingerprint: expected_contract.fingerprint.clone(), }, schema, ) @@ -1018,34 +974,6 @@ mod tests { ); } - #[tokio::test] - async fn commit_rejects_stale_schema_bundle_fingerprint() { - let store = open_store().await; - let current = store.load_workspace_config("w-config").unwrap().unwrap(); - let schema = WorkspaceConfigSchemaBundle::compose([ConfigSchemaContribution::new( - "builtin:web", - "web", - "1", - "{ web = {}; }", - ) - .unwrap()]) - .unwrap(); - let error = store - .evaluate_workspace_config_candidate_with_schema( - "w-config", - &ConfigCommitRequest { - base_revision: current.snapshot.revision, - base_digest: current.snapshot.digest, - changes: Vec::new(), - entrypoints: vec![path(MAIN_CONFIG_ENTRYPOINT)], - toolchain_fingerprint: current.contract.fingerprint, - }, - schema, - ) - .unwrap_err(); - assert!(error.to_string().contains("toolchain fingerprint mismatch")); - } - #[test] fn active_state_evaluation_rejects_provider_fingerprint_drift() { let snapshot = ConfigTreeSnapshot::from_entries( @@ -1361,10 +1289,11 @@ mod tests { let store = open_store().await; let current = store.load_workspace_config("w-config").unwrap().unwrap(); let candidate = store - .preview_workspace_config( + .evaluate_workspace_config_candidate( "w-config", - &ConfigPreviewRequest { - changes: vec![ + &commit_request( + ¤t, + vec![ update_main(¤t, "{}"), ConfigTreeChange::Create { path: path("module.dcdl"), @@ -1372,8 +1301,7 @@ mod tests { content: "{}".into(), }, ], - entrypoints: vec![path(MAIN_CONFIG_ENTRYPOINT)], - }, + ), ) .unwrap(); @@ -1469,12 +1397,9 @@ mod tests { }, ] { let error = store - .preview_workspace_config( + .evaluate_workspace_config_candidate( "w-config", - &ConfigPreviewRequest { - changes: vec![change], - entrypoints: vec![path(MAIN_CONFIG_ENTRYPOINT)], - }, + &commit_request(¤t, vec![change]), ) .unwrap_err(); assert!(error.to_string().contains("cannot be")); @@ -1484,14 +1409,11 @@ mod tests { #[tokio::test] async fn browser_cannot_replace_server_owned_entrypoint_contract() { let store = open_store().await; + let current = store.load_workspace_config("w-config").unwrap().unwrap(); + let mut request = commit_request(¤t, Vec::new()); + request.entrypoints = vec![path("other.dcdl")]; let error = store - .preview_workspace_config( - "w-config", - &ConfigPreviewRequest { - changes: Vec::new(), - entrypoints: vec![path("other.dcdl")], - }, - ) + .evaluate_workspace_config_candidate("w-config", &request) .unwrap_err(); assert!(error.to_string().contains("must be exactly [main.dcdl]")); } @@ -1514,12 +1436,6 @@ mod tests { content: "{ broken = ; }".into(), }], entrypoints: vec![path(MAIN_CONFIG_ENTRYPOINT)], - toolchain_fingerprint: ToolchainContract::new( - DEFAULT_SCHEMA_VERSION, - vec![path(MAIN_CONFIG_ENTRYPOINT)], - DEFAULT_IMPORT_POLICY_VERSION, - ) - .fingerprint, }, ) .unwrap_err(); @@ -1587,7 +1503,6 @@ mod tests { content: "{ answer = 2; }".into(), }], entrypoints: first.contract.entrypoints.clone(), - toolchain_fingerprint: first.contract.fingerprint.clone(), }, ) .unwrap(); @@ -1598,27 +1513,6 @@ mod tests { assert_eq!(revision, first.snapshot); } - #[tokio::test] - async fn commit_rejects_mismatched_toolchain_fingerprint() { - let store = SqliteWorkspaceStore::in_memory().unwrap(); - store.upsert_workspace(&workspace()).await.unwrap(); - let current = store.load_workspace_config("w-config").unwrap().unwrap(); - let error = store - .evaluate_and_commit_workspace_config( - "w-config", - &ConfigCommitRequest { - base_revision: current.snapshot.revision, - base_digest: current.snapshot.digest.clone(), - changes: vec![update_main(¤t, "{ answer = 42; }")], - entrypoints: vec![path(MAIN_CONFIG_ENTRYPOINT)], - toolchain_fingerprint: "sha256:stale-toolchain".into(), - }, - ) - .unwrap_err(); - assert!(matches!(error, Error::WorkspaceConfigConflict(_))); - assert!(store.load_workspace_config("w-config").unwrap().is_some()); - } - #[tokio::test] async fn migration_materializes_main_for_existing_workspace_without_config() { let conn = rusqlite::Connection::open_in_memory().unwrap(); @@ -1653,9 +1547,7 @@ mod tests { .join("../../web/workspace/src/lib/workspace/config-source/generated/types"); let config = ts_rs::Config::default().with_out_dir(&output); WorkspaceConfigState::export_all(&config).unwrap(); - EvaluatedConfigCandidate::export_all(&config).unwrap(); ConfigCommitRequest::export_all(&config).unwrap(); - ConfigPreviewRequest::export_all(&config).unwrap(); } #[test] diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 095d88fb..8afb4280 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -62,7 +62,7 @@ use crate::companion::{ CompanionStatusResponse, CompanionTranscriptProjection, }; use crate::config::{BackendRuntimesConfigFile, RemoteRuntimeConfigFile, resolve_remote_runtime}; -use crate::config_source::{ConfigCommitRequest, ConfigPreviewRequest}; +use crate::config_source::ConfigCommitRequest; use crate::hosts::{ ConfigBundleCheckResult, ConfigBundleSyncResult, DiagnosticSeverity, EMBEDDED_RUNTIME_ID, EmbeddedWorkerRuntime, HostSummary, RemoteRuntimeConfig, RemoteWorkerRuntime, @@ -1156,10 +1156,6 @@ pub fn build_router(api: WorkspaceApi) -> Router { "/api/w/{workspace_id}/config/source-tree", get(scoped_get_workspace_config_tree), ) - .route( - "/api/w/{workspace_id}/config/source-tree/preview", - post(scoped_preview_workspace_config_tree), - ) .route( "/api/w/{workspace_id}/config/source-tree/commit", post(scoped_commit_workspace_config_tree), @@ -2514,21 +2510,6 @@ async fn scoped_get_workspace_config_entry( Ok(Json(entry)) } -async fn scoped_preview_workspace_config_tree( - State(api): State, - AxumPath(path): AxumPath, - Json(request): Json, -) -> ApiResult> { - validate_workspace_scope(&api, &path.workspace_id)?; - let candidate = api.config_store.preview_workspace_config_with_schema( - &path.workspace_id, - &request, - api.config_schema_registry.compose()?, - )?; - crate::prompt_settings::validate_evaluated_prompt_catalog(&candidate.evaluation)?; - Ok(Json(candidate)) -} - async fn scoped_commit_workspace_config_tree( State(api): State, AxumPath(path): AxumPath, @@ -14114,7 +14095,6 @@ mod tests { }, ], entrypoints: current.contract.entrypoints.clone(), - toolchain_fingerprint: current.contract.fingerprint.clone(), }; let candidate = api .config_store diff --git a/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte b/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte index dc243431..b3d6cfa9 100644 --- a/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte +++ b/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte @@ -4,7 +4,6 @@ import { commitConfigTree, fetchConfigTree, - previewConfigTree, } from "./api.ts"; import { ConfigSourceToolchain } from "./toolchain.ts"; import type { @@ -23,15 +22,15 @@ let diagnostics = $state([]); let status = $state("Loading source tree…"); let busy = $state(false); - let draftChanges = $state([]); + let workingChanges = $state([]); let baseRevision = $state(0); let baseDigest = $state(""); let renamePath = $state(""); let baseSnapshot = $state.raw(null); - let preflightDigest = $state(""); let conflict = $state(false); - let candidateContract = $state(null); - let toolchain: ConfigSourceToolchain | null = null; + let toolchain = $state.raw(null); + let analysisReady = $state(false); + let analysisGeneration = 0; const paths = $derived( treeState ? Object.keys(treeState.snapshot.entries).toSorted() : [], @@ -40,8 +39,7 @@ treeState && selectedPath ? treeState.snapshot.entries[selectedPath] : undefined, ); const mainSelected = $derived(selectedPath === MAIN_ENTRYPOINT); - const dirty = $derived(draftChanges.length > 0 || (selected ? source !== selected.content : source.length > 0)); - const commitReady = $derived(dirty && preflightDigest === treeState?.snapshot.digest); + const dirty = $derived(workingChanges.length > 0 || (selected ? source !== selected.content : source.length > 0)); onMount(() => { toolchain = new ConfigSourceToolchain(); @@ -49,7 +47,30 @@ return () => toolchain?.close(); }); + $effect(() => { + const analyzer = toolchain; + const path = selectedPath; + const value = source; + const ready = analysisReady; + const generation = ++analysisGeneration; + diagnostics = []; + if (!analyzer || !path || !ready) return; + + const timer = setTimeout(() => { + void analyzer.analyze(path, value).then((result) => { + if (generation === analysisGeneration) diagnostics = result; + }).catch((error) => { + if (generation === analysisGeneration) status = `Analyze failed: ${String(error)}`; + }); + }, 250); + return () => { + clearTimeout(timer); + if (generation === analysisGeneration) analysisGeneration += 1; + }; + }); + async function reload() { + analysisReady = false; try { treeState = await fetchConfigTree(workspaceId); if (!selectedPath || !treeState.snapshot.entries[selectedPath]) { @@ -60,11 +81,11 @@ baseRevision = treeState.snapshot.revision; baseDigest = treeState.snapshot.digest; await toolchain?.setSnapshot(treeState.snapshot, treeState.contract.schema_bundle); - draftChanges = []; + analysisReady = true; + workingChanges = []; renamePath = selectedPath; diagnostics = []; conflict = false; - candidateContract = null; status = `Revision ${treeState.snapshot.revision} · ${treeState.snapshot.digest.slice(0, 20)}…`; } catch (error) { status = String(error); @@ -76,9 +97,7 @@ if (!change || !toolchain) return; const candidate = await toolchain.applyChanges([change]); if (treeState) treeState = { ...treeState, snapshot: candidate }; - if (baseSnapshot) draftChanges = await toolchain.changesBetween(baseSnapshot, candidate); - preflightDigest = ""; - candidateContract = null; + if (baseSnapshot) workingChanges = await toolchain.changesBetween(baseSnapshot, candidate); conflict = false; } @@ -114,27 +133,21 @@ return [MAIN_ENTRYPOINT]; } - async function analyze() { - if (!toolchain || !treeState || !selectedPath) return; - diagnostics = await toolchain.analyze(selectedPath, source); - status = diagnostics.length === 0 ? "No diagnostics." : `${diagnostics.length} diagnostic(s).`; - } - async function format() { if (!toolchain) return; try { source = await toolchain.format(source); - await analyze(); + status = "Formatted source. Changes remain local until Commit succeeds."; } catch (error) { status = String(error); } } - async function formatDraftSources() { + async function formatWorkingSources() { if (!toolchain || !treeState || !baseSnapshot) return; await stageCurrent(); const paths = new Set(); - for (const change of draftChanges) { + for (const change of workingChanges) { if (change.kind === "create" || change.kind === "update") paths.add(change.path); if (change.kind === "rename") paths.add(change.to); } @@ -157,74 +170,33 @@ if (!formattedAny) return; treeState = { ...treeState, snapshot: candidate }; - draftChanges = await toolchain.changesBetween(baseSnapshot, candidate); + workingChanges = await toolchain.changesBetween(baseSnapshot, candidate); source = candidate.entries[selectedPath]?.content ?? source; - preflightDigest = ""; - candidateContract = null; conflict = false; } - async function requestCandidatePreview() { - if (!toolchain) throw new Error("config source toolchain is unavailable"); - const candidate = await previewConfigTree(workspaceId, { - changes: draftChanges, - entrypoints: entrypoints(), - }); - await toolchain.evaluate(candidate.contract); - diagnostics = []; - candidateContract = candidate.contract; - preflightDigest = candidate.snapshot.digest; - return candidate; - } - - function recordCandidateError(error: unknown) { + function recordCommitError(error: unknown) { const message = String(error); conflict = message.includes("conflict") || message.includes("base revision/digest mismatch"); status = conflict ? `${message} Reload the authoritative tree before editing again.` : message; } - async function preview() { - if (!treeState) return; - busy = true; - try { - await formatDraftSources(); - if (draftChanges.length === 0) { - status = "No draft changes to preview."; - return; - } - const candidate = await requestCandidatePreview(); - status = `Preview valid · projection ${candidate.evaluation.projection_digest.slice(0, 20)}…`; - } catch (error) { - recordCandidateError(error); - } finally { - busy = false; - } - } - async function commit() { if (!treeState) return; busy = true; try { - await formatDraftSources(); - if (draftChanges.length === 0) { - status = "No draft changes to commit."; + await formatWorkingSources(); + if (workingChanges.length === 0) { + status = "No working changes to commit."; return; } - if (preflightDigest !== treeState.snapshot.digest || !candidateContract) { - await requestCandidatePreview(); - } - const contract = candidateContract; - if (!contract) throw new Error("candidate preview did not return a toolchain contract"); treeState = await commitConfigTree(workspaceId, { base_revision: baseRevision, base_digest: baseDigest, - changes: draftChanges, - entrypoints: contract.entrypoints, - toolchain_fingerprint: contract.fingerprint, + changes: workingChanges, + entrypoints: entrypoints(), }); - draftChanges = []; - preflightDigest = ""; - candidateContract = null; + workingChanges = []; conflict = false; baseSnapshot = $state.snapshot(treeState.snapshot); baseRevision = treeState.snapshot.revision; @@ -234,21 +206,21 @@ diagnostics = []; status = `Committed formatted revision ${treeState.snapshot.revision}.`; } catch (error) { - recordCandidateError(error); + recordCommitError(error); } finally { busy = false; } } async function discardAndReload() { - draftChanges = []; + workingChanges = []; source = ""; await reload(); } async function reloadAndReapply() { if (!toolchain || !treeState) return; - const localChanges = [...draftChanges]; + const localChanges = [...workingChanges]; const remote = await fetchConfigTree(workspaceId); baseSnapshot = structuredClone(remote.snapshot); baseRevision = remote.snapshot.revision; @@ -256,14 +228,12 @@ await toolchain.setSnapshot(remote.snapshot, remote.contract.schema_bundle); try { const candidate = await toolchain.applyChanges(localChanges); - draftChanges = localChanges; + workingChanges = localChanges; treeState = { ...remote, snapshot: candidate }; selectedPath = candidate.entries[selectedPath] ? selectedPath : Object.keys(candidate.entries).toSorted()[0] ?? ""; source = selectedPath ? candidate.entries[selectedPath].content : ""; conflict = false; - preflightDigest = ""; - candidateContract = null; - status = "Local changes reapplied to the latest revision. Preview again before Commit."; + status = "Local changes reapplied to the latest revision. Commit to persist them."; } catch (error) { conflict = true; status = `Local changes conflict with the latest revision: ${String(error)}. Discard local changes or resolve against a fresh reload.`; @@ -276,7 +246,7 @@ selectedPath = path; source = "{}\n"; diagnostics = []; - status = `Drafting new source ${path}. It is not persisted until Commit succeeds.`; + status = `Creating local source ${path}. It is not persisted until Commit succeeds.`; } async function deleteEntry() { @@ -288,14 +258,12 @@ }; const candidate = await toolchain.applyChanges([change]); treeState = { ...treeState, snapshot: candidate }; - if (baseSnapshot) draftChanges = await toolchain.changesBetween(baseSnapshot, candidate); - preflightDigest = ""; - candidateContract = null; + if (baseSnapshot) workingChanges = await toolchain.changesBetween(baseSnapshot, candidate); conflict = false; selectedPath = Object.keys(candidate.entries).toSorted()[0] ?? ""; source = selectedPath ? candidate.entries[selectedPath].content : ""; renamePath = selectedPath; - status = "Delete staged. Preview and Commit to persist the candidate tree."; + status = "Delete staged locally. Commit to persist the working tree."; } async function renameEntry() { @@ -311,13 +279,11 @@ }; const candidate = await toolchain.applyChanges([change]); treeState = { ...treeState, snapshot: candidate }; - if (baseSnapshot) draftChanges = await toolchain.changesBetween(baseSnapshot, candidate); - preflightDigest = ""; - candidateContract = null; + if (baseSnapshot) workingChanges = await toolchain.changesBetween(baseSnapshot, candidate); conflict = false; selectedPath = to; source = candidate.entries[to]?.content ?? ""; - status = `Rename to ${to} staged. Preview and Commit to persist.`; + status = `Rename to ${to} staged locally. Commit to persist it.`; } @@ -342,7 +308,7 @@
{ event.preventDefault(); createEntry(); }}> - +
@@ -356,9 +322,7 @@ - - - + diff --git a/web/workspace/src/lib/workspace/config-source/api.ts b/web/workspace/src/lib/workspace/config-source/api.ts index 94bc8ebd..dfa09dfa 100644 --- a/web/workspace/src/lib/workspace/config-source/api.ts +++ b/web/workspace/src/lib/workspace/config-source/api.ts @@ -1,9 +1,7 @@ import type { ConfigCommitRequest, ConfigEntry, - ConfigPreviewRequest, ConfigTreeSnapshot, - EvaluatedConfigCandidate, WorkspaceConfigTreeResponse, } from "./types.ts"; @@ -55,20 +53,6 @@ export async function fetchConfigRevision( ); } -export async function previewConfigTree( - workspaceId: string, - request: ConfigPreviewRequest, - 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, diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/ConfigCommitRequest.ts b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigCommitRequest.ts index ab262091..8c5fbd90 100644 --- a/web/workspace/src/lib/workspace/config-source/generated/types/ConfigCommitRequest.ts +++ b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigCommitRequest.ts @@ -2,4 +2,4 @@ import type { ConfigTreeChange } from "./ConfigTreeChange"; import type { VirtualPath } from "./VirtualPath"; -export type ConfigCommitRequest = { base_revision: number, base_digest: string, changes: Array, entrypoints: Array, toolchain_fingerprint: string, }; +export type ConfigCommitRequest = { base_revision: number, base_digest: string, changes: Array, entrypoints: Array, }; diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/ConfigPreviewRequest.ts b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigPreviewRequest.ts deleted file mode 100644 index 6bf8acb1..00000000 --- a/web/workspace/src/lib/workspace/config-source/generated/types/ConfigPreviewRequest.ts +++ /dev/null @@ -1,5 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConfigTreeChange } from "./ConfigTreeChange"; -import type { VirtualPath } from "./VirtualPath"; - -export type ConfigPreviewRequest = { changes: Array, entrypoints: Array, }; diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/EvaluatedConfigCandidate.ts b/web/workspace/src/lib/workspace/config-source/generated/types/EvaluatedConfigCandidate.ts deleted file mode 100644 index f71ba29a..00000000 --- a/web/workspace/src/lib/workspace/config-source/generated/types/EvaluatedConfigCandidate.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConfigTreeSnapshot } from "./ConfigTreeSnapshot"; -import type { EvaluationResult } from "./EvaluationResult"; -import type { ToolchainContract } from "./ToolchainContract"; - -export type EvaluatedConfigCandidate = { base_revision: number, base_digest: string, snapshot: ConfigTreeSnapshot, contract: ToolchainContract, evaluation: EvaluationResult, }; diff --git a/web/workspace/src/lib/workspace/config-source/toolchain.ts b/web/workspace/src/lib/workspace/config-source/toolchain.ts index 311e99c8..510b4a00 100644 --- a/web/workspace/src/lib/workspace/config-source/toolchain.ts +++ b/web/workspace/src/lib/workspace/config-source/toolchain.ts @@ -2,7 +2,6 @@ import type { ConfigDiagnostic, ConfigTreeChange, ConfigTreeSnapshot, - ToolchainContract, WorkspaceConfigSchemaBundle, } from "./types.ts"; import { jsonWorkerMessage } from "./toolchain-message.ts"; @@ -20,7 +19,6 @@ type Command = | Omit, "id"> | Omit, "id"> | Omit, "id"> - | Omit, "id"> | Omit, "id"> | Omit, "id">; @@ -68,9 +66,6 @@ export class ConfigSourceToolchain { analyze(path: string, source?: string): Promise { return this.#request({ kind: "analyze", path, source }); } - evaluate(contract: ToolchainContract) { - return this.#request({ kind: "evaluate", contract }); - } async complete( path: string, source: string, diff --git a/web/workspace/src/lib/workspace/config-source/toolchain.worker.ts b/web/workspace/src/lib/workspace/config-source/toolchain.worker.ts index 9095114e..d8333b2a 100644 --- a/web/workspace/src/lib/workspace/config-source/toolchain.worker.ts +++ b/web/workspace/src/lib/workspace/config-source/toolchain.worker.ts @@ -3,7 +3,6 @@ import init, { apply_changes, changes_between, complete_current, - evaluate_current, format_source, set_schema_bundle, set_snapshot, @@ -20,7 +19,6 @@ export type ConfigSourceWorkerRequest = | { id: number; kind: "apply_changes"; changes: ConfigTreeChange[] } | { id: number; kind: "changes_between"; base: unknown; candidate: unknown } | { id: number; kind: "analyze"; path: string; source?: string } - | { id: number; kind: "evaluate"; contract: unknown } | { id: number; kind: "complete"; @@ -65,9 +63,6 @@ self.onmessage = async ( } result = analyze_snapshot(snapshot, request.path, request.source); break; - case "evaluate": - result = evaluate_current(request.contract); - break; case "complete": result = complete_current( request.path, diff --git a/web/workspace/src/lib/workspace/config-source/types.ts b/web/workspace/src/lib/workspace/config-source/types.ts index 6358f5e5..73e44afe 100644 --- a/web/workspace/src/lib/workspace/config-source/types.ts +++ b/web/workspace/src/lib/workspace/config-source/types.ts @@ -13,6 +13,4 @@ export type { EvaluationResult } from "./generated/types/EvaluationResult.ts"; export type { ToolchainContract } from "./generated/types/ToolchainContract.ts"; export type { VirtualPath } from "./generated/types/VirtualPath.ts"; export type { ConfigCommitRequest } from "./generated/types/ConfigCommitRequest.ts"; -export type { ConfigPreviewRequest } from "./generated/types/ConfigPreviewRequest.ts"; -export type { EvaluatedConfigCandidate } from "./generated/types/EvaluatedConfigCandidate.ts"; export type { WorkspaceConfigState as WorkspaceConfigTreeResponse } from "./generated/types/WorkspaceConfigState.ts"; diff --git a/web/workspace/test/config-source/api.test.ts b/web/workspace/test/config-source/api.test.ts index c5202276..e2008110 100644 --- a/web/workspace/test/config-source/api.test.ts +++ b/web/workspace/test/config-source/api.test.ts @@ -6,7 +6,6 @@ import { fetchConfigEntry, fetchConfigRevision, fetchConfigTree, - previewConfigTree, } from "../../src/lib/workspace/config-source/api.ts"; function response(body: unknown, status = 200): Response { @@ -16,7 +15,7 @@ function response(body: unknown, status = 200): Response { }); } -Deno.test("config source API stays workspace-scoped and separates preview from commit", async () => { +Deno.test("config source API commits directly through the workspace scope", async () => { const calls: Array<{ url: string; init?: RequestInit }> = []; const fetcher = ((input: string | URL | Request, init?: RequestInit) => { calls.push({ url: String(input), init }); @@ -26,37 +25,38 @@ Deno.test("config source API stays workspace-scoped and separates preview from c await fetchConfigTree("w/one", fetcher); await fetchConfigRevision("w/one", 7, 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: [], - toolchain_fingerprint: "sha256:toolchain", }, fetcher); assertEquals(calls.map((call) => call.url), [ "/api/w/w%2Fone/config/source-tree", "/api/w/w%2Fone/config/source-tree/revisions/7", "/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[3].init?.method, "POST"); - assertEquals(calls[4].init?.method, "POST"); assert( - String(calls[4].init?.body).includes('"base_digest":"sha256:base"'), + 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 () => { +Deno.test("config source API surfaces failed evaluation instead of treating it as a successful commit", async () => { const fetcher = (() => Promise.resolve( new Response("structured diagnostics", { status: 422 }), )) as typeof fetch; let message = ""; try { - await previewConfigTree("w", { changes: [], entrypoints: [] }, fetcher); + await commitConfigTree("w", { + base_revision: 1, + base_digest: "sha256:base", + changes: [], + entrypoints: [], + }, fetcher); } catch (error) { message = String(error); } diff --git a/web/workspace/test/config-source/editor-state.test.ts b/web/workspace/test/config-source/editor-state.test.ts index 2e4c23f8..8ce6dda9 100644 --- a/web/workspace/test/config-source/editor-state.test.ts +++ b/web/workspace/test/config-source/editor-state.test.ts @@ -99,7 +99,7 @@ Deno.test("main entrypoint always enables the fixed schema wrapper", async () => ); }); -Deno.test("preview and commit format every draft Decodal source", async () => { +Deno.test("commit formats every working Decodal source without a preview roundtrip", async () => { const source = await Deno.readTextFile( new URL( "../../src/lib/workspace/config-source/ConfigSourceEditor.svelte", @@ -108,7 +108,7 @@ Deno.test("preview and commit format every draft Decodal source", async () => { ); assert( - source.includes("async function formatDraftSources()") && + source.includes("async function formatWorkingSources()") && source.includes('change.kind === "create" || change.kind === "update"') && source.includes('change.kind === "rename"') && source.includes('entry.content_type !== "decodal"') && @@ -116,15 +116,39 @@ Deno.test("preview and commit format every draft Decodal source", async () => { "all changed Decodal entries should be formatted rather than only the selected source", ); assert( - source.includes("await formatDraftSources();") && - source.includes("await requestCandidatePreview();") && + source.includes("await formatWorkingSources();") && + source.includes("entrypoints: entrypoints()") && source.includes("Committed formatted revision"), - "preview and commit should format first and refresh stale preflight before persistence", + "commit should format first and send the working changes directly to Backend authority", ); assert( - !source.includes( - "Preview the complete candidate successfully before Commit.", + !source.includes("previewConfigTree") && + !source.includes("requestCandidatePreview") && + !source.includes("toolchain_fingerprint"), + "normal commit must not depend on a separate preview or client-echoed toolchain fingerprint", + ); +}); + +Deno.test("config diagnostics analyze continuously with debounce and generation fencing", async () => { + const source = await Deno.readTextFile( + new URL( + "../../src/lib/workspace/config-source/ConfigSourceEditor.svelte", + import.meta.url, ), - "format-driven candidate changes should trigger automatic preflight instead of a dead-end error", + ); + + assert( + source.includes("setTimeout(() =>") && + source.includes("}, 250)") && + source.includes("analyzer.analyze(path, value)") && + source.includes("generation === analysisGeneration") && + source.includes("analysisReady"), + "source changes should trigger only the latest debounced analysis after snapshot initialization", + ); + assert( + !source.includes("onclick={analyze}") && + !source.includes(">Analyze") && + !source.includes(">Preview"), + "manual Analyze and Preview controls should be removed", ); });