开发环境选型、迁移与长期治理
“在我机器上能跑”已经变成三种不同的机器
同一个服务在 macOS 开发机上通过,在 Windows 开发机上因文件名大小写失败,在 CI 的 Linux 容器里又因系统库缺失退出。团队为修问题先后加了 Shell 初始化脚本、语言版本管理器和容器配置,后来为了内核一致性引入 VM,又为统一算力接入远程工作区。工具越多,入口越多:有人在宿主机执行测试,有人在容器里执行,有人连接昨天未停止的远程磁盘。每条路径单独都能工作,合起来却没有谁能说明哪条才是可支持的环境。
选型真正决定的是隔离层和责任边界。Shell 只能组织进程环境,包环境能固定更多工具与库,容器增加文件系统和进程隔离但共享宿主内核,VM 拥有独立内核,远程工作区再把计算、存储、网络和身份移到平台侧。层级越高,不会自动越可复现;它只是把更多输入转成可声明对象,同时引入新的平台管理服务、费用、凭据和退出责任。
先用故障事实决定需要隔离哪一层
不要从产品名称开始。先在失败机器和成功机器各运行一次环境探针,把差异落到进程、工具、用户态文件系统、内核、硬件或网络身份。下面的脚本不读取秘密,只输出可公开比较的事实;项目可按语言补充编译器、动态库和数据库客户端版本。
#!/usr/bin/env bash
set -euo pipefail
printf 'os='; uname -s
printf 'arch='; uname -m
printf 'kernel='; uname -r
printf 'shell=%s\n' "${SHELL:-unknown}"
printf 'locale=%s\n' "${LC_ALL:-${LANG:-unknown}}"
printf 'timezone='; date +%Z
rm -f .EnvCaseProbe .envcaseprobe
touch .EnvCaseProbe
printf 'case_sensitive='
if test -e .envcaseprobe; then echo no; else echo yes; fi
rm -f .EnvCaseProbe .envcaseprobe
git --version
printf 'source=%s\n' "$(git rev-parse HEAD 2>/dev/null || echo no-git)"如果差异只在 PATH、环境变量和命令别名,Shell 层已经足够;如果语言运行时、CLI 或用户态系统库漂移,需要包环境;如果进程树、文件系统和依赖服务互相污染,容器更合适;如果问题依赖内核模块、系统服务、文件系统语义或强隔离,进入 VM;如果本地硬件不足、数据不能落地、网络入口必须集中或创建销毁需要审计,再考虑远程工作区。发现 uname -r 不同却企图用容器消除内核差异,就是选错隔离层。
把决策输入提交到仓库,避免它只留在会议结论里:
# environment-decision.yaml
schema: 1
supported_platforms:
- linux-amd64
required_isolation:
process_environment: true
toolchain_and_system_libraries: true
filesystem_and_process_tree: true
kernel: false
dedicated_compute: false
data_policy:
source_may_leave_device: true
production_data_allowed: false
network:
private_dependencies_required: true
inbound_ports: "owner-only"
lifecycle:
max_interactive_start_seconds: 180
disposable: true
offline_recovery_required: falsesupported_platforms 决定构建矩阵,不能写成“主流系统”;五个 isolation 字段决定最低层级。source_may_leave_device 为假时,SaaS 远程工作区即使体验更好也不能进入候选。production_data_allowed 默认应为假,开发便利不能替代数据授权。max_interactive_start_seconds 是演示值,团队应从现有启动分位数和等待成本确定;disposable 为真意味着个人修改必须回到 Git 或外部持久存储,工作区本身不能成为唯一副本。
用同一项目探针试跑五种候选
候选环境必须执行同一份环境探针合同和项目探针合同。前者比较环境事实,后者完成一次真实依赖安装、编译、测试或服务请求。仓库应实现稳定的进入、诊断、项目验证和销毁入口,并让入口名称映射到真实脚本或平台适配器:
tools/
<environment-diagnostics>
<project-verification>
<environment-entry>
<environment-destroy>
environment-decision.yaml进入接口根据团队当前支持的模式启动环境,销毁接口只清理本次创建且能由资源 ID 证明归属的对象。项目探针必须检查功能结果,例如启动服务后请求健康端点并断言响应字段,不能只检查运行时版本。探针输出包含模式、源码提交、环境声明摘要、平台、开始耗时和结果,但不包含 token、内网地址或用户目录。
Shell 适合把已经一致的机器变得可预测
Shell 方案的启用成本最低。创建一个严格模式脚本,显式计算仓库根目录并只设置项目需要的变量:
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export PATH="$repo_root/.tools/bin:$PATH"
export APP_ENV=development
exec "$@"把这段脚本保存为项目实际入口并实现两类探针后,验收时才执行它们。验收条件还包括子进程退出后 APP_ENV 不残留。这里的 PATH 顺序决定项目工具是否覆盖系统工具,set -u 会把缺失变量提前暴露,exec 保留下游退出码。Shell 不固定编译器和系统库,也隔离不了后台服务;若两台机器的工具版本、动态库或文件系统语义不同,继续堆初始化脚本只会把差异藏得更深。
反向验收可在试验目录的 PATH 前放一个假的 git,让它输出错误版本。环境探针应明确失败并显示实际命令路径;如果仍通过,说明探针只检查“命令存在”,没有检查来源和版本。清理时只删除试验目录中的假命令并重新打开 Shell,不修改用户全局 profile。
包环境适合固定工具链与用户态依赖
当差异进入运行时和系统库,可用 Nix devShell、Guix shell 或团队批准的包环境把工具集合与锁文件纳入仓库。以 Nix 为例,安装完成后由项目声明提供入口:
Nix 候选的真实入口应来自项目已经提交的 flake.nix 与 flake.lock,由团队在其中实现项目探针后再运行 nix develop,不能把一个尚不存在的探针路径写成已验证命令。验收条件是新 Shell 看到锁定工具,退出后宿主 profile 不被替换。关键字段不是“用了 Nix”,而是 flake input 的锁、目标 system、devShell 输出和 substituter 信任。包环境仍使用宿主内核、网络、用户 HOME 与很多文件系统行为;应用需要内核功能、完整 init system 或隔离后台进程时,它不够。
正向验收在一台未安装项目工具的干净机器从已提交的 flake 进入环境;反向验收请求一个声明未支持的目标 system。前者应从锁恢复工具,后者应在进入环境前明确失败。若错误平台悄悄回退到宿主工具,项目包装器应拒绝,而不是让“能运行”掩盖环境身份丢失。
容器适合固定用户态文件系统与进程树
容器候选从固定 digest 的基础镜像开始,并把源码挂载、缓存和凭据分开。安装 Docker 或兼容引擎后先验证 daemon,再构建试点镜像:
只有项目已经提供可审查的 Containerfile、固定 digest 的基础镜像和真实探针后,才生成具体的 docker build / docker run 命令。运行合同至少包含只读根文件系统、受限临时目录、显式源码挂载、固定工作目录、最小网络和 --rm 清理语义;不要照抄并不存在的镜像名或探针路径。只读根文件系统用于暴露误写,临时文件系统承载受控写入,bind mount 让代码修改留在宿主,禁网试验用于证明探针是否可离线。验收证据是项目行为一致且退出后无运行实例。需要下载依赖时应在构建阶段或明确的缓存代理中完成,运行阶段开放网络也要记录目的和出站范围。
容器共享宿主内核,挂载 Docker socket 等同于把宿主控制权交给容器,bind mount 也会把宿主文件权限和大小写语义带进来。反向验收可移除所需只读 mount,探针应报“输入缺失”;若它从镜像旧层里找到同名源码并通过,说明构建上下文污染了运行结果。清理只按本次构建记录的容器 ID 和镜像 digest 删除试点对象,不执行会影响其他项目的全局 prune。
VM 适合内核、系统服务和强隔离差异
当项目依赖特定内核、systemd、eBPF、文件系统、VPN 客户端或与宿主隔离的守护进程,VM 才能把内核纳入环境。Vagrant 是一种可审查的试点入口;安装 Vagrant 与一个受支持 provider 后先确认版本,再显式选择 provider 创建机器:
Vagrant 候选必须先提交真实 Vagrantfile,锁定 box 版本或校验和,并选定本机已经安装的 provider;尖括号 provider 占位符不能当作 Shell 命令执行。provider 决定虚拟化实现和可用功能,CPU、内存、磁盘和共享目录配置直接影响容量与文件语义。Vagrant 在机器创建后会记住 provider,具体行为可查阅 Vagrant provider 用法。验收时记录 VM 与宿主的内核身份、两类探针结果、对象 ID 和 provider 状态;halt 只证明计算停止,磁盘通常仍存在。
反向验收选择一个确认未安装的 provider,创建动作应在生成 VM 前失败并显示 provider 不可用。更危险的反例是共享 HOME、SSH agent 或云凭据目录;即使 VM 隔离了内核,这些共享也能把宿主权限带进去。项目只挂载工作目录,凭据使用短期注入。清理使用项目真实 Vagrant 配置执行销毁,并在 provider 侧按 VM ID 确认实例及专属磁盘消失;只执行 halt 不是退出证明。
远程工作区适合集中算力、网络和数据驻留
远程工作区把模板、provisioner、计算、持久存储、agent 与编辑器连接组成一条生命周期。它适合本地算力不足、私网依赖只能从受控网络访问、源码或数据必须留在指定区域、以及创建删除必须集中审计的场景。它不适合离线开发、网络延迟敏感、平台不可退出或数据政策不允许上传源码的团队。
启用试点时,先创建最小权限的模板发布身份和工作区运行身份;发布身份只能更新试点模板,运行身份只能读取试点仓库、目标网络和项目级秘密。模板固定环境声明摘要、计算规格、磁盘、允许端口、空闲停止和删除策略。创建一个无真实数据的工作区,连接后运行两个项目探针,再停止、重启和删除。
远程候选必须直接使用所选平台的真实 API 或受版本控制的适配器,禁止虚构统一的 workspace CLI。适配器合同至少覆盖创建、执行探针、停止、启动、删除和按对象 ID 查询,并保存请求 ID、模板摘要和平台返回状态。验收证据包括创建记录绑定模板版本与源码提交、停止后计算计费状态与持久盘状态分离、重启后声明摘要不变,以及删除后控制面和底层资源查询都不可用。平台若支持空闲停止和休眠删除,应在试点创建前启用;例如 Coder 的 Workspace Scheduling 区分 autostop、activity detection 与 dormancy,团队需要确认哪些能力属于所用版本,不能把 UI 里看见的选项当成已对所有旧工作区生效。
反向验收撤销试点身份的仓库读取权限,再创建工作区。正确结果是 clone 或初始化阶段明确拒绝并清理失败资源;具体状态码由所选平台 API 定义,不能硬编码为所有产品都返回 403。若平台回用了带源码的旧快照而成功,说明预构建绕过了最新授权,必须停用该快照并审查来源。另一个反例是删除后对象 ID 仍能启动,这说明删除只处理控制记录,底层计算或磁盘尚未销毁。
选型结论来自最低充分隔离,不来自层级崇拜
当 Shell 与包环境已经能让受支持平台通过探针,容器增加的构建时间、磁盘和 daemon 权限可能没有回报。当容器无法消除内核差异,继续调整 layer 也不会得到 VM 的隔离。当 VM 已满足内核和数据要求,远程平台只有在集中网络、算力弹性、审计或本地设备风险带来明确收益时才值得引入。
决策记录应写出被观察到的故障、最低隔离层、仍逃逸的输入、项目探针、回退入口和重新评估信号。硬件加速是典型信号:容器能透传 GPU 但依赖宿主驱动,VM 可能需要直通,远程工作区可能提供匹配规格但引入排队和费用。没有任何一层能用一个 lockfile 同时固定驱动、硬件、区域服务与外部网络行为。
候选进入试点前先冻结一份至少覆盖两个发布周期的 A 轨基线。不能拿 B 轨的热缓存与 A 轨的冷启动比较,也不能把业务测试失败算成环境失败。每条记录都要带平台、缓存状态、项目规模分桶和模板代际,才有资格汇总分位数:
# environment-baseline.yaml
window_days: 28
segments: [platform, cache_state, project_size]
slo:
create_success_ratio: 0.98
interactive_start_p95_seconds: 180
project_probe_success_ratio: 0.99
orphan_resource_ratio: 0
cost:
currency: CNY
developer_hour: "<team-approved-loaded-cost>"
dimensions: [compute, storage, snapshot, egress, license, support_hours, wait_hours]数值只是决策文件的形态,实际阈值来自团队基线和业务预算。create_success_ratio 只统计平台成功创建且 agent 可连接,project_probe_success_ratio 才统计项目行为;把两者混合会让代码错误掩盖平台退化。interactive_start_p95_seconds 从用户发起到可执行第一条项目命令,不是控制面返回对象 ID。orphan_resource_ratio 的分母是本批次请求创建的对象,分子是结束保留期后仍有计算、磁盘、快照、IP 或凭据残留的请求;它不是“控制台看不到工作区”。
双轨迁移从影子执行开始,而不是一次切换所有人
迁移先把旧入口标记为 A 轨,新入口标记为 B 轨,两条轨道都消费同一提交和同一项目探针。A 轨继续承担日常开发,B 轨先做影子执行,不写共享环境、不发布制品、不使用生产数据。每次执行记录环境声明摘要、平台、创建耗时、探针耗时、退出码、失败分类、残留资源和估算成本。
{
"mode": "B",
"source_revision": "<commit-sha>",
"environment_digest": "sha256:<digest>",
"platform": "linux-amd64",
"create_seconds": "<measured>",
"probe_seconds": "<measured>",
"result": "<pass|fail>",
"failure_class": "<classified-or-null>",
"residual_resources": "<measured>"
}第一批选择能容忍失败、依赖代表性足够且愿意反馈的项目维护者。B 轨连续产生稳定证据后,第二批让一部分真实任务默认进入 B 轨,但 A 轨保持可用;第三批才把 B 轨设为默认,把 A 轨降为显式回退。迁移期间禁止两轨共享可写包缓存、同一后台数据库和同一个持久 HOME,否则 B 轨通过可能只是借用了 A 轨状态。
批次不是按人数平均切片,而是按风险单元推进。先覆盖一种受支持平台、一个数据等级和一类代表性项目,再逐步增加平台、网络区域、持久化需求与硬件规格;每增加一个维度,都重新计算样本和停止阈值。状态只能按 shadow -> opt-in -> default -> retire-old 前进,任何状态都可进入 halted;halted 只能在故障 fixture 重现、修复、反向验证和 owner 放行后开启新批次,不能原地修改统计窗口继续推进。
批次执行器应调用 A、B 两个真实适配器,分别保存结构化探针结果,再交给版本化比较规则。入口脚本随仓库提交,平台 API 版本与适配器版本进入运行记录;不存在的通用命令不能出现在操作手册中。比较器只忽略明确列出的耗时、临时路径和无语义日志顺序,必须比较工具身份、构建摘要、测试结果和服务行为。把完整日志直接 diff 会制造噪声;把所有差异都忽略又会让漂移失去证据。比较规则上线前用一对故意不同的 fixture 做拒绝验收,并保存非零退出和差异字段。
停止阈值要在迁移前写进执行器
“有问题再回退”太晚。每个批次应有自动停止条件:B 轨出现未授权数据访问、真实凭据落盘或删除后资源仍可访问,立即停止;同一故障分类连续出现并超过团队设定样本比例,暂停扩批;创建耗时或失败率持续劣于 A 轨预算,不再增加用户;无法解释的项目结果差异出现一次就冻结默认切换。
# migration-policy.yaml
batch: pilot-2
max_users: 20
stop:
security_policy_failures: 0
unexplained_probe_differences: 0
residual_resources_after_delete: 0
create_p95_regression_ratio: 1.50
environment_failure_ratio: 0.05
observation:
min_completed_runs_per_track: 30
consecutive_windows: 2
rollback:
default_mode: A
preserve_failed_environment_hours: 4这些数值是演示值,不是通用承诺。安全失败、无法解释的行为差异和删除残留不等待样本窗口,首次出现就停止;性能与可靠性阈值则要满足最小样本数,并连续跨过约定窗口才触发,避免单次冷启动误报。create_p95_regression_ratio 必须和同平台、同缓存状态的 A 轨基线比较;environment_failure_ratio 要排除项目测试本身失败,再按 DNS、权限、容量、模板和平台故障分型。preserve_failed_environment_hours 给排障留证据,但到期必须销毁,且保留期间不得继续计入普通可用工作区。
执行器检测到阈值后进入 halted 状态:停止新建 B 轨、禁止扩大批次,并把尚未完成默认切换的流量留在 A 轨;如果 B 已是默认,则按预演过的回退条件切回 A。它不自动删除失败对象,而是先导出脱敏日志、模板版本、对象 ID 和声明摘要,再按保留策略清理。修复后从同一提交重跑故障 fixture,失败分类归零、清理证明成立并经 owner 批准后才能创建新批次;不能在原批次上改阈值把红灯调绿。
漂移要区分声明漂移、实例漂移和平台漂移
声明漂移发生在仓库期望值与当前模板、镜像、box 或包锁不同。实例漂移发生在工作区创建后手工安装软件、修改系统文件或长期保留 HOME。平台漂移来自宿主内核、provider、runner、远程镜像、网络策略或平台升级,即使仓库没有变化也会出现。
每天比较声明摘要能发现第一类;每次启动运行项目实现的环境探针并与创建基线比较能发现第二类;定期从空状态新建、禁用缓存并覆盖所有受支持平台,才能发现第三类。只检查 Git diff 看不到远端模板被管理员修改,也看不到旧工作区从未消费新模板。
故障证据也不同。声明漂移通常表现为期望 digest 与实际 digest 不同;实例漂移表现为同一 workspace ID 的工具路径或文件摘要变化;平台漂移常表现为同一声明在新 runner 上出现内核、证书、DNS、CPU 指令或挂载行为差异。修复顺序是先冻结新实例,确认权威声明,再决定重建实例、回退平台版本或暂时缩小支持平台。
凭据和敏感数据随着隔离层迁移
Shell 和包环境通常继承用户全部环境变量与 HOME;容器通过 mount、env 或 secret 注入;VM 可能共享 SSH agent 和目录;远程工作区由平台、provisioner、agent 和用户会话分别持有身份。迁移时不能把旧轨的个人高权限 token 原样复制给 B 轨。
发布模板、创建工作区、读取源码、访问依赖和用户调试应是不同身份。机器身份使用短期凭据并按项目限制,用户身份只在交互阶段注入;Fork 和外部贡献代码不能自动获得组织秘密。探针只判断凭据是否存在、权限是否符合预期,不打印值。撤销实验使用试点身份:撤销后旧会话应在令牌过期或主动刷新时失败,新工作区创建立即失败,删除工作区后平台不再保留可用凭据。
数据策略同样需要逐层验证。容器的 bind mount、VM 的共享目录和远程工作区的持久盘都可能让测试数据比进程活得更久。开发环境使用合成或脱敏数据,端口默认仅 owner 可见,日志不记录请求头和连接串。任何模式一旦必须接触受限数据,就要单独定义驻留、访问、审计和销毁证明,不能沿用普通开发环境默认值。
成本模型要把人的等待和闲置资源放在一起
Shell 成本主要是个人维护与差异排障;包环境增加下载、Store 和构建缓存;容器增加镜像构建、registry、磁盘和 daemon 支持;VM 增加镜像、provider 许可、磁盘与本地资源占用;远程工作区增加控制服务、计算、持久盘、快照、出口流量、日志和可能的座席费用。只比较订阅价格会低估本地故障时间,只比较启动速度又会忽略空闲实例。
按项目和模式记录创建次数、冷启动与热启动分位数、失败后重试、活跃时长、空闲运行时长、持久字节、快照字节、网络出口和支持工时。比较时使用同一成本窗口:平台费用加构建与存储费用,再加开发者等待时间和支持工时乘以团队约定的时间成本;迁移收益则是被消除的等待、故障恢复和本地维护成本。远程工作区的停止与删除分开计量:停止可能保留磁盘和快照,删除后日志与备份仍可能按策略保存。预构建也不是免费命中,它把每位开发者重复等待换成集中构建和存储。FinOps 的单位经济口径要求成本绑定到可衡量单位;这里至少同时看“每个成功环境会话成本”和“每个通过项目探针的成本”,避免低价但高失败率的方案看起来更优,可参考 FinOps Unit Economics。
每个成功会话成本 =
(计算 + 存储 + 快照 + 出口 + 许可 + 支持工时成本 + 等待时间成本)
/ 成功进入并完成项目探针的会话数分母必须排除平台创建成功但项目探针失败的会话,同时单独报告失败成本,不能通过丢弃失败请求美化单位成本。迁移期还要把 A、B 双轨并行费、培训、适配器开发和退出清理计入一次性迁移成本;达到稳态后再与 A 轨基线比较。成本超预算是否停止扩批,必须在迁移策略里与可靠性阈值同级,而不是月底账单出来后再追责。
容量保护应落到入口。每个用户和项目限制并发工作区、CPU、内存、磁盘和 GPU;创建队列达到保护阈值时拒绝或降级规格;空闲停止由服务端执行,不能依赖用户记得关机;失败创建设置自动清理,但保留足够的脱敏诊断日志。成本异常必须能追到 owner、模板版本和资源 ID,不能只看到一张组织总账单。
owner 负责的是环境产品,不是某个配置文件
一个可支持环境至少需要明确的服务 owner、每个业务项目的接入 owner,以及安全和成本的咨询入口。服务 owner 维护支持平台、模板发布、升级批次、状态公告和退出工具;项目 owner 维护项目探针、依赖声明和例外到期;使用者负责把代码提交到权威仓库、报告可复现故障并停止不再使用的资源。
长期指标要能触发动作。环境创建成功率下降时按失败分类处理;启动变慢时区分下载、排队、provision、快照恢复和项目初始化;旧模板实例占比上升时强制进入升级批次;无 owner 项目停止创建新工作区;删除证明缺失时停止自动扩容。指标只报总平均值,会把某个平台、某个区域或某类大型项目的问题摊平。
回退不是回到旧电脑,而是恢复受支持入口
B 轨成为默认后,A 轨仍要在约定窗口内执行项目探针。回退包包含旧入口脚本、旧声明摘要、仍受信的包或镜像身份、数据迁移方向和凭据撤销顺序。若 B 轨产生了只能在新环境读取的本地状态,回退前先导出到中立格式并校验;不能依赖复制整个 HOME 或磁盘快照,因为那会把实例漂移一起带回去。
回退验收从一个无本地修改的 B 轨实例开始:提交或导出工作成果,停止 B 轨,使用 A 轨从同一提交运行项目探针;只有行为结果一致,才删除 B 轨对象并撤销其临时凭据。若 A 轨已经无法从空状态创建,它就不是回退路径,只是一台尚未坏掉的旧机器。
退出证明决定旧轨能否真正退役
迁移完成不是“多数人已经使用 B 轨”,而是 A 轨不再承载唯一能力或数据。先冻结 A 轨新建,观察是否仍有合法使用;再导出必须保留的声明、审计记录和中立格式数据;随后撤销模板发布身份、运行身份、registry 读取权和网络入口;删除 VM、工作区、持久盘、快照、预构建与项目级缓存;最后从一台干净机器验证 A 轨命令无法再创建资源,而 B 轨仍能从仓库声明重建。
删除证据至少能关联对象 ID、删除动作、执行身份和控制面及底层资源的查询结果。平台删除 API 返回成功但底层磁盘仍存在,不算退出;VM 实例从管理界面消失但其专属磁盘或快照仍存在,也不算退出;共享基础 box 或镜像是否删除应由独立保留策略决定,不能把它与实例销毁混为一谈。旧 token 未撤销,即使没有实例也保留了重建通道。由于审计日志本身可能包含仓库和用户信息,长期保存时只保留必要字段并按访问策略脱敏。
资源核销要以迁移批次清单做集合差,而不是抽查几个工作区。创建时记录控制面对象 ID、provider 资源 ID、磁盘、快照、缓存 namespace、网络入口和身份授权;退役时逐类查询,结果只能是“按保留策略继续存在且有 owner/到期日”或“已删除且不可再访问”。出现未知对象、无 owner 对象或查询权限不足,都不能关闭批次。共享镜像和缓存即使保留,也要移除 A 轨专属写权限,并证明 B 轨不依赖旧发布身份。
长期责任按控制对象分开:平台 owner 对模板、控制服务、SLO 和退出工具负责;安全 owner 对身份、数据边界和撤销时限负责;FinOps owner 对分摊口径、预算和异常负责;项目 owner 对探针、声明与例外到期负责。责任表必须绑定值班入口和替补人,不能只写团队名称。季度演练至少包含一次 A 轨回退、一次凭据撤销、一次禁用缓存冷建和一次批次资源全量核销;任何一项无法完成,都说明 B 轨尚未形成可长期支持的环境产品。
最后安排两项退出验收:新开发者在全新设备、无旧缓存条件下从 B 轨声明创建环境,运行项目探针,停止并删除;随后模拟平台不可用,从导出的声明、源码和中立数据恢复到预先约定的替代层。前一项用于证明日常入口可持续,后一项用于证明供应商、provider 或控制服务不是不可退出的唯一真相。只有两项都留下可复核证据,环境迁移才从“换了工具”变成可长期治理的工程能力。
