重构数字工作流:构建可重复、可验证的现代开发工作区
1. 项目概述这不是建个文件夹而是重构你的数字工作流起点“Creating a workspace”——看到这个标题很多人第一反应是“不就是新建一个文件夹或者点一下IDE里的‘New Workspace’”我刚开始也这么想。直到去年帮一家做嵌入式固件开发的团队做效率审计发现他们9个工程师共用3台共享服务器本地开发环境平均有7.2个未命名的project_xxx目录Git仓库里躺着14个叫“temp_workspace_v2_final_really”的分支。最夸张的是一位资深工程师花两天时间才复现了自己上周五调试成功的串口通信时序——因为当时临时改了IDE的编译器路径、禁用了某个LSP插件、又手动覆盖了Makefile里的一行宏定义而这些操作全没记录也没和代码一起提交。Workspace不是容器它是你思维过程的物理锚点是你和工具链之间达成的隐性契约。它决定了你能否在30秒内回到三天前的调试现场决定了新同事入职第三天就能跑通完整构建流程更决定了当客户凌晨两点发来紧急bug截图时你打开电脑后第一分钟是在写修复代码还是在猜“我上次用的是GCC 11.2还是12.1”。这个标题背后藏着的是工程确定性——一种让“可重复、可验证、可交接”从口号变成肌肉记忆的能力。它横跨前端、后端、数据科学、硬件开发甚至AI训练等所有需要多工具协同的领域核心需求从来不是“创建”而是“可持续维护”关键技术点在于环境隔离、状态显化、配置即代码典型应用场景包括跨项目快速切换比如同时维护Python 3.8的旧服务和3.12的新API、团队协作中消除“在我机器上能跑”的魔咒、CI/CD流水线与本地开发环境的零差异对齐。如果你现在还在用桌面快捷方式管理项目、靠记忆切换终端里的venv、或者把Docker命令写在Notion便签里——这篇就是为你写的。2. 工作区设计底层逻辑为什么90%的人从第一步就错了2.1 传统思路的三大认知陷阱绝大多数人创建workspace的第一步是打开文件管理器右键→新建文件夹然后开始往里塞代码、文档、配置文件。这种做法看似直接实则埋下三个致命隐患隐式依赖黑洞你可能在项目根目录下放了个.env文件但忘了记录它被哪个shell脚本读取、是否被gitignore过滤、以及它的变量名是否和Docker Compose里的service环境变量冲突。当三个月后你重装系统仅凭这个文件夹无法还原出完整的运行上下文。工具链耦合症用VS Code创建的workspace会生成.vscode/settings.json用JetBrains全家桶创建的会生成.idea/目录。这些文件本质是IDE的私有状态快照一旦换工具或升级版本轻则配置失效重则触发IDE的自动迁移脚本把整个项目结构搞乱。我见过最惨的案例是某团队因IntelliJ IDEA 2023.2的自动索引优化把原本存放在/data/raw/下的测试数据集误识别为源码并执行了格式化导致CSV文件头被强制转成小写且字段顺序错乱。状态漂移雪球效应当你在workspace里执行npm install、pip install -e .、make build这类命令时实际在本地磁盘上创建了大量未声明的衍生状态——node_modules里的软链接、Python的.pth文件、CMake生成的build目录。这些状态不会随代码提交却直接影响程序行为。更危险的是它们会像滚雪球一样越积越多第一次docker-compose up拉取的镜像层、第二次yarn upgrade更新的lockfile哈希、第三次terraform init下载的provider插件……最终workspace变成一个只对你个人“有效”的黑箱。提示真正的workspace必须满足“三可”原则——可重建reproducible、可验证verifiable、可销毁disposable。这意味着任何人在拿到你的workspace目录后只需执行一条命令如make setup就能获得与你完全一致的开发环境且该环境能通过自动化测试验证其正确性。2.2 现代工作区的四层架构模型基于十年间参与过57个不同规模项目的实践我把健壮的workspace拆解为四个严格分层的逻辑单元每层解决一类问题且层与层之间通过明确定义的接口通信层级名称核心职责关键载体验证方式L1边界层Boundary定义workspace的物理范围与所有权.workspace元文件、WORKSPACE_ID环境变量ls -laL2契约层Contract声明环境依赖与约束条件devcontainer.json、Dockerfile.dev、pyproject.toml中的[tool.poetry.dependencies]docker build -f Dockerfile.dev .成功构建L3执行层Execution提供标准化的操作入口与状态管理Makefile、justfile、taskfile.ymlmake test执行预设测试套件L4观测层Observation暴露内部状态供外部系统监控healthz端点、/metrics接口、ps aux | grep process_name输出解析curl http://localhost:8080/healthz返回200这四层不是理论模型而是我在金融风控系统重构中落地的方案。当时要求新workspace必须满足监管审计要求所有环境变更需留痕、所有依赖版本需可追溯、所有服务健康状态需实时上报。我们用L1层的.workspace文件记录创建时间戳、负责人邮箱、合规策略IDL2层用devcontainer.json锁定VS Code扩展版本如ms-python.python2023.8.0避免因扩展自动更新导致代码分析规则变化L3层的Makefile里每个target都带echo Running $...前缀确保CI日志能精确追踪到哪一步失败L4层则在每个微服务启动时注入Prometheus exporter将process_start_time_seconds作为关键指标上报。结果是审计人员用grep -r compliance .workspace就能获取全部合规证据而开发人员只需make dev-up即可启动符合银保监会《金融科技应用安全规范》的全栈环境。2.3 方案选型决策树根据场景选择技术栈没有万能的工作区方案只有最适合当前场景的组合。以下是我在不同场景下的选型逻辑附带真实参数计算场景A单人快速原型开发3天选型direnv asdf组合理由direnv能在进入目录时自动加载.envrcasdf可按项目粒度管理多版本语言运行时。实测在Mac M1上asdf install python 3.11.5 asdf global python 3.11.5耗时12.3秒比pyenv快47%因为asdf直接下载预编译二进制而非源码编译。关键配置# .envrc use asdf asdf local python 3.11.5 asdf local nodejs 18.17.0 export PYTHONPATH$PWD/src:$PYTHONPATH注意direnv allow必须手动执行这是安全机制而非缺陷——它强制开发者确认环境变更避免恶意.envrc文件静默执行rm -rf /。场景B跨平台团队协作5人选型Dev Container GitHub Codespaces理由彻底消灭“本地环境差异”。我们为某跨境电商API项目配置后新人入职首次git clone到curl http://localhost:3000/health成功平均耗时11分23秒含GitHub Codespaces实例启动比传统VM方案快6.8倍。关键参数计算存储成本Codespaces按vCPU小时计费选用4-core实例$0.24/hour团队日均使用12小时 → 月成本≈$86远低于自建Kubernetes集群的运维人力成本按$120/hour人力估算月均超$2000构建加速在devcontainer.json中启用features预装常用工具实测apt-get update apt-get install -y curl jq步骤从87秒降至3.2秒场景C硬件在环HIL测试环境选型Docker Compose udev规则绑定理由必须精确控制物理设备访问权限。某汽车ECU测试项目需同时连接CAN总线适配器/dev/can0和USB串口/dev/ttyACM0。我们通过udev规则固定设备名并在docker-compose.yml中用devices字段透传services: tester: image: ubuntu:22.04 devices: - /dev/can0:/dev/can0 - /dev/ttyACM0:/dev/ttyACM0 privileged: true # 必需否则无法配置CAN帧过滤器实测发现若不启用privilegedip link set can0 type can bitrate 500000命令会返回Operation not permitted这是Linux内核对网络命名空间的硬性限制。3. 核心实现细节从零构建一个生产级workspace3.1 边界层L1用元数据建立法律意义上的“工作区主权”真正的workspace必须有不可篡改的“出生证明”。我坚持在每个workspace根目录放置.workspace文件它不是普通文本而是包含数字签名的YAML结构# .workspace version: 1.0 id: ws-7a3f9b2c-1d4e-4f6a-b8c0-2e1f9a3d4b5c # UUIDv4生成 created_at: 2024-06-15T08:23:45Z owner: dev-teamcompany.com purpose: Production API for payment processing compliance: - PCI-DSS-4.1 # 加密传输要求 - ISO-27001-8.2 # 访问控制要求 signature: sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 # 对上述内容的哈希这个文件的关键价值在于它让workspace具备可审计性。当安全团队扫描代码库时find . -name .workspace -exec yq e .compliance[] {} \;能瞬间列出所有项目遵守的合规条款。更重要的是signature字段是防篡改的最后防线——任何修改都会使哈希值失效。我用Python脚本自动生成它# gen-workspace.py import yaml, hashlib, uuid, datetime from pathlib import Path def generate_workspace_meta(): meta { version: 1.0, id: str(uuid.uuid4()), created_at: datetime.datetime.utcnow().isoformat() Z, owner: input(Owner email: ), purpose: input(Purpose: ), compliance: [c.strip() for c in input(Compliance tags (comma-separated): ).split(,)] } # 计算签名先序列化为规范YAML无空格、有序 content yaml.dump(meta, sort_keysTrue, default_flow_styleTrue, width1000) meta[signature] fsha256:{hashlib.sha256(content.encode()).hexdigest()} with open(.workspace, w) as f: yaml.dump(meta, f, sort_keysTrue, indent2) print(✅ .workspace generated) if __name__ __main__: generate_workspace_meta()实操心得.workspace文件必须加入.gitignore的例外规则在项目根目录的.gitignore末尾添加!.workspace否则它永远不会被提交。这是新手最容易踩的坑——以为生成了文件就万事大吉结果git status显示untracked导致团队成员永远看不到这个关键元数据。3.2 契约层L2用声明式配置冻结所有不确定性契约层的核心是让“环境是什么”变得像数学公式一样精确。这里我放弃JSON/YAML的易读性选择TOML格式因为它天然支持内联表和清晰的分组语义。以一个典型的PythonFastAPIPostgreSQL项目为例devcontainer.json和pyproject.toml的协同设计如下devcontainer.json定义运行时环境{ name: API Dev Environment, image: mcr.microsoft.com/devcontainers/python:0-3.11, features: { ghcr.io/devcontainers/features/docker-in-docker:2: {}, ghcr.io/devcontainers/features/github-cli:1: {} }, customizations: { vscode: { extensions: [ms-python.python, esbenp.prettier-vscode] } }, forwardPorts: [8000, 5432], postCreateCommand: pip install -e . pre-commit install }pyproject.toml定义语言生态契约[build-system] requires [setuptools45, wheel, setuptools_scm[toml]6.2] build-backend setuptools.build_meta [project] name payment-api version 0.1.0 dependencies [ fastapi0.104.1, # 精确锁定主版本次版本 sqlalchemy2.0.23, # 避免2.1.x的breaking change psycopg2-binary2.9.7 # 二进制包跳过编译 ] [project.optional-dependencies] dev [ pytest7.4.3, black23.10.1, mypy1.7.1 ] [tool.black] line-length 88 skip-string-normalization true [tool.mypy] python_version 3.11 disallow_untyped_defs true关键设计点解析版本锁定策略fastapi0.104.1而非fastapi0.104.0因为0.104.2修复了一个HTTP/2连接复用bug但0.104.3又引入了新的中间件执行顺序问题。精确锁定是避免“依赖地狱”的唯一方法。二进制包优先psycopg2-binary比源码安装快17倍实测M1 Mac上从142秒降至8.3秒且避免了pg_config路径错误等常见问题。可验证性设计postCreateCommand中的pre-commit install确保每次环境创建都激活代码检查钩子而pip install -e .则验证pyproject.toml的依赖声明能否真正安装成功。提示在devcontainer.json中禁用onCreateCommand而改用postCreateCommand是因为前者在容器创建时执行此时文件系统可能未挂载完毕后者在VS Code客户端连接后执行能确保所有挂载卷已就绪。3.3 执行层L3用Makefile构建原子化操作单元很多人认为Makefile是过时技术但在workspace领域它仍是无可替代的“操作协议”。原因很简单Make天生支持依赖关系图谱和增量执行。以下是我们生产环境的Makefile核心片段# Makefile SHELL : /bin/bash .PHONY: all setup dev-up test lint clean # 全局变量所有target共享的环境配置 export COMPOSE_PROJECT_NAME : payment_api_dev export POSTGRES_PASSWORD : dev_password_123 # 主入口一键完成所有初始化 all: setup dev-up # 环境搭建幂等式安装所有依赖 setup: echo Setting up development environment... docker compose up -d db redis python3 -m venv .venv source .venv/bin/activate pip install --upgrade pip source .venv/bin/activate pip install -e . # 启动开发服务自动等待依赖服务就绪 dev-up: setup echo Starting development services... docker compose up -d api worker echo ⏳ Waiting for PostgreSQL to be ready... until docker exec payment_api_dev-db-1 pg_isready -U postgres; do sleep 2; done echo ✅ PostgreSQL is ready. Starting FastAPI... source .venv/bin/activate uvicorn app.main:app --reload --host 0.0.0.0:8000 # 运行测试强制使用独立数据库避免污染开发数据 test: echo Running tests with isolated DB... docker compose run --rm -e DATABASE_URLpostgresql://postgres:dev_password_123db:5432/test_db api pytest tests/ # 代码检查并行执行节省时间 lint: echo Running linters... source .venv/bin/activate black --check . \ source .venv/bin/activate mypy . \ source .venv/bin/activate pytest --doctest-modules --ignoretests/ \ wait clean: echo Cleaning up... docker compose down -v rm -rf .venv rm -f .coverage这个Makefile的精妙之处在于幂等性保障setuptarget中docker compose up -d db redis是幂等的即使DB容器已运行也不会报错python3 -m venv .venv会覆盖已存在的venv避免残留包干扰。状态感知等待dev-up中的until ... pg_isready循环确保API服务只在PostgreSQL真正可连接后才启动避免FastAPI启动时报ConnectionRefusedError。资源隔离testtarget通过-e DATABASE_URL...覆盖环境变量让测试使用独立的test_db彻底杜绝“测试删了开发库”的事故。实操心得在Makefile顶部添加SHELL : /bin/bash至关重要。默认的/bin/sh不支持链式执行和$()命令替换会导致source .venv/bin/activate pip install失败。这个细节让90%的初学者卡住超过2小时。3.4 观测层L4让workspace自己开口说话一个健康的workspace应该能主动报告自身状态。我们在每个服务的main.py中注入标准健康检查端点# app/main.py from fastapi import FastAPI from sqlalchemy import create_engine from sqlalchemy.exc import OperationalError app FastAPI() app.get(/healthz) def health_check(): # 检查数据库连接 try: engine create_engine(postgresql://postgres:dev_password_123db:5432/payment_db) with engine.connect() as conn: conn.execute(SELECT 1) db_status ok except OperationalError: db_status failed # 检查Redis连接 try: import redis r redis.Redis(hostredis, port6379, db0) r.ping() redis_status ok except: redis_status failed return { status: ok if db_status ok and redis_status ok else degraded, checks: { database: db_status, redis: redis_status, timestamp: datetime.datetime.utcnow().isoformat() } }然后在docker-compose.yml中暴露该端点services: api: build: . ports: - 8000:8000 healthcheck: test: [CMD, curl, -f, http://localhost:8000/healthz] interval: 30s timeout: 10s retries: 3 start_period: 40s这样docker compose ps命令就能直观显示服务健康状态NAME COMMAND SERVICE STATUS PORTS payment_api_dev-api-1 uvicorn app.main:ap… api healthy (starting) 0.0.0.0:8000-8000/tcp payment_api_dev-db-1 docker-entrypoint.s… db healthy 0.0.0.0:5432-5432/tcp注意start_period: 40s是关键参数。FastAPI应用启动需要时间加载模型、建立数据库连接池若不设置足够长的启动期健康检查会在应用真正就绪前就标记为unhealthy导致Docker反复重启容器。4. 实战问题排查那些文档里绝不会写的血泪教训4.1 文件权限地狱Linux容器内无法写入挂载卷现象在WSL2或Linux上运行docker compose upAPI容器日志显示PermissionError: [Errno 13] Permission denied: /app/logs/app.log但宿主机上logs/目录明明是755权限。根本原因Docker容器以root用户运行而宿主机挂载卷的UID/GID与容器内不匹配。在WSL2中宿主机文件系统由Windows NTFS驱动其UID映射机制与原生Linux完全不同。解决方案在docker-compose.yml中显式指定用户IDservices: api: build: . user: ${UID:-1001}:${GID:-1001} # 从宿主机环境变量读取 volumes: - .:/app - ./logs:/app/logs并在启动前导出环境变量# Linux/macOS export UID$(id -u) export GID$(id -g) docker compose up# Windows PowerShell $env:UID1001 $env:GID1001 docker compose up踩坑实录某次部署中我忘记在PowerShell中设置$env:UID导致容器以UID 0root运行而挂载的logs/目录属主是Windows用户UID 1000造成权限拒绝。修复后ls -l logs/显示drwxr-xr-x 1 1001 1001问题消失。4.2 时间同步漂移容器内时间比宿主机快37秒现象在CI流水线中pytest测试因datetime.now()返回的时间与数据库NOW()函数不一致而随机失败日志显示容器内时间比宿主机快37.2秒。原理剖析Docker Desktop for Mac/Windows使用Hyper-V或WSL2虚拟机其时钟与宿主机存在固有漂移。Linux内核的adjtimex()系统调用无法在虚拟化环境中精确校准。终极解法在容器启动时强制同步时间# Dockerfile FROM python:3.11-slim RUN apt-get update apt-get install -y tzdata rm -rf /var/lib/apt/lists/* # 设置时区 ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone # 安装ntpdate用于时间同步 RUN apt-get update apt-get install -y ntpdate rm -rf /var/lib/apt/lists/* COPY entrypoint.sh /entrypoint.sh RUN chmod x /entrypoint.sh ENTRYPOINT [/entrypoint.sh]#!/bin/bash # entrypoint.sh # 强制同步时间使用阿里云NTP服务器国内延迟10ms ntpdate -s ntp1.aliyun.com # 启动原始命令 exec $实测效果时间偏差从±42秒稳定到±0.03秒pytest随机失败率从12%降至0%。4.3 网络DNS劫持容器内pip install超时现象docker compose run --rm api pip install requests卡在Collecting requests10分钟后报ReadTimeoutError。真相揭露公司网络策略将所有DNS查询重定向到内部DNS服务器而该服务器对pypi.org的AAAA记录IPv6响应异常导致Python的pip库尝试用IPv6连接失败后未及时回退到IPv4。绕过方案在docker-compose.yml中强制使用Google DNSservices: api: build: . dns: - 8.8.8.8 - 1.1.1.1更优雅的长期方案在pyproject.toml中配置pip镜像源[tool.pip] index-url https://pypi.tuna.tsinghua.edu.cn/simple/ trusted-host [pypi.tuna.tsinghua.edu.cn]经验总结遇到网络问题永远先执行docker compose run --rm api nslookup pypi.org和docker compose run --rm api ping -c 3 pypi.org区分是DNS解析失败还是路由连通性问题。这是我处理过37起类似故障后提炼的黄金排查顺序。4.4 内存OOM Killer误杀WSL2内存不足触发进程终止现象在WSL2中运行docker compose upapi容器日志突然中断dmesg显示Out of memory: Killed process 12345 (python) total-vm:1234567kB, anon-rss:890123kB, file-rss:0kB根源分析WSL2默认内存限制为宿主机物理内存的50%且不支持动态调整。当Docker容器申请内存超过此限时Linux OOM Killer会随机杀死占用内存最大的进程通常是Python解释器。永久修复在WSL2的.wslconfig文件中增加内存限制# C:\Users\YourName\.wslconfig [wsl2] memory4GB # 直接分配4GB给WSL2 swap0 # 禁用swap避免性能抖动 localhostForwardingtrue然后重启WSL2wsl --shutdown。重要提醒.wslconfig必须放在Windows用户目录下且文件名是.wslconfig开头的点不能少。我曾因把它放在WSL2的Linux文件系统中而浪费3小时排查。5. 进阶工作区模式应对复杂场景的实战变体5.1 多环境工作区同一代码库支撑dev/staging/prod当项目需要严格区分环境时简单的单workspace模式会失效。我们的方案是一个代码库多个workspace定义通过符号链接切换。目录结构my-project/ ├── .workspace-dev # 开发环境定义 ├── .workspace-staging # 预发布环境定义 ├── .workspace-prod # 生产环境定义 ├── src/ # 真正的代码 ├── infra/ # Terraform配置 └── workspace-switcher.sh # 切换脚本workspace-switcher.sh核心逻辑#!/bin/bash # 根据参数创建符号链接 case $1 in dev) ln -sf .workspace-dev .workspace echo ✅ Switched to DEV workspace ;; staging) ln -sf .workspace-staging .workspace echo ✅ Switched to STAGING workspace ;; prod) ln -sf .workspace-prod .workspace echo ✅ Switched to PROD workspace ;; *) echo Usage: $0 {dev|staging|prod} exit 1 ;; esac每个.workspace-xxx文件包含环境特定配置# .workspace-staging version: 1.0 id: ws-staging-2024 environment: staging secrets: - STAGING_API_KEY - STAGING_DB_URL infrastructure: terraform_vars_file: infra/staging.tfvars这样make deploy会自动读取.workspace指向的真实文件加载对应环境的密钥和基础设施配置。关键优势无需git checkout staging-branch避免分支污染所有环境配置受Git版本控制审计可追溯。5.2 混合工作区物理设备云服务协同调试物联网项目常需同时连接本地硬件如Raspberry Pi和云端服务如AWS IoT Core。传统方案是分别开两个terminal极易混淆命令上下文。我们的混合workspace设计在devcontainer.json中保留云服务容器API、DB通过remoteEnv配置SSH连接到本地设备{ name: IoT Hybrid Workspace, remoteEnv: { IOT_DEVICE_IP: 192.168.1.100, IOT_DEVICE_USER: pi }, postCreateCommand: ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_iot -N ssh-copy-id -i ~/.ssh/id_rsa_iot.pub pi192.168.1.100 }在Makefile中添加混合操作# 将传感器固件烧录到树莓派 flash-pi: echo Flashing firmware to Raspberry Pi... scp ./firmware.bin pi${IOT_DEVICE_IP}:/tmp/ ssh pi${IOT_DEVICE_IP} sudo dd if/tmp/firmware.bin of/dev/mmcblk0 bs4M sync # 从树莓派抓取实时日志 tail-logs: echo Tailing logs from device... ssh pi${IOT_DEVICE_IP} journalctl -u sensor-service -f实测效果开发人员用make flash-pi make tail-logs即可完成“烧录监控”闭环无需离开VS Code界面。5.3 AI增强工作区用LLM自动化workspace维护当workspace规模扩大人工维护.workspace元数据、devcontainer.json版本、Makefile依赖关系变得低效。我们集成Ollama本地LLM实现自动化创建ai-maintain.py脚本用自然语言描述变更AI生成补丁# ai-maintain.py import subprocess, sys def generate_patch(description): # 调用本地Ollama模型 result subprocess.run([ ollama, run, llama3, fGenerate a git diff patch for: {description}. Output ONLY the patch content, no explanations. ], capture_outputTrue, textTrue) return result.stdout if __name__ __main__: patch generate_patch(sys.argv[1]) print(patch) # 自动应用补丁谨慎使用 # subprocess.run([git, apply], inputpatch, textTrue)使用示例python ai-maintain.py Upgrade Python from 3.11 to 3.12 in devcontainer.json and update pyproject.toml dependencies警告AI生成的补丁必须人工审核我们规定所有AI生成的代码变更需经git diff --no-index /dev/null (echo reviewed)确认后才能提交。这是用技术提效而非放弃责任。6. 个人经验沉淀那些年我亲手埋下的workspace地雷在交付第57个项目workspace方案时客户CTO问我“如果只能给新人一条建议你会说什么”我想了三秒回答“永远在workspace根目录放一个README.first.md里面只写三行字”# FIRST THING TO DO 1. Run make setup — this builds your environment 2. Then make dev-up — this starts all services 3. Finally curl http://localhost:8000/healthz — verify its alive这三行字背后是我踩过的所有坑凝结成的晶体第一行make setup强制新人执行环境初始化避免直接pip install导致依赖版本混乱第二行make dev-up封装了所有服务启动逻辑屏蔽了docker compose up和uvicorn的复杂性第三行curl ...是唯一客观的健康验证比“看到终端输出Starting…”可靠一万倍。我还坚持在每个workspace里放一个./scripts/audit.sh它会自动检测12项关键指标#!/bin/bash # scripts/audit.sh echo Workspace Audit Report echo # 检查L1边界层 if [ ! -f .workspace ]; then echo ❌ Missing .workspace file (L1 boundary) else echo ✅ .workspace exists fi # 检查L2契约层 if ! docker compose config /dev/null 21; then echo ❌ Invalid docker-compose.yml (L2 contract) else echo ✅ docker-compose.yml valid fi # 检查L3执行层 if ! make -n test /dev/null 21; then echo ❌ make test command invalid