主题
决策记录(ADR)
这里登记影响面大、回头看仍然成立的架构决策。每条按「背景、决定、为什么不是另一条路、来源」四段写:背景讲当时撞上了什么问题,决定讲选了什么,第三段讲被否掉的方案及其否决理由,来源给出可以复核的文件。
两条阅读须知:
- 编号一经发布不复用,全站 guide 页里的 ADR-xx 指针都指向本页编号。已推翻的决策保留原文并加状态标记(先例:ADR-01 被 ADR-15 推翻),翻案需用户裁决并在原条目销账。
- 宪法级约束的仲裁存录在
editor/docs/refactor/ARCHITECTURE-RULES.md§11;铁律的执行面在同一文件。本页是「为什么这么定」的叙事层,两处分工不重复。
溯源注:2026-10-11 参考材料下线后,历史条目引用的部分路径(
MapboxStudio/、MapStudio4/PROGRESS.md等)已删除,原文查 git 历史;引用RXJS-SIGNAL-GUIDE/STATE-MANAGEMENT-GUIDE等旧文档处,现合并于editor/docs/refactor/DEVELOPMENT-GUIDE.md。
决策之间不是孤立的,下图是主要依赖关系:四文档模型(12)是数据面的根,store 与撤销栈(03)、diff 同步(19)、表达式内核(17)都从它分叉;R7.0 裁决(15)改写了状态管理的原语边界;简化轮(20)是对前述决策执行两年后的一次瘦身。
ADR-02 无 Router 的单工作台单页
- 背景:编辑器是单屏三栏工作台(图层树、编辑列、画布),没有多页面导航需求。早期有人提议加 Router 用 URL 承载选中图层,方便分享链接。
- 决定:不引入 Router 模块;面板与弹层的开合全部走
ui.service的信号,跨组件「打开面板并联动」走计数信号通道(计数而非布尔,未挂载期请求不丢、消费即焚)。 - 为什么不是另一条路:URL 承载状态意味着每次选中变化都要写 history,撤销栈和浏览器后退键会打架;单页工具的分享场景由项目文件导出承担,不需要深链。
- 影响:新增「页面」级导航需重新决策;
editor/package.json至今无 router 依赖。 - 来源:
core/ui.service.ts、ARCHITECTURE-RULES §2 路由级状态条目。
ADR-03 自写 signal store:immer patches 撤销栈与跨文档事务
- 背景:状态总量可预估(样式、界面意图、表单缓存、交互档四个域),撤销是编辑器的硬需求。2026-09 重构时评估过现成状态库。
- 决定:
@Injectablestore 类,内部 private signal 经asReadonly()暴露,写入只走命名方法(方法即审计点)。样式变更统一经StyleStore.update(draft, label):immer produce 产出新文档的同时拿到 patches,patches 直接入撤销栈,撤销就是反向 applyPatches。一个操作同时改样式与 ms-config 的场景(如底图开关联动)登记为跨文档命令(core/history/command.ts的 CROSS_DOCUMENT_COMMANDS),transact 保证单条撤销跨文档一致。运行时态(hover、选中、调试开关)不持久化也不入撤销栈。 - 为什么不是另一条路:NgRx 的 action/effect 样板在四个 store 的规模下是纯开销,且撤销仍要自建;快照 diff 式撤销(每次存全文、撤销时算差异)既慢又难与写操作对齐,patches 天然就是「文档怎么变」的精确表达。每个命令手写 do/undo 双实现曾被考虑,双倍代码且容易失步,被否。
- 影响:新状态先归类四文档模型,不轻易造第五个 store;「config 不回滚」的旧备案在 R6 被跨文档事务推翻。
- 来源:
core/style-store.service.ts、core/history/command.ts、tests/app/core/history/command.test.ts。
ADR-07 dev mock 是 vite 插件,生产是 Rust server
- 背景:离线开发要求不依赖公网瓦片,但构建产物不能携带 mock;两套端点语义还必须一致,否则「dev 好的生产坏」。
- 决定:dev 期
editor/mock/vite-plugin-mock.mjs挂 vite 的 dev + preview 钩子提供全部 mock 端点;生产由server/(Rust,axum)一体化承接:托管editor/dist静态产物、逐项对齐/mock/*兼容端点、加/api/*上传与档案、/tiles|fonts|sprite规范路径。前端无 API base 配置,全部同源相对路径。模拟地形(Terrarium 编码确定性 DEM)在 server 与 mock 两端逐字节一致。 - 为什么不是另一条路:mock 打进产物是语义污染;Node 生产服务相比 Rust 单体在部署物与资源占用上都不占优;给前端配环境变量切 base 会引入「忘记配置」这一整类故障。
- 影响:部署必须带 server 或自建反代;两端语义差异由
server/API.md逐项对照仲裁。 - 来源:
editor/vite.config.ts、server/README.md、server/API.md。
ADR-08 双引擎:maplibre 直挂 + GISSDK 适配器
- 背景:用户决策 D1 确认编辑器要作为 ng-mfe Portal 子应用嵌入 GISSDK 宿主,而 GISSDK 内核的 maplibre 版本(6.4.1)与编辑器自带版本(6.11.2)不同,两个大版本要在一个页面里共存。
- 决定:
MapAdapter接口(core/map/map-adapter.ts)加两个实现:直挂 maplibre 与GissdkMapAdapter(GISSDKcreateMap建图、getNativeAs('maplibre')取原生实例,diff 与事件全走原生,销毁只经 facade)。GISSDK 运行时经window.__MAPSTUDIO_GISSDK__构建期解耦注入,MapService.makeMapAdapter据此选择,上层组件零改动,两个 maplibre 以独立 module graph 共存。新地图能力必须过三问门禁(双引擎都支持吗、降级面是什么、spec 依据在哪)并登记进core/map/map-capabilities.ts,对账测试强制「native API 消费 ⊆ 矩阵登记」。 - 为什么不是另一条路:只留直挂等于放弃 D1 集成;业务层写
if (engine)分支会让版本感知泄漏到每个消费点,适配器缝把引擎差异锁在一个文件里。 - 影响:业务层不得感知引擎版本号;conformance 项的清账进度见 MAP-CAPABILITY-MATRIX。
- 来源:
core/map/map-adapter.ts、src/mfe.main.ts、docs/notes/framework-calibration.md、editor/docs/refactor/MAP-CAPABILITY-MATRIX.md。
ADR-09 MapLibre worker 构建期落盘 .js
- 背景:maplibre-gl 6.11.2 的 worker 是 module 型
.mjs。部署到静态服务器后首帧超时,三线取证(mime_guess 新旧版本、nginx 默认 MIME 表)确认根因:部分静态服务把.mjs当application/octet-stream回,浏览器拒绝把它当 module worker 载入。 - 决定:vite 插件
maplibreWorkerAssetsPlugin构建期发射assets/maplibre-gl-worker.js与-shared.js,并改写 worker 内部的相对 import;插件在源码里找不到预期字面量就直接抛错,防止上游发行结构变化后静默产出坏包。生产 worker URL 显式指向/assets/maplibre-gl-worker.js;server 侧对.mjs/.js显式回text/javascript。 - 为什么不是另一条路:运行时 fetch worker 再用 blob 包装,MIME 问题换个形态依旧存在;要求所有部署环境修 MIME 配置不现实,把问题在构建期消灭最便宜。
- 影响:升级 maplibre 时构建插件有防漂移抛错兜底;自建静态服务仍需自查 MIME。
- 来源:
editor/vite.config.ts、REFACTOR-CHANGELOG R6.11 节、server/src/main.rs的 static_script_mime。
ADR-10 纯函数层与测试树分离
- 背景:src 里混着生产代码与
.spec.ts,组件文件里混着可单测的纯逻辑与模板胶水,测试想写快也没有落点。 - 决定:两条规矩一起立。其一,可复用逻辑下沉为纯函数模块:大文件按
*.fns.ts/*-presets.ts/*.model.ts拆(先例:layer-list-fns、basemap-presets),静态数据表与查询逻辑分家(先例:core/spec-tables.ts,SpecService 只剩查询)。复用优先级成文:配置复用 > 组合 > 函数 > Service > 指令管道 > 组件 > 继承(禁止)> 复制(禁止)。其二,tests/镜像树与 src 分离,vitest 跑 node 环境不加载 Angular 编译插件,组件行为交给 e2e,单测聚焦逻辑与守卫。 - 为什么不是另一条路:Component Harness / testbed 那套组件级单测需要维护 Angular 编译基建,收益抵不过成本,备案不建(铁律 §11);纯逻辑塞进 service 方法则每个测试都要先实例化服务。
- 影响:「动态 Schema 渲染器」重写 prop-editor 的提案被同一条复用优先级否决:控件分派的复杂度来自 MapLibre spec 本身,模板分支是诚实的(铁律 §10 备案)。
- 来源:
editor/vitest.config.ts、ARCHITECTURE-RULES §4、core/spec-tables.ts头注释。
ADR-11 全量汉化与中文视觉基线
- 背景:2026-09-19 用户决策界面全量汉化。Mapbox Studio 英文基准帧的像素对比随之失去语义,但「改了没发现」的回归风险还在。
- 决定:规范词汇走
shared/spec-zh/三口径字典(规范翻译、界面语义、枚举值);视觉回归改为自拍中文基线:tools/visual-baseline.mjs对 11 个状态截图做 pixelmatch 对比(阈值 0.4%),基线 PNG 随仓库提交,有意视觉变更先--update再随 PR 提交。英文 capture/compare 流水线封存。注释与文案另有契约:注释统一分节符与文件头格式(guide/standards),需求编号从注释里清出去(SIMPLIFICATION-PLAN S6):「为什么」留在注释,「哪一轮改的」去 git log 查。 - 为什么不是另一条路:界面做双语切换超出产品定位;直接放弃视觉门禁等于汉化后的回归裸奔;基线帧放 CI 机器上现拍会引入跨机器画布噪声。
- 影响:跨机器画布微噪声是已知局限,nightly 固定 runner 缓解;
MapboxStudio/基准帧已于 2026-10-11 下线,实测档案存续于docs/analysis/。 - 来源:
tools/visual-baseline.mjs、roadmap 2026-09-19 条目、shared/spec-zh/。
ADR-12 界面意图收敛为 ms-config 第四文档
- 背景:显示名、分组、附属交互、过滤器骨架这些「界面意图」曾以
mapstudio:*约定键写进样式文档 metadata。后果是导出的样式 JSON 带着编辑器私货,纯 maplibre 宿主加载校验直接拒绝。 - 决定:独立 ms-config 文档承载全部界面意图:schema.json 是机器真源,codec 双向转换,v1 旧档走迁移注册表,装载统一归一层(迁移、归一、校验三段);样式文档零约定键,导出净化
sanitizeExportedStyle兜底剥离;config 与样式分叉时弹「保留样式还是应用配置」的裁决层。文档版本与值词表版本分轴(version/valueVersion)。 - 为什么不是另一条路:继续约定键加导出时剥离,剥离规则会随功能膨胀越积越多,且样式文档永远背着编辑器私有键,分域是一次性付清。
- 影响:打开旧档自动迁移(幂等,宽松校验逐条丢弃坏段);Schema 与运行时漂移由读盘测试门禁检出。
- 来源:
docs/spec/config-simplification/、core/config/、用户决策 D5(roadmap 决策表)。
ADR-15 R7.0 裁决翻转:RxJS + Tailwind + CDK 渐进采纳
- 背景:任务书模板要求「完全遵循 RxJS 响应式 + 函数式架构、尽量使用 Tailwind 与 Angular CDK」,与 v2 宪法仲裁表(ADR-01 的零 subscribe、ADR-05/06 的禁 Tailwind/CDK)正面冲突。2026-09-30 用户明令采纳模板结论。
- 决定:分四阶段渐进迁移而非一次性重写。阶段 1(R7.0)依赖接线、style-fetch RxJS 化、Tailwind 无 preflight 接入、cdkTrapFocus;阶段 2(R7.1)计数通道 Subject 化、请求面收编 core(组件零 fetch);阶段 3(R7.2)autosave debounce 与 Debug 有界重试 operator 化,rAF 合帧评估后备案不迁;阶段 4(R-53,2026-10-14)两轮评估关账:store 读取面 interop 经 POC 加 bench 互证与 computed 平价(0.91 到 1.01 倍),结论不迁移;CDK Overlay 经 14 项能力差距表对比,自研层栈在注册表、箭头钳位、列缘锚定上反而是 CDK 缺口,结论不迁,保留点状采纳(新增无锚定的全局弹层可用 global())。订阅纪律:手写
.subscribe(必须同语句配takeUntilDestroyed,架构测试做双向文本窗口扫描。 - 为什么不是另一条路:维持 v2 仲裁违背用户裁决;一次性全量重写在四千余单测与视觉基线 0.000% 门禁下不可验证,被明确否决。
- 影响:推翻 ADR-01(RxJS 部分),更新 ADR-05/06;存量 signals 照常运行;preflight 永不启用;Material 不引入。
- 来源:用户 2026-09-30 决策、
editor/docs/refactor/DEVELOPMENT-GUIDE.md§2、REFACTOR-CHANGELOG R7.0 至 R7.2、docs/analysis/store-interop-assessment.md、docs/analysis/cdk-overlay-evaluation.md。
ADR-16 底图双通道:basemap 单槽 + styleImports 条目化多槽
- 背景:「导入另一份 style」其实有两类语义:背景底图(整段替换、恒垫底、要参与插槽编队)与数据补充导入(多份并行、不垫底)。初代实现把两者混在同一个 Imports 卡片里,语义纠缠。
- 决定:拆成两条平行通道。
basemap单槽语义:config 里background与basemap分键存「选了什么」与「并入了什么」,四插槽(top / middle / lower / bottom)锚点投影,成员驻留可逆;styleImports条目化多槽:imp-N条目由抽屉导入段管理。底图真实图层行插在插槽之间显示,用户层开启底图时自动进第一插槽。 - 为什么不是另一条路:单通道加类型开关会让每个消费点都先判断「这次导入是哪种」,两条通道各自的数据模型都简单。
- 影响:预设切换按 prevMembership 快照还原;移除或关闭底图的单条撤销会连带驻留回滚;新建图层插入位定在最后一枚底图层之后。
- 来源:
core/config/config.model.ts头注释、features/basemap-presets.ts。
ADR-17 表达式内核 AST 化(双实现切换后删旧)
- 背景:属性值的解析与序列化原是手写直析实现,嵌套表达式编辑(构建器会话)需要结构化 AST 和命令式 reducer;上游官方包不提供满足双向编辑需求的开源内核,评估过、确认过才动手。
- 决定:E1 至 E5 建
core/expression/AST 内核:89 算子注册表(旧关键词 83 全覆盖加 spec 新增 6)、能力合成纯函数、四层 advisory 类型检查、官方 createExpression 包装的求值器、22 命令纯函数 reducer 的会话层。E6 以USE_AST_INTERNAL双实现开关加 G7 逐值相等守卫翻切,全门禁绿后删旧实现;G8 词表链恒等守卫(config 到 formValue 到 AST 再回到 formValue 与 config,恒等)钉住长期等价;R6 起补运行语义等价门禁(官方求值对账)。公共 APIparsePropertyValue/serializeEditable逐字不变,40 余消费方零改动;升级链的表达式构造全部经 AST 工厂,禁止手写表达式数组。 - 为什么不是另一条路:直析实现上叠补丁意味着每个新形态都在解析器里加特例,终有一天加不动;一次性删旧换新在没有逐值相等守卫时不可验证。
- 影响:非规范插值子句(如
['linear', 5])为已知降级面,已备案。 - 来源:
core/expression/、design/README.md+design/expression-engine.md、tests/app/core/config/g8-config-chain.test.ts、roadmap E1 至 E6 条目。
ADR-19 样式 diff 单点同步(增量下发,不整份 setStyle)
- 背景:每次编辑后整份 setStyle 会闪屏、丢相机状态,且在几百图层的样式上开销不可接受。地图必须是被驱动的投影,不是第二个真源。
- 决定:
StyleStore每次变更后由diffStyles产出StyleDiff(删除、新增、移动、逐层 paint/layout/filter/zoomRange 变更、根级标志),map.service按这份增量单点同步地图,rAF 合帧把同帧多次变更合成一次 apply。StyleDiff 类型与 diff 纯函数住在core/map/style-diff.ts(2026-10-16 简化轮 S1 从 style-store 头部搬出):map 子树只需要这份类型与函数,不必反向依赖 store 本体,这斩断了全仓唯一的 core 级模块环。 - 为什么不是另一条路:整份 setStyle 视觉与性能双输;地图反向驱动样式会让真源分裂成两份,撤销栈无法对账。环的另一种解法(setStyleGetter 回调)运行时已存在,但那是绕,类型搬家才是治。
- 影响:性能实测(bench):500 层 update 全管线约 6ms、diffStyles 线性约 2.3ms;新样式根属性的接入点是 StyleDiff 加标志位加 applyDiff 三点联动(目录契约)。
- 来源:
core/map/style-diff.ts头注释、core/map/map.service.ts、roadmap M0 节、SIMPLIFICATION-PLAN §2.1。
ADR-20 简化重构:先搬家与单源化,拆大件单独立项
- 背景:2026-10-16 全量读码体检(
editor/docs/refactor/SIMPLIFICATION-PLAN.md)结论:架构骨架健康(分层、store、纯函数层、测试纪律都在),问题集中在少数巨型文件(stops-editor 四千余行、layer-list 近四千行)、一条模块环、一批复制粘贴的守卫代码(「样式已受保护」toast 逐字重复 24 处)、注释里数百处需求编号。 - 决定:按价值风险比分层执行。本轮只做低风险移动与单源化:S1 StyleDiff 搬家斩环(见 ADR-19);S2 style-store 尾部约 700 行底图交付纯函数移到
core/config/basemap-delivery.ts(上帝文件从 2851 行降到约 2100 行,作者链可独立单测);S3 SpecService 静态表外移core/spec-tables.ts(974 降到 392 行,数据归数据、逻辑归逻辑);S4 锁定守卫单源化为UiService.notifyStyleLocked()加STYLE_LOCKED_TOAST常量,改文案只动一处。S5(sprite 回落链收敛)、S6(注释去编号化收尾)与 N1 至 N8(拆 stops-editor、拆 layer-list、config-codec 三分、ownConfig 收口等)排下一轮,动静大的拆分单独立项,不与注释清理混一个 diff。 - 为什么不是另一条路:一次性全量拆大件收益最大但每个都要配套测试与视觉基线验证,混在一起出问题无法定位;只登记不动手则环与守卫复制的维护成本每天都在付。
- 影响:style-diff.ts、basemap-delivery.ts、spec-tables.ts 三个新纯函数域文件成为后续拆分的样板;N4(ownConfig 17 处手调收口)动事务语义,前置条件是跨文档 undo 对账测试。
- 来源:
editor/docs/refactor/SIMPLIFICATION-PLAN.md全文、core/ui.service.ts、roadmap 简化轮条目。
已收档或并档的决策(保留编号,一句话索引)
| 编号 | 一句话 | 现状 |
|---|---|---|
| ADR-01 | 2026-09-30 被 ADR-15 推翻,原文查 git 历史;残留纪律演化为「subscribe 必配 takeUntilDestroyed」 | |
| ADR-04 | Vite + @analogjs/vite-plugin-angular,不用 angular/cli | 有效;动机与分包细节见 guide/tech-stack 与 build-deploy |
| ADR-05 | 2026-09-30 ADR-15 解禁渐进采纳(仅 theme + utilities,preflight 永不启用) | |
| ADR-06 | ADR-15 解禁 CDK 点状采纳(指令形态优先);Material 仍不引入;R-53 评估结论不迁 | |
| ADR-13 | docs-site 选型 VitePress | 有效;CJK 分词本地搜索与 plantuml 构建期渲染见 docs-site/src/.vitepress/config.ts |
| ADR-14 | (编号未使用) | — |
| ADR-18 | CI 分层门禁(PR 快检 + nightly 全回归) | 有效;分层细节与各门禁清单归 guide/testing |