@guandata/guanetl 0.1.18 → 0.1.20

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## @guandata/guanetl 0.1.20 - 2026-07-20
4
+
5
+ - `save` 会按“同一输出目录 + 同名 + 唯一匹配”自动恢复已绑定输出的节点 id 和 dsId,避免 AI 重写输出节点 id 时误建新数据集并断开下游依赖。
6
+ - 移除或替换已绑定输出时默认阻止 `direct-save`;仅在显式使用 `--allow-output-replacement` 后放行,并提示调用方自行迁移下游 dsId 引用。
7
+ - `preview` 返回 0 行时不再误报“预览验证通过”,默认输出数据质量 warning;新增 `--require-nonempty`,供自动化流程在空结果时返回错误并阻止后续保存。
8
+ - 新建 ETL 首次 `save` 缺少服务端 edit 基线时改为输出带 `save.first_save_fallback` 标记的正常回退信息;其他 edit 读取失败继续以 `save.edit_fallback` warning 呈现。
9
+ - 全局安装或升级后自动刷新 AI Skill,并随包提供完整使用说明和参考资料。
10
+
11
+ ## @guandata/guanetl 0.1.19 - 2026-07-08
12
+
13
+ - 新增 `move` 命令,支持将一个或多个智能 ETL 移动到指定 ETL 目录,并在接口异常时读回确认移动结果。
14
+ - `run` 新增 `--run-upstream` 与配套 `--dry-run`,支持递归解析智能 ETL 上游链路、按拓扑顺序执行并等待每个节点完成。
15
+ - `run --wait` 遇到同一 ETL 已在运行的 40001 响应时,会查找当前运行中的任务并等待其完成,减少级联触发后重复 run 的误判失败。
16
+ - `export` 静态检查新增 JOIN 键类型不一致 warning,提示 STRING/LONG 等隐式 coercion 风险。
17
+
3
18
  ## @guandata/guanetl 0.1.18 - 2026-07-01
4
19
 
5
20
  - 创建 ETL 时目录类型诊断更清晰,会识别误用工作流/经典数据流目录的场景,并提示使用智能 ETL 目录。
package/README.md CHANGED
@@ -18,18 +18,26 @@ npm link
18
18
  guanetl edit <etl_id> --dir <work_dir>
19
19
  guanetl export --dir <work_dir>
20
20
  guanetl preview <node_id> --dir <work_dir>
21
+ guanetl preview <node_id> --dir <work_dir> --require-nonempty
21
22
  guanetl save --dir <work_dir>
22
23
  ```
23
24
 
24
25
  说明:npm 包名为 `@guandata/guanetl`,用户侧 CLI 命令统一为 `guanetl`。
25
26
 
26
- 标准 ETL 写入闭环:`create/edit → export → preview → save → run --wait`。
27
+ 标准 ETL 写入闭环:`create/edit → export → preview → save → run --wait`。如果目标 ETL 依赖的智能 ETL 上游也需要刷新,可先用 `guanetl run <etl_id> --run-upstream --dry-run` 查看拓扑计划,再用 `guanetl run <etl_id> --run-upstream` 从最上游依次执行并等待完成。
27
28
 
28
- > **触发成功 ETL 执行成功**:`run` 返回"执行已触发"仅表示后端接受了请求。使用 `run --wait` 等待终态,FAILED 时会展示真实错误消息。触发前会检查直接上游数据集状态;若发现上游处于失败态,会先输出警告但继续执行,可用 `--skip-upstream-check` 跳过检查。
29
+ `preview` 返回 0 行时默认输出 warning 并保持兼容的成功退出码;自动化发布或要求输出必须有数据时使用 `preview --require-nonempty`,并仅在命令成功后继续 `save`。
30
+
31
+ 新建 ETL 首次 `save` 时若服务端尚无 edit 基线,CLI 会输出 `信息 [save.first_save_fallback]` 并使用本地 base,这是正常路径。`警告 [save.edit_fallback]` 则表示其他 edit 读取故障后发生了兼容回退,需要检查网络、权限和本地基线。
32
+
33
+ > **触发成功 ≠ ETL 执行成功**:`run` 返回"执行已触发"仅表示后端接受了请求。使用 `run --wait` 等待终态,FAILED 时会展示真实错误消息;如果同一 ETL 已被级联触发并正在运行,`run --wait` 会改为等待当前运行中的任务。触发前会检查直接上游数据集状态;若发现上游处于失败态,会先输出警告但继续执行,可用 `--skip-upstream-check` 跳过检查。
34
+ > `run --run-upstream` 会包含目标 ETL 本身,并对计划内每个 ETL 等待终态;任一上游执行失败时会停止后续节点。
29
35
 
30
36
  新建 ETL 时注意目录树不同:`create --parent-dir` 使用 ETL 目录树 id,输出数据集目录使用 DATA_SET 目录树 id。可用 `guancli etl tree` / `guancli ds tree` 分别查询,或用 `guanetl mkdir-pair` 成对创建。`guancli workflow tree` 是工作流/经典数据流目录树,不能作为智能 ETL 的 `create --parent-dir`。
31
37
 
32
- `export` 成功后会自动输出本地静态检查提示;其中 `BasicCalculator` 的每个 `Formula` 都应显式填写 `Type`,否则可能导致下游原生 GroupBy/Join 节点出现静态类型警告。
38
+ 移动已有 ETL 使用 `guanetl move <etl_id> [etl_id...] --dir-id <etl_dir_id>`;目标目录同样来自 `guancli etl tree`,可先加 `--dry-run` 查看请求体。
39
+
40
+ `export` 成功后会自动输出本地静态检查提示;其中 `BasicCalculator` 的每个 `Formula` 都应显式填写 `Type`,否则可能导致下游原生 GroupBy/Join 节点出现静态类型警告。JOIN 键两侧类型不一致时会提示隐式 coercion 风险,建议统一键类型。
33
41
 
34
42
  也可以为 AI Coding Assistant 安装 Skill:
35
43
 
@@ -37,8 +45,24 @@ guanetl save --dir <work_dir>
37
45
  guanetl install-skill
38
46
  ```
39
47
 
48
+ > `npm install -g` / `npm link` 全局安装时会通过 postinstall 自动执行一次 skill 安装/刷新;上述命令用于手动重装或排查。CI 等无需 skill 的环境可设 `GUAN_SKIP_INSTALL_SKILL=1` 跳过。
49
+
40
50
  ## 版本更新
41
51
 
52
+ ### @guandata/guanetl 0.1.20
53
+
54
+ - `save` 会在同目录同名输出唯一匹配时自动恢复原输出节点 id 和 dsId,避免编辑已有 ETL 时误建新输出数据集。
55
+ - 替换或移除已绑定输出默认阻断;确需替换时显式使用 `--allow-output-replacement`,并自行迁移下游 dsId 引用。
56
+ - 预览结果为空时会明确提醒;自动化流程可要求必须返回数据后再继续保存。
57
+ - 全局安装或升级后自动刷新 AI Skill,并随包提供完整使用说明和参考资料。
58
+
59
+ ### @guandata/guanetl 0.1.19
60
+
61
+ - 新增 `move` 命令,支持将一个或多个智能 ETL 移动到指定 ETL 目录,并在接口异常时读回确认移动结果。
62
+ - `run` 新增 `--run-upstream` 与配套 `--dry-run`,支持递归解析智能 ETL 上游链路、按拓扑顺序执行并等待每个节点完成。
63
+ - `run --wait` 遇到同一 ETL 已在运行的 40001 响应时,会查找当前运行中的任务并等待其完成,减少级联触发后重复 run 的误判失败。
64
+ - `export` 静态检查新增 JOIN 键类型不一致 warning,提示 STRING/LONG 等隐式 coercion 风险。
65
+
42
66
  ### @guandata/guanetl 0.1.18
43
67
 
44
68
  - 创建 ETL 时目录类型诊断更清晰,会识别误用工作流/经典数据流目录的场景,并提示使用智能 ETL 目录。
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Best-effort post-install hook: refresh the installed AI skill right after
5
+ * a global npm install/upgrade, so users cannot forget to run
6
+ * `guanetl install-skill` and end up with a stale SKILL.md.
7
+ *
8
+ * Constraints:
9
+ * - MUST never fail the npm install: every failure path still exits 0.
10
+ * - Only runs for global installs (`npm install -g` / `npm link`); a local
11
+ * `npm install` inside a project never touches the user's home dirs.
12
+ * - Escape hatch: GUAN_SKIP_INSTALL_SKILL=1 skips entirely (e.g. CI images
13
+ * that install the CLI but never run an AI agent are skipped by default).
14
+ */
15
+
16
+ "use strict";
17
+
18
+ const { spawnSync } = require("child_process");
19
+ const path = require("path");
20
+
21
+ const CLI_NAME = "guanetl";
22
+
23
+ function skipReason() {
24
+ if (process.env.GUAN_SKIP_INSTALL_SKILL === "1") return "GUAN_SKIP_INSTALL_SKILL=1";
25
+ if (process.env.npm_config_global !== "true") return "not a global install";
26
+ if (process.env.CI) return "CI environment";
27
+ return "";
28
+ }
29
+
30
+ function main() {
31
+ const reason = skipReason();
32
+ if (reason) {
33
+ console.log(`[${CLI_NAME}] postinstall: skipping automatic install-skill (${reason}).`);
34
+ return;
35
+ }
36
+ console.log(`[${CLI_NAME}] postinstall: refreshing AI skill (${CLI_NAME} install-skill)...`);
37
+ const result = spawnSync(
38
+ process.execPath,
39
+ [path.join(__dirname, "run.js"), "install-skill"],
40
+ { stdio: "inherit", env: process.env, timeout: 180000 }
41
+ );
42
+ if (result.error || result.status !== 0) {
43
+ console.warn(
44
+ `[${CLI_NAME}] postinstall: automatic skill install did not complete` +
45
+ (result.error ? ` (${result.error.message})` : ` (exit ${result.status})`) +
46
+ `; run \`${CLI_NAME} install-skill\` manually to refresh SKILL.md.`
47
+ );
48
+ }
49
+ }
50
+
51
+ try {
52
+ main();
53
+ } catch (err) {
54
+ console.warn(
55
+ `[${CLI_NAME}] postinstall: ${err.message}; run \`${CLI_NAME} install-skill\` manually.`
56
+ );
57
+ }
58
+ process.exit(0);
package/bin/run.js CHANGED
@@ -86,10 +86,36 @@ if (process.argv[2] === "version" && process.argv.length === 3) {
86
86
  process.exit(0);
87
87
  }
88
88
 
89
+
90
+ // 定位 npm 自带的 npx-cli.js 并用 node 直接执行(与 guanskill 保持一致),
91
+ // 避免 Windows 上 spawn npx.cmd 的两类问题:shell:true 不做参数 quoting
92
+ // (全局安装路径含空格时会被拆参),以及新版 Node 禁止无 shell 的 .cmd
93
+ // spawn(CVE-2024-27980)。postinstall 场景下 npm_execpath 必然可用。
94
+ function resolveNpxInvocation() {
95
+ const executableDir = path.dirname(process.execPath);
96
+ const candidates = [];
97
+ if (process.env.npm_execpath) {
98
+ candidates.push(path.join(path.dirname(process.env.npm_execpath), "npx-cli.js"));
99
+ }
100
+ candidates.push(path.join(executableDir, "node_modules", "npm", "bin", "npx-cli.js"));
101
+ candidates.push(path.resolve(executableDir, "..", "node_modules", "npm", "bin", "npx-cli.js"));
102
+ candidates.push(path.resolve(executableDir, "..", "lib", "node_modules", "npm", "bin", "npx-cli.js"));
103
+ const npxCliPath = candidates.find((candidate) => fs.existsSync(candidate));
104
+ if (npxCliPath) {
105
+ return { command: process.execPath, argsPrefix: [npxCliPath] };
106
+ }
107
+ if (process.platform === "win32") {
108
+ throw new Error("Cannot locate npm/bin/npx-cli.js for safe Windows execution");
109
+ }
110
+ return { command: "npx", argsPrefix: [] };
111
+ }
112
+
89
113
  if (process.argv[2] === "install-skill") {
90
114
  const pkgRoot = path.join(__dirname, "..");
91
115
  const extraArgs = process.argv.slice(3);
92
116
  const args = [
117
+ // --yes: postinstall 等非交互环境下 npx 需要免确认下载 skills CLI
118
+ "--yes",
93
119
  "skills",
94
120
  "add",
95
121
  pkgRoot,
@@ -100,7 +126,12 @@ if (process.argv[2] === "install-skill") {
100
126
  ...extraArgs,
101
127
  ];
102
128
  console.log("Installing guanetl to AI coding assistants...");
103
- const result = spawnSync("npx", args, { stdio: "inherit", env: process.env, shell: true });
129
+ const npxInvocation = resolveNpxInvocation();
130
+ const result = spawnSync(npxInvocation.command, [...npxInvocation.argsPrefix, ...args], {
131
+ stdio: "inherit",
132
+ env: process.env,
133
+ shell: false,
134
+ });
104
135
  if (result.error) throw result.error;
105
136
  const status = result.status || 0;
106
137
  if (status === 0) installBuddySkills(pkgRoot, "guanetl");
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@guandata/guanetl",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "观远 ETL 本地开发工具 - 拉取、编辑、导出、预览、保存 ETL",
5
5
  "bin": {
6
6
  "guanetl": "bin/run.js"
7
7
  },
8
8
  "scripts": {
9
+ "postinstall": "node bin/postinstall.js",
9
10
  "build": "node scripts/build.js && node scripts/sync-skill.js",
10
11
  "test:build-script": "node scripts/build.test.js",
11
12
  "changelog": "node ../../scripts/generate-release-changelog.js .",
@@ -132,7 +132,8 @@ guanetl edit <etl_id> --dir <work_dir>
132
132
 
133
133
  ```bash
134
134
  guanetl export --dir <work_dir>
135
- guanetl preview <node_id> --dir <work_dir>
135
+ # 保存闭环默认要求预览至少返回 1 行;明确允许合法空结果时可省略 --require-nonempty
136
+ guanetl preview <node_id> --dir <work_dir> --require-nonempty
136
137
  guanetl save --dir <work_dir> --dry-run
137
138
  guanetl save --dir <work_dir>
138
139
  guanetl run <etl_id> --wait # 可选:保存后触发执行并等待完成
@@ -140,11 +141,14 @@ guanetl run <etl_id> --wait # 可选:保存后触发执行并等待
140
141
 
141
142
  **再次修改已 save 过的 ETL 时**:即使本地还保留着上次的工作目录,也必须重新执行 `guanetl edit <etl_id> --dir <新目录>` 拉取服务端最新版本,不要在旧工作目录上直接改了再 save。原因:服务端版本可能已经变化,直接复用旧 base 会导致合并冲突(如"输出数据集目录中存在同名文件")。
142
143
 
143
- 如果 `save` direct-save 前提示输出数据集绑定风险,按提示处理,不要手动删除旧输出数据集。常见原因是本地改动把已有 `OUTPUT_DATASET` 节点的 dsId/dataSource 绑定丢掉、新建了同目录同名输出节点,或用 `guands dataset rename` 改了输出数据集名但 `etl.go` `outputDsName` 没同步。修复优先级:重新 `edit` 到新目录、保持原输出节点 id、同步 `outputDsName`;确实要新建独立输出时,同时更换 `OUTPUT_DATASET` 节点 id,并设置新的 `outputDsName` 或 `parentDirId`。
144
+ `save` 会协调已保存 ETL 的输出身份:当本地 `OUTPUT_DATASET` 节点 id 变化,但它与服务端已绑定输出在同一目录、同名且双方唯一时,CLI 会自动恢复原节点 id dsId,继续原地更新已有输出数据集。该协调不会修改本地 `_exported.json`,保存影响报告会显示 `save.output_binding_reconciled`。
144
145
 
145
- 只追加输出列是安全例外:新增列、不改名、不删已有列、不改已有列类型,并保持原 `OUTPUT_DATASET` 节点 id `outputDsName` 时,可以原地 `save`,保存前仍需 `preview` 目标输出确认结果。改列名、删列、改类型、换输入数据集、重接输出链路或更换输出节点 id 都按下游绑定风险处理。
146
+ 如果 `save` direct-save 前提示输出数据集绑定风险,按提示处理,不要手动删除或重命名旧输出数据集。跨目录、重名歧义、删除旧输出或无法唯一匹配时,CLI 不会猜测并默认停止保存。修复优先级:重新 `edit` 到新目录、同步 `outputDsName`、确认输出目录;确实要替换已绑定输出时,设置新的节点 id、`outputDsName``parentDirId`,并显式加 `--allow-output-replacement`。该选项只放行保存,不会迁移任何下游 dsId 引用。
147
+
148
+ 只追加输出列是安全例外:新增列、不改名、不删已有列、不改已有列类型,并保持输出名称和目录时,可以原地 `save`,保存前仍需 `preview` 目标输出确认结果。改列名、删列、改类型、换输入数据集或重接输出链路仍按下游 schema 风险处理;输出节点 id 变化仅在唯一匹配时自动协调。
146
149
 
147
150
  保存前优先执行 `save --dry-run` 查看影响报告;`--format json` 可用于自动化检查。dry-run 不调用 direct-save,若报告出现阻断风险,先修 `etl/` 后重新 `export -> preview -> save --dry-run`。
151
+ 如果报告出现“输出字段风险”,表示同一输出数据集中的同名字段发生类型或来源变化,服务端可能重建该列 fdId;保存前先用 `guands dataset cards <输出dsId>` 看下游卡片,保存执行后再按列名核对并重绑页面卡片 fdId。
148
152
 
149
153
  ### 新建 ETL
150
154
 
@@ -167,9 +171,16 @@ guanetl create --name "ETL名称" --dir <work_dir> --parent-dir <etl_dir_id> --o
167
171
  `create` / `save` 会校验目录 id 类型:`--parent-dir` 必须来自 ETL 树,输出数据集目录必须来自 DATA_SET 树。只有在确认目录 id 正确但当前账号无法读取目录树时,才使用 `--skip-dir-check`。
168
172
  如果错误提示目录 id 属于 `MASTER_FLOW`,说明拿到了工作流/经典数据流目录 id;智能 ETL 改用 `guancli etl tree` 查询,工作流/数据流创建改用 `guanwf create --parent-dir`。
169
173
 
174
+ 移动已有智能 ETL 用 `move`,目标目录同样必须来自 `guancli etl tree`。可先加 `--dry-run` 查看将提交的 `/api/etl/move` 请求体;批量移动直接追加多个 ETL ID:
175
+
176
+ ```bash
177
+ guanetl move <etl_id> --dir-id <etl_dir_id> --dry-run
178
+ guanetl move <etl_id1> <etl_id2> --dir-id <etl_dir_id>
179
+ ```
180
+
170
181
  4. 在 `etl/` 中实现 ETL。
171
182
  5. 再执行 `export -> preview -> save`。
172
- 6. 需要立即执行时:`run <etl_id> --wait`。
183
+ 6. 需要立即执行时:`run <etl_id> --wait`;需要先刷新可识别的智能 ETL 上游链路时,先用 `run <etl_id> --run-upstream --dry-run` 看计划,再用 `run <etl_id> --run-upstream` 执行。
173
184
 
174
185
  ### 修复失败
175
186
 
@@ -191,14 +202,18 @@ guanetl create --name "ETL名称" --dir <work_dir> --parent-dir <etl_dir_id> --o
191
202
 
192
203
  `save --dry-run` 只执行到合并和保存影响检查,不执行第 4 步。
193
204
 
205
+ 新建 ETL 首次保存前,服务端可能还没有可供 edit API 读取的完整基线。此时 `save` 会使用 `create` 生成的本地 `_base_etl.json`,并输出 `信息 [save.first_save_fallback]`;这是正常的首次保存路径,不代表 create 或 save 失败。其他服务端 edit 读取失败会输出 `警告 [save.edit_fallback]`,表示虽然已回退本地 base,但仍需确认网络、权限及本地基线是否可靠。
206
+
194
207
  用户不需要手写服务端保存 payload。只要 `etl/` 修改正确、`export` 通过,`save` 就能完成服务端保存。
195
208
 
196
- ### run --wait 与任务状态
209
+ ### run --wait、run --run-upstream 与任务状态
197
210
 
198
211
  > **触发成功 ≠ ETL 执行成功**。`run` 返回 `✓ ETL 执行已触发` 仅表示后端接受了执行请求,ETL 可能在运行中失败。
199
212
 
200
213
  - 不加 `--wait` 时,`run` 只触发并返回 `taskId`。
201
214
  - 加 `--wait` 后,CLI 会轮询任务状态直到终态(FINISHED / FAILED / CANCELED),FAILED 时会展示真实错误消息。
215
+ - 需要递归刷新智能 ETL 上游链路时,用 `run <etl_id> --run-upstream`。CLI 会读取目标 ETL 的输入数据集,按数据集的生产 ETL 继续向上解析,生成拓扑顺序后从最上游依次执行,包含目标 ETL;每个 ETL 都会等待终态,任一失败即停止并报告失败节点。
216
+ - 不确定上游范围时,先用 `run <etl_id> --run-upstream --dry-run` 输出执行计划;dry-run 只读拓扑,不触发任何 ETL。
202
217
  - 触发前,`run` 会检查直接上游数据集状态;若发现上游处于 `FAILED`/`失败` 态,会先输出警告但继续触发执行。确认不需要检查时可加 `--skip-upstream-check`。
203
218
  - 如果不加 `--wait` 后想查状态:`task status <taskId>` 或 `task wait <taskId>`。
204
219
 
@@ -226,7 +241,7 @@ guancli task detail <taskId>
226
241
 
227
242
  ### preview 返回 0 行
228
243
 
229
- preview 成功但行数为 0 是合法结果,**不一定代表有错误**。排查顺序:
244
+ preview 成功但行数为 0 是合法结果,**不一定代表有错误**。CLI 默认返回成功但会输出 warning,且不会再宣称“预览验证通过”。自动化发布或确认输出必须有数据时使用 `preview --require-nonempty`;零行将返回非零退出码,应停止后续 `save`。排查顺序:
230
245
  1. 确认输入数据集本身有数据(用 `guancli ds preview <dsId> --limit 5`)
231
246
  2. 如果节点有 FILTER_ROWS 或 WHERE 条件,临时简化筛选条件再 preview
232
247
 
@@ -339,7 +339,7 @@ func DefineETL() []Node {
339
339
  始终按这个顺序:
340
340
 
341
341
  1. `guanetl export --dir <work_dir>`
342
- 2. 需要看结果时:`guanetl preview <node_id> --dir <work_dir>`
342
+ 2. 需要看结果时:`guanetl preview <node_id> --dir <work_dir>`;保存前要求非空时追加 `--require-nonempty`,零行会返回错误并阻止命令链继续执行
343
343
  3. 保存前先看影响:`guanetl save --dir <work_dir> --dry-run`
344
344
  4. 确认无误后:`guanetl save --dir <work_dir>`
345
345
 
@@ -347,16 +347,21 @@ func DefineETL() []Node {
347
347
 
348
348
  ## save 输出数据集冲突
349
349
 
350
- `save` 会先拉取服务端当前 ETL,再把本地 `_exported.json` 合并进去。修改已保存 ETL 时,输出节点必须保留服务端已有输出数据集绑定,否则 direct-save 可能把它当成“创建新输出数据集”,触发“输出数据集目录中存在同名文件”。
350
+ `save` 会先拉取服务端当前 ETL,再把本地 `_exported.json` 合并进去。修改已保存 ETL 时,输出节点必须复用服务端已有输出数据集绑定,否则 direct-save 可能把它当成“创建新输出数据集”,触发“输出数据集目录中存在同名文件”。
351
+
352
+ 新建 ETL 首次保存时,服务端 edit API 可能返回 ETL 不存在。若当前本地 base 与工作区 ETL ID 一致且尚无 actions,CLI 会将其识别为正常首次保存回退并输出 `save.first_save_fallback`;其他 edit 故障使用 `save.edit_fallback` warning,不应静默当作首次保存。
353
+
354
+ 当本地输出节点 id 被重新生成,但 `outputDsName + parentDirId` 与一个服务端已绑定输出唯一匹配时,`save` 会在请求副本中自动恢复原节点 id 和 dsId,并在影响报告中输出 `save.output_binding_reconciled`。源 `_exported.json` 不会被改写。跨目录、双方重名歧义、删除旧输出或无法唯一匹配时不会自动协调。
351
355
 
352
356
  - 再次修改已保存 ETL,优先重新执行 `guanetl edit <etl_id> --dir <新目录>`,不要长期复用旧工作目录。
353
357
  - 只追加输出列时,可以原地 `save`:新增列,不改名、不删已有列、不改已有列类型,并保持原 `OUTPUT_DATASET` 节点 id 和 `outputDsName`。
354
358
  - 修改已有输出 schema 时,保持原 `OUTPUT_DATASET` 节点 id;改列名、删列、改已有列类型、换输入数据集或重接输出链路都可能影响下游绑定,保存前必须重新 `preview` 并评估下游。
355
359
  - 如果用 `guands dataset rename` 改过 ETL 输出数据集名称,必须同步 `etl.go` 中 `BasicOutputDataset(..., outputDsName, ...)` 或 `BasicOutputDatasetInDir(..., outputDsName, ...)` 的名称。
356
360
  - 不要为了绕过同名错误手动删除旧输出数据集;旧输出通常仍被 ETL 依赖。
357
- - 如果确实要创建新输出数据集,必须同时更换 `OUTPUT_DATASET` 节点 id,并设置新的 `outputDsName` 或 `parentDirId`;只改名称会被视为已有输出绑定风险。
361
+ - 如果只是在保留旧输出的同时增加新输出,使用新的节点 id 和不同的 `outputDsName` 或 `parentDirId` 即可。
362
+ - 如果确实要移除已绑定输出并创建新输出数据集,必须使用新的节点 id、`outputDsName` 或 `parentDirId`,并显式加 `--allow-output-replacement`。该选项不会迁移下游 dsId 引用,调用方必须自行完成依赖迁移。
358
363
  - `save` 如果在 direct-save 前提示输出绑定风险,先修本地 `etl.go` / `meta.json`,再重新 `export -> preview -> save`。
359
- - `save --dry-run` 只生成保存影响报告,不调用 direct-save;需要机器可读结果时加 `--format json`。报告里出现阻断级输出绑定风险时,必须先修本地定义,不能继续真实 `save`。
364
+ - `save --dry-run` 只生成保存影响报告,不调用 direct-save;需要机器可读结果时加 `--format json`。报告里出现阻断级输出绑定风险时,必须先修本地定义,或在确认主动替换且已规划下游迁移后显式使用 `--allow-output-replacement`。
360
365
 
361
366
  ## Appendix: Framework Surface
362
367