# 验收与测试规范 > 规范性文档。任何新增需求或修复建议必须在这里增加可自动化或可重复人工执行的验收场景。 - ui-brand-dsh-scholar:主 Header、Sidebar、Standalone 解锁页、浏览器 title、Chat 欢迎语/导出和 Settings 卡片均显示 `dsh Scholar`,可见 UI 不含旧组合品牌 `dsh Research`/`DSH Research`;`Research Kernel` 与 API 路径不受影响;中英文 locale parity 和 standalone 首屏副本校验通过。 - readme-current-product-shot:`README.md` 的产品首图引用 `docs/assets/dsh-scholar-home-zh.png`,并展示当前 DSH 会话内的 `dsh Scholar` 页签、中文 Scholar 首页和 `Open in new page` 入口;界面发生可见变化时同步替换该图片,不能只保留旧截图。 ## 1. 测试层级 | 层级 | 范围 | 原则 | |---|---|---| | Unit | Schema、状态、统计、path、diagnostic parser、i18n | 纯函数和稳定错误 | | Module contract | Kernel、Client、Runner、BFF、UI state | 通过模块接口,用真实 SQLite/CAS | | Integration | DSH Agent composition、standalone sidecar/BFF、SSE、TeX | 多模块真实协议 | | Security | AuthZ、CSRF、路径、容器、日志、self-mod | 阻断发布 | | Recovery | kill -9、lease、stream reconnect、DB/CAS | 不丢状态、不重复执行 | | UI | 两种 locale、keyboard、a11y、responsive | 用户可完成核心任务 | | Scientific | RunSet、Evidence、Claim、引用、负结果 | 防止科研语义漂移 | | Reproduction | Bundle、clean-room、PDF | 从空环境复现 | ## 1.1 两阶段验收策略 - **代码实现阶段**不要求开发机连接真实远端、mTLS CA、完整 DSH host、GPU、浏览器矩阵或生产 Docker/TeX 环境;实现者必须完成生产代码路径、build/typecheck、能运行的 unit/module/static checks,并为不可用环境写出确定的人工步骤、期望结果、失败判据和取证项; - 环境不可用只能产生 `NOT_RUN_MANUAL_PENDING`,不能产生 PASS;它不阻塞后续代码实现、提交或合并,但状态最多为“已实现未验收”,并持续阻止发布; - **人工验收阶段**按 `manual-acceptance.md` 执行。一次有效记录至少绑定 commit SHA、目标环境、操作者、时间、场景 ID、实际结果和证据位置;失败项回到 hardening 的“部分/未实现”,并先内化修复建议再改代码; - CI 是可选的重复执行载体,不是开发实现的前置条件。环境具备后若运行 CI,仍适用零 SKIP、断言数大于 0 和 fail-closed 规则;人工验收不能把未执行场景批量勾选为通过。 ## 2. Project 与 Gate - create-project-defaults:gate-only、预算和 integrity 默认值正确; - transition-revision-conflict:旧 revision 返回 409; - gate-state-cannot-transition:四个 Gate 控制状态返回 422;五种 Gate type 均有独立流程; - agent-cannot-decide-gate:工具目录和 Agent HTTP 无 Decision; - human-principal-durable:Decision 重读仍有 principal/tenant/session; - gate-atomicity:在 target/Gate/Decision/Project/Outbox 每个故障点均无部分提交; - concurrent-decision:两个请求只有一个成功; - budget-gate-resume:只允许 payload 声明的 resume_to。 ## 2.1 结构化 NextAction(GUIDE-01) - next-action-every-phase:17 个 ProjectStatus 每个至少投影一个 schema 合法动作,未知/未来状态退化 `code='unknown'` 的只读动作,不抛错、不生成 mutation CTA; - next-action-code-stable:相同状态输入两次投影产生相同 code/id 集合(确定性机器码); - next-action-required-gaps:缺少前置条件(`approved_contract`、`succeeded_runs`、`proposed_idea`)时动作 `state=blocked` 且 `required` 列出缺失项,满足后转 ready; - next-action-legacy-derivation:`next_actions: string[]` 与 `next_actions_v2` 中非 done 动作的 label 完全一致(终态为空数组),UI/API 旧消费端不受破坏; - next-action-pending-gate:pending gate 产生 gate 决策动作(budget → `budget_resolve`,其余 → `gate_decide`),base 动作已引用的 gate 不重复; - next-action-budget-block:预算超限 + pending Budget Gate → `budget_resolve` `state=blocked`、`required=['budget_headroom']`、`blocking=true`; - next-action-failed-job:失败/retryable 作业产生 `job_retry`(attempts 未耗尽 ready;耗尽 blocked + `repair_decision`,capability=pi); - next-action-phase-flow:每阶段主动作与状态机一致——DRAFT→scope_gate_submit、SCOPED→survey_run、SURVEYING→idea_generate、IDEATING→idea_gate_approve、IDEA_APPROVED/BASELINE_REPRO→contract_register+baseline_reproduce、CONTRACT_APPROVED/EXPERIMENTING→pilot_formal_submit+evidence_verify、EVIDENCE_READY→manuscript_write、WRITING→reviewer_run、REVIEWING→release_bundle+release_gate、RELEASE_READY→release_gate、BLOCKED_GATE→gate_resolve(+budget_resolve)、FAILED→project_stop、ARCHIVED/RELEASED/STOPPED→done; - next-action-revision:动作携带依赖对象 revision(gate 决策=project.revision,run 动作=contract version,idea gate=idea version)。 ## 3. Project 隔离与 Artifact - 相同 idempotency key 在两个项目生成独立 Job; - 相同 Blob 在两个项目有独立 artifact_id 和授权; - 跨项目 Artifact、Terminal、TeX、Evidence 读取返回 404; - binary round-trip 对 PDF/image/random bytes hash 一致; - malformed percent path 返回 JSON 400,不崩溃; - CAS 原子写、重复 put、孤儿 GC、缺失 Blob scan。 ### 3.1 单文件 Artifact Upload(UPLOAD-01 服务端/BFF 层,api-contracts.md §7) - upload-success:真实 `curl -F` multipart(kind + file part)→ 201;artifact_id=`sha256:` 与内容 sha256 一致(hash 由服务端计算,客户端不可声明);artifact GET 回读字节与上传字节完全一致;恰好 32 MiB 的文件上传成功; - upload-too-large:>32 MiB(含 33 MiB 真实请求)→ 413 `payload_too_large`,错误消息含 limit 与实测字节数,不产生 artifact 行;降低 `ResearchKernel.UPLOAD_MAX_FILE_BYTES` 静态上限后同规则生效(可配置上限); - upload-path-traversal:filename=`../…`、绝对路径、NUL、Windows 盘符、含路径分隔符 → 422 `invalid_file_name`(两种分隔符均拒绝,execution-runtime.md §4 同源约束); - upload-multipart-validation:非 multipart Content-Type → 415 `unsupported_media_type`;boundary 缺失/畸形或 body 解析失败 → 400 `invalid_multipart`;缺 file part → 422 `missing_file`;多个文件 part → 422 `multiple_files`;kind 非枚举 → 422 `invalid_kind`;未知 project → 404 `project_not_found`(无枚举); - upload-hash-binding:stage 与 finalize 各复算一次 sha256 并核对;staged 字节被篡改(hash 不匹配)→ 422 `stage_corrupted` 且 staged 文件回滚、无 artifact 行、无 CAS blob; - upload-staged-finalize:stage 后只存在会话 id 命名的 `stage_.part/.json`(CAS root 下 staged-uploads/),无 artifact 行;finalize 原子 rename 进 CAS blob 槽并插行;缺 stage → 409 `artifact_stage_expired`; - upload-idempotent:同 project + sha256 + file_name 重传 → HTTP 200 `reused: true`,返回原 artifact_id,artifact 行数与 CAS blob 数不增长(kernel 与 BFF 双面); - upload-gc:`cleanupStagedUploads(maxAgeMs)` 只清理超过 grace period 的 staged 文件(.part+.json),fresh stage 保留;已 finalize 的 blob 永不被 GC 触碰; - upload-bff-passthrough:multipart 经 standalone BFF(bearer + 同源 Origin)原样透传 → 201,字节回读一致;BFF 无 bearer → 401、foreign Origin → 403、未知/跨项目 → 404(membership fail-closed 与其它 /v1 写一致); - 浏览器拖拽上传 UI、intake quarantine/scan UI 属浏览器层验收(Playwright 类环境不可用时如实记录为剩余,不宣称关闭)。 ## 4. Runner 与 Manifest - formal/baseline/pilot/reproduce/latex-compile 拒绝 subprocess; - smoke 默认容器(RUN-02):subprocess runner 下未标记 trusted_fixture 的 smoke job(script 尝试写宿主 marker 文件)必须 failed/environment,错误消息含 trusted-smoke-fixture(execution-runtime.md §1),且宿主 marker 文件必须不存在——证明脚本从未在宿主执行; - trusted-smoke-fixture 放行(RUN-02):同一 subprocess runner 下显式 `payload.trusted_fixture=true` 的 smoke job 正常 succeeded,脚本输出/metrics artifact 可断言;docker 模式 smoke 无需标记,始终容器执行、不受影响; - 非 echo 空 command 和 message-only payload 失败; - secure Job 缺 approved Contract、Snapshot、digest 被拒绝; - formal/baseline/pilot/reproduce 必须绑定同项目、`status=approved`、带 Human Gate Decision 且版本冻结的 Contract;draft/foreign/missing Contract 一律 422; - `image_digest` 必须与受信任 lock 中的 `@sha256:<64hex>` 完全一致;tag、`latest`、缺 digest 和提交后换 digest 一律拒绝; - code snapshot 和每个 `data_artifact_ids` 必须存在、属于同一项目、hash 可重验;跨项目或缺失输入不得进入 queued; - Snapshot traversal、symlink escape、duplicate normalized path 被拒绝; - Docker uid 非 root、network none、read-only、cap drop、memory/cpu/pids/time 生效; - timeout/cancel/Runner crash 无孤儿容器; - heartbeat 阻止 lease 重领;过期 lease 恢复 retryable; - stale generation/token 的 heartbeat、chunk、complete 全 409; - invalid signature、unknown key、missing/cross-project Artifact 拒绝; - complete transaction 故障不出现 succeeded 无 Artifact。 - formal/baseline/pilot/reproduce 成功必须存在 `output_contract.metrics` 指定路径的 MetricsFileV1;缺文件、空 metrics、NaN/Infinity、重复 metric、非 Contract metric、run/contract/seed 不匹配或 stdout fallback 均不得 succeeded; - heartbeat、Terminal frame、Artifact finalize 和 complete 都必须携带当前 owner/generation/token;缺任一字段、旧 generation、未来 generation 或错误 token 均 409,不能保留 owner-only 兼容放行。 - local/remote-plan-parity:同一 ExecutionPlan 在 LocalDocker/RemoteAgent 使用同一 run_id、snapshot/image/config/output contract;target 不能改写; - remote-mtls-capability:未知/撤销 service、plan 签名错误、capability mismatch、offline/draining 均 fail closed; - remote-partition-fencing:分区后 spool 有界;lease 过期的旧 Agent frames/finalize/complete 全拒绝,不产生合成成功; - no-silent-fallback:remote 失败不自动转 LocalDocker/subprocess;显式新 attempt 才能换 target; - configurable-execution-environments:Runner Target/Profile Registry 可经权威 API 与 Settings 创建/编辑/禁用 `local-process`、`local-docker`、`remote-ssh`;用户入口使用“本机开发 / Docker CPU / Docker NVIDIA / 远程 SSH”预设,类型切换只显示相关字段,草稿、加载错误、字段错误、CAS 冲突与连接测试报告不得丢失。当前项目默认 target 可在 Settings 选择并以 `expected_revision` CAS 保存,禁用/排空 target、stale revision、非 PI/operator 写入均拒绝;持久 `unknown` 只作观测摘要,远端 online/offline 权威来自认证 Agent Registry 新鲜 heartbeat。目标选项格式与 health 枚举必须走 zh/en 字典。Docker 指定镜像只接受 PI/Operator 登记到 RunnerTarget allowlist 的完整 digest;CPU 参数零 `--gpus`,NVIDIA all 精确生成 `--gpus all`,设备 `0,2` 精确生成单个 `--gpus device=0,2`,空/重复/负数/非数字 ID 和任意 flags 均拒绝。Docker/image/NVIDIA toolkit/driver/device preflight 任一失败时不调用 `docker run`,返回稳定可翻译诊断,且不回退 CPU/本机/其他 target。`/run`、`/reproduce` 顶层 `runner_target_id` 验证“Job 覆盖 > 项目默认”,project/job/reproduction 只引用 opaque IDs 并固定 target/profile/environment revision+hash。remote-ssh 的 endpoint、known-host/CA、credential 仅以服务端 SecretRef 存在且 safe view/日志/Job/ExecutionPlan 零泄漏;三个 SecretRef 的 `scheme/name/version?/scope?` 往返保真。revision CAS、权限、撤权、轮换和新旧 attempt pin 均有负向测试。**2026-08-15 已实现 typed runtime、digest/NVIDIA 表单、spawn-time preflight、GPU manifest/capability、0025 与核心自动回归;Settings 保存时真实连接测试 API/UI、真实 Docker/GPU/SSH 与 heartbeat→持久 health bridge 尚未完成,因此本场景仍不得标记 PASS。** - runner-target-exact-dispatch:本地 runner 必须用显式 `--target-id`(未给时按 mode 选择内置 local-process/local-docker id)按 `runner_target_id` + kind 双重 claim,并在 spawn 前再次核对 exact id;同 kind 的 target A 不能执行 target B。claim 时重新读取当前 registry:disabled、draining、kind/revision/hash 漂移的 target/job 不得获得 lease;过滤后的 stale 候选不得占用 claim `limit` 并饿死其后的有效任务。claim 与真正 spawn 之间还必须再次从权威 registry 读取 target,校验 enabled、draining、kind、revision 与 config hash;任何漂移都以 environment failure 关闭当前 lease,绝不启动旧配置。remote online/offline 的执行权威来自认证 Agent Registry 的新鲜 heartbeat,持久 Target `health` 在 bridge 落地前仅为 UI 观测摘要——unknown 不得展示为 online,offline 摘要也不能覆盖更新鲜的认证 Agent 状态,二者不一致必须进入人工验收/告警且绝不本机回退。Target CAS 的条件更新必须检查 `changes===1`;项目创建与项目默认设置均在落库前校验 target 状态及 target/profile kind 兼容。 - runner-target-security-fixtures:所有 security/eval fixture 在选择 `isolated-subprocess` profile 时必须同时显式 pin `target_local_process_v1`;选择 Docker profile 时 pin/默认到 local-docker。fixture 自身 profile/target 不匹配导致的 422 不能被空响应 JSON parse 掩盖,也不能误报为待测安全 invariant 失败。 - reproduction-target-binding:论文复现 spec 的 `execution_binding.target_id` 必须解析为当前已登记、enabled、非 draining 且与 runner profile kind 兼容的 RunnerTarget;`environment_lock.target_id/revision/hash` 必须与 binding 和 registry 当前值一致,不能接受未知 target、伪造 revision/hash 或冲突 pin。启动 attempt 时重新解析并固化 target revision/hash;目标漂移后旧 spec/attempt 必须 fail closed,用户只能显式更新 spec 或创建新 attempt,禁止回退本机。 - remote-cas-resume:断点拉取/上传重试后 hash/size 一致,跨项目或 stale generation 拒绝。 - remote-plan-build-failure:Fleet 从 Kernel claim 后若 profile/target pin 校验或 ExecutionPlan 构建失败,必须用当前 generation/token 将该 lease 明确完成为 environment failure;不得把 Job 留在 running 直到 lease 过期,也不得加入 pending/claimed 去重集后静默阻塞。 ### 4.1 RUN-REMOTE-01 接口层(ExecutionTarget/plan/注册表/调度 schema;真实 mTLS 传输未实现) - execution-plan-schema:ExecutionPlan 固定 project/job/run/lease/profile/config/image digest/snapshot/artifact refs/limits/network/output contract 并签名(signature/payload_sha256/signed_by;Ed25519 验签 + payload_sha256 复算,篡改或未签名一律 fail closed,不执行);plan `.strict()` 拒绝计划外字段——address/certificate/SSH bootstrap 等连接信息不得进入 plan/注册记录(只由服务端 Config/SecretRef 解析,Job/UI 只见 opaque profile/target ID 与安全健康摘要); - execution-plan-immutable:target 不得改写 plan——prepare() zod 校验 + 深度冻结 + fingerprint,start() 复算 fingerprint 对账,plan 变异或未先 prepare → ExecutionPlanMutationError(execution-target.test.ts); - local-docker-adapter-contract:LocalDockerAdapter 实现 ExecutionTarget port(prepare(plan)/start(plan)/attach(run)/cancel(run)/wait(run));docker 参数由 plan 纯函数映射(buildLocalDockerArgs:--network none、--user 65534:65534、--read-only、--cap-drop ALL、--security-opt no-new-privileges、--pids-limit/--memory/--cpus 取自 plan.limits、输入只读 + outputs 唯一 rw 挂载、--tmpfs、固定 image digest),与既有容器基线逐项一致;command/run_id 一律取自 plan,dockerRun 引擎行为不变; - remote-agent-registry:注册/心跳更新 health.last_seen/offline 判定(status 非 online 或 last_seen 超时即 offline,未注册 fail closed);注册记录只含 opaque target_id/agent_id/capabilities{os,arch,runner_ver,images}/labels/health/cert_fingerprint,`.strict()` 拒绝 address/certificate(remote-fleet.test.ts); - scheduled-target-pure:scheduledTarget(plan, registrations, policy) 纯函数——capability 匹配(images/os/arch/runner_ver 下限)、offline/draining 拒绝、无匹配 → 明确 retryable 错误(offline/draining/capability_mismatch/no_capable_target/policy_blocked 可区分),任务留在队列/标记 retryable;bound target 不可用时默认不回退 LocalDocker(policy.allow_bound_fallback_to_local 显式开启才回退),结果面永不出现 subprocess;policy 可配置(prefer local/local_only/allow_remote); - remote-not-implemented-fail-closed:真实 mTLS 传输未实现——createRemoteRunnerAgent 的任何执行/注册方法抛 RemoteRunnerAgentNotImplementedError(明确环境错误,绝不静默降级); - 上述场景的端到端形态(remote-mtls-capability/remote-partition-fencing/no-silent-fallback/remote-cas-resume 的真实 mTLS 传输验收)待远端传输实现后关闭,本轮仅接口层(hardening-v0.2-status.md §3 RUN-REMOTE-01)。 ### 4.2 RUN-REMOTE-01 wire 协议 + 服务端/代理端(mock 传输;真实 mTLS 证书验证受限) 协议定义见 [remote-runner-wire.md](remote-runner-wire.md)(HTTP+JSON,消息 schema 全 zod:`research-schemas/src/remote-runner-wire.ts`,两端共享;生产必须 mTLS service identity,本地 wire 用 `x-service-token` 等价实现并文档注明生产差异)。覆盖(tests/unit/remote-wire.test.ts 15/15,InMemoryFleetTransport JSON round-trip mock 传输 + 真实 HTTP loopback): - remote-wire-register-heartbeat:注册 acknowledged、心跳 accepted(含 draining 与 capability/labels 更新)、未注册 agent → 404 agent_not_registered(fail closed);wire schema `.strict()` 拒绝 address/certificate → 422 validation_error; - remote-wire-claim-match:target_id 精确 + capability(images/os/arch/runner_ver)匹配才分发;claim 响应含签名 ExecutionPlan(Ed25519 可验)+ lease generation/token,run_id/lease 与 kernel claim 一致;**无匹配 target → 任务留在 pending(retryable),绝不静默改派/回退 LocalDocker**;agent 断连期间服务端保留 outstanding,恢复后 resume 返回同一 claim(同一 claim_id/run_id/lease); - remote-wire-cas-hash:CAS 拉取后代理端复算 sha256——响应 hash 与内容不一致、寻址 hash 与内容不一致均拒绝执行(executor 未被调用,fail closed);自洽则正常执行; - remote-wire-frames:全局 seq 单调、stream_seq 按通道、exit 帧最后、逐帧 lease_generation;artifacts staged + finalize 由服务端复算 sha256,篡改 → 409 cas_hash_mismatch / cas_size_mismatch(不落库); - remote-wire-complete-fencing:complete 携带 Ed25519 签名 run_manifest(kernel 侧 §12.7 验签通过)+ fencing 字段;**lease 过期被新 claim 抢占后旧 agent 的 complete → 409 lease_stale,claim 置 settled,后续 frames/complete 全拒,无合成成功**;agent last_seen 超时 → claims 409 agent_offline(retryable),心跳恢复可用; - remote-wire-spool:断网期间 frames/stage/finalize/complete 全部本地有界 spool,恢复后按序重放并完成 Job;spool 有界——frames 条目可被淘汰并先补发 gap frame(dropped 区间/字节数可见,不静默丢弃),exit_frame/complete 不可淘汰,spool 满 → 本地失败(fail closed,无合成成功);resume 幂等(回放同一 claim 不重复执行); - remote-wire-http-auth:HTTP 面 x-service-token 缺失/错误 → 403 service_token_required;正确 token 下 register/heartbeat/claim/CAS/frames/artifacts/complete 全链路可用(HttpRemoteFleetTransport + 真实 HTTP loopback)。 - fleet-bin-mode-exclusive(FLEET-01,tests/unit/fleet-bin.test.ts):runner 二进制三角色互斥——缺省 local;`--fleet-server ` → fleet-server;`--agent ` → agent;两者同给 → 拒绝(FleetCliConfigError,互斥);任一 fleet 角色与 `--mode` 同给 → 拒绝(`--mode` 仅本地模式有意义);parseCli/validateConfig 对新增 fleet 键(runner.fleet_server_port/runner.fleet_url/runner.agent_id/runner.fleet_target_id/runner.fleet_public_key)解析/校验/默认合并,未知 flag 仍 unknown_config_key; - fleet-bin-http-chain(FLEET-01,tests/unit/fleet-bin.test.ts):startFleetServer 把 wire 挂真实 node:http listener(固定端口绑定、baseUrl 可达、x-service-token 缺失 → 403);runFleetAgentMain 客户端循环(register → heartbeat → claim → 执行 → frames/artifacts/complete)全链经真实 HTTP 完成 Job succeeded,frames seq 单调 + exit 最后、manifest 带 fencing 字段; - fleet-bin-http-spool(FLEET-01,tests/unit/fleet-bin.test.ts):离线 spool 恢复在 HTTP 层成立——断网(transport_unreachable)期间 frames/stage/finalize/complete 全量进本地有界 spool(Job 保持 running,无合成成功),恢复后按序重放完成 Job; - fleet-bin-nomatch-target(FLEET-01,tests/unit/fleet-bin.test.ts):无匹配 target(HTTP 层)→ 任务留在服务端 pending(retryable,绝不静默改派/回退 LocalDocker);匹配 target 的 agent 注册后拿到同一 Job(同一 claim_id/run_id/lease);未注册 agent 的 claims → 404 agent_not_registered。 - fleet-bin-frames-lease-headers(FLEET-01,tests/unit/fleet-bin.test.ts):appendTerminalFramesWithLease 的 request 绑定回归——真实 node:http 接收端断言 POST /v1/jobs/{id}/terminal-frames 携带 x-lease-owner/x-lease-token/x-service-token 头且 body(run_id/frames/max_log_bytes)原样送达(修复 009531c 引入的未绑定 request 调用:本地 runner 帧静默丢失、fleet 转发 502 kernel_unreachable)。 - **剩余(如实记录,属后续阶段)**:真实 mTLS 证书链(CA 签发/吊销/轮换)验收、真实远端 sandbox 隔离验收、跨主机网络分区故障注入、Remote PTY 与浏览器 UI——本轮协议层用 service-token 等价实现,生产差异见 remote-runner-wire.md §3。 ## 5. Terminal - interleaved-output:stdout/stderr 按全局 seq 重放,单通道仍完整; - live-view:运行中 UI 逐 chunk 更新,不等任务完成; - reconnect-after-seq:断线后无重复无缺失; - retention-gap:请求已淘汰 seq 先收到 gap 和 dropped bytes,随后至少收到 retained_from_seq 起的 retained frame;自动测试必须等待 gap 与 retained frame 两种事件都被 SSE parser 消费,不能把“已看到 gap”误当成同一网络 chunk 中后续 frame 已到达; - overflow:达到上限显示 truncated,最终 log Artifact 可下载; - terminal-dom-bounded:Playwright 在 Job 未完成前观察到 stdout/stderr DOM 增量;全局 seq 单调、只生成安全文本节点,保留窗口滚动后 DOM 行数不超过配置上限; - download-full-log:overflow/gap 后下载动作必须读取最终 log Artifact,字节等于 canonical 完整或明确截断日志,不能仅导出浏览器内 retained lines; - exit-replay:成功、非零、signal、timeout、cancel 的最终原因永久可读; - terminal-frame-integrity:同 (run_id, seq) 重放内容(frame_kind/stream_seq/channel/text/byte_offset/byte_length/payload_json)不同 → 409 `terminal_frame_conflict`(storage-migrations.md §4,不再 INSERT OR IGNORE 静默);内容相同 → 幂等跳过(重放语义保留);retention 已淘汰的 seq 重放不报错(storage-migrations.md §4,tests/unit/terminal.test.ts); - backpressure:慢客户端不使 Runner/Kernel 内存无限增长; - hidden-tab:暂停渲染后恢复到 latest; - ansi-injection:OSC clipboard/link/title 和 HTML 不能执行; - log-authz:无 job_log_read、无 token、跨项目、撤权连接均被拒绝; - cancel-kills-process:UI 只有确认停止后显示 cancelled。 - cancel-timeout-distinct:非合作进程在 deadline 内完成 TERM→KILL 兜底、无孤儿;cancelled 与 timed_out 在 exit、重载和下载日志中保持不同终态。 - pty-real-interaction:真实 shell/TUI 接受 input、返回 output,resize 生效,INT/TERM/KILL 和 exit 可辨; - pty-web-terminal-adapter:浏览器 terminal adapter 的公开 seam 接收 emulator `onData`,逐字节保持 UTF-8/控制字符并调用 `PtyClientModel.sendText`;PTY output 只按未消费 `server_seq` 增量写入 emulator,重复模型通知不重复输出;gap/exit 作为独立状态;ResizeObserver/fit 只在 cols/rows 变化时调用 `resize`;dispose 后 keyboard/paste/observer/output 均不再生效。测试必须先在旧 plain-text panel 上稳定失败,再由真实 xterm-compatible 接线变绿; - pty-reconnect-seq:detach/reconnect 使用 generation/after_seq 无重复,retention 淘汰先发 gap; - pty-control-authz:跨项目、无 terminal_write、撤权、旧 generation、重复/乱序 client_seq 拒绝; - pty-safe-open:任意 endpoint/SSH credential/Docker socket/host cwd/argv 被 schema 拒绝,只能 preset + relative cwd; - pty-not-evidence:PTY output 不能被 Metrics/RunManifest/Evidence/Decision 路径引用;Run Terminal 永远没有 input route。 - pty-interface-state-machine(PTY-01 接口层,无真实 tty,adapter 未实现——tests/unit/pty-session.test.ts 13/13):open→attached→detached→closed 状态机(closed 终态、close 幂等、attach/detach generation 递增供重连 fencing);idle TTL 超时→close(activity 重置 TTL);权限撤销立即 detach(进程存活);client_seq 幂等(重复 seq 回放 no-op、乱序/跳号 409 pty_client_seq_out_of_order);输出 server_seq 单调、after_seq 重放无重复无缺失;retention_bytes 有界淘汰先发 gap(含 dropped_bytes/gap_from_seq);lease token/expiry 固定;spawn 失败→adapter_failed 关闭并保留审计行;closed 后 control/appendOutput 均拒绝; - pty-http-validation(PTY-01 接口层,同上):POST /v1/pty/sessions 参数校验(坏 preset/extra key/非法 config_hash→422 validation_error、未知 project/workspace→404、host cwd→422),**adapter 未注册一律 501 pty_adapter_not_implemented 且不创建惰性会话行**;kernel-created 会话可经 HTTP 驱动(control 200 幂等 + delivered=false 如实标注、GET 会话、frames?after_seq= 200 seq/gap 投影、close control 生效);未知会话 control/frames→404、after_seq 非整数→422;控制帧不泄漏进输出流; - pty-not-evidence-kernel(PTY-01 接口层):pty 活动(input/resize/signal/output/exit/close)后 Jobs/Runs/Evidence/Claims/Gates/Artifacts 计数全 0,帧只落 pty_frames(terminal_frames 为 0)——接口层无任何 pty→业务表写路径(tests/unit/pty-session.test.ts)。 - pty-local-real-tty(PTY-01 LocalPtyAdapter,tests/unit/pty-local.test.ts):真实 pseudo-terminal(python3 `pty.fork()` 桥,Node 无内置 PTY)——打开 preset shell 后 bytes 回显与命令输出可见(`echo` 圆环);resize 生效(`stty size` 反映新 cols/rows,132x43);信号按 pty 前台进程组投递:INT 中断前台作业而 shell 存活、TERM 终止前台作业(交互式 shell 按设计忽略 SIGTERM,真实终端语义)、KILL 终止 shell 且 exit frame 带 signal=SIGKILL(exit frame 严格晚于最终输出字节);显式 close 拆除真实 tty(adapter live 计数归零,无孤儿进程/进程组)。 - pty-local-detach-reconnect(同上):detach 后进程存活且输出继续流入 session store(断开不杀进程,execution-runtime.md §6.1);reconnect(attach)generation 递增供 fencing;after_seq 重放无重复无缺失(server_seq 单调)。 - pty-local-idle-ttl(同上):idle TTL 超时(kernel 持有 sweep 定时器 + 显式 ptySweepIdle)→ session 以 idle_ttl 关闭且真实 tty 被拆除。 - pty-local-env-whitelist(同上):shell 环境只有白名单(PATH/TERM/LANG/…);DSH_SCHOLAR_KERNEL_TOKEN / DSH_SCHOLAR_SERVICE_TOKEN / 模型凭据(OPENAI_API_KEY)在 shell 中不可见;`$HOME` 被重定向进 workspace sandbox(host `/home/…` 不可达),host 路径永不进入子进程环境。 - pty-local-safe-boundaries(同上):adapter 层防御性拒绝 host cwd / `..` 段 / 未知 preset(即使绕过 kernel schema);python3 缺失 → spawn 诚实失败 → 会话 adapter_failed 关闭且保留审计行。 - pty-local-http-open(同上):注册真实 adapter 后 `POST /v1/pty/sessions` 缺 `x-principal-id` → 422 principal_required、非成员 → 404 project_not_found、成员 → 201 真实会话(principal/adapter_id 钉在行上);control 携带他人 principal → 403 pty_principal_mismatch;owner control delivered=true、frames?after_seq= 可回放真实输出;close control 关闭会话并拆除 tty;无 adapter 注册时仍 501 pty_adapter_not_implemented 且不创建惰性会话(pty-session.test.ts 保持)。 - pty-local-not-evidence(同上):真实 adapter 会话(input/resize/signal/output/exit/close)后 Jobs/Runs/Evidence/Claims/Gates/Artifacts 计数全 0,帧只落 pty_frames——真实 tty 也不产生 Metrics/Manifest/accepted Evidence/Gate Decision。 - 浏览器 TUI 验收:在真实浏览器打开 LocalPtyAdapter bash,聚焦终端输入 `printf 'WEBTERM_OK\\n'` 并看到输出;验证方向键历史、Tab、Ctrl+C、Unicode/粘贴、`vim`/`top` 或等价 alternate-screen TUI、自动 fit/resize、detach-reconnect、Dock right↔bottom、640/720/1024 视口。代码实现与 adapter 单测不能代替该项;未运行时记 `NOT_RUN_MANUAL_PENDING`。 ## 6. Analysis、Evidence 与 Claim - metrics schema identity、有限数值、MetricSpec direction; - duplicate seed、mixed metric、mixed contract、minimum_n 拒绝; - paired bootstrap 输入相同输出字节一致; - lower-is-better 的 effect 解释正确; - public Evidence 路由不能提交 verified/accepted; - Evidence provenance 状态机为 draft_unverified→verified→accepted;public 只创建 draft,Analysis Worker 只创建 verified,只有 Verifier/Auditor internal accept 可转 accepted; - draft/legacy/verified Evidence 在 accepted 之前都不能支持 Claim;伪造 body `status=accepted`、非 service identity、跨项目 accept 必须拒绝; - accepted transition 必须重验 RunManifest、Contract、RunSet、Analysis Artifact 和 service Principal,并留下 request_id/Outbox; - 缺 effect/CI/n 或 CI 跨无效区间为 inconclusive; - contradicted Claim 不被 Manuscript 当正面结论; - Analysis Artifact、图表和稿件数字一致。 ## 7. TeX Workbench - create-document 生成 paper.tex/references.bib 和 revision; - ui-new-file:通过 UI 新建根内文件后 tree/GET 返回正确 path/kind/media/version;新文件 create-if-absent 不得用会被 positive schema 拒绝的 `expected_version=0`; - tree 只含根内路径,文件 kind/media/version 正确; - save expected version 成功并生成新 immutable revision; - 并发保存一个 200、一个 409,无丢失更新; - delete/rename/asset upload 执行 path 和 version 校验; - compile 自动冻结当前 manifest,不能读取之后编辑; - snapshot-frozen-bytes:冻结时 snapshot store 保存每文件可物化字节;冻结后编辑/删除当前文件,GET snapshot-files?revision=&path= 仍返回冻结 revision 的字节且 hash 与 manifest 一致;未知 revision/path 404、参数缺失/非法 422;Runner 物化只读 snapshot store 字节(无当前文件读取),不可读或 hash 不匹配一律硬失败(tex-build.test.ts 负向:revision 不可物化、字节被篡改、路径逃逸); - dirty-before-compile:修改或清空非空文件后立即显示 dirty;dirty 基线必须是文件 GET/最近保存内容(tree/GET 无 content,不能作为基线);revert 到已保存字节(含 '')后 dirty=false(manuscript-dirty.test.ts); - clear-revert-cas:清空非空文件(content='')是 CAS 可见变更(新 version + 新 revision,hash=sha256(''));清空/恢复用过期 expected_version 一律 409;revert 恢复原字节(tex-workspace.test.ts); - save-conflict-terminates-compile:并发保存后 document revision 前进,本地保存 409;随后用旧 revision 编译必须在冻结与提交两处被 409 document_version_conflict 拒绝,不得创建 latex-compile Job、不得创建 build 行、不得产出 PDF(tex-build.test.ts HTTP 集成 + tex-kernel.test.ts 单元);carried manifest revision 与 tex_revision 不一致同样 409; - compile 期间文件被修改:build 输入仍是冻结 revision 的字节(snapshot store 按 revision 返回原字节,hash 与 job payload manifest 一致),新编辑只前进 document revision 供 stale 判定; - build-job-linkage:POST builds 201 响应携带 build.job_id、build.revision(输入 revision)与 job;UI 用 job_id 接入同一 Job 的 Terminal SSE(GET /v1/jobs/{job_id}/terminal);build.revision < document.revision 时旧 PDF 显示 stale;queued/running 期间 Compile 按钮禁用(防重复提交); - pdflatex + bibtex/biber + 多遍编译得到非空 PDF; - shell escape、network、越界文件访问被拒绝; - compile Terminal 实时显示,完整 log 进 Artifact; - build-terminal-dom:编译完成前 UI 已显示真实 latex/bibtex 输出;该输出和最终 compile-log Artifact 可重放且绑定同一 build/input manifest; - LaTeX error 解析到 file/line,点击定位编辑器; - undefined citation 和 missing file 有结构化诊断; - 源文件改变后旧 PDF 显示 stale; - PDF Content-Type、Blob preview、download hash 正确; - build history 可重放日志和 PDF; - clean-room 能用 Bundle 中 TeX 源重新构建同等 PDF 结构。 - workspace-vscode-flow:Explorer/create/open/tabs/search/edit/move/delete/upload/download/history/snapshot 全部走 Workspace Revision/CAS; - workspace-binary-and-conflict:图片/PDF/随机 bytes hash 一致;大/未知文件只读;并发保存/上传给 base/current/local 且不覆盖; - workspace-watch:change seq 重连无重复,retention gap 触发 resync;跨项目/路径越界拒绝; - workspace-interface(WORK-01 接口层已升级为磁盘 adapter,见 §7.1——tests/unit/workspace-store.test.ts 15/15):list/read/write/delete/move + workspace revision/每路径 version/strong etag(`"-"`);预期 version/etag 不匹配一律 409 workspace_version_conflict/workspace_etag_conflict(无静默 last-write-wins);expected_version=0 create-if-absent(存在→409)、缺文件 + N>0→409;二进制 CAS(服务端计算 sha256、CAS blob 回读字节一致、相同 bytes 幂等复用同一 blob、文本写二进制节点 422 workspace_binary_read_only、二进制 move 按 blob 引用);路径安全(绝对/`..`/`.`/NUL/反斜杠/Windows 盘符/空段拒绝,tree 只含 root-relative 路径);move 目标已存在→409 workspace_move_destination_exists;history op ledger(create/write/delete/move + workspace revision);`dir` 节点由路径前缀投影; - workspace-tex-facade(WORK-01 接口层,同上):TeX 文档作为 `manuscript` workspace 经同一 WorkspaceStoreLike 契约读写(workspace_id=`ws_`、版本/etag/hash 与 tex 权威一致、写后 tex store 直接可见且 document revision 前进——无第二套字节/revision 权威);CAS 冲突穿透 facade;删除/移动经 facade 生效;二进制写→422;history 映射 tex 历史; - live-preview:保存成功后(POST /v1/documents/{id}/preview-builds,或 kernel previewAutoTrigger 自动路径)进入 server 端 debounce(默认 800ms 可配置),Kernel 持有定时器并写持久化 tex_preview_pending;合并窗口内多次保存只产生一个 preview build,编译结束前 UI 已见日志/诊断;新 revision 使旧 PDF stale(build.revision < document.revision → build 记录 stale=true)并 supersede 旧 preview;preview build 记录状态 queued/running/superseded/succeeded/failed(queued 被取代时标 cancelled),带 preview=true 与 superseded_by/superseded_at;preview 提交响应携带 job_id 与输入 revision(同一 Job 的 live Terminal SSE 与 stale 判定);UI 重连/内核重启后 GET preview-builds 投影(pending+builds)可恢复,preview 状态不只在浏览器 debounce timer(tex-preview.test.ts:debounce 合并、取消 queued、running→superseded、stale、权威 supersede、去重、重启持久化、previewAutoTrigger); - preview-vs-compile:preview 运行同一固定 TeX image/禁网/no-shell-escape(复用 latex-compile runner 路径,payload.preview=true),但不产 Evidence、不冻结/不参与权威 manifest 链;显式 Compile 固定 manifest/config/image,创建权威 latex-compile Job 时 supersede 该 document 全部非终态 preview,且不被后续 preview 取消/取代(活跃权威编译期间 preview flush 跳过,不排队冗余容器)。 ### 7.1 通用 Workspace 磁盘 Adapter(WORK-01 adapter 轮,api-contracts.md §17) 通用 workspace 的**真实磁盘 adapter**(替换接口层的 DB-only 文本内联实现;tests/unit/workspace-store.test.ts 15/15 + tests/security/run-workspace-tests.sh 38/38,详见 hardening-v0.2-status.md §3 WORK-01 行): - ws-disk-layout:每个项目一个 workspace 根 `dataDir/workspaces/{project_id}/{workspace_id}/`(目录链 chmod 0750、文件 0640),节点字节是树内真实文件(规范化路径);元数据(path/kind/media/size/version/hash/etag/updated_at)存 `workspace_nodes`(磁盘 adapter 不再写 `content` 列);`ensure` 即建根; - ws-atomic-write:写入先落目标目录内临时文件(`.ws-tmp-`)再原子 rename——读方永远看不到半写文件,写后无临时残留; - ws-revision-etag:每次 mutation 前进 workspace revision 与每路径 version,etag=`"-"` 随之变化(单调、确定性); - ws-cas-conflict:预期 version/etag 不匹配一律 409 `workspace_version_conflict`/`workspace_etag_conflict`(无静默 last-write-wins);`expected_version=0` create-if-absent(已存在→409);move 目标已存在→409 `workspace_move_destination_exists`;delete/move 均走 source CAS; - ws-move-delete:move 原子 rename 磁盘文件(hash 保持、旧路径消失、目标 version 重置为 1);delete 移除磁盘文件并保留历史; - ws-binary:任意字节节点(不强制 UTF-8)——multipart `assets` 上传(≤32 MiB 复用 UPLOAD-01 上限,服务端 sha256,路径字段走同一规范化);GET `blobs?path=` 返回原始字节 + media type(扩展名映射或 octet-stream)+ 强 etag 头;`blob_sha256` 同时注册进 artifact CAS(按内容幂等),工作字节以树文件为准; - ws-size-cap:单节点 >32 MiB(`WORKSPACE_MAX_FILE_BYTES`,复用 upload limits)→ 413 `workspace_file_too_large`(multipart 路径为 413 `payload_too_large`);超过 readJson 32 MiB 上限的 JSON 文本写请走 assets 路径; - ws-path-safety:绝对路径/`..`/`.`/NUL/反斜杠/Windows 盘符/空段 → 422 `invalid_path`;磁盘层额外拒绝路径上**任意** symlink(读、写、删、移动前 lstat 每个已存在组件,→ 422 `workspace_symlink`)——通用 workspace 树只含普通文件,是 snapshot-walk“symlink 不得逃出根”的严格超集;宿主在树内埋 link 指向根外也不能被读写穿透(负向:escape target 字节不变); - ws-history:每路径保留最近 `HISTORY_KEEP_VERSIONS`=8 个版本字节于 `dataDir/workspaces/.ws-meta/{workspace_id}/history/{path}@{version}`(树外,用户路径不可达);GET nodes `?path=&version=N` 回退读(当前版本读活文件,旧版本读历史字节,删除后版本仍可读——undo);超出保留窗口 → 404 `workspace_file_not_found`;history op ledger 不变; - ws-watch:GET nodes `?after_revision=N` → 该 revision 之后被触碰路径的当前节点 + `deleted` tombstone 列表(`since >= 当前 revision` → 空集),watch/change 重连 feed(TeX facade 无 per-op ledger,保守整树上报); - ws-search:POST search 双模式——路径:`prefix` 和/或 `glob`(AND),路径前缀与 `*`/`?` glob(`*` 不跨 `/`)匹配,`dir` 节点投影参与;**内容搜索已实现**(commit 98243ff):`q`(或 `mode=content`)+ 可选 `case_sensitive`,对文本节点(binary=0 且 media 文本类、无 NUL magic)做线性 UTF-8 行扫描,返回 `{info, hits:[{path, match_count, matches:[{line, snippet}]}], truncated}`——每文件最多 20 个匹配(match_count 为真实总数)、单次最多 50 个命中文件(truncated 标记)、>512 KiB 文件整文件跳过(绝不部分扫描)、大小写不敏感默认、非法 UTF-8 容错;空/纯空白 q → 422 `invalid_query`,q 与 prefix/glob 混用或 `mode='path'` 携带 q → 422 `invalid_search_params`;**无全文索引**——线性扫描,大数据集性能受限(如实记录,索引属后续增强); - ws-http-routes:`POST/GET /v1/projects/{id}/workspaces`(创建/列出 code/manuscript/scratch,manuscript 含 TeX facade 工作区)、`GET .../workspaces/{wsid}/tree`、`GET/POST/DELETE .../workspaces/{wsid}/nodes`(读/写/删 + watch/rollback)、`POST .../workspaces/{wsid}/moves`、`POST .../workspaces/{wsid}/search`(路径 prefix/glob + 内容 q/mode=content,同端点双模式)、`POST .../workspaces/{wsid}/assets`(multipart)、`GET .../workspaces/{wsid}/blobs?path=`(原始字节);跨项目 workspace → 404 `workspace_not_found`(路径项目绑定,BFF membership/role 检查同其它 /v1 写);BFF multipart 原始字节/原 boundary 透传 + bearer/Origin/CSRF/membership 与既有 /v1 写一致; - ws-facade:TeX 文档经同一接口可读/写(workspace_id=`ws_`、kind=manuscript、版本/etag/hash 与 tex 权威一致——无第二套字节/revision 权威);facade search/listSince/readVersion 语义与取舍见 execution-runtime.md §12.2; - 浏览器编辑器 UI(tabs/search/watch/upload、move/history 面板、Problems、集成 PTY)与桌面/窄屏/冲突/路径/二进制**浏览器**验收:无 Playwright 类环境,如实保留(§4 行 96 剩余)。 ## 8. UI 与 i18n - zh/en 字典 key 完全一致,静态检查阻止硬编码 chrome; - browser client 源码必须纳入 strict `tsc --noEmit`,不得只由 tsdown 跳过类型后转译; - persisted locale > browser regional locale > zh; - setLocale 后已开 modal、tabs、aria、Terminal status、TeX chrome 更新;测试不得导航或 reload,必须在一个 render tick 内断言可见 text/title/aria/status 与 `html[lang]` 同步切换; - i18n-runtime(tests/unit/i18n-runtime.test.ts):locale 切换后 tab/header/model/density/aria/document title 由纯 chrome 模型(i18n/chrome.ts)重新求值,pipeline 步骤(phasePipeline())、Terminal status/meta/exit、状态 pill/sidebar/search 状态文案(statusLabel())随 locale 重求值;已开 modal 经 overlay registry(registerOverlayRebuild/relocalizeOpenOverlays)重开;t() 遇全字典缺 key 返回 raw key 并在开发模式 console.warn 一次/键(含字典路径提示),注入 setMissingKeyReporter 收集器可断言;zh→en→zh 往返文案一致;unknown enum 仍原样显示;静态 zh/en parity 断言保持; - Intl 显式使用 active locale,不能出现中英文日期混用; - unknown enum 和 wire/model/Terminal/TeX raw text 原样显示; - standalone 首屏和 token error 双语,html lang 正确; - i18n-static-chrome:新增或现有 header、status pill、tooltip、placeholder、aria、toast、空态不得硬编码英文/中文;zh/en namespace 与 key 精确一致,缺 key 在开发模式 warning、CI fail; - 页面不依赖 DSH LocaleFace/ThemeFace/slots,locale/theme 只由 standalone adapter 管理; - 所有核心动作仅键盘可完成;focus trap、aria-live、contrast、reduced motion 通过; - 640/720/1024 px 无不可达控件; - 关闭页面后 SSE、interval、Blob URL、listener 清理; - 同一 fixture 在 standalone 重启前后产生等价页面和操作结果。 - ui-artifact-preview-runtime-lifecycle:以空 `Content-Type` + 登记 `text/html`/伪装 `.txt` 验证仍为 download-only;以无/错误 `Content-Length` 的 >1 MiB chunked 文本验证 reader 在预算处 cancel、页面不完整缓冲。批量下载、单项延迟下载、Artifact 详情/预览和 Manuscript PDF 在切项目、切文档、离开 Artifacts/Manuscript 面板、locale 切换、Escape 与 unload 时统一失效:旧请求/轮询不得触发迟到下载、覆盖新项目、新建 Object URL 或保留旧 PDF;详情与预览均重建 zh/en chrome/ARIA,所有关闭路径恢复原触发元素焦点,同项目背景重绘后按 Artifact ID 恢复到替换行或安全控件。Manuscript builds 与 preview-builds 各自只允许一个 in-flight poll,乱序响应按 sequence 丢弃;权威区只选 `preview=false` 最新成功 build,并在 build_id 变化时替换 URL,preview 区不得串入;在旧 build 有 PDF、较新 succeeded build 的 `pdf_artifact=null` 场景,两区都必须 revoke/清空旧 PDF,较新 queued/running/failed 时保留的上一 PDF 必须在 PDF 区标 stale;较新 succeeded build 的 PDF fetch/解码失败时,保留的旧 PDF 也必须因 build_id 不匹配标 stale。编辑器任一未保存输入立即局部标记主 PDF stale,不得通过全页 rerender 打断 textarea identity、焦点或选区。保存后的 preview hook 纳入同一 generation AbortController,离面板/切项目时不继续提交客户端请求。脏保存期间切项目不能继续 Compile 新项目,Regenerate 迟到不能改写新上下文;workspace GET 只有明确 404 才可 POST ensure,401/500/网络失败零写。自动层至少覆盖分类与有界 stream seam、generation/owner/sequence guard 和 lifecycle wiring;真实 Blob URL、ShadowRoot、原生 codec/PDF 与跨项目视觉链仍记 `NOT_RUN_MANUAL_PENDING`。 - ui-start:无项目首屏只有 Init/Resume/Upload 三项主行动,高级设置不可见;Resume 显示 status/pending Gate/NextAction; - ui-guide:所有非终态项目显示结构化 NextAction 的 state/reason/required/revision/CTA;409 刷新,unknown action 不执行; - ui-next-action-v2-cards(client 逻辑层):Overview 渲染 `next_actions_v2` 结构化卡——code 徽标、三态视觉类名(ready/blocked/done)、label 优先按 i18n key 翻译(未登记 code 原样显示 kernel label,属 wire 数据)、reason 行、required 缺失前置条件列表(blocked 点击展开)、route/白名单 interaction 进入真正可完成动作的界面(nav.ts `#tab=` 深链 + activeTab 切换;ideas/contracts/release/overview 收敛到 Overview tab)、blocking 说明;done 灰显禁用、blocked(有缺口时)禁用、ready 高亮;`code='unknown'` 只读、无 CTA、不构造 mutation(api-contracts.md §21);缺失 `next_actions_v2` 时回退 legacy `next_actions: string[]` 列表(向后兼容),畸形 v2 字段安全退化; - ui-survey-next-action-cta:SCOPED 的 `survey_run` 必须是 `route=chat`、`required_by=agent`;点击 CTA 后 active project 不变、Chat 打开、draft 等于 `/survey `(无 problem 时 `/survey `),但 connector/Corpus/Outbox 计数保持不变,直到用户确认发送。不得导航到空 Runs,也不得自动发送。成功发送后 Corpus Snapshot、`SCOPED→SURVEYING` 和 Outbox 同事务落地,projection 主动作变 `idea_generate`;刷新/重连不得重新显示已完成的 `survey_run`。 - ui-survey-phase-runs-empty:构造 `project.status=SURVEYING`、至少一个 `frozen=true/source_status=complete` Corpus Snapshot、`jobs=[]`、主 NextAction=`idea_generate`。zh 阶段 pill 必须显示“调研已就绪”,en 显示“SURVEY READY”,不得显示“调研中”或暗示活动 connector。Runs 仍为 0,空态明确“调研已完成,尚未创建实验运行”并存在可键盘触发的 Overview CTA;点击只导航、不创建 Job/Run/新 Corpus。其他阶段零 Job 仍用通用空态,有 Job 但筛选无匹配时仍用 `runs.noMatch`。纯模型测试覆盖三种分支,i18n runtime 覆盖 zh/en 阶段文案;浏览器回归同时断言页面不出现误导组合。 - ui-refresh-focus-stability:开启默认 8 秒刷新,分别在 Project Sidebar 搜索、Chat composer、Terminal 搜索、Workspace editor/search、Manuscript editor 中输入且把光标置于非末尾;跨越至少两个轮询周期及一次投影变更后,ShadowRoot 内 active element、文本、selectionStart/End、scrollTop、dirty/草稿均保持。背景刷新在可编辑控件聚焦时延迟,focusout 后只补一次合并刷新;SSE/PTY 输出在期间仍增量到达。手动操作引起的必要重绘必须恢复焦点/选区,IME composition 不中断;并发延迟 API 响应不得乱序重绘。PTY emulator DOM identity 仍保持不变。自动测试覆盖刷新合并/延迟纯 seam 与 ShadowRoot 焦点恢复;真实 IME/两轮询观感记 `NOT_RUN_MANUAL_PENDING`。 - ui-chat-slash-caret:DSH 与 standalone composer 逐键输入 `/`、`s`;命令菜单同步打开、异步候选 settle 与 session mirror 更新前后,textarea DOM identity 和 focus 不变,selectionStart/End 分别为 1/2,最终值为 `/s`。全局监听器收到 Shadow DOM retarget 后的 host `event.target` 时,必须通过 `composedPath()` 发现内部 textarea 并保持零接管、零重绘;全局 `/`、starter、命令面板和补全项预填后 caret 位于预填文本末尾;editable/IME/repeat 不触发全局快捷键或页面级重绘。自动测试覆盖 composed-path 判定;真实 standalone Chromium 回归必须断言 `/` 后 selection=1、继续输入得到 `/n`/selection=2。 - ui-chat-natural-turn-guidance:显式 slash 仍原样进入 direct command;collecting project 的普通文本只回答 current Grill question;confirmed project 的普通文本进入 natural turn,不能得到 `Unknown command`。分别用 zh/en 输入“查看当前进展/有哪些想法/看审批/看运行任务/调研 ”与同义句,断言解析到 canonical status/ideas/gates/jobs/survey operation,显式参数和 project/session 保持;每次响应包含最新阶段、NextAction reason/required_by/阻断和一个可达引导。未知/歧义只回答或给候选,Human-only、Gate、Brief confirm、adopt、release、blocked、future NextAction 零副作用;外部附件/OCR prompt 不参与 intent。模型 adapter 不可用时仍返回 deterministic guidance,不退化为 slash error;自然语言与显式 slash 的 ACL/idempotency/refs 等价。 - ui-chat-brief-conversation:name-only 项目进入 Chat 后,当前 Grill question 必须位于 `.chat-stream` 的 assistant question turn,页面可编辑自由文本区只有 `.chat-composer-input`;不得出现独立 Brief textarea/Submit 表单、重复 next question 或把 composer 推成第二个区域。普通 composer 回答只 POST 当前 question code/revision/disposition=answered,成功后原 user answer 留在 transcript、assistant 只确认已记录、下一题在 transcript 末尾推进且 placeholder 切为回答提示;Skip/暂时未知从问题 turn 发出对应 disposition 并以 user action 留痕。异步推进、8 秒刷新、session/project 切换不得抢焦点或把回答写入别的 session。七题完成后同一位置显示 Brief preview + PI confirm,confirmed 后 prompt 消失且 Scope Gate 仍只由显式 PI 操作创建。zh/en copy、ARIA、键盘和 main/right/bottom surface 等价;自动层覆盖 router/payload/i18n/无第二 textarea,真实视觉与焦点记 `MANUAL-BRIEF-CONVERSATION`。 - ui-chat-harness-freeform-bridge:confirmed project 在 DSH Scholar iframe 输入开放问题时,父窗口只接受已配置、与 Host 不同 origin 的 loopback standalone,或与 Host 同主机但不同端口/origin 的 HTTPS standalone,并同时校验 exact iframe source;跨主机 HTTPS、远端明文 HTTP 与同 origin 嵌入只能 fallback。Scholar Host RPC 通过 Harness `ctx.llm` 进行无 tools 的回答;请求只含在首次 await 前冻结的当前 project/session 最近 12 条有界文本与裁剪 projection,模型选择来自 Host,不接受浏览器 provider/model/credential。同一 iframe request-id 去重且最多一个 in-flight,超时/卸载/URL 切换必须取消 LLM。正常响应显示自由回答;Host 缺失、超时、model error 或新页面模式回退阶段回答且不出现 `Unknown command`。模型返回合法 direct slash 时显示可编辑“使用命令”,点击只预填且异步重绘后 caret 在末尾;未知/旧聚合/Brief confirm/Release Decision 等 Human-only command 被丢弃。确定性 read intent 与权威投影 ready 的 `survey_run` 仍可自动走 canonical operation,其他 write 只预填,模型建议绝不自动执行。zh/en 必须分别输出对应语言,fallback、“使用命令”、ARIA 和 locale 切换保持双语。自动测试覆盖 source/origin 校验、受信 origin、LLM 多 chunk 组装/错误、command allowlist、fallback 与 await 期间 project/session 隔离;真实 DSH provider、超时取消和真实浏览器 zh/en 记 `NOT_RUN_MANUAL_PENDING`。 - ui-dsh-scholar-iframe-failure-recovery:standalone bootstrap 与宿主以 exact source、exact origin、`protocol=1` 完成 ready/query 握手;8 秒无 ready、`iframe` error、TLS/CSP 或网络失败时卸载失败 iframe,以 zh/en `role=alert` 显示重试、新页面诊断入口和非默认 URL 的本机默认恢复入口。相同 URL 重试必须创建新 iframe,并重新绑定 ready 与 Host Chat bridge;配置 URL 改变或恢复默认值时,失败状态不得粘滞,新 iframe 必须先挂载后绑定 listener。进入失败态、URL 变化、重试或卸载必须 abort 旧 Host Chat RPC,迟到结果不得回写。父子页从可信配置或 origin-only referrer 固定 exact origin;请求和响应均以该 origin 作为 `targetOrigin` 并同时校验 exact source、origin、协议/request-id,禁止 `'*'`,父 `WindowProxy` 导航后不得泄露投影或接受伪造响应。CSP 只接受配置的精确 HTTPS 或 loopback HTTP ancestor,并拒绝通配符、远端明文 HTTP、userinfo/path/query/hash;Host Chat 只接受不同 origin loopback 或同主机 HTTPS 跨端口关系。Token 解锁表单只发送字符串 token,结构化 `unauthorized` 显示本地化 `Invalid token`,不得回显 `[object Object]`。自动测试覆盖状态机、attempt key、listener/abort 依赖、source/origin/targetOrigin、CSP parser 与 token 错误;真实浏览器必须覆盖“阻断 8443 → 失败态 → 解除阻断 → 同 URL 重试恢复”。URL 恢复、窄屏、键盘和屏幕阅读器仍记 `NOT_RUN_MANUAL_PENDING`,不能据桌面宽屏 smoke 提升为“已验收”。 - ui-standalone-runtime-style-nonce:standalone 每个 HTML 响应生成唯一 nonce,CSP 的 `script-src/style-src`、bootstrap script/style 与客户端运行时插入 Shadow DOM 的主 `