@weotro/dx 0.2.2 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,135 @@
1
+ import { accessSync, closeSync, constants, openSync, readFileSync, readSync, readdirSync, statSync } from 'node:fs'
2
+ import { basename, join } from 'node:path'
3
+
4
+ export const standardCommands = ['setup', 'start', 'build', 'test', 'db', 'deploy', 'lint']
5
+ const testTypes = ['unit', 'e2e']
6
+ const databaseActions = ['migrate', 'seed', 'reset', 'status']
7
+ const own = (object, key) => Object.hasOwn(object, key)
8
+ const isObject = value => value !== null && typeof value === 'object' && !Array.isArray(value)
9
+
10
+ export function inspectScript(entry) {
11
+ const name = basename(entry)
12
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) return `入口 ${entry} 名称非法;请使用小写字母开头及小写字母、数字或连字符。`
13
+ if (name === 'check' || name === 'initial') return `入口 ${entry} 使用内置保留名称;请重命名应用脚本。`
14
+ try {
15
+ if (!statSync(entry).isFile()) return `入口 ${entry} 不是普通文件;请提供可执行脚本。`
16
+ accessSync(entry, constants.R_OK | constants.X_OK)
17
+ const descriptor = openSync(entry, 'r')
18
+ try {
19
+ const prefix = Buffer.alloc(256)
20
+ const length = readSync(descriptor, prefix, 0, prefix.length, 0)
21
+ const bytes = prefix.subarray(0, length)
22
+ const newline = bytes.indexOf(10)
23
+ const firstLine = bytes.subarray(0, newline < 0 ? length : newline)
24
+ if (firstLine.length > 255 || !/^#![ \t]*\/[^\s\0]+(?:[ \t]|$)/.test(firstLine.toString()) || firstLine.includes(0)) {
25
+ return `入口 ${entry} 缺少有效 shebang;请在首行指定解释器绝对路径,例如 #!/bin/sh,首行不得超过 255 字节(不含换行)。`
26
+ }
27
+ } finally {
28
+ closeSync(descriptor)
29
+ }
30
+ } catch {
31
+ return `无法读取或执行入口 ${entry};请检查文件、链接目标与读取和执行权限(chmod +x)。`
32
+ }
33
+ return null
34
+ }
35
+
36
+ function inspectDeclaration(contract, errors) {
37
+ const report = (location, message) => errors.push(`dx/contract.json ${location}:${message}`)
38
+ const fields = (value, allowed, location) => {
39
+ if (!isObject(value)) {
40
+ report(location, '必须是对象。')
41
+ return false
42
+ }
43
+ if (Object.keys(value).some(key => !allowed.includes(key))) report(location, '存在未知字段;请仅保留契约规定的字段。')
44
+ for (const key of allowed) {
45
+ if (!own(value, key)) report(`${location}.${key}`, '缺少必填字段。')
46
+ }
47
+ return true
48
+ }
49
+ const capability = (value, location, unit = false) => {
50
+ if (!isObject(value)) {
51
+ report(location, '能力声明必须是对象。')
52
+ return
53
+ }
54
+ if (Object.keys(value).some(key => !['status', 'reason'].includes(key))) report(location, '能力声明存在未知字段;仅允许 status 和 reason。')
55
+ if (!own(value, 'status') || !['implemented', 'mock'].includes(value.status)) report(`${location}.status`, '必须为 implemented 或 mock。')
56
+ if (own(value, 'reason') && (typeof value.reason !== 'string' || !value.reason.trim())) report(`${location}.reason`, '必须是非空字符串。')
57
+ if (value.status === 'mock' && !own(value, 'reason')) report(`${location}.reason`, 'mock 必须提供非空的不适用原因。')
58
+ if (unit && value.status === 'mock') report(location, 'unit 必须真实实现,不能声明为 mock。')
59
+ }
60
+ const targets = (value, location, { required, forbidden = [], unit = false } = {}) => {
61
+ if (!fields(value, ['targets'], location)) return
62
+ const declarations = value.targets
63
+ if (!isObject(declarations) || Object.keys(declarations).length === 0) {
64
+ report(`${location}.targets`, '必须声明至少一个目标。')
65
+ return
66
+ }
67
+ if (required && !own(declarations, required)) report(`${location}.targets`, `必须显式声明聚合目标 ${required}。`)
68
+ Object.entries(declarations).forEach(([target, declaration], index) => {
69
+ // Use ordinals instead of application-defined keys to keep diagnostics free of argument values.
70
+ const targetLocation = `${location}.targets[${index + 1}]`
71
+ if (!target.trim() || target.startsWith('-') || /[\u0000-\u001f\u007f]/.test(target)) report(targetLocation, '目标名必须非空、不得以连字符开头或包含控制字符。')
72
+ if (forbidden.includes(target)) report(targetLocation, '此命令不允许使用该保留聚合目标。')
73
+ capability(declaration, targetLocation, unit)
74
+ })
75
+ }
76
+
77
+ if (!fields(contract, ['schemaVersion', 'commands'], '$')) return
78
+ if (contract.schemaVersion !== 1) report('schemaVersion', '仅支持 schemaVersion: 1;请按 dx 0.3.0 契约迁移。')
79
+ if (!fields(contract.commands, standardCommands, 'commands')) return
80
+ const commands = contract.commands
81
+ capability(commands.setup, 'commands.setup')
82
+ targets(commands.start, 'commands.start', { required: 'stack', forbidden: ['all'] })
83
+ for (const command of ['build', 'lint', 'deploy']) targets(commands[command], `commands.${command}`, { required: 'all', forbidden: ['stack'] })
84
+ if (fields(commands.test, ['types'], 'commands.test') && fields(commands.test.types, testTypes, 'commands.test.types')) {
85
+ for (const type of testTypes) targets(commands.test.types[type], `commands.test.types.${type}`, { required: 'all', forbidden: ['stack'], unit: type === 'unit' })
86
+ }
87
+ if (fields(commands.db, ['actions'], 'commands.db') && fields(commands.db.actions, databaseActions, 'commands.db.actions')) {
88
+ for (const action of databaseActions) targets(commands.db.actions[action], `commands.db.actions.${action}`, { forbidden: ['all', 'stack'] })
89
+ }
90
+ }
91
+
92
+ export function inspectContract(root) {
93
+ const errors = []
94
+ const scripts = join(root, 'dx/scripts')
95
+ try {
96
+ const entries = readdirSync(scripts).sort()
97
+ for (const command of standardCommands) {
98
+ if (!entries.includes(command)) errors.push(`缺少必选入口 ${join(scripts, command)};请创建带 shebang 且可执行的应用脚本。`)
99
+ }
100
+ for (const name of entries) {
101
+ const error = inspectScript(join(scripts, name))
102
+ if (error) errors.push(error)
103
+ }
104
+ } catch {
105
+ errors.push('无法扫描 dx/scripts;请创建脚本目录并检查读取和搜索权限。')
106
+ }
107
+ let contract = null
108
+ try {
109
+ contract = JSON.parse(readFileSync(join(root, 'dx/contract.json'), 'utf8'))
110
+ } catch {
111
+ errors.push('无法读取或解析 dx/contract.json;请创建有效 JSON 声明,并按 dx-adopt 的 0.3.0 迁移指引补齐契约。')
112
+ return { contract, errors }
113
+ }
114
+ inspectDeclaration(contract, errors)
115
+ return { contract, errors }
116
+ }
117
+
118
+ export function assertTarget(contract, command, positionals) {
119
+ if (!standardCommands.includes(command) || command === 'setup') return
120
+ const declaration = contract?.commands?.[command]
121
+ let targets
122
+ let target
123
+ if (command === 'test' || command === 'db') {
124
+ const [kind, requestedTarget] = positionals
125
+ const variants = command === 'test' ? declaration?.types : declaration?.actions
126
+ targets = isObject(variants) && own(variants, kind) ? variants[kind]?.targets : undefined
127
+ target = requestedTarget
128
+ } else {
129
+ targets = declaration?.targets
130
+ target = positionals[0]
131
+ }
132
+ if (!isObject(targets) || typeof target !== 'string' || !own(targets, target)) {
133
+ throw new Error('请求的目标或调用组合未在 dx/contract.json 中声明;请使用已声明目标,或更新应用实现与契约。')
134
+ }
135
+ }
package/lib/env.js CHANGED
@@ -3,23 +3,14 @@ import { join } from 'node:path'
3
3
  import { parse } from 'dotenv'
4
4
  import { expand } from 'dotenv-expand'
5
5
 
6
- const flags = new Map([
7
- ['--prod', 'production'], ['--staging', 'staging'], ['--dev', 'development'],
8
- ['--test', 'test'], ['--e2e', 'e2e'],
9
- ])
10
6
  const nodeEnvironments = new Map([
11
7
  ['development', 'development'], ['staging', 'production'], ['production', 'production'],
12
8
  ['test', 'test'], ['e2e', 'test'],
13
9
  ])
14
10
 
15
- export function loadEnvironment(root, args, inherited = process.env) {
16
- const separator = args.indexOf('--')
17
- const selectors = separator < 0 ? args : args.slice(0, separator)
18
- const explicit = [...flags].find(([flag]) => selectors.includes(flag))?.[1]
19
- const requested = String(explicit || inherited.APP_ENV || inherited.NODE_ENV || 'development').toLowerCase()
20
- const environment = requested === 'local' || requested === 'dev' ? 'development' : requested
11
+ export function loadEnvironment(root, environment, inherited = process.env) {
21
12
  if (!nodeEnvironments.has(environment)) {
22
- throw new Error('无法选择环境;请使用 --dev、--prod、--staging、--test、--e2e,或设置有效的 APP_ENV / NODE_ENV。')
13
+ throw new Error('无法加载环境;命令必须提供有效的环境选择。')
23
14
  }
24
15
 
25
16
  const env = { ...inherited, APP_ENV: environment, NODE_ENV: nodeEnvironments.get(environment) }
@@ -0,0 +1,79 @@
1
+ import { standardCommands } from './contract.js'
2
+
3
+ const environments = new Map([
4
+ ['--dev', 'development'], ['--staging', 'staging'], ['--prod', 'production'],
5
+ ['--test', 'test'], ['--e2e', 'e2e'],
6
+ ])
7
+ const usages = {
8
+ setup: 'dx setup [环境标志] [-- 应用参数...]',
9
+ start: 'dx start <目标|stack> [环境标志] [-- 应用参数...]',
10
+ build: 'dx build <目标|all> [环境标志] [-- 工具参数...]',
11
+ lint: 'dx lint <目标|all> [环境标志] [-- 工具参数...]',
12
+ deploy: 'dx deploy <目标|all> [环境标志] [-- 应用参数...]',
13
+ test: 'dx test <unit|e2e> <目标|all> [-- 工具参数...]',
14
+ db: 'dx db <migrate|seed|reset|status> <目标> [环境标志] [-- 应用参数...]',
15
+ check: 'dx check',
16
+ }
17
+
18
+ export function commandHelp(command) {
19
+ const usage = usages[command]
20
+ if (!usage) return null
21
+ const detail = command === 'check'
22
+ ? '静态检查 dx/contract.json 和全部公开入口;不加载环境或执行应用。'
23
+ : command === 'test'
24
+ ? 'unit 固定使用 test 环境,e2e 固定使用 e2e 环境;禁止环境标志。'
25
+ : '默认 dev;最多一个环境标志:--dev、--staging、--prod、--test、--e2e。'
26
+ return `用法:${usage}\n${detail}\n-h / --help 显示本帮助,不执行应用。\n`
27
+ }
28
+
29
+ export function parseInvocation(command, args) {
30
+ const separator = args.indexOf('--')
31
+ const head = separator < 0 ? args : args.slice(0, separator)
32
+ const tail = separator < 0 ? [] : args.slice(separator)
33
+ const standard = standardCommands.includes(command)
34
+ if ((standard || command === 'check') && head.some(arg => arg === '--help' || arg === '-h')) {
35
+ return { help: commandHelp(command) }
36
+ }
37
+ if (command === 'check') {
38
+ if (args.length) throw new Error('check 不接受参数;请使用 dx check 或 dx check --help。')
39
+ return { args: [], positionals: [] }
40
+ }
41
+
42
+ const positionals = []
43
+ const forwarded = []
44
+ const selectors = []
45
+ for (const arg of head) {
46
+ if (environments.has(arg)) {
47
+ selectors.push(environments.get(arg))
48
+ } else if (arg === '--help' || arg === '-h') {
49
+ forwarded.push(arg)
50
+ } else if (arg.startsWith('-')) {
51
+ throw new Error('未知选项;应用或工具专属参数必须放在首个 -- 后。')
52
+ } else {
53
+ positionals.push(arg)
54
+ forwarded.push(arg)
55
+ }
56
+ }
57
+ if (command === 'test' && selectors.length) {
58
+ throw new Error('test 命令禁止环境标志;请删除标志,由 unit/e2e 确定环境。')
59
+ }
60
+ if (selectors.length > 1) {
61
+ throw new Error('最多允许一个环境标志;不同标志同时出现或相同标志重复均不允许。')
62
+ }
63
+ if (standard) {
64
+ const count = command === 'setup' ? 0 : command === 'test' || command === 'db' ? 2 : 1
65
+ if (positionals.length !== count || positionals.some(arg => !arg.trim())) {
66
+ throw new Error(`位置参数不符合契约;用法:${usages[command]}`)
67
+ }
68
+ if (command === 'test' && !['unit', 'e2e'].includes(positionals[0])) {
69
+ throw new Error('测试类型仅支持 unit 或 e2e;请使用 dx test <unit|e2e> <目标|all>。')
70
+ }
71
+ if (command === 'db' && !['migrate', 'seed', 'reset', 'status'].includes(positionals[0])) {
72
+ throw new Error('数据库动作仅支持 migrate、seed、reset、status;其他操作请使用扩展命令。')
73
+ }
74
+ }
75
+ const environment = command === 'test'
76
+ ? positionals[0] === 'unit' ? 'test' : 'e2e'
77
+ : selectors[0] || 'development'
78
+ return { args: [...forwarded, ...tail], positionals, environment }
79
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weotro/dx",
3
- "version": "0.2.2",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,17 +19,24 @@
19
19
  "lib/logger.js",
20
20
  "lib/version.js",
21
21
  "lib/env.js",
22
+ "lib/contract.js",
23
+ "lib/invocation.js",
22
24
  "lib/setup.js",
23
25
  "lib/codex-initial.js",
24
26
  "skills",
25
27
  "example/README.md",
26
28
  "example/.env.development",
29
+ "example/dx/contract.json",
30
+ "example/dx/lib/arguments.mjs",
31
+ "example/dx/lib/arguments.test.mjs",
32
+ "example/dx/lib/commands.mjs",
27
33
  "example/dx/scripts/setup",
28
34
  "example/dx/scripts/start",
29
35
  "example/dx/scripts/build",
30
36
  "example/dx/scripts/test",
31
37
  "example/dx/scripts/db",
32
38
  "example/dx/scripts/deploy",
39
+ "example/dx/scripts/lint",
33
40
  "LICENSE",
34
41
  "README.md",
35
42
  "package.json"
@@ -1,96 +1,63 @@
1
1
  ---
2
2
  name: dx-adopt
3
- description: 为项目接入 @weotro/dx 0.2.0 及以上版本,迁移旧版、审查升级适配或精简项目命令;实现项目入口、环境和进程管理,验证 worktree setup、构建、测试及 CI。
3
+ description: 为应用接入或切换到 @weotro/dx 0.3.0,补齐命令入口和能力声明;提供自包含向导、接入经验及回归验收。
4
4
  ---
5
5
 
6
- # DX 项目接入、升级与适配审查
6
+ # DX 0.3.0 接入与切换
7
7
 
8
- 在用户指定的应用仓库完成接入;未指定时使用当前仓库。适用于已有 monorepo 和新项目,不要求使用 Nx、Prisma 或 PM2。产出能运行的项目脚本和接入说明,不仅修改版本号。只读咨询则给出基于该项目的差异和迁移方案。
8
+ 在用户指定的应用仓库完成接入,未指定时使用当前仓库。保留现有包管理器和工具链,不要求 Nx、Prisma 或 PM2。产出可运行入口、声明及验收证据;只读咨询给出项目差异和方案。本技能及参考自包含,安装后无需访问 dx 源码仓库。
9
9
 
10
- 本技能随 dx 分发,引用文件均位于本技能目录,安装后无需访问 dx 源码仓库。最低接入版本为 0.2.0;契约参考记录的是 0.2.1 行为,实际包需核验,版本下限不等于对未来版本的兼容保证。
10
+ ## 获取随包文档
11
11
 
12
- ## 1. 确定项目状态
12
+ 使用实际安装的 0.3.0 执行 `dx initial` 后,本文件、[0.3.0 应用契约](references/dx-0.3-contract.md)、[接入与切换向导](references/switch-to-0.3.md)和[经验与回归检查](references/adoption-regressions.md)均位于 `~/.agents/skills/dx-adopt/`,`~/.claude/skills/dx-adopt` 链接到同一份内容。应用可直接要求“使用 $dx-adopt 将当前项目切换到 dx 0.3.0 并完成验证”。
13
13
 
14
- 读取适用的 AGENTS.md、工作区差异、package.json、锁文件、CI 和实际任务入口。确认目标是使用 dx 的应用,而非误把 dx 工具仓库当成应用。保留现有包管理器和工具链。
14
+ 以上资料随 npm 包和技能完整分发,不需要源码仓库。需要包内 README 或可运行示例时,全局安装目录为 `npm root -g` 输出下的 `@weotro/dx`;项目安装目录为 `node_modules/@weotro/dx`。版本与安装来源须和实际执行的 dx 一致。
15
15
 
16
- 核对 dx 的实际来源和版本:区分全局 `dx`、项目依赖与 CI 安装步骤。可用 `command -v dx`、`dx --version` 和项目包管理器的依赖查询核对;CLI 不存在时从依赖与调用点继续盘点,不阻塞脚本编写。不要用会自动下载其他版本的命令代替版本核验。Node 工具链保持现状,仅在实际运行或所用 API 出现兼容性证据时处理,尊重用户排除范围。
16
+ ## 1. 确定项目与版本
17
17
 
18
- 按证据选择路径:
18
+ 读取 AGENTS.md、工作区差异、package.json、锁文件、CI 和真实任务入口,确认目标为使用 dx 的应用。核对全局 dx、项目依赖和 CI 的实际来源及版本;可用 command -v dx、dx --version 和依赖查询。CLI 缺失时继续盘点,不用自动下载其他版本的命令替代核验。
19
19
 
20
- | 项目状态 | 执行路径 |
21
- | --- | --- |
22
- | 依赖 0.1.x,或仍使用 dx/config、旧 internal runner、旧 CLI 参数 | 阅读 [0.1.x 迁移](references/migrate-0.1.md),建立旧能力到新脚本的对应关系后实施 |
23
- | 从未接入 dx,或新建项目直接采用 >=0.2.0 | 跳过旧配置迁移,从现有工具命令生成下文应用入口 |
24
- | 已是 0.2.x,但部分入口缺失或行为错误 | 对齐目标版本并对照契约只补缺口,保留已工作的脚本,避免重复生成 |
20
+ 先读 [0.3.0 应用契约](references/dx-0.3-contract.md),按[接入与切换向导](references/switch-to-0.3.md)完成入口、声明、调用方和验收。schemaVersion 为 1;首次接入从真实工具命令生成入口,已有项目保留实际功能并直接对齐 0.3.0,已符合契约的部分按行为证据复核。
25
21
 
26
- 三条路径都先阅读 [0.2.1 能力与调用契约](references/dx-0.2-contract.md)。文件中说明哪些能力由 dx 提供,哪些必须由应用实现。
22
+ 沿用安装方式,对齐开发、依赖和 CI 的版本策略,实际版本须支持 0.3.0 契约。尚未发布时用明确的本地待测包,不把 registry latest 或声明版本当作行为证明。保留锁文件,全局项目不顺手增加本地 dx;本地依赖须有独立安装引导,全局 dx 可在应用依赖安装前 setup。仅有兼容性证据时调整应用 Node 工具链。
27
23
 
28
- ## 2. 落实版本与入口
24
+ ## 2. 实现入口与声明
29
25
 
30
- 沿用项目的安装方式,将依赖声明、开发说明与 CI 对齐到一致的版本策略,最低要求 >=0.2.0,不默认钉死某个补丁版本。全局安装默认使用 latest,并记录实际解析版本;项目有明确锁定策略时遵从该策略,本地依赖保留正常锁文件。全局项目不顺手引入第二套本地入口。检查 registry 是否提供可用版本,安装后核验实际契约,不把 latest 当作固定版本或兼容性证明。
26
+ 补齐 setup/start/build/test/db/deploy/lint 七个合法、可执行、带 shebang 的入口,按 [JSON 示例](references/dx-0.3-contract.md#契约声明)编写 dx/contract.json。标准语法和字段严格遵循参考,按完整调用组合声明 implemented 或 mock,显式列出必需聚合,由应用实现聚合行为。
31
27
 
32
- 全局安装示例为 `npm install -g @weotro/dx@latest`,随后执行 `dx --version`。全局 dx 可在应用尚未安装依赖时运行 setup;只放在应用 devDependencies 中的 dx 不能假定新 clone 后已可执行,必须保留安装 dx 的引导步骤。
28
+ 不适用能力仍提供 mock:声明非空原因,实际输出命令名称及原因并返回 0,验收记为跳过。unit 所有目标必须真实实现;需要但未完成的能力不能冒充不适用。implemented 声明不是行为证据。
33
29
 
34
- 在约定的项目根创建 `dx/scripts/`,只实现需要的命令:
30
+ 公开入口放 dx/scripts,辅助实现和测试可放 dx/lib,应用策略可放 dx/config;后两者不是 dx 自动加载契约。保留现有合理布局,迁移目录同步 import、根路径、工具 inputs、fixture、CI 和文档。无扩展名 Node 脚本的模块模式应匹配项目及最低 Node 版本,不能假定项目为 ESM。
35
31
 
36
- | 入口 | 应用需要实现的内容 |
37
- | --- | --- |
38
- | setup | 基础复制完成后的依赖安装、必要文件生成等准备;可重复执行并保留用户本地配置 |
39
- | start | 已存在的启动目标;monorepo 的 stack 必须映射到项目定义的全部服务,单应用只启动指定服务 |
40
- | build | 根据实际目标和 APP_ENV 调用构建工具,明确需要的 NODE_ENV 与工具配置 |
41
- | test | 单元、集成、E2E 的分流、路径参数和所需服务,不能靠命令名猜测 dx 的环境选择 |
42
- | db | 实际数据库子命令、前置检查及确认规则 |
43
- | deploy | 项目部署过程、环境检查、确认、失败处理和恢复 |
32
+ 薄包装调用真实实现,避免 dx → package script → dx 循环,不依赖 dx 私有 lib 或恢复旧配置解释器。独立职责可用扩展,不写入标准声明;check/initial 为保留名称。
44
33
 
45
- 这六类是按需采用的标准入口,不是必须凑齐或只能保留六条的限制。命令接入或精简先按用户任务确定职责,再决定公开入口;有独立职责和实际消费者的扩展可以保留。精简请求不能只缩短帮助文本,还应处理重复包装和误导性别名。具体判断与验收见 [命令职责与精简](references/adoption-regressions.md#命令职责与精简)。
34
+ lint 表达静态检查,单测及脚本守卫归入 test unit,独立集成测试使用扩展。追查 dependsOn 和 package forwarder,避免下层仍隐式混合。组合验证采用清楚命名扩展或显式序列。精简与审查读 [命令职责与精简](references/adoption-regressions.md#命令职责与精简)。
46
35
 
47
- `lint` 表达静态检查,单测和脚本守卫测试归入 `test`。检查真实调用链,包括 Nx target、dependsOn 和 package forwarder,避免顶层已经拆开、下层仍隐式跑测试。若现有项目明确要求组合验证,保留为清楚命名的组合入口或显式命令序列;用户要求拆分时同步调整相关规则,不让旧约定覆盖当前意图。
36
+ ## 3. 参数、环境与准备
48
37
 
49
- 脚本名无扩展名,带有效 shebang 并设置可执行权限。内部逻辑可放入项目自己的 `.mjs` / `.cjs` 或 Shell 文件;无扩展名 Node 脚本的模块模式应与目标项目及最低 Node 版本匹配,不能假定每个项目都是 ESM。
38
+ 命令决定环境,APP_ENV/NODE_ENV 只传递结果。test unit 固定 test、test e2e 固定 e2e,禁止额外环境标志;其他应用命令含扩展默认 dev,最多一个环境标志,重复也报错。应用直接消费结果,不再次解析 dx 标志或根据 NODE_ENV 重新选择环境层。
50
39
 
51
- 推荐将无扩展名公开入口放在 `dx/scripts/`,实现模块及配套测试放在 `dx/lib/`,项目仍消费的策略配置放在 `dx/config/`。后两者是项目组织建议,不是 DX 自动加载契约;现有合理布局可以保留。迁移目录时同步导入、相对根路径、Nx targets/inputs、测试 fixture、CI 和文档,并检查旧路径消费者。
40
+ dx 删除自身消费的环境标志,保留类型、动作、目标、首个 -- 和后续每项参数。工具专属选项放在 -- 后,按数组转交,明确下游是否消费分隔符;不把后续内容重新当作 dx 选项,不拼接 shell 命令或 eval。
52
41
 
53
- 若已有项目脚本可复用,使用薄包装。例如确认 `dx/lib/build.mjs` 已实现后:
42
+ dx 加载公共两层环境,应用不重复加载;额外服务文件明确所有者与覆盖关系。dx start admin --prod 或 dx start admin --staging 仍在当前机器运行,不改成 SSH 或远程部署。应用可为构建子进程单独控制优化模式,同时保留选定应用配置。
54
43
 
55
- ```sh
56
- #!/bin/sh
57
- exec node dx/lib/build.mjs "$@"
58
- ```
44
+ setup 顺序为全量契约及输入检查 → 缺失 .local 复制 → 环境加载 → 应用 setup。先补齐全部入口和声明,通过 dx check 再 setup。应用 setup 幂等且不递归调用 dx setup;缺少凭据时说明准备方式,不能生成虚假秘密。应用负责 start stack 成员、顺序、失败与停止方式,所有入口按真实进程树幂等清理后代。
59
45
 
60
- 包装调用实际工具或实现文件,避免 `dx build` → package script → `dx build` 的循环。不要导入 dx 私有 lib、恢复旧配置解释器,或为每个项目重建通用任务框架。原有 lint 等任务可通过同名脚本继续暴露;`initial` 是 dx 保留命令。
46
+ ## 4. 更新使用方与验收
61
47
 
62
- 实现项目帮助和未知目标的明确失败。需要的入口必须调用真实实现;不需要的入口可以不创建。没有完成的构建、测试、数据库或部署步骤不能用 `echo` / `exit 0` 占位后宣称接入完成。
48
+ 同步 package scripts、CI、容器及部署调用、测试服务启动命令、开发说明和 agent 指令。旧功能清理须有去向与消费者切换证据。升级、审查及参数/环境/构建/进程/布局变化,按 [适配回归检查](references/adoption-regressions.md)相关分支取得隔离与真实运行证据。
63
49
 
64
- ## 3. 接好参数、环境与准备顺序
50
+ 涉及多品牌或多环境部署时,读取[部署坐标与私有 profile](references/adoption-regressions.md#部署坐标与私有-profile)及[远端资源与部署状态](references/adoption-regressions.md#远端资源与部署状态);构建会改写源码目录时,读取[临时生成文件与恢复](references/adoption-regressions.md#临时生成文件与恢复);并行安排验证前,核对[验证任务的并发边界](references/adoption-regressions.md#验证任务的并发边界)。这些是按项目能力采用的适配规则,不要求新增品牌、部署平台或审批流程。
65
51
 
66
- 按契约使用 dx 注入的环境,不再自行运行相同两层 dotenv 加载。项目若有额外服务专属文件,明确由哪个脚本加载及覆盖关系。
52
+ 逐项记录:
67
53
 
68
- 实现和说明 `start` 时,将环境选择与运行位置分开:`dx start admin --prod` 使用当前机器项目根的 `.env.production`、`.env.production.local` 在当前机器启动 admin。`--prod` / `--staging` 等只选择配置及 APP_ENV / NODE_ENV;应用 start 应保持本机启动语义,不将这些标志解释成 SSH、远程启动或部署。具体示例与资源连接边界见契约的“环境”部分。
54
+ - 实际安装源与版本,根和子目录发现正确工程,入口执行权限能随 clone 保留。
55
+ - dx check 静态合规;隔离无效声明、缺入口或损坏扩展时,环境加载、setup 复制和业务未发生。
56
+ - 非敏感标记验证实际两层覆盖、默认环境、继承变量不能选环境、冲突标志拒绝及最终工具 argv,不输出真实环境集合。
57
+ - fresh clone 依赖引导成立,linked worktree 缺失 .local 在 setup 前可用且已有值不覆盖;无主 checkout 单独说明。
58
+ - 真实 unit 运行通过,其他 implemented 能力取得相关运行证据;每个 mock 验证日志命令、原因及退出码 0,记为跳过。start 验证单目标、stack 成员及后代收尾。
59
+ - db/deploy 路由、前置校验与失败行为有相关验证,未执行真实部署明确记录,不将模拟或返回 0 当作发布成功。
69
60
 
70
- dx 识别环境标志但不会从 argv 删除它们。应用消费首个 `--` 前属于环境选择的标志后,再把工具参数逐项传给工具;`--` 后的数据不得再次当作 dx 标志解析。明确工具是否需要保留或消费这个分隔符。禁止把 argv 重新拼成 shell 命令或用 eval。
61
+ 标准帮助无业务副作用,dx setup --help 不复制文件;扩展帮助及 -- --help 是真实调用,不假定无副作用或通用 dry-run。按既有授权处理真实数据库和部署,其余用本地隔离资源验证。
71
62
 
72
- setup 的顺序是 **dx 复制缺失的 .local 文件 → dx 加载环境 → 应用 setup**。先创建有效的应用 setup 入口,基础复制才会执行。应用 setup 不必再次调用 dx 的基类,也不能递归执行 `dx setup`。新 clone 没有本地凭据来源时,给出准备方法,不能凭空生成有效秘密。
73
-
74
- 对构建、E2E、预发和生产选择分别核对 APP_ENV / NODE_ENV。需要测试层的调用明确使用 `--test` 或 `--e2e`;需要生产构建的应用在自己的构建实现中控制工具行为。不要恢复旧 dx 对 Next/Nx 命令文本的隐式识别。
75
-
76
- 确认 `start stack` 的服务成员、依赖顺序、失败策略和停止方式。单应用也可能经包管理器、Nx、Shell 创建多层后代,所有入口都须按实际进程树收尾;Ctrl-C 与 SIGTERM 处理要幂等,不能只验证直接子进程退出。
77
-
78
- ## 4. 更新使用方并验收
79
-
80
- 更新 package scripts、CI、容器或部署调用、开发说明与相关 agent 指令,让它们使用已实现的新入口。旧项目的删除时机与覆盖表按迁移参考执行;新项目无需创建兼容文件或迁移旧功能。
81
-
82
- 升级、适配审查,以及修改命令职责、环境、参数、构建模式、进程管理或脚本目录时,阅读 [适配回归检查](references/adoption-regressions.md),按相关分支取得隔离测试与实际运行证据。只读审查输出按严重度排序的发现、文件位置和复现证据;修复请求则补齐对应回归并推进到验证完成。已有测试通过不代表未覆盖的契约正确。
83
-
84
- 使用目标项目自身的验证方式,按实际副作用选择测试环境。项目适配不自动包含生产部署、正式数据库迁移或远端发布;已有明确授权的操作继续执行,否则先用本地、隔离资源或项目已有的模拟入口验证对应链路。不要把真实部署当作通用冒烟。
85
-
86
- 验收应得到以下证据,而非仅列命令:
87
-
88
- - `dx --version` 对应所选安装源,`dx --help` 在根和子目录发现正确入口;用 Git 文件模式确认脚本执行权限会随 clone 保留。
89
- - 在隔离工程用非敏感标记值验证两层覆盖、显式环境选择及 argv 边界;不输出真实环境变量集合。
90
- - 新 clone 的依赖引导能成立;linked worktree 中缺失的 .local 在应用 setup 前可用,已有本地值不覆盖。没有主 checkout 的环境单独记录,不能假定 CI 自动拥有本地文件。
91
- - 应用 setup、实际构建与相关测试通过。验证 start 的单应用和 stack 成员及退出收尾;无相应服务时注明不适用。
92
- - db/deploy 若属需求,其参数路由、前置校验与失败行为有测试;未获授权的生产执行记录为未执行,不写成部署验证通过。
93
-
94
- `dx --help` 只做发现;`dx <command> --help` 会运行应用脚本,尤其 `dx setup --help` 仍会先复制文件。只有确认项目帮助处理无业务副作用后,才把它用作冒烟。不要假定 dx 提供通用 dry-run。
95
-
96
- 交付说明写清项目走哪条路径、安装源与版本、已实现命令及用法、环境文件与 setup 行为、实际验证、未完成项。升级任务同时给出旧能力的去向;首次接入只给出新增入口。未指定提交或发布时,完成本地改动与验证即可。
63
+ 涉及分发时,在隔离 home 验证 initial 同步本技能及全部参考;涉及 dx 包交付时检查运行模块、示例契约、lint 和技能参考入包。交付区分静态通过、真实通过、mock 跳过及未验证项,说明版本、命令、环境、setup 和旧能力去向。未指定提交或发布时完成本地改动与验证即可。
@@ -2,18 +2,20 @@
2
2
 
3
3
  适用于 DX 升级审查及相关修复。以下来自实际迁移中“入口测试通过,但真实行为错误”的案例;按项目涉及的分支使用,不要求其他项目采用相同框架、服务名或脚本实现。
4
4
 
5
+ 本文随 0.3.0 技能分发。标准接口以[应用契约](dx-0.3-contract.md)为准,接入顺序见[切换向导](switch-to-0.3.md)。
6
+
5
7
  ## 命令职责与精简
6
8
 
7
9
  先从帮助与实际路由建立“入口 → 实现 → 消费者 → 副作用”的对应关系,再决定删除、合并或保留。迁移旧版不等于逐个复刻旧命令,也不等于删除所有自定义扩展。
8
10
 
9
11
  - 相同实现的别名优先收敛为一个明确入口;安装、清理和缓存包装若没有项目特有职责,可回到项目允许的包管理器入口。`setup` 的依赖安装与必要生成仍是完整准备流程,不能仅因包含 install 就视为重复。
10
- - 同类操作可收为子命令,例如将发布计划和执行收进 `deploy`、将 OpenAPI 导出收进 `build`。这些是组织示例,不是 DX 内置路由或所有项目的强制命名;品牌、翻译、私有环境管理等独立职责无需硬塞进六类入口。
12
+ - 同类操作遵守标准位置语法:deploy/build 只接一个目标,工具专属动作或模式放 -- 后;独立计划、品牌、翻译及私有环境管理可使用明确扩展。七个必选入口仍须齐全。
11
13
  - 目标名必须符合覆盖范围:曾有 `deploy all` 实际只部署默认后端,应改为明确 target 或真正的全量编排。帮助首页展示简短用途,目标与参数留给命令级帮助。
12
14
  - 合并部署入口只改变路由,不绕过原适配器的环境校验、确认、产物清理、扫描及远端核验;分别保留只读 plan、远程 run、单目标部署与本地制品构建的边界。
13
15
 
14
16
  ### lint 与 test 的边界
15
17
 
16
- 遇到“lint 为什么必须带 --test”时,先追查是否把单测混进 lint,而不是先修改 DX 的默认环境。`--test` 选择环境,不是开启测试的开关;无环境依赖的静态检查通常可直接 `dx lint`,也无需强制 `--dev`。需要测试配置的测试仍显式选择对应环境。
18
+ 遇到 lint 混入测试时追查真实调用链。静态检查使用 dx lint all 或明确目标,默认 dev;单测使用 dx test unit all,E2E 使用 dx test e2e all,自动选择测试环境且禁止额外环境标志。
17
19
 
18
20
  一次实际拆分中,除了顶层先跑后端单测,`scripts:lint` 还执行整套 `node --test`。只删除顶层 test 调用并没有完成拆分。把脚本测试移到独立 test target,保留分类 forwarder、注册覆盖、缓存策略和明确执行入口;原来要求两类检查的后端验证与 CI 改为显式运行 lint 和 test,避免测试覆盖随改名丢失。参数也应按新职责核验,例如 `--fix` 只传给静态检查,脚本测试聚合入口若不支持过滤就明确拒绝,不能静默忽略。
19
21
 
@@ -23,18 +25,18 @@
23
25
 
24
26
  用隔离替身捕获实际 argv 和调用次数:普通 lint 不调用测试、fix 只影响静态检查、独立测试仍选对环境;合并入口到达原安全适配器,未知目标或不支持的参数在业务工具启动前失败。按影响范围跑已有注册表与适配回归,区分替身验证、真实工具运行和未执行的全仓检查,不因命令精简而运行真实发布。
25
27
 
26
- 核对根与子目录的 DX 命令发现,以及项目帮助。删除顶层命令需要处理 `dx/scripts/` 的可执行入口,只从 usage 中隐藏不能改变 `dx --help` 的发现结果。测试按 DX 的文件名、可执行权限和 shebang 规则识别公开入口;目录里的 `.mjs` 辅助文件不等于公开命令,不能直接断言全部目录项都是命令。
28
+ 核对根与子目录发现及帮助。删除扩展需处理公开入口和消费者,七个标准入口保持齐全。dx/scripts 只放合法公开入口,辅助模块移到 dx/lib 等目录;全量检查也会拒绝损坏扩展及保留名称。标准帮助由 dx 提供,应用工具帮助经 -- --help,扩展帮助仍执行应用。
27
29
 
28
30
  ## 环境选择与隔离
29
31
 
30
- DX 在应用入口运行前加载环境。入口发现是 E2E 后再调用 `dx ... --e2e`,会把第一次加载的 development 独有变量作为继承环境带入第二次调用,也会影响变量展开。仅覆盖 APP_ENV/NODE_ENV 或删除已知变量无法还原原始环境。
32
+ DX 在应用入口运行前按命令类型选择环境。直接调用 dx test unit/e2e 加目标,不在已加载 development 的包装入口内再次切到测试环境,以免继承普通变量污染或影响展开。仅覆盖 APP_ENV/NODE_ENV 无法清除继承普通变量。
31
33
 
32
- 推荐调用方明确传 `--e2e` / `--test`,入口检查选定环境是否匹配测试套件;不匹配时在启动工具前失败,并给出正确命令。已通过调用者 APP_ENV 选对环境时可接受,是否强制显式标志由项目决定。同步 CI、Playwright webServer、测试内启动命令、文档和 agent 指令,避免保留隐式测试环境示例。
34
+ 同步 CI、Playwright webServer、测试内启动命令和说明:test unit/e2e 不传环境标志;其启动的非测试 dx 命令默认仍为 dev,需显式选择如 dx start stack --e2e。调用者 APP_ENV/NODE_ENV 不能选环境。
33
35
 
34
36
  隔离工程用不同的非秘密标记值验证:
35
37
 
36
38
  - development 独有变量不会由入口自动切换环境带入 E2E;错环境入口在启动测试或连接服务前失败。
37
- - 显式 E2E 的展开结果仅来自调用者及 E2E 两层;`.env.test` 不再是 E2E 的前置层。
39
+ - test e2e 的展开结果仅来自调用者普通变量及 E2E 两层;.env.test 和 development 都不是前置层。已有 APP_ENV/NODE_ENV 不改变选择。
38
40
  - 公开文件覆盖继承环境、local 覆盖公开文件;CI 的同名值和占位符也包含在覆盖测试内。
39
41
 
40
42
  需要把旧 `.env.test.local` 的缺项迁到 `.env.e2e.local` 时,只迁测试需要的键,保留目标已有值与文件权限,不输出秘密。实际测试使用的 DSN、播种参数、Redis 和登录凭据须在独立环境中齐备。
@@ -51,9 +53,9 @@ APP_ENV 表示应用配置,NODE_ENV/工具 mode 影响打包行为。`dx build
51
53
 
52
54
  ## 参数边界
53
55
 
54
- 先按第一个 `--` 拆分应用参数与工具参数,再处理应用侧环境标志、帮助和未知选项。为每个入口明确是否支持透传,以及下游消费还是保留分隔符;不支持时明确失败。
56
+ dx 已消费环境标志并处理标准帮助;应用按首个 -- 分开位置参数和工具参数,不重新解析 dx 环境标志。为每个入口明确透传支持及下游分隔符处理,不支持的工具参数明确失败。
55
57
 
56
- 隔离捕获下游 argv,覆盖 `-- --help`、`-- --prod`、带空格的过滤条件、空参数及未知选项。`--` 后的内容不能触发项目帮助或改变环境。经过 Nx、JSON 环境载体等多层包装时,验证最终工具收到的数组;补一个真实过滤测试,确认只执行选定用例,避免参数被中间层丢弃。真实资源相关测试仍受项目授权边界约束。
58
+ 隔离捕获下游 argv,覆盖 `-- --help`、`-- --prod`、带空格的过滤条件、空参数及未知选项。`--` 后的内容不参与 dx 的帮助或环境解析,应用按约定交给工具处理。经过 Nx、JSON 环境载体等多层包装时,验证最终工具收到的数组;补一个真实过滤测试,确认只执行选定用例,避免参数被中间层丢弃。真实资源相关测试仍受项目授权边界约束。
57
59
 
58
60
  ## 单应用与 Stack 进程回收
59
61
 
@@ -81,12 +83,19 @@ APP_ENV 表示应用配置,NODE_ENV/工具 mode 影响打包行为。`dx build
81
83
 
82
84
  `dx/scripts/` 是公开入口契约;`dx/lib/`(实现与配套测试)和 `dx/config/`(项目策略)是可选的集中布局。移动时核对:
83
85
 
86
+ 采用集中布局且存在相关能力时,可按职责组织 `dx/lib/brand/`(品牌生成、投影、资源与就绪检查)和 `dx/lib/release/`(发布编排、平台适配器与构建入口),测试与实现同目录;品牌身份与发布拓扑仍由项目各自的配置真源维护。这是组织方式示例,不是 DX 强制目录契约。
87
+
84
88
  - 包装入口、相对 import、基于 import.meta.url 的根定位、测试复制 fixture。
85
89
  - Nx targets/inputs、lint glob、容器 COPY、CI、部署配置、文档与 agent 指令。
90
+ - 测试分类 forwarder 及注册覆盖;发现工作区测试时过滤已经移走但尚未暂存删除的旧路径。生成文件头部的生成器路径从生成器修改并重新生成,不手改派生产物。
86
91
  - 根与子目录调用、Git 可执行文件模式、至少一个真实命令;搜索旧路径确认没有活动消费者。
87
92
 
88
93
  setup 使用隔离 Git 主 checkout、linked worktree 和独立 clone 验证:复制发生在应用 setup 前,已有 local 不覆盖、权限保留,独立 clone 不继承另一 clone 的秘密。应用 setup 保持幂等;仅存在 `dx/config/` 不等于有效的 `dx/scripts/setup`。
89
94
 
95
+ 再次 setup 不会刷新已有 `.local` 内容;需要补充配置时逐项处理缺项,保留现有值和权限。缺少环境文件也不会自动阻止数据库或部署,必需变量、连接目标和确认条件由应用在工具启动前验证。
96
+
97
+ 切换入口时保留仍被使用的 Prisma schema、PM2 ecosystem、Nx 配置和 dotenv 依赖。只清理已经没有消费者的包装或策略文件;端口冲突处理、服务准备、重试与失败恢复需在项目实现中有明确归属,不能因为统一了命令入口就遗漏。
98
+
90
99
  ## 数据库、部署与失败归因
91
100
 
92
101
  读取目标项目授权规则并核实实际连接目标。项目已授权 DEV/E2E reset、重建或迁移时,可在核实目标后处理校验和冲突等环境问题并重跑,不重复询问同一授权;这不是其他项目的默认授权。staging/production 的操作需要该项目用户针对环境及操作的明确授权,本地启动和 `--yes` 不能代替授权。
@@ -98,3 +107,47 @@ setup 使用隔离 Git 主 checkout、linked worktree 和独立 clone 验证:
98
107
  部署凭据采用工具支持的非 argv 通道;例如核实所用 Vercel CLI 支持后通过 `VERCEL_TOKEN` 传递,避免 `--token` 暴露在进程参数和命令日志中。用假令牌检查最终 argv 与输出。CI 需要临时 local 凭据文件时,在创建文件前注册清理,再写入并运行命令,覆盖写入或后续步骤失败的路径。
99
108
 
100
109
  失败按复现证据区分本次回归、既有问题与环境问题;必要时在不覆盖当前工作区的隔离基线复现。相关问题解决并复验后更新已知失败记录。纯技能或文档更新只检查结构、引用与差异,不因此重跑业务全仓测试。
110
+
111
+ ## 部署坐标与私有 profile
112
+
113
+ 多品牌或多环境项目把品牌、应用环境、目标、团队及项目作为一组部署坐标,在入口解析后显式传入实际构建和上传进程。CLI 收到了 `--brand`,不等于下游环境已经包含品牌。明确选择器的优先级:显式部署参数可以覆盖继承值,或在冲突时拒绝;不能静默使用另一品牌。构建缓存也须覆盖实际影响产物的品牌与环境输入。
114
+
115
+ 用隔离替身或非秘密标记观察最终子进程,覆盖未设置选择器、继承另一品牌、嵌套包装及不同环境;验证目标项目与产物品牌一致,不仅断言入口解析结果。平台若将预发项目也作为其 production target 部署,须明确区分应用环境与平台 target,按实际坐标判断是否发错环境。
116
+
117
+ 私有 profile 保存凭据与部署坐标,品牌公开域名、CDN、名称等构建配置从项目品牌真源派生;不要因为它们通过环境变量传递就维护多份真源。需要规范 dotenv 路径的工具由包装器按需临时物化,退出后清理;持久 profile、提交模板和临时文件的职责分开。私有文件保持 Git 忽略及限制权限,日志和异常不输出真实 token 或整份环境。
118
+
119
+ 一个 profile 共用团队 ID 时,切换团队需同步核对该 profile 下所有目标的项目 ID,避免新团队与旧项目混用。先准备并校验完整的新映射,再更新相关键,保留无关配置和权限。缺失 profile 可从对应模板创建,缺失服务凭据明确列出,不默认复制另一品牌的数据库、SSH 或服务身份。
120
+
121
+ ## 远端资源与部署状态
122
+
123
+ 在用户授权的团队创建或调整项目之前,用凭据只读核对账号、目标团队及已有项目。认证成功不代表有目标团队权限;团队 slug 用于查找,回读的 ID 用于配置。查重时同时核对归属与名称,创建后回读确认再写本地映射。根据所用 API/CLI 版本核对参数,不把更新接口的字段假定为创建接口也接受;失败后先确认是否已创建资源,再决定重试。
124
+
125
+ 平台可能另行校验 Git 提交作者的成员权限和代码托管账号关联,例如 Vercel 的 `TEAM_ACCESS_REQUIRED`。遇到阻断时核对提交作者、token 所属账号及团队权限,按平台支持的方式使用真实获授权身份;不要把 token 有效当作作者权限证明,也不通过伪造作者或重写历史绕过限制。
126
+
127
+ 上传完成与生成部署 URL 都不等于发布完成。按部署 ID 查询远端真实状态,绑定预期团队、项目和应用环境;遇到平台的失败终态(如 `BLOCKED`、`ERROR`、`CANCELED`)立即报告安全的原因,不能一直显示 `Building…`。等待设置有限超时,并提供精确部署链接;超时后状态未知时先查询原部署,不自动重复上传或撤销远端资源。
128
+
129
+ 用替身覆盖失败终态、状态长期不变、API 请求失败和归属不匹配。只有远端达到预期成功状态且所需产物核验通过才报告部署成功。交付分别说明资源创建、私有配置写入、完整 profile 校验、实际部署与远端验证;只填好了平台参数但缺其他必需配置时,不能宣称已经可部署。
130
+
131
+ ## 临时生成文件与恢复
132
+
133
+ 先区分长期派生产物与临时构建投影,再决定 Git 跟踪策略。根据 fresh clone、编辑器、构建准备和漂移检查的需要保留或调整跟踪,不为隐藏部署残留直接忽略文件;取消跟踪并不会清除磁盘上残留的另一品牌配置。调整跟踪属于单独的依赖与准备流程变更。
134
+
135
+ 在第一次临时改写之前,为本次会触及的明确文件集合保存原始字节、权限及是否存在。边界覆盖所有会改写它们的准备、构建和部署步骤;外层准备已改写之后才备份,会把投影残留当作原始状态。文件集合从实际生成器确定并随其输出变化维护,不照搬其他项目的固定名单。
136
+
137
+ 在成功、失败及 SIGINT/SIGTERM 路径恢复部署前状态:原来不存在的临时文件移除,原来的本地修改保留;构建产物按项目策略保留。不能以 `git restore .`、全仓 reset 或无差别重新生成代替快照恢复,这些操作可能覆盖无关修改或部署前已有的生成文件编辑。既有残留先验证与已知投影一致,再按当前授权恢复标准生成态。
138
+
139
+ 恢复必须等待本次写入进程及后代停止,不能只等待直接子进程退出。信号处理保留到收尾完成;恢复失败返回非零,并保留原失败原因及可恢复证据,不把失败报告成成功。有并行发布时按共享写入范围串行、加互斥或隔离工作区,避免两个快照交错恢复;无法捕获的强杀不承诺由 finally 恢复,需要时设计持久备份及下次运行的恢复入口。
140
+
141
+ 回归验证从真实 DX 入口进入,用隔离目录和部署替身产生实际文件改写,覆盖:
142
+
143
+ - 成功、构建或上传失败、最外层 SIGINT/SIGTERM;原退出码或中断状态继续传播。
144
+ - 部署前有本地编辑、原文件缺失、非默认权限与原始字节;无关文件及构建产物不被误恢复。
145
+ - 子进程收到停止信号后仍延迟写入;断言最终恢复发生在最后一个写入者退出之后。
146
+
147
+ 仅匹配脚本中的 try/finally、restore 字符串或模拟返回 0,不足以证明恢复生效。
148
+
149
+ ## 验证任务的并发边界
150
+
151
+ 并行前沿真实依赖链检查读写集合。lint 可能依赖类型检查和契约生成,build 也可能写同一目录;多个看似只读的验证入口还可能自动安装依赖。共享暂存目录、生成代码、品牌投影或 node_modules 的命令应串行,或放入隔离工作区;单个编排器内部的依赖调度不等于多个外部进程之间有锁。
152
+
153
+ 并发后出现重复声明、暂存目录消失、移动文件失败或 bin 链接创建失败时,先确认相关进程已经结束,再通过正常生成或安装入口串行恢复并复验。用证据区分调度冲突与代码回归,不通过修改生成代码、放宽依赖策略或忽略错误来消除症状。共享输入未再变化且相关检查已通过时,不重复扩大验证。