universal-dev-standards 6.14.0-beta.2 → 6.14.0-beta.3

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.
@@ -8,7 +8,7 @@ standard:
8
8
 
9
9
  meta:
10
10
  version: "1.1.0"
11
- updated: "2026-09-29"
11
+ updated: "2026-09-30"
12
12
  source: core/open-work-tracking.md
13
13
  description: >
14
14
  三種不同的「工作不見了」的方式(沒地方記新想法、等待中沒有解除條件、
@@ -218,6 +218,9 @@ standard:
218
218
  - named_unresolved
219
219
  - unnamed # 唯一的違反
220
220
  coverage: "辨認路徑/指令/測試名稱/編號本身是樣式比對,涵蓋率未知(OWT-011);認不出的格式回報為未點名"
221
+ fields_read: "參考程序以同一份詞彙讀三種形狀:小節標題、行內標籤、表頭在詞彙內的表格欄(該欄每一列各算一個欄位)"
222
+ ragged_table_row: "欄數與表頭不一致的列列為判定不了、不當成空白;別處沒有違反時結束碼為 2(不是通過)"
223
+ empty_field: "空白、—、-、已完成的儲存格只計數不評估,不算違反(OWT-019 判斷寫了的下一步,沒寫是另一種失效)"
221
224
 
222
225
  # ── 刻意不採納(來源:使用者轉貼的提示詞,作者不明,僅借設計形狀)──────────
223
226
  not_adopted:
@@ -366,7 +366,7 @@ DEX-003 扮演的角色相同。上面每一條都指名了 artefact 與它們
366
366
 
367
367
  ## What enforces this standard
368
368
 
369
- **Nothing in UDS gates on it, and that is recorded rather than implied.** UDS states the relations a carrier of open work must satisfy; whether anything decides them is the adopting project's call, per the [writing constraint](#how-this-standard-is-written--and-why-it-is-written-that-way) above — the same boundary [deferred-item-exit](deferred-item-exit.md) draws for its own exits. Since 1.1.0 UDS does ship one **reference decision procedure** for OWT-017–OWT-019 — `uds open-work next-action | revision | separation` from the npm package (`uds open-work self-test` runs the checker's own arms; from a clone of the UDS repository `node scripts/check-open-work-tracking.mjs` runs the same code) — offered as evidence in the OWT-015 sense — it has been observed to fail against violating samples — for an adopter to run or to reimplement. It is not wired into any UDS release gate, because UDS carries no open-work carrier for it to check.
369
+ **Nothing in UDS gates on it, and that is recorded rather than implied.** UDS states the relations a carrier of open work must satisfy; whether anything decides them is the adopting project's call, per the [writing constraint](#how-this-standard-is-written--and-why-it-is-written-that-way) above — the same boundary [deferred-item-exit](deferred-item-exit.md) draws for its own exits. Since 1.1.0 UDS does ship one **reference decision procedure** for OWT-017–OWT-019 — `uds open-work next-action | revision | separation` from the npm package (`uds open-work self-test` runs the checker's own arms; from a clone of the UDS repository `node scripts/check-open-work-tracking.mjs` runs the same code) — offered as evidence in the OWT-015 sense — it has been observed to fail against violating samples — for an adopter to run or to reimplement. It is not wired into any UDS release gate, because UDS carries no open-work carrier for it to check. For OWT-019 it reads a next-action field in three shapes, all through one vocabulary: a heading section, an inline label, and every row of a table column whose header is in that vocabulary. A table row whose cell count differs from its header is listed as undecidable (never read as empty; with no violation elsewhere the exit code is 2, not a pass), and an empty, `—`, `-` or done cell is counted and not evaluated — not a violation, because OWT-019 judges a next action that was written, and a missing one is a different failure it does not decide.
370
370
 
371
371
  **UDS 不對本標準設任何閘門,而這件事是被記錄的,不是被暗示的。** UDS 陳述一個承載開放工作的地方
372
372
  必須滿足的關係;有沒有東西去判定它,依上面的[寫法約束](#how-this-standard-is-written--and-why-it-is-written-that-way),
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  source: ../../CHANGELOG.md
3
- source_version: 6.14.0-beta.2
4
- translation_version: 6.14.0-beta.2
3
+ source_version: 6.14.0-beta.3
4
+ translation_version: 6.14.0-beta.3
5
5
  last_synced: 2026-09-30
6
6
  status: current
7
7
  ---
@@ -17,6 +17,23 @@ status: current
17
17
 
18
18
  ## [Unreleased]
19
19
 
20
+ ## [6.14.0-beta.3] - 2026-09-30
21
+
22
+ > **测试版**——以 `npm install -g universal-dev-standards@beta` 安装。要测什么、如何退回正式版:[docs/PRE-RELEASE.md](../../docs/PRE-RELEASE.md)。
23
+ >
24
+ > **既有采用者:请执行一次 `uds update`。**旧版 `uds init` 写进 pre-commit hook 的是单行 `npx uds check`,它可能请 npm 去拿一个叫 `uds`、但不是本项目的软件包。新安装不再写这一行,而 `uds update` 会替换既有 `.husky/pre-commit` 里 UDS 自己写的那一行。另外:`uds uninstall` 只移除能证明是 UDS 写的东西(早期 UDS 安装的项目会保留一部分文件并说明原因),`uds open-work next-action` 读得懂写成表格列的下一步。
25
+
26
+ ### 变更
27
+
28
+ - **`uds open-work next-action` 现在读得懂写成 Markdown 表格列的“下一步”,不再只认小节标题与行内标签。** 表头在“下一步”词汇内的列(`Next action`、`Next step`、`下一步`、`下一動`,以及新增的 `回來要做什麼`)每一行都会被读取,报告附带行号与该行第一格内容,便于定位。词汇仍然只有一份:标题、行内标签与表头都读同一份。放在引用块(`> | … |`)里的表格现在看得见;代码片段内或反斜杠之后的 `|` 不再切开单元格。列数与表头不一致的行会列为 `UNDECIDABLE`(无法判定),不会被当成空白;别处没有违反时退出码为 2,因为干净的结果只涵盖了字段的一部分。空白、`—`、`-` 的单元格只计数、不评估,也不算 OWT-019 违反。此事是在真实工作记录上量测 DEC-122 H2 基准时发现的:80 行中有 32 行的列数与表头不同。`回來要做什麼` 是该工作记录实际使用的表头(其文字说明把该列叫作 下一動),属未校准判断(OWT-016)。检查器自身的突变测试新增八个表格突变,原有十七个仍然转红。
29
+
30
+ ### 修复
31
+
32
+ - **`uds uninstall` 不再留下 UDS 自己写的文件,也不再移除它无法证明是 UDS 写的东西。** 走过 `init` → `update --with-hooks` → `uninstall -y` 之后,它报告“已移除 5、已跳过 1、错误 0”,却留下 `scripts/hooks/` 下的 15 个 hook 脚本、一个空的 `.codex/`、一份生成标头仍指向已删除 `.standards/` 的 AGENTS.md,以及 `uds init` 写入的 `.git/hooks/pre-commit` 脚本主体。现在的规则是:整个文件只有在 manifest 记录了“UDS 写的”(`installedArtifacts`,由 `init` 与 `update --with-hooks` 写入)**并且**内容仍与记录的哈希相符时才会删除。其他一律保留,并在输出中说明原因(`kept: modified since UDS wrote it`、`kept: no install record — ...`)。文件夹只有在 UDS 创建且现已为空时才移除;采用者自己的 `scripts/hooks/*.mjs`、`.agents/rules/*` 与 hook 条目都会保留。由旧版 UDS 安装的项目没有记录,其脚本、AGENTS.md 生成文字与原生 pre-commit 主体会被保留并说明,而不是猜测。每一行“已移除”现在都对应一次真实的删除或修改,带着错误结束的运行也会以非 0 结束。
33
+ - **`uds uninstall` 在没有人能回答时不再画出提示或抛出错误堆栈,做不了事时也不再以 0 结束。** `--dry-run` 从不提示(它不写任何东西),并预览所有类别。没有 `--yes` 又没有终端时,实际运行会以退出码 2 拒绝,而不是假定“是”;提示被关闭时退出码为 130;项目未初始化时退出码为 1。
34
+ - **`uds init --with-hooks` 在 Windows 上不再打印 `'chmod' is not recognized`。** pre-commit hook 原本用 try/catch 包住的 `execSync("chmod +x ...")` 赋予执行权限;catch 对代码隐藏了失败,但 `execSync` 已先把 cmd.exe 的错误送到终端。现在改用 `fs.chmodSync`,并在没有执行位的 Windows 上跳过此步骤。
35
+ - **安全性:`uds init` 写入的 pre-commit hook 不再向 npm 要一个叫 `uds`、但不是本项目的包。** 该 hook 原本是单行 `npx uds check`。`npx` 先找 `node_modules/.bin` 与 `PATH`,两处都没有才去 npm registry,而 registry 上的 `uds` 是不相干的项目(维护者 wizawu、`github.com/wizawu/uds`、v0.3.6、2022 年后未更新、目前没有 `bin`)。装了 UDS 的机器不受影响;没装的 clone 则会用名称去抓陌生人的包——目前无害只是因为该包*尚*无可执行文件,对方一旦发布带 `uds` bin 的版本,每位采用者的每次 commit 都会执行它。`--no-install` 不是解法:用会记录每个请求的本机 registry 实测(npm 10.9.9、11.20.0、12.1.0,三者一致),`npx --no-install uds` 仍会发出 `GET /uds`,而 `npx --no-install --package=universal-dev-standards uds` 完全找不到全局安装。hook 现在两者都不用:它在项目的 `node_modules/.bin`、再到 `PATH` 找 `universal-dev-standards`(包本名,只有本项目能发布)并执行 `check`;两处都找不到时打印该装什么并以非 0 退出——不跳过检查、不下载任何东西,而且即使采用者自己的命令排在后面,检查失败也会拦下 commit。`uds uninstall` 依标记整块移除新写法。给人看的文字同样修正:生成的 `CLAUDE.md`/`AGENTS.md` 区块内的警告行与 hook 提示改写为 `npx universal-dev-standards init` / `update`(警告行多了几个 token,所以 `scripts/prompt-footprint-baseline.json` 依实测值各调高 3–6)。**既有采用者:**`uds update`(除了 `--skills`、`--commands`、`--integrations-only`、`--standards-only` 与 `--rollback` 之外的所有模式,以及 `--with-hooks`;`--plan` 只报告不写入)会替换 `.husky/pre-commit` 中 UDS 自己写的那一行——只认 UDS 曾生成过的两种确切写法(`npx uds check`,以及较早的 `npx uds check --standard checkin-standards`),且必须紧接在 `# UDS Standard Check` 标记下方;你自己写或改过的行不会被动,并会连同行号报告。此步骤在“已是最新版本”的提前返回**之前**执行,所以标准已是最新的采用者也会被处理。`uds check` 现在会警告仍使用裸名称的 hook。新增一个测试遍历 `npm pack` 出货的全部内容,只要有字符串以包运行器(npx、bunx、pnpm dlx、yarn dlx、npm exec)执行裸名称 `uds` 就会失败;非 Node 项目的原生 hook(`uds check`,只从 `PATH` 解析、不经 registry)不受影响,保持原样。
36
+
20
37
  ## [6.14.0-beta.2] - 2026-09-30
21
38
 
22
39
  > **测试版**——以 `npm install -g universal-dev-standards@beta` 安装。要测什么、如何退回正式版:[docs/PRE-RELEASE.md](../../docs/PRE-RELEASE.md)。
@@ -15,7 +15,7 @@ status: current
15
15
 
16
16
  > **语言**: [English](../../README.md) | [繁體中文](../zh-TW/README.md) | 简体中文
17
17
 
18
- **版本**: 6.14.0-beta.2 (Pre-release) | **发布日期**: 2026-09-30 | **授权**: [双重授权](../../LICENSE) (CC BY 4.0 + MIT)
18
+ **版本**: 6.14.0-beta.3 (Pre-release) | **发布日期**: 2026-09-30 | **授权**: [双重授权](../../LICENSE) (CC BY 4.0 + MIT)
19
19
 
20
20
  语言无关、框架无关的软件项目文档标准。通过 AI 原生工作流,确保不同技术栈之间的一致性、质量和可维护性。
21
21
 
@@ -13,7 +13,7 @@ status: current
13
13
  <!-- UDS_SUPPORTED_VERSIONS_START -->
14
14
  | 版本 | 支持状态 |
15
15
  |------|--------|
16
- | 6.14.0-beta.2 | ✅ 预发布版本 |
16
+ | 6.14.0-beta.3 | ✅ 预发布版本 |
17
17
  | 6.13.1 | ✅ 最新正式版 |
18
18
  | < 6.0.0 | ❌ 已终止支持 |
19
19
  <!-- UDS_SUPPORTED_VERSIONS_END -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  source: ../../CHANGELOG.md
3
- source_version: 6.14.0-beta.2
4
- translation_version: 6.14.0-beta.2
3
+ source_version: 6.14.0-beta.3
4
+ translation_version: 6.14.0-beta.3
5
5
  last_synced: 2026-09-30
6
6
  status: current
7
7
  ---
@@ -17,6 +17,23 @@ status: current
17
17
 
18
18
  ## [Unreleased]
19
19
 
20
+ ## [6.14.0-beta.3] - 2026-09-30
21
+
22
+ > **測試版**——以 `npm install -g universal-dev-standards@beta` 安裝。要測什麼、如何退回正式版:[docs/PRE-RELEASE.md](../../docs/PRE-RELEASE.md)。
23
+ >
24
+ > **既有採用者:請執行一次 `uds update`。**舊版 `uds init` 寫進 pre-commit hook 的是單行 `npx uds check`,它可能請 npm 去拿一個叫 `uds`、但不是本專案的套件。新安裝不再寫這一行,而 `uds update` 會替換既有 `.husky/pre-commit` 裡 UDS 自己寫的那一行。另外:`uds uninstall` 只移除能證明是 UDS 寫的東西(早期 UDS 安裝的專案會保留一部分檔案並說明原因),`uds open-work next-action` 讀得懂寫成表格欄的下一步。
25
+
26
+ ### 變更
27
+
28
+ - **`uds open-work next-action` 現在讀得懂寫成 Markdown 表格欄的「下一步」,不再只認小節標題與行內標籤。** 表頭在「下一步」詞彙內的欄(`Next action`、`Next step`、`下一步`、`下一動`,以及新加的 `回來要做什麼`)每一列都會被讀,回報帶行號與該列第一格內容,方便定位。詞彙仍然只有一份:標題、行內標籤與表頭都讀同一份。放在引用區塊(`> | … |`)裡的表格現在看得見;程式碼片段內或反斜線之後的 `|` 不再切開儲存格。欄數與表頭不一致的列會列為 `UNDECIDABLE`(判定不了),不會被當成空白;別處沒有違反時結束碼為 2,因為乾淨的結果只涵蓋了欄位的一部分。空白、`—`、`-` 的儲存格只計數、不評估,也不算 OWT-019 違反。此事是在真實工作紀錄上量測 DEC-122 H2 基準時發現的:80 列中有 32 列的欄數與表頭不同。`回來要做什麼` 是該工作紀錄實際使用的表頭(它的文字說明把該欄叫作 下一動),屬未校準判斷(OWT-016)。檢查器自己的突變測試新增八個表格突變,原有十七個仍然轉紅。
29
+
30
+ ### 修正
31
+
32
+ - **`uds uninstall` 不再留下 UDS 自己寫的檔案,也不再移除它無法證明是 UDS 寫的東西。** 走過 `init` → `update --with-hooks` → `uninstall -y` 之後,它回報「已移除 5、已跳過 1、錯誤 0」,卻留下 `scripts/hooks/` 底下 15 個 hook 腳本、一個空的 `.codex/`、一份生成標頭仍指向已刪除 `.standards/` 的 AGENTS.md,以及 `uds init` 寫入的 `.git/hooks/pre-commit` 腳本主體。現在的規則是:整個檔案只有在 manifest 記錄了「UDS 寫的」(`installedArtifacts`,由 `init` 與 `update --with-hooks` 寫入)**而且**內容仍與記錄的雜湊相符時才會刪除。其他一律保留,並在輸出說明原因(`kept: modified since UDS wrote it`、`kept: no install record — ...`)。資料夾只有在 UDS 建立且現已為空時才移除;採用者自己的 `scripts/hooks/*.mjs`、`.agents/rules/*` 與 hook 項目都會保留。由舊版 UDS 安裝的專案沒有記錄,其腳本、AGENTS.md 生成文字與原生 pre-commit 主體會被保留並說明,而不是猜測。每一行「已移除」現在都對應一次真實的刪除或修改,帶著錯誤結束的執行也會以非 0 結束。
33
+ - **`uds uninstall` 在沒有人能回答時不再畫出提示或噴錯誤堆疊,做不了事時也不再以 0 結束。** `--dry-run` 從不提示(它不寫任何東西),並預覽所有類別。沒有 `--yes` 又沒有終端機時,實際執行會以結束碼 2 拒絕,而不是假定「是」;提示被關閉時結束碼為 130;專案未初始化時結束碼為 1。
34
+ - **`uds init --with-hooks` 在 Windows 上不再印出 `'chmod' is not recognized`。** pre-commit hook 原本用 try/catch 包住的 `execSync("chmod +x ...")` 賦予執行權限;catch 對程式碼藏起了失敗,但 `execSync` 已先把 cmd.exe 的錯誤送到終端機。現在改用 `fs.chmodSync`,並在沒有執行位元的 Windows 上略過此步驟。
35
+ - **安全性:`uds init` 寫入的 pre-commit hook 不再向 npm 要一個叫 `uds`、但不是本專案的套件。** 該 hook 原本是單行 `npx uds check`。`npx` 先找 `node_modules/.bin` 與 `PATH`,兩處都沒有才去 npm registry,而 registry 上的 `uds` 是不相干的專案(維護者 wizawu、`github.com/wizawu/uds`、v0.3.6、2022 年後未更新、目前沒有 `bin`)。裝了 UDS 的機器不受影響;沒裝的 clone 則會用名稱去抓陌生人的套件——目前無害只是因為該套件*尚*無可執行檔,對方一旦發布帶 `uds` bin 的版本,每位採用者的每次 commit 都會執行它。`--no-install` 不是解法:用會記錄每個請求的本機 registry 實測(npm 10.9.9、11.20.0、12.1.0,三者一致),`npx --no-install uds` 仍會發出 `GET /uds`,而 `npx --no-install --package=universal-dev-standards uds` 完全找不到全域安裝。hook 現在兩者都不用:它在專案的 `node_modules/.bin`、再到 `PATH` 找 `universal-dev-standards`(套件本名,只有本專案能發布)並執行 `check`;兩處都找不到時印出該裝什麼並以非 0 結束——不跳過檢查、不下載任何東西,而且即使採用者自己的指令排在後面,檢查失敗也會擋下 commit。`uds uninstall` 依標記整塊移除新寫法。給人看的文字同樣修正:產生的 `CLAUDE.md`/`AGENTS.md` 區塊內的警告行與 hook 提示改寫為 `npx universal-dev-standards init` / `update`(警告行多了幾個 token,所以 `scripts/prompt-footprint-baseline.json` 依實測值各調高 3–6)。**既有採用者:**`uds update`(除了 `--skills`、`--commands`、`--integrations-only`、`--standards-only` 與 `--rollback` 之外的所有模式,以及 `--with-hooks`;`--plan` 只回報不寫入)會替換 `.husky/pre-commit` 中 UDS 自己寫的那一行——只認 UDS 曾產生過的兩種確切寫法(`npx uds check`,以及較早的 `npx uds check --standard checkin-standards`),且必須緊接在 `# UDS Standard Check` 標記下方;你自己寫或改過的行不會被動,並會連同行號回報。此步驟在「已是最新版本」的提前返回**之前**執行,所以標準已是最新的採用者也會被處理。`uds check` 現在會警告仍使用裸名稱的 hook。新增一個測試走訪 `npm pack` 出貨的全部內容,只要有字串以套件執行器(npx、bunx、pnpm dlx、yarn dlx、npm exec)執行裸名稱 `uds` 就會失敗;非 Node 專案的原生 hook(`uds check`,只從 `PATH` 解析、不經 registry)不受影響,維持原樣。
36
+
20
37
  ## [6.14.0-beta.2] - 2026-09-30
21
38
 
22
39
  > **測試版**——以 `npm install -g universal-dev-standards@beta` 安裝。要測什麼、如何退回正式版:[docs/PRE-RELEASE.md](../../docs/PRE-RELEASE.md)。
@@ -15,7 +15,7 @@ status: current
15
15
 
16
16
  > **語言**: [English](../../README.md) | 繁體中文 | [简体中文](../zh-CN/README.md)
17
17
 
18
- **版本**: 6.14.0-beta.2 (Pre-release) | **發布日期**: 2026-09-30 | **授權**: [雙重授權](../../LICENSE) (CC BY 4.0 + MIT)
18
+ **版本**: 6.14.0-beta.3 (Pre-release) | **發布日期**: 2026-09-30 | **授權**: [雙重授權](../../LICENSE) (CC BY 4.0 + MIT)
19
19
 
20
20
  語言無關、框架無關的軟體專案文件標準。透過 AI 原生工作流,確保不同技術堆疊之間的一致性、品質和可維護性。
21
21
 
@@ -13,7 +13,7 @@ status: current
13
13
  <!-- UDS_SUPPORTED_VERSIONS_START -->
14
14
  | 版本 | 支援狀態 |
15
15
  |------|--------|
16
- | 6.14.0-beta.2 | ✅ 預發布版本 |
16
+ | 6.14.0-beta.3 | ✅ 預發布版本 |
17
17
  | 6.13.1 | ✅ 最新正式版 |
18
18
  | < 6.0.0 | ❌ 已終止支援 |
19
19
  <!-- UDS_SUPPORTED_VERSIONS_END -->
@@ -2,8 +2,8 @@
2
2
  source: ../../../core/open-work-tracking.md
3
3
  source_version: 1.1.0
4
4
  translation_version: 1.1.0
5
- last_synced: 2026-09-29
6
- source_hash: 0fcf3df23e12
5
+ last_synced: 2026-09-30
6
+ source_hash: fb60809a8ff2
7
7
  status: current
8
8
  ---
9
9
 
@@ -286,7 +286,7 @@ DEX-003 扮演的角色相同。上面每一條都指名了 artefact 與它們
286
286
  是採用專案的決定——與 [deferred-item-exit](deferred-item-exit.md) 對自己出口劃的界線相同。
287
287
  自 1.1.0 起,UDS 為 OWT-017–OWT-019 附上一支**參考判定程序**——npm 安裝包裡的 `uds open-work next-action | revision | separation`(`uds open-work self-test` 只跑檢查器自己的自測臂;在 UDS repo 的副本裡,`node scripts/check-open-work-tracking.mjs` 跑的是同一份程式)——
288
288
  作為 OWT-015 意義上的證據——它已被觀察到對違反的樣本回報失敗——供採用者直接執行或自行重做。
289
- 它沒有接進任何 UDS 發版閘門,因為 UDS 本身沒有承載開放工作的地方可供它檢查。
289
+ 它沒有接進任何 UDS 發版閘門,因為 UDS 本身沒有承載開放工作的地方可供它檢查。對 OWT-019,它以同一份詞彙讀三種形狀的「下一步」欄位:小節標題、行內標籤、以及表頭在該詞彙內的表格欄(該欄每一列各算一個欄位)。欄數與表頭不一致的表格列會被列為「判定不了」(不會被當成空白;若別處沒有違反,結束碼是 2,不是通過);空白、`—`、`-` 或已完成的儲存格只計數、不評估——它不算違反,因為 OWT-019 判斷的是「寫了的下一步有沒有點名對象」,沒寫是另一種失效,它不判定。
290
290
 
291
291
  本標準做的事,是讓那個決定顯形:OWT-014 保證這裡每一條**能**被判定,OWT-015 固定
292
292
  「一次判定要算數需要什麼」,OWT-005/OWT-011 固定「一次不完整的判定容許印出什麼」。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-dev-standards",
3
- "version": "6.14.0-beta.2",
3
+ "version": "6.14.0-beta.3",
4
4
  "description": "CLI tool for adopting Universal Development Standards",
5
5
  "keywords": [
6
6
  "documentation",
@@ -1281,6 +1281,15 @@ export function checkPreCommitWiring(projectPath, msg) {
1281
1281
  const result = checkPreCommitHookWiring(projectPath);
1282
1282
  if (!result.relevant) return; // 沒有 UDS 管理的 hook
1283
1283
 
1284
+ // 與 wiring 無關的獨立缺陷面:hook 即使已接上,仍可能請 npm 去解析裸名稱 `uds`
1285
+ // ——npm registry 上那個名稱不是本專案。見 git-hooks.js buildPreCommitBlock。
1286
+ if (result.legacyBareUds) {
1287
+ console.log(chalk.yellow((msg.hookBareUdsTitle || '⚠ [pre-commit] {file} asks npm to run the bare name "uds", which on the npm registry is an unrelated package.')
1288
+ .replace('{file}', result.hookFile)));
1289
+ console.log(chalk.gray(msg.hookBareUdsFix || ' Fix: run `uds update`.'));
1290
+ console.log();
1291
+ }
1292
+
1284
1293
  if (result.wired) {
1285
1294
  // 🔴 「已確認會執行」在 POSIX 上為真,在 Windows 上不一定——git for
1286
1295
  // Windows 沒有 POSIX 的 ENOEXEC → /bin/sh 後備機制,缺 shebang 的 hook
@@ -1,7 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { createSpinner } from '../utils/spinner.js';
3
- import { execSync } from 'child_process';
4
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'fs';
5
4
  import { join } from 'path';
6
5
  import {
7
6
  manifestExists as isInitialized,
@@ -28,7 +27,9 @@ import { guardAgainstSelfAdoption } from '../utils/detect-self-adoption.js';
28
27
  import { readInstallYaml } from '../utils/config-manager.js';
29
28
  import { resolveIntegrationTargetFile } from '../utils/integration-generator.js';
30
29
  import { withFileTransaction } from '../utils/transaction.js';
31
- import { wireGitHooksPath, getLocalHooksPathConfig, stripLegacyHuskyShLine, ensureShebang } from '../utils/git-hooks.js';
30
+ import { newRecorder, mkdirTracked, recordFile, persistRecorder, RECORD_KINDS } from '../core/install-records.js';
31
+ import { wireGitHooksPath, getLocalHooksPathConfig, stripLegacyHuskyShLine, ensureShebang, hookRunsUdsCheck, buildPreCommitBlock } from '../utils/git-hooks.js';
32
+ import { migrateLegacyHuskyHook } from '../utils/legacy-hook-migration.js';
32
33
 
33
34
  /**
34
35
  * Init command - initialize standards in current project
@@ -213,7 +214,11 @@ export async function initCommand(options) {
213
214
  skillHashes: skillsResults.skillHashes,
214
215
  commandHashes: skillsResults.commandHashes,
215
216
  integrationBlockHashes: integrationResults.integrationBlockHashes,
216
- manifestIntegrationConfigs: integrationResults.manifestIntegrationConfigs
217
+ manifestIntegrationConfigs: integrationResults.manifestIntegrationConfigs,
218
+ createdIntegrationFiles: [
219
+ ...(integrationResults.createdFiles ?? []),
220
+ ...(agentsMdResult.created ? [agentsMdResult.path] : [])
221
+ ]
217
222
  };
218
223
  },
219
224
  // A half-installed project must never be committed: if any installer
@@ -258,9 +263,13 @@ export async function initCommand(options) {
258
263
  }
259
264
 
260
265
  // 4.6. Install enforcement hooks (if --with-hooks)
266
+ // What this step writes is recorded in the manifest (installedArtifacts) so
267
+ // `uds uninstall` can remove exactly it — hook scripts, and the folders UDS had
268
+ // to create for them — and leave everything the adopter put there alone.
269
+ const installRecorder = newRecorder();
261
270
  if (config.withHooks) {
262
271
  const { installHooks } = await import('../installers/hooks-installer.js');
263
- const hookResult = installHooks(projectPath);
272
+ const hookResult = installHooks(projectPath, { recorder: installRecorder });
264
273
  if (hookResult.installed) {
265
274
  console.log(chalk.green(
266
275
  ` ✓ Enforcement hooks installed (${hookResult.scriptsCount} scripts, `
@@ -292,7 +301,7 @@ export async function initCommand(options) {
292
301
  const selectedTools = config.integrations || config.aiTools || [];
293
302
  if (selectedTools.includes('codex')) {
294
303
  const { installCodexHooks } = await import('../installers/hooks-installer.js');
295
- const codexResult = installCodexHooks(projectPath);
304
+ const codexResult = installCodexHooks(projectPath, { recorder: installRecorder });
296
305
  if (codexResult.installed) {
297
306
  console.log(chalk.green(' ✓ Codex Stop hook installed (turn-completion-integrity)'));
298
307
  // Measured 2026-09-28 (codex-cli 0.155.1): Codex skips a project hook
@@ -306,7 +315,7 @@ export async function initCommand(options) {
306
315
  }
307
316
  if (selectedTools.includes('gemini-cli')) {
308
317
  const { installGeminiHooks } = await import('../installers/hooks-installer.js');
309
- const geminiResult = installGeminiHooks(projectPath);
318
+ const geminiResult = installGeminiHooks(projectPath, { recorder: installRecorder });
310
319
  if (geminiResult.installed) {
311
320
  console.log(chalk.green(' ✓ Gemini CLI AfterAgent hook installed (turn-completion-integrity; Gemini CLI is legacy — see the standard)'));
312
321
  } else {
@@ -315,7 +324,7 @@ export async function initCommand(options) {
315
324
  }
316
325
  if (selectedTools.includes('antigravity')) {
317
326
  const { installAgyHooks } = await import('../installers/hooks-installer.js');
318
- const agyResult = installAgyHooks(projectPath);
327
+ const agyResult = installAgyHooks(projectPath, { recorder: installRecorder });
319
328
  if (agyResult.installed) {
320
329
  console.log(chalk.green(' ✓ Antigravity CLI Stop hook installed (turn-completion-integrity; .agents/hooks.json)'));
321
330
  // Verified 2026-09-29 (agy 1.2.12): single turn, no tool calls, `agy -p`.
@@ -328,6 +337,15 @@ export async function initCommand(options) {
328
337
  }
329
338
  }
330
339
 
340
+ // 4.7. Record the integration files UDS created from nothing, and the hook
341
+ // files/folders written above. Their text outside the UDS marker block (the
342
+ // `> Auto-generated by ...` header, the generated sections) is what makes
343
+ // "delete the whole file" provable at uninstall time.
344
+ for (const created of combinedResults.createdIntegrationFiles ?? []) {
345
+ recordFile(installRecorder, projectPath, created, RECORD_KINDS.INTEGRATION_FILE);
346
+ }
347
+ persistRecorder(projectPath, installRecorder);
348
+
331
349
  // 5. Setup Pre-commit Hook
332
350
  await setupHuskyHook(projectPath);
333
351
 
@@ -339,7 +357,7 @@ export async function initCommand(options) {
339
357
  * - Node.js projects: use husky
340
358
  * - Non-Node.js projects: write native .git/hooks/pre-commit
341
359
  */
342
- export async function setupHuskyHook(projectPath, { allowInTest = false } = {}) {
360
+ export async function setupHuskyHook(projectPath, { allowInTest = false, platform = process.platform } = {}) {
343
361
  // 2026-07-30:本函式在跑測試時改寫了 universal-dev-standards 自己
344
362
  // ——`npm install --save-dev husky`、改 package.json、建 .husky/pre-commit。
345
363
  //
@@ -359,6 +377,9 @@ export async function setupHuskyHook(projectPath, { allowInTest = false } = {})
359
377
 
360
378
  const isNodeProject = existsSync(join(projectPath, 'package.json'));
361
379
 
380
+ // What this function creates, for `uds uninstall` (see core/install-records.js).
381
+ const recorder = newRecorder();
382
+
362
383
  if (isNodeProject) {
363
384
  console.log(chalk.cyan('Configuring Pre-commit Hook...'));
364
385
 
@@ -436,7 +457,7 @@ export async function setupHuskyHook(projectPath, { allowInTest = false } = {})
436
457
  // 3. Ensure .husky directory exists
437
458
  if (!existsSync(huskyDir)) {
438
459
  try {
439
- mkdirSync(huskyDir, { recursive: true });
460
+ mkdirTracked(recorder, projectPath, huskyDir);
440
461
  } catch (e) {
441
462
  console.log(chalk.red(` ✗ Failed to create .husky directory: ${e.message}`));
442
463
  return;
@@ -445,8 +466,10 @@ export async function setupHuskyHook(projectPath, { allowInTest = false } = {})
445
466
 
446
467
  // 4. Add pre-commit hook content
447
468
  const preCommitPath = join(huskyDir, 'pre-commit');
448
- const udsCmd = 'npx uds check';
449
469
 
470
+ // Only a hook file UDS creates from nothing can later be proven to be wholly
471
+ // its own; one that already existed is appended to and stays the adopter's.
472
+ const preCommitExisted = existsSync(preCommitPath);
450
473
  try {
451
474
  // husky v9's own templates carry no shebang, no `_/husky.sh` sourcing
452
475
  // (that is v8 syntax, deprecated in v9 and removed in v10). Verified
@@ -464,6 +487,18 @@ export async function setupHuskyHook(projectPath, { allowInTest = false } = {})
464
487
  // — cheap, harmless on POSIX, and required on Windows. Existing files
465
488
  // are appended to, never rewritten — their contents are the adopter's,
466
489
  // not ours; ensureShebang only ever prepends a missing first line.
490
+ // A hook an older UDS wrote asks npx for the bare name `uds`, which on the
491
+ // npm registry is an unrelated package (see buildPreCommitBlock). Swap the
492
+ // line UDS itself wrote for the current block before anything else reads
493
+ // the file; a line the adopter edited is left alone and reported below.
494
+ const legacy = existsSync(preCommitPath) ? migrateLegacyHuskyHook(projectPath) : null;
495
+ if (legacy?.state === 'migrated') {
496
+ console.log(chalk.green(' ✓ Replaced the pre-commit line an older UDS wrote (it asked npm to resolve the bare name "uds", which is not this project) with one that runs only the installed UDS CLI'));
497
+ }
498
+ for (const k of legacy?.kept ?? []) {
499
+ console.log(chalk.yellow(` ⚠ .husky/pre-commit line ${k.line} ("${k.text}") asks npm to resolve the bare name "uds", which on the npm registry is an unrelated package. UDS did not write this exact line, so it was left alone — change it to "universal-dev-standards check" (with the CLI installed in this project or on PATH).`));
500
+ }
501
+
467
502
  let content = existsSync(preCommitPath) ? readFileSync(preCommitPath, 'utf-8') : '';
468
503
 
469
504
  // A pre-existing file may still carry husky v8's `_/husky.sh` sourcing
@@ -479,10 +514,10 @@ export async function setupHuskyHook(projectPath, { allowInTest = false } = {})
479
514
  const { content: destripped, removed: hadLegacyLine } = stripLegacyHuskyShLine(content);
480
515
  content = destripped;
481
516
 
482
- const needsAppend = !content.includes('uds check');
517
+ const needsAppend = !hookRunsUdsCheck(content);
483
518
  if (needsAppend) {
484
519
  const sep = content && !content.endsWith('\n') ? '\n' : '';
485
- content = `${content}${sep}\n# UDS Standard Check\n${udsCmd}\n`;
520
+ content = `${content}${sep}\n${buildPreCommitBlock()}`;
486
521
  }
487
522
 
488
523
  const { content: shebanged, added: addedShebang } = ensureShebang(content);
@@ -490,10 +525,22 @@ export async function setupHuskyHook(projectPath, { allowInTest = false } = {})
490
525
 
491
526
  if (hadLegacyLine || needsAppend || addedShebang) {
492
527
  writeFileSync(preCommitPath, content, 'utf-8');
493
- try {
494
- execSync(`chmod +x ${preCommitPath}`);
495
- } catch {
496
- // Ignore chmod failures on systems that don't support it
528
+ if (!preCommitExisted) recordFile(recorder, projectPath, '.husky/pre-commit', RECORD_KINDS.GIT_HOOK);
529
+ // 🔴 Was `execSync(`chmod +x ...`)` inside try/catch. On Windows that
530
+ // spawned cmd.exe, which printed "'chmod' is not recognized as an
531
+ // internal or external command" straight to the user's terminal —
532
+ // execSync inherits the child's stderr by default, so the catch hid the
533
+ // failure from the code and left it loud for the human (adopter report,
534
+ // 6.14.0-beta.2, Windows 11). No shell is needed to change a mode, and
535
+ // Windows has no exec bit to set (git for Windows runs the hook through
536
+ // its own sh regardless), so it is skipped there.
537
+ if (platform !== 'win32') {
538
+ try {
539
+ chmodSync(preCommitPath, 0o755);
540
+ } catch {
541
+ // A filesystem that refuses the mode change (FAT, some mounts) is not
542
+ // a reason to fail the install; git reports a non-executable hook itself.
543
+ }
497
544
  }
498
545
  }
499
546
 
@@ -512,6 +559,8 @@ export async function setupHuskyHook(projectPath, { allowInTest = false } = {})
512
559
  console.log(chalk.red(` ✗ Failed to configure pre-commit hook: ${e.message}`));
513
560
  }
514
561
 
562
+ persistRecorder(projectPath, recorder);
563
+
515
564
  // 5. Wire git so the hook ACTUALLY runs — this is the fix. Setting
516
565
  // `core.hooksPath` ourselves is what husky's own bootstrap does internally
517
566
  // (verified against husky ^9.1.7's source: `git config core.hooksPath
@@ -567,7 +616,7 @@ export async function setupHuskyHook(projectPath, { allowInTest = false } = {})
567
616
 
568
617
  if (existsSync(hookPath)) {
569
618
  const existingContent = readFileSync(hookPath, 'utf-8');
570
- if (existingContent.includes('uds check')) {
619
+ if (hookRunsUdsCheck(existingContent)) {
571
620
  console.log(chalk.gray(' ✓ Pre-commit hook already configured'));
572
621
  } else {
573
622
  // Never clobber an adopter's own hook (this fix — it used to be
@@ -597,6 +646,10 @@ uds check 2>/dev/null || true
597
646
  echo "Pre-commit checks passed"
598
647
  `;
599
648
  writeFileSync(hookPath, hookContent, { mode: 0o755 });
649
+ // Created from nothing (the branch above is "file does not exist"), so the
650
+ // whole file is UDS's — record its hash for `uds uninstall`.
651
+ recordFile(recorder, projectPath, '.git/hooks/pre-commit', RECORD_KINDS.GIT_HOOK);
652
+ persistRecorder(projectPath, recorder);
600
653
  console.log(chalk.green(' ✓ Installed .git/hooks/pre-commit (native git hook)'));
601
654
  }
602
655
  } catch (e) {