From d94c546450b5da571569d91c8dec77f94051fcf4 Mon Sep 17 00:00:00 2001 From: que01 Date: Fri, 28 Aug 2026 09:25:09 +0800 Subject: [PATCH] fix: recompile after saving direct edits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Saving wrote the v2 document but nothing else, and `/api/state` serves the outputs of the last compile — so a refresh showed the pre-edit geometry and a successful save was indistinguishable from a failed one. Reported as "saved, refreshed, edit gone"; the document on disk was in fact correct, resolving as `exact` with the constraint reaching the profile. Save now recompiles and clears the preview overlay, since the baseline carries the edit afterwards and would otherwise draw it twice. The recompile is reported separately from the save: if it fails the message says the edit was saved and names the compile error, rather than claiming the save failed. The existing identity test could not have caught this. An ignored `editsFile` and an empty document produce the same output, so it proved nothing about a non-empty one. The API test now compiles with and without a saved constraint and asserts the outputs differ. Co-Authored-By: Claude Opus 5 (1M context) --- test/workbench-edit-api.js | 27 +++++++++++++++++++++++++++ workbench/client/src/App.tsx | 21 +++++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/test/workbench-edit-api.js b/test/workbench-edit-api.js index ee01d9b..573610b 100644 --- a/test/workbench-edit-api.js +++ b/test/workbench-edit-api.js @@ -7,6 +7,7 @@ const path = require('path'); const { compileInput } = require('../src/compile/compiler'); const { ensureRevisionStore } = require('../src/compile/road-revisions'); const { editPreview, editState, rebaseEdits, saveEdits } = require('../workbench/server'); +const { snapshot: outputSnapshot } = require('./fixture-baseline'); const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'workbench-edit-api-')); const input = { @@ -119,5 +120,31 @@ assert.deepEqual( assert.equal(reloaded.constraintStates[0].status, 'exact'); assert.equal(reloaded.constraintStates[0].applied, true); +// A saved document has to reach the geometry, not just the file. `compileInput()` +// is the only caller that passes `editsFile`, and the workbench serves the outputs +// it wrote — so a stage that ignored the document would make a save look successful +// while a refresh still showed the pre-edit map. The existing identity test cannot +// catch that: an ignored document and an empty one produce the same output. +const noEdits = { + ...input, + outDir: path.join(workspace, 'outputs', 'no-edits'), + stagingDir: path.join(workspace, 'outputs', '_no-edits'), + areaConfigSnapshotFile: initialized.paths.activeAreaConfig, +}; +const withEdits = { + ...input, + outDir: path.join(workspace, 'outputs', 'with-edits'), + stagingDir: path.join(workspace, 'outputs', '_with-edits'), + areaConfigSnapshotFile: initialized.paths.activeAreaConfig, + editsFile: initialized.paths.activeEdits, +}; +compileInput(noEdits); +compileInput(withEdits); +assert.notDeepEqual( + outputSnapshot(withEdits).files, + outputSnapshot(noEdits).files, + 'a saved constraint must change the compiled output, or saving cannot survive a refresh', +); + fs.rmSync(workspace, { recursive: true, force: true }); console.log('workbench edit API tests passed'); diff --git a/workbench/client/src/App.tsx b/workbench/client/src/App.tsx index 48e1a0c..64159d3 100644 --- a/workbench/client/src/App.tsx +++ b/workbench/client/src/App.tsx @@ -277,15 +277,16 @@ function App() { const saveDirectEdits = async () => { if (!session) return; const fragment = session.fragment(); + let version: number; try { const result = await api.saveEdits({ expectedDocumentVersion: session.documentVersion, constraints: fragment.constraints, operations: fragment.operations, }); - session.markSaved(result.documentVersion); + version = result.documentVersion; + session.markSaved(version); touch(); - setStatus(`直接编辑已保存,文档版本 ${result.documentVersion}`); } catch (error) { if (!isVersionConflict(error)) { setStatus(`保存失败:${(error as Error).message}`); @@ -298,6 +299,22 @@ function App() { if (Number.isInteger(current)) session.setDocumentVersion(current as number); touch(); setStatus(`保存冲突:文档已被改到版本 ${current ?? '未知'},请刷新后重做本次编辑。`); + return; + } + // The document is written, but `/api/state` serves the outputs of the last + // compile — without recompiling, a refresh shows the pre-edit geometry and a + // successful save is indistinguishable from a failed one. Reported separately + // because at this point the save has already succeeded. + try { + setStatus('直接编辑已保存,正在重新生成几何...'); + const next = await api.compile(); + setState(next); + setSelected((current) => next.compiled.model.roads.find((road) => road.id === current?.id) || null); + // The baseline now carries the edit, so the preview overlay would draw it twice. + setPreview(null); + setStatus(`直接编辑已保存并重新生成,文档版本 ${version}`); + } catch (error) { + setStatus(`直接编辑已保存(版本 ${version}),但重新生成失败:${(error as Error).message}`); } }; const stage = (change: Override) =>