ADR 0018: Editing an existing macro's source is done by splicing the original .bin, not re-synthesizing it
Status: Retracted (2026-07-24) by ADR 0019. The edit-source-by-splice mechanism (write source at MODULEOFFSET 0, zero the offset, reset _VBA_PROJECT to the recompile cookie) does not work: Excel runs a module's compiled p-code, not its source, so a spliced module either fails to load or silently runs stale code. Its "clean" GUI verdict never clicked Enable Content. Editing an existing module's source now lives in the offline tools/vba-compiler (in-place mode). Retained here for historical context. · Originally Accepted (2026-07-23) · extended ADR 0017 with the edit-existing case · amends nothing in ADR 0016's read view.
Context
ADR 0017 §2.3 shipped authoring from scratch: Workbook.setVbaProject({modules}) synthesizes a fresh vbaProject.bin from a module list. By construction that project is reference-free and host-default. writeVbaProject emits no REFERENCE* records and a fixed host block, and it rejects document and designer modules because their host linkage (the ThisWorkbook/Sheet1 code-behind wiring) cannot be synthesized without the host.
That leaves a real, common need unmet: take an existing .xlsm, change one module's source, often a document code-behind like ThisWorkbook, and re-emit it with the project's references, host-extender info, and every other module preserved exactly. From-scratch authoring cannot do this: rebuilding the project from a source list would drop everything the source list does not model (references, host linkage, untouched modules' p-code). The whole value here is preservation, and re-synthesis is preservation's opposite.
Decision
Splice, don't re-synthesize.
editVbaModuleSources(bin, edits)(internal, insrc/vba/project-editor.ts;editsis aMap<moduleName, newSource>) operates on the original.binbytes and keeps every CFB stream verbatim except:- the edited module's source stream, replaced with
compressContainer(encode(newSource))at MODULEOFFSET 0, no p-code; - the
dirstream, decompressed, with only the edited modules'MODULEOFFSETrecords zeroed, then recompressed; _VBA_PROJECT, replaced with the 7-byte recompile-from-source cookie (the same unmatchable header §2.3c uses), so Excel recompiles the edited modules on open.
References,
PROJECTLibFlags, constants,PROJECT/PROJECTwmtext, host-extender info, and every other module survive because we never touch the streams that hold them. Preservation by not-destroying is strictly higher fidelity than model-and-re-emit. Parse-first, fail-closed: a malformed original raisesVbaParseErrorbefore any mutation; an unknown module or an unrepresentable character raisesVbaAuthorError.- the edited module's source stream, replaced with
This amends nothing in the read view. ADR 0016's
VbaProjectstays a source-only, read-only projection. The editor is a bytes-to-bytes transform that never models references or host info into the read view, so it cannot silently drop what it does not model, which is the failure mode that sank the re-synthesis alternative. That alternative was rejected because it would require modeling every [MS-OVBA]REFERENCE*and host record in the reader and re-emitting it in the writer: more code and more break risk for strictly less fidelity.Document and designer modules are editable, which is the standout advantage. Because host linkage already lives in the preserved
dir/PROJECTstreams, the splice inherits it rather than synthesizing it. This is exactly the module class ADR 0017 §2.3c rejects for from-scratch authoring. Editing aThisWorkbookcode-behind in a real workbook is verified to open clean in Excel (below).Two public entry points, differing in fidelity.
Workbook.setVbaModuleSource(name, source)(model level) readsvbaProjectBytes, splices, and routes the result back through thevbaProjectBytessetter, inheriting fail-closed validation, preserved-ref rebuild, signature-drop, and macro content-type with zero writer changes. It throws if no project is attached. The map-shaped primitiveeditVbaModuleSourcesis also barrel-exported for the batch/functional path.editXlsxVbaModuleSource(xlsx, name, source)/editXlsxVbaModuleSources(xlsx, edits)(package level,src/io/xlsx/edit-vba.ts) unzip the package, locatexl/vbaProject.binvia the reader's OPC resolution (_rels/.rels→ officeDocument → workbook.rels→vbaProjectrel), splice, drop any stale signature (part, rels entry, and content-type override, everyvbaProjectSignature*flavour), then re-zip. Onlyxl/vbaProject.binchanges; every other part is byte-for-byte.
The package-level path is the highest-fidelity way to edit an existing
.xlsm, and it exists because the model path can perturb strict parts. The model round-trip (readXlsx, thensetVbaModuleSource, thenwriteXlsx) re-serializes the whole package from the parsed model, so it preserves only what the model captures. On a rich real-world workbook that round-trip perturbs parts Excel is strict about and Excel prompts to repair on open, and this is not the VBA edit's fault: a control round-trip with no VBA edit at all (readXlsxthenwriteXlsx) repairs identically. It is a pre-existing, VBA-independentwriteXlsxwhole-package fidelity gap, where our writer re-emits a simplified package and the minimal corpus round-trip passes precisely because it is minimal. The package-preserving path sidesteps it entirely by never re-authoring anything but the macro project. Guidance: to edit a macro in a real file whose non-macro content must survive exactly, useeditXlsxVbaModuleSource(s).setVbaModuleSourcepluswriteXlsxremains correct for model-built or minimal workbooks and until the general writer reaches whole-package parity. This is captured in the function JSDoc.Editing the project drops a stale signature. A signature validates the old project bytes; the instant those change it is invalid. Both paths drop it (the model path inherits ADR 0017 §2.1's signature-drop closure; the package path removes the signature part, its relationship, and its content-type override directly) so the package advertises no signature rather than a broken one.
Verification
Real Excel (excel-gui-automation open-verdict probe, Excel 16.0, interactive session; recorded facts, not CI) on a genuine 10-module Excel-authored workbook with 7 document code-behinds, a 45 KB procedural JsonConverter, and real references:
- splicing a procedural module into the original package (swapping only
vbaProject.bin) is clean; - splicing a document module (
ThisWorkbook/Contacts) into the original package is clean, screenshot-confirmed (normal title, macros-disabled security bar, no repair). This is the from-scratch-impossible case, opening clean and recompiling. editXlsxVbaModuleSourceediting a document module end-to-end is clean.
The _VBA_PROJECT recompile cookie interacts fine with a real p-code'd project, retiring the risk that a recompile-all header would confuse a preserved p-code project. CI locks the parse round-trip (unit tests) and a security-cluster corpus case (test/corpus/cases/xlsm-vba-edit-module-source-preserves-references.case.ts) that drives the public path and asserts a REFERENCEREGISTERED record survives verbatim, a document module edits in place, an untouched code-page-1251 module stays byte-identical, and _VBA_PROJECT resets to the cookie.
Consequences
- Positive: editing an existing macro's source is now in scope, including document and designer code-behinds, which from-scratch authoring cannot touch, with references, host info, and untouched modules preserved.
editXlsxVbaModuleSource(s)makes the natural functional path Excel-clean on rich real files today, without waiting on the general writer. - Scope unchanged elsewhere: the read view is untouched; adding or removing modules or references is not in this slice (edit-existing-source only); executing macros remains permanently out of scope (ADR 0013).
- Flagged separately, now diagnosed and largely closed: the general
writeXlsxwhole-package repair-prompt on rich real workbooks (surfaced by the no-edit control probe) was a broader writer-fidelity investigation, independent of this feature. Diagnosed since: a referential-integrity sweep of the re-emitted package found the sole structural break to be a dropped external-workbook link, where the writer droppedxl/externalLinks/*and the workbook's<externalReferences>while keeping the[1]…formulas that resolve through it, dangling the reference. Fixed by carrying external links through the workbook preserved-reference net (external targets and the<externalReferences>ordering now round-trip; corpus caseexternal-workbook-link-survives-roundtrip). The remaining differences on such a file (tables, theme and doc-props re-emitted in reduced-but-valid form; calcChain regenerated by Excel) are not repair causes. A confirming Excel re-probe of the model path is the outstanding step (a probe, not CI, per ADR 0013). The package-preserving path remains the highest-fidelity choice for VBA edits regardless. Seedocs/knowledge/specs/external-workbook-reference-formulas.mdanddocs/knowledge/specs/excel-repair-on-open-structural-constraints.md. - Revisit when: a consumer needs to add or remove modules or references (a new slice modeling those records), or when the general writer reaches whole-package parity, at which point the model path matches the package path on fidelity and the guidance in decision 5 can relax.
Related: ADR 0017 (VBA authoring in scope; from-scratch synthesis), ADR 0016 (VBA read view, unchanged here), ADR 0013 (Excel as a test oracle, never a runtime), docs/knowledge/specs/xlsm-macro-preservation.md.