universal-dev-standards 6.7.2 → 6.7.4
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/bundled/locales/zh-CN/CHANGELOG.md +24 -2
- package/bundled/locales/zh-CN/README.md +1 -1
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/CHANGELOG.md +24 -2
- package/bundled/locales/zh-TW/README.md +1 -1
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/package.json +1 -1
- package/src/commands/check.js +13 -4
- package/src/reconciler/actual-state-scanner.js +70 -4
- package/src/reconciler/backup-manager.js +29 -2
- package/src/reconciler/desired-state-calculator.js +26 -3
- package/src/reconciler/diff-engine.js +56 -13
- package/src/reconciler/index.js +69 -3
- package/src/reconciler/plan-executor.js +13 -2
- package/src/utils/registry.js +31 -0
- package/src/utils/skills-installer.js +222 -44
- package/standards-registry.json +7 -7
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
source: ../../CHANGELOG.md
|
|
3
|
-
source_version: 6.7.
|
|
4
|
-
translation_version: 6.7.
|
|
3
|
+
source_version: 6.7.4
|
|
4
|
+
translation_version: 6.7.4
|
|
5
5
|
last_synced: 2026-08-18
|
|
6
6
|
status: current
|
|
7
7
|
---
|
|
@@ -17,6 +17,28 @@ status: current
|
|
|
17
17
|
|
|
18
18
|
## [Unreleased]
|
|
19
19
|
|
|
20
|
+
## [6.7.4] - 2026-08-18
|
|
21
|
+
|
|
22
|
+
### 修正
|
|
23
|
+
|
|
24
|
+
- **6.7.3 让 `uds check` 每个 repo 报出 52 个不存在的「缺失」文件。** 6.7.3 的两个改动要**同时在场**才会互相作用:R1 给了 skill 内容哈希,使它们第一次走进 `unchanged` 分支;而 R6b 会把那个分支的条目收进 `manifest.fileHashes`。那张表是用 `isFile()` 验证的,而 skill 是目录,于是每一个都被报成缺失。**没有任何东西被删除——错的是记录不是磁盘**(已验证:59 个 skill 目录与其内容完好)。`verifiedPristine` 现在限缩于文件类别;而且**修好写入端不会让已经写进去的东西消失**,所以 `uds update` 也会清掉 `fileHashes` 里的 `.claude/skills/` 与 `.claude/commands/` 键——**一个坏掉的键不会有第二次机会被访问**,因为磁盘上没有任何东西对应它。
|
|
25
|
+
- **commands 也改为内容比对(XSPEC-382 R7)。** R1 只涵盖 skills,把 commands 留在 `hash: null`,于是无条件重装的分支对它们仍然活着。与 skills 同一个形状:`resolveCommandContent(name, agent, locale)` 说明一次安装会包含什么(locale 选择+英文回退,加上逐 agent 的转换),安装器写它、计划器哈希它。
|
|
26
|
+
- **装了 locale 版的人,拿到的 SKILL.md 指向从来不会被安装的文件。** locale 包不是英文来源的完整副本——实测 **zh-TW 59 个中有 4 个、zh-CN 59 个中有 5 个**缺少英文有出货的文件——而安装器把 locale 目录整包换上去,于是那些伴随文件根本没被写入。其中两个缺口**是被引用的**:zh-TW 的 `dev-workflow-guide/SKILL.md` 引用 `workflow-phases.md` 三次、`testing-guide/SKILL.md` 引用 `test-skeleton-templates.md` 三次。**回退改为逐文件而非逐 skill**——locale 有就用 locale 的,没有就用英文的。
|
|
27
|
+
- **UDS 不出货的文件,不再让一个 skill 永远停在「已变更」。** 安装一个 skill 现在会移除其目录下**解析结果没有指名**的顶层文件(实地发现:`deploy-assistant/guide.md`,它不在任何 UDS skills 树里,而且从来就不在)。范围由同一个 provenance 判准界定,也就是让采用者自己的 skill 目录不进入此处任何路径的那一个——**已验证手写的 skill 目录与其额外文件在 `--force` 之后完好无损**——子目录一律不动,因为安装器从来没有写过它们。
|
|
28
|
+
|
|
29
|
+
## [6.7.3] - 2026-08-18
|
|
30
|
+
|
|
31
|
+
### 修正
|
|
32
|
+
|
|
33
|
+
- **skill 改为内容比对,升级不再重印 55 行毫无意义的变更。** diff 两端都硬写 `hash: null`,于是每个 skill 都是无条件重装。**两端各算来源目录的哈希行不通**——安装不是逐字节复制:locale 版的 `SKILL.md` 会被并入英文 frontmatter(`brainstorm-assistant`:23,753 字节的 zh-TW 来源变成 23,866 字节的安装结果)、来源是运行期依 locale 逐 skill 选择并可回退英文、子目录被跳过。照那样做,55 个全都会显示为内容**变更**,每次升级皆然——**与真的变更无从分辨,比那个已知的无信号更糟**。改为只有一个函数:`resolveSkillFiles(name, locale)` 说明一次安装会包含什么,**安装器写它、计划器哈希它**,两者因此不可能漂移。与真实已安装的项目对账:110 个文件逐字节相符、0 个不符,18 个采用者自写的 skill 正确地解不出来。actual 端**只对 UDS 管理的目录计算哈希**;采用者自己的 skill 永远不被比较、也永远不会变成删除候选。真实升级中的 `Update (57)` 现在是 `Update: 0, Unchanged: 127`。
|
|
34
|
+
- **`uds check --restore` 对 72 个受追踪标准中的 64 个无法还原。** 它拿 `entry.endsWith(fileName)` 去比对 `manifest.standards`,而那些条目**自 3.4.0 起是 ID(`commit-message`)而非路径**——这个比较永远不可能为真。能用的那 8 个是仍存路径格式的 `options/`,**这正是失败从来看起来不像全面失败的原因**;其余一律报告「Could not determine source」。同一段 ID→来源的解析在这个文件里已经存在两次,而这一处从来没拿到过,所以修法是在 `registry.js` 收敛出一支解析器,**与它必须一致的那支文件名解析器配对**。
|
|
35
|
+
- **一个逐字节正确的标准,没办法停止被报成「已修改」。** actual state 是从磁盘算哈希的,所以与上游相符的文件被归为 `unchanged`、不产生动作、也永远不会被重新哈希——而 reconciliation 在计划为空时提前返回、连 manifest 都不写。**没有回头路。** diff 现在报告它**证明过**与上游相同的那些文件,并在提前返回之前补正记录。**刻意做得很窄**:只有在证明磁盘与 desired 相符之后才记录,所以手改永远不会被吸收。把记录同步成磁盘上的任何内容,会让 `uds check` 从此再也报不出任何被改过的标准。
|
|
36
|
+
- **备份不含 skills,而且没有任何东西说出这件事。** skill 是目录,而备份对它们调用 `copyFileSync`,那在每个平台都会抛(本机两个文件系统实测皆 ENOTSUP——**不是临时目录的产物**)。失败被藏了两层:执行器只在**一个都没成功**时中止,于是单一一次成功掩盖了任意数量的失败;备份 manifest 没有 errors 字段,使得「129 个计划路径备了 74 个」在磁盘上与完整备份无从分辨。修正前于真实 repo 测量:备份 manifest 记录 74 个路径、而计划有 129 个动作,其中 **55 个 skill 目录一个都不在里面**——**一个不涵盖它即将覆写的最大一块的回复点**。现在目录递归复制、manifest 记录 `failedToBackUp` 与 `coverage: {planned, backedUp, failed}`,且**任一**备份失败即中止整次执行:拒绝覆写一个没能先复制起来的文件,正是备份的用途。
|
|
37
|
+
|
|
38
|
+
### 变更
|
|
39
|
+
|
|
40
|
+
- **无条件重装的折叠保留,措辞放宽。** commands 仍然没有内容比对,所以那个分支是活的。它没有跟着 skill 那一半一起移除,因为**一个静默停止套用的折叠,与一个本来就没东西可折的计划,长得一模一样**。
|
|
41
|
+
|
|
20
42
|
## [6.7.2] - 2026-08-18
|
|
21
43
|
|
|
22
44
|
### 修正
|
|
@@ -15,7 +15,7 @@ status: current
|
|
|
15
15
|
|
|
16
16
|
> **语言**: [English](../../README.md) | [繁體中文](../zh-TW/README.md) | 简体中文
|
|
17
17
|
|
|
18
|
-
**版本**: 6.7.
|
|
18
|
+
**版本**: 6.7.4 | **发布日期**: 2026-08-18 | **授权**: [双重授权](../../LICENSE) (CC BY 4.0 + MIT)
|
|
19
19
|
|
|
20
20
|
语言无关、框架无关的软件项目文档标准。通过 AI 原生工作流,确保不同技术栈之间的一致性、质量和可维护性。
|
|
21
21
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
source: ../../CHANGELOG.md
|
|
3
|
-
source_version: 6.7.
|
|
4
|
-
translation_version: 6.7.
|
|
3
|
+
source_version: 6.7.4
|
|
4
|
+
translation_version: 6.7.4
|
|
5
5
|
last_synced: 2026-08-18
|
|
6
6
|
status: current
|
|
7
7
|
---
|
|
@@ -17,6 +17,28 @@ status: current
|
|
|
17
17
|
|
|
18
18
|
## [Unreleased]
|
|
19
19
|
|
|
20
|
+
## [6.7.4] - 2026-08-18
|
|
21
|
+
|
|
22
|
+
### 修正
|
|
23
|
+
|
|
24
|
+
- **6.7.3 讓 `uds check` 每個 repo 報出 52 個不存在的「遺失」檔案。** 6.7.3 的兩個改動要**同時在場**才會互相作用:R1 給了 skill 內容雜湊,使它們第一次走進 `unchanged` 分支;而 R6b 會把那個分支的項目收進 `manifest.fileHashes`。那張表是用 `isFile()` 驗證的,而 skill 是目錄,於是每一個都被報成遺失。**沒有任何東西被刪除——錯的是紀錄不是磁碟**(已驗證:59 個 skill 目錄與其內容完好)。`verifiedPristine` 現在限縮於檔案類別;而且**修好寫入端不會讓已經寫進去的東西消失**,所以 `uds update` 也會清掉 `fileHashes` 裡的 `.claude/skills/` 與 `.claude/commands/` 鍵——**一個壞掉的鍵不會有第二次機會被造訪**,因為磁碟上沒有任何東西對應它。
|
|
25
|
+
- **commands 也改為內容比對(XSPEC-382 R7)。** R1 只涵蓋 skills,把 commands 留在 `hash: null`,於是無條件重裝的分支對它們仍然活著。與 skills 同一個形狀:`resolveCommandContent(name, agent, locale)` 說明一次安裝會包含什麼(locale 選擇+英文回退,加上逐 agent 的轉換),安裝器寫它、計畫器雜湊它。
|
|
26
|
+
- **裝了 locale 版的人,拿到的 SKILL.md 指向從來不會被安裝的檔案。** locale 包不是英文來源的完整複本——實測 **zh-TW 59 個中有 4 個、zh-CN 59 個中有 5 個**缺少英文有出貨的檔——而安裝器把 locale 目錄整包換上去,於是那些伴隨檔根本沒被寫入。其中兩個缺口**是被引用的**:zh-TW 的 `dev-workflow-guide/SKILL.md` 引用 `workflow-phases.md` 三次、`testing-guide/SKILL.md` 引用 `test-skeleton-templates.md` 三次。**回退改為逐檔而非逐 skill**——locale 有就用 locale 的,沒有就用英文的。
|
|
27
|
+
- **UDS 不出貨的檔案,不再讓一個 skill 永遠停在「已變更」。** 安裝一個 skill 現在會移除其目錄下**解析結果沒有指名**的頂層檔案(實地發現:`deploy-assistant/guide.md`,它不在任何 UDS skills 樹裡,而且從來就不在)。範圍由同一個 provenance 判準界定,也就是讓採用者自己的 skill 目錄不進入此處任何路徑的那一個——**已驗證手寫的 skill 目錄與其額外檔案在 `--force` 之後完好無損**——子目錄一律不動,因為安裝器從來沒有寫過它們。
|
|
28
|
+
|
|
29
|
+
## [6.7.3] - 2026-08-18
|
|
30
|
+
|
|
31
|
+
### 修正
|
|
32
|
+
|
|
33
|
+
- **skill 改為內容比對,升級不再重印 55 列毫無意義的變更。** diff 兩端都硬寫 `hash: null`,於是每個 skill 都是無條件重裝。**兩端各算來源目錄的雜湊行不通**——安裝不是逐位元複製:locale 版的 `SKILL.md` 會被併入英文 frontmatter(`brainstorm-assistant`:23,753 bytes 的 zh-TW 來源變成 23,866 bytes 的安裝結果)、來源是執行期依 locale 逐 skill 選擇並可回退英文、子目錄被略過。照那樣做,55 個全都會顯示為內容**變更**,每次升級皆然——**與真的變更無從分辨,比那個已知的無訊號更糟**。改為只有一個函式:`resolveSkillFiles(name, locale)` 說明一次安裝會包含什麼,**安裝器寫它、計畫器雜湊它**,兩者因此不可能漂移。與真實已安裝的專案對帳:110 個檔逐位元相符、0 個不符,18 個採用者自寫的 skill 正確地解不出來。actual 端**只對 UDS 管理的目錄計算雜湊**;採用者自己的 skill 永遠不被比較、也永遠不會變成刪除候選。真實升級中的 `Update (57)` 現在是 `Update: 0, Unchanged: 127`。
|
|
34
|
+
- **`uds check --restore` 對 72 個受追蹤標準中的 64 個無法還原。** 它拿 `entry.endsWith(fileName)` 去比對 `manifest.standards`,而那些條目**自 3.4.0 起是 ID(`commit-message`)而非路徑**——這個比較永遠不可能為真。會動的那 8 個是仍存路徑格式的 `options/`,**這正是失敗從來看起來不像全面失敗的原因**;其餘一律回報「Could not determine source」。同一段 ID→來源的解析在這個檔案裡已經存在兩次,而這一處從來沒拿到過,所以修法是在 `registry.js` 收斂出一支解析器,**與它必須一致的那支檔名解析器配對**。
|
|
35
|
+
- **一個逐位元正確的標準,沒辦法停止被報成「已修改」。** actual state 是從磁碟算雜湊的,所以與上游相符的檔案被歸為 `unchanged`、不產生動作、也永遠不會被重新雜湊——而 reconciliation 在計畫為空時早退出、連 manifest 都不寫。**沒有回頭路。** diff 現在回報它**證明過**與上游相同的那些檔案,並在早退出之前補正記錄。**刻意做得很窄**:只有在證明磁碟與 desired 相符之後才記錄,所以手改永遠不會被吸收。把記錄同步成磁碟上的任何內容,會讓 `uds check` 從此再也報不出任何被改過的標準。
|
|
36
|
+
- **備份不含 skills,而且沒有任何東西說出這件事。** skill 是目錄,而備份對它們呼叫 `copyFileSync`,那在每個平台都會拋(本機兩個檔案系統實測皆 ENOTSUP——**不是暫存目錄的產物**)。失敗被藏了兩層:執行器只在**一個都沒成功**時中止,於是單一一次成功掩蓋了任意數量的失敗;備份 manifest 沒有 errors 欄位,使得「129 個計畫路徑備了 74 個」在磁碟上與完整備份無從分辨。修正前於真實 repo 量測:備份 manifest 記錄 74 個路徑、而計畫有 129 個動作,其中 **55 個 skill 目錄一個都不在裡面**——**一個不涵蓋它即將覆寫的最大一塊的回復點**。現在目錄遞迴複製、manifest 記錄 `failedToBackUp` 與 `coverage: {planned, backedUp, failed}`,且**任一**備份失敗即中止整次執行:拒絕覆寫一個沒能先複製起來的檔案,正是備份的用途。
|
|
37
|
+
|
|
38
|
+
### 變更
|
|
39
|
+
|
|
40
|
+
- **無條件重裝的摺疊保留,措辭放寬。** commands 仍然沒有內容比對,所以那個分支是活的。它沒有跟著 skill 那一半一起移除,因為**一個靜默停止套用的摺疊,與一個本來就沒東西可摺的計畫,長得一模一樣**。
|
|
41
|
+
|
|
20
42
|
## [6.7.2] - 2026-08-18
|
|
21
43
|
|
|
22
44
|
### 修正
|
|
@@ -15,7 +15,7 @@ status: current
|
|
|
15
15
|
|
|
16
16
|
> **語言**: [English](../../README.md) | 繁體中文 | [简体中文](../zh-CN/README.md)
|
|
17
17
|
|
|
18
|
-
**版本**: 6.7.
|
|
18
|
+
**版本**: 6.7.4 | **發布日期**: 2026-08-18 | **授權**: [雙重授權](../../LICENSE) (CC BY 4.0 + MIT)
|
|
19
19
|
|
|
20
20
|
語言無關、框架無關的軟體專案文件標準。透過 AI 原生工作流,確保不同技術堆疊之間的一致性、品質和可維護性。
|
|
21
21
|
|
package/package.json
CHANGED
package/src/commands/check.js
CHANGED
|
@@ -7,7 +7,7 @@ import { execSync } from 'child_process';
|
|
|
7
7
|
import { readManifest, writeManifest, isInitialized, copyStandard, copyIntegration } from '../utils/copier.js';
|
|
8
8
|
import {
|
|
9
9
|
getAllStandards,
|
|
10
|
-
getRepositoryInfo, resolveStandardFilename } from '../utils/registry.js';
|
|
10
|
+
getRepositoryInfo, resolveStandardFilename, resolveStandardSourcePath } from '../utils/registry.js';
|
|
11
11
|
import {
|
|
12
12
|
computeFileHash,
|
|
13
13
|
compareFileHash,
|
|
@@ -749,11 +749,20 @@ function removeFromManifest(manifest, relativePath) {
|
|
|
749
749
|
*/
|
|
750
750
|
export function getSourcePathFromRelative(manifest, relativePath) {
|
|
751
751
|
const fileName = basename(relativePath);
|
|
752
|
+
const format = manifest.format || 'ai';
|
|
752
753
|
|
|
753
|
-
// Check standards
|
|
754
|
+
// Check standards.
|
|
755
|
+
//
|
|
756
|
+
// Compare the RESOLVED filename, not the raw entry. Manifests have stored IDs
|
|
757
|
+
// (`commit-message`) rather than paths since 3.4.0, and `'commit-message'
|
|
758
|
+
// .endsWith('commit-message.ai.yaml')` is false for every one of them —
|
|
759
|
+
// so this returned null for 64 of 72 tracked standards and `uds check
|
|
760
|
+
// --restore` reported "Could not determine source" for all of them. The eight
|
|
761
|
+
// that worked were the `options/` entries, still stored as paths, which is
|
|
762
|
+
// why the failure never looked total. (XSPEC-382 R6)
|
|
754
763
|
for (const std of manifest.standards) {
|
|
755
|
-
if (std
|
|
756
|
-
return std;
|
|
764
|
+
if (resolveStandardFilename(std, format) === fileName) {
|
|
765
|
+
return resolveStandardSourcePath(std, format);
|
|
757
766
|
}
|
|
758
767
|
}
|
|
759
768
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
11
|
+
import { createHash } from 'crypto';
|
|
11
12
|
import { join, relative } from 'path';
|
|
12
13
|
import { readManifest } from '../core/manifest.js';
|
|
13
14
|
import { computeFileHash, computeIntegrationBlockHash } from '../utils/hasher.js';
|
|
@@ -312,6 +313,45 @@ function shippedCommandNames() {
|
|
|
312
313
|
// nothing on disk distinguishes them from the adopter's own work. Leaving a few
|
|
313
314
|
// stale directories with a warning is a better way to fail than deleting files
|
|
314
315
|
// somebody hand-wrote.
|
|
316
|
+
/**
|
|
317
|
+
* Hash an installed skill directory the way the desired side hashes a resolved
|
|
318
|
+
* one: top-level files only, sorted by name, name and content both fed in.
|
|
319
|
+
*
|
|
320
|
+
* Written against `computeSkillContentHash`'s format rather than calling it,
|
|
321
|
+
* because the input here is paths on disk and there is nothing gained by
|
|
322
|
+
* materialising a second array first. The format is the contract; if it changes
|
|
323
|
+
* in one place and not the other every skill reports as changed, which the
|
|
324
|
+
* paired tests in `skill-content-hash.test.js` exist to catch.
|
|
325
|
+
*/
|
|
326
|
+
function hashInstalledSkillDir(dirPath) {
|
|
327
|
+
let entries;
|
|
328
|
+
try {
|
|
329
|
+
entries = readdirSync(dirPath, { withFileTypes: true });
|
|
330
|
+
} catch {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const names = entries.filter((e) => e.isFile()).map((e) => e.name).sort();
|
|
335
|
+
if (names.length === 0) return null;
|
|
336
|
+
|
|
337
|
+
const h = createHash('sha256');
|
|
338
|
+
for (const name of names) {
|
|
339
|
+
let content;
|
|
340
|
+
try {
|
|
341
|
+
content = readFileSync(join(dirPath, name), 'utf-8');
|
|
342
|
+
} catch {
|
|
343
|
+
// Unreadable file → no trustworthy hash. Returning a partial one would
|
|
344
|
+
// claim the directory matches upstream when part of it was never read.
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
h.update(name);
|
|
348
|
+
h.update('\0');
|
|
349
|
+
h.update(content);
|
|
350
|
+
h.update('\0');
|
|
351
|
+
}
|
|
352
|
+
return `sha256:${h.digest('hex')}`;
|
|
353
|
+
}
|
|
354
|
+
|
|
315
355
|
function isUdsProvenance(skillName) {
|
|
316
356
|
return sourceEntryNames().has(skillName);
|
|
317
357
|
}
|
|
@@ -335,9 +375,18 @@ function scanSkills(state, projectPath, manifest) {
|
|
|
335
375
|
? getRelativePath(projectPath, join(skillsDir, skillName))
|
|
336
376
|
: join(skillsDir, skillName);
|
|
337
377
|
|
|
378
|
+
// Content hash of what is actually installed, over the same file set and
|
|
379
|
+
// the same algorithm the desired side uses (XSPEC-382 R1).
|
|
380
|
+
//
|
|
381
|
+
// Computed ONLY for UDS-managed skills. An adopter's own skill has no
|
|
382
|
+
// desired counterpart, so a hash for it could only ever feed a comparison
|
|
383
|
+
// against nothing — and this scanner has already been the site of a
|
|
384
|
+
// defect that proposed deleting fourteen of dev-platform's hand-written
|
|
385
|
+
// skills. It does not get a second chance to reason about them.
|
|
386
|
+
const udsManaged = isUdsProvenance(skillName);
|
|
338
387
|
state.skills.set(key, {
|
|
339
388
|
relativePath: relPath,
|
|
340
|
-
hash:
|
|
389
|
+
hash: udsManaged ? hashInstalledSkillDir(join(skillsDir, skillName)) : null,
|
|
341
390
|
size: null,
|
|
342
391
|
category: 'skill',
|
|
343
392
|
sourcePath: null,
|
|
@@ -350,7 +399,7 @@ function scanSkills(state, projectPath, manifest) {
|
|
|
350
399
|
// the skills folder used to be assumed UDS-managed, so a plan for a repo
|
|
351
400
|
// with hand-written skills proposed deleting them: dev-platform's would
|
|
352
401
|
// have removed fourteen. (XSPEC-343 R2)
|
|
353
|
-
udsManaged
|
|
402
|
+
udsManaged
|
|
354
403
|
}
|
|
355
404
|
});
|
|
356
405
|
}
|
|
@@ -385,9 +434,26 @@ function scanCommands(state, projectPath, manifest) {
|
|
|
385
434
|
? getRelativePath(projectPath, join(cmdsDir, entry.name))
|
|
386
435
|
: join(cmdsDir, entry.name);
|
|
387
436
|
|
|
437
|
+
// Content hash of the installed file, for UDS-managed commands only —
|
|
438
|
+
// same reasoning as skills: an adopter's own command has no desired
|
|
439
|
+
// counterpart to compare against. (XSPEC-382 R7)
|
|
440
|
+
const cmdUdsManaged = shippedCommandNames().has(cmdName);
|
|
441
|
+
let cmdHash = null;
|
|
442
|
+
if (cmdUdsManaged) {
|
|
443
|
+
try {
|
|
444
|
+
cmdHash = `sha256:${createHash('sha256')
|
|
445
|
+
.update(readFileSync(join(cmdsDir, entry.name), 'utf-8'))
|
|
446
|
+
.digest('hex')}`;
|
|
447
|
+
} catch {
|
|
448
|
+
// Unreadable → no hash. A partial answer would claim a match that
|
|
449
|
+
// was never checked.
|
|
450
|
+
cmdHash = null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
388
454
|
state.commands.set(key, {
|
|
389
455
|
relativePath: relPath,
|
|
390
|
-
hash:
|
|
456
|
+
hash: cmdHash,
|
|
391
457
|
size: null,
|
|
392
458
|
category: 'command',
|
|
393
459
|
sourcePath: null,
|
|
@@ -396,7 +462,7 @@ function scanCommands(state, projectPath, manifest) {
|
|
|
396
462
|
level,
|
|
397
463
|
commandName: cmdName,
|
|
398
464
|
scanned: true,
|
|
399
|
-
udsManaged:
|
|
465
|
+
udsManaged: cmdUdsManaged
|
|
400
466
|
}
|
|
401
467
|
});
|
|
402
468
|
}
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
existsSync,
|
|
18
18
|
mkdirSync,
|
|
19
19
|
copyFileSync,
|
|
20
|
+
cpSync,
|
|
21
|
+
statSync,
|
|
20
22
|
readFileSync,
|
|
21
23
|
writeFileSync,
|
|
22
24
|
readdirSync,
|
|
@@ -86,7 +88,15 @@ export function createBackup(projectPath, plan) {
|
|
|
86
88
|
errors.push(`Failed to write backup .gitignore: ${err.message}`);
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
// Backup each
|
|
91
|
+
// Backup each entry.
|
|
92
|
+
//
|
|
93
|
+
// Skills are DIRECTORIES (`.claude/skills/<name>`), and `copyFileSync` on a
|
|
94
|
+
// directory throws — ENOTSUP on macOS, EISDIR on Linux. Every skill in every
|
|
95
|
+
// plan failed here, and the failures went nowhere (see the partial-failure
|
|
96
|
+
// note below). Measured on a real repo before the fix: a vibeops backup
|
|
97
|
+
// recorded 74 files for a plan of 129 actions, with **0 of the 55 skill
|
|
98
|
+
// directories** among them — a rollback point that did not cover the largest
|
|
99
|
+
// part of what was about to be overwritten. (XSPEC-382 R6)
|
|
90
100
|
for (const relativePath of filesToBackup) {
|
|
91
101
|
const sourcePath = join(projectPath, relativePath);
|
|
92
102
|
if (!existsSync(sourcePath)) continue;
|
|
@@ -94,7 +104,11 @@ export function createBackup(projectPath, plan) {
|
|
|
94
104
|
const targetPath = join(backupDir, relativePath);
|
|
95
105
|
try {
|
|
96
106
|
mkdirSync(dirname(targetPath), { recursive: true });
|
|
97
|
-
|
|
107
|
+
if (statSync(sourcePath).isDirectory()) {
|
|
108
|
+
cpSync(sourcePath, targetPath, { recursive: true });
|
|
109
|
+
} else {
|
|
110
|
+
copyFileSync(sourcePath, targetPath);
|
|
111
|
+
}
|
|
98
112
|
backedUp.push(relativePath);
|
|
99
113
|
} catch (err) {
|
|
100
114
|
errors.push(`Failed to backup ${relativePath}: ${err.message}`);
|
|
@@ -116,6 +130,19 @@ export function createBackup(projectPath, plan) {
|
|
|
116
130
|
}))
|
|
117
131
|
},
|
|
118
132
|
backedUpFiles: backedUp,
|
|
133
|
+
// What could NOT be backed up, and the resulting gap.
|
|
134
|
+
//
|
|
135
|
+
// The manifest previously recorded only successes, with no errors field at
|
|
136
|
+
// all — so a backup covering 74 of 129 planned paths was indistinguishable
|
|
137
|
+
// on disk from one covering all of them. Anyone reaching for this directory
|
|
138
|
+
// is reaching for it because something went wrong; it has to say what it
|
|
139
|
+
// does not contain. (XSPEC-382 R6)
|
|
140
|
+
failedToBackUp: errors.slice(),
|
|
141
|
+
coverage: {
|
|
142
|
+
planned: filesToBackup.length,
|
|
143
|
+
backedUp: backedUp.length,
|
|
144
|
+
failed: filesToBackup.length - backedUp.length
|
|
145
|
+
},
|
|
119
146
|
projectPath
|
|
120
147
|
};
|
|
121
148
|
|
|
@@ -24,7 +24,11 @@ import {
|
|
|
24
24
|
} from '../config/ai-agent-paths.js';
|
|
25
25
|
import {
|
|
26
26
|
getAvailableSkillNames,
|
|
27
|
-
getAvailableCommandNames
|
|
27
|
+
getAvailableCommandNames,
|
|
28
|
+
resolveSkillFiles,
|
|
29
|
+
computeSkillContentHash,
|
|
30
|
+
resolveCommandContent,
|
|
31
|
+
computeCommandContentHash
|
|
28
32
|
} from '../utils/skills-installer.js';
|
|
29
33
|
import { MARKETPLACE_NAMES_SENTINEL } from '../core/manifest.js';
|
|
30
34
|
|
|
@@ -328,6 +332,9 @@ function calculateSkills(state, projectPath, manifest) {
|
|
|
328
332
|
}
|
|
329
333
|
|
|
330
334
|
const installations = skills.installations || [];
|
|
335
|
+
// The locale the project installed with; skills fall back to English per skill
|
|
336
|
+
// when a locale variant is missing, which `resolveSkillFiles` handles.
|
|
337
|
+
const skillsLocale = skills.locale || 'en';
|
|
331
338
|
|
|
332
339
|
for (const installation of installations) {
|
|
333
340
|
const { agent, level } = installation;
|
|
@@ -341,9 +348,19 @@ function calculateSkills(state, projectPath, manifest) {
|
|
|
341
348
|
: null;
|
|
342
349
|
|
|
343
350
|
if (relativeBase) {
|
|
351
|
+
// Content hash of what installing this skill WOULD produce.
|
|
352
|
+
//
|
|
353
|
+
// Asked of `resolveSkillFiles` — the same function the installer writes
|
|
354
|
+
// from — so the two answers cannot drift. Hashing the source directory
|
|
355
|
+
// instead does not work: installing is not a verbatim copy (locale
|
|
356
|
+
// selection, English frontmatter merged into localized SKILL.md,
|
|
357
|
+
// subdirectories skipped), and a planner that computes it differently
|
|
358
|
+
// from the installer reports every skill as changed on every upgrade.
|
|
359
|
+
// (XSPEC-382 R1)
|
|
360
|
+
const resolved = resolveSkillFiles(skillName, skillsLocale);
|
|
344
361
|
state.skills.set(`skill:${agent}:${level}:${skillName}`, {
|
|
345
362
|
relativePath: relativeBase,
|
|
346
|
-
hash:
|
|
363
|
+
hash: resolved.error ? null : computeSkillContentHash(resolved.files),
|
|
347
364
|
size: null,
|
|
348
365
|
category: 'skill',
|
|
349
366
|
sourcePath: null,
|
|
@@ -364,6 +381,9 @@ function calculateSkills(state, projectPath, manifest) {
|
|
|
364
381
|
function calculateCommands(state, projectPath, manifest) {
|
|
365
382
|
const commands = manifest.commands;
|
|
366
383
|
if (!commands || !commands.installed) return;
|
|
384
|
+
// Commands are installed alongside skills and share their locale; fall back
|
|
385
|
+
// to the skills locale, then English.
|
|
386
|
+
const commandsLocale = commands.locale || manifest.skills?.locale || 'en';
|
|
367
387
|
|
|
368
388
|
const commandNames = getAvailableCommandNames();
|
|
369
389
|
if (commandNames.length === 0) {
|
|
@@ -389,9 +409,12 @@ function calculateCommands(state, projectPath, manifest) {
|
|
|
389
409
|
: null;
|
|
390
410
|
|
|
391
411
|
if (relativeBase) {
|
|
412
|
+
// Content hash of what installing this command WOULD produce, from the
|
|
413
|
+
// same function the installer writes from. (XSPEC-382 R7)
|
|
414
|
+
const resolvedCmd = resolveCommandContent(commandName, agent, commandsLocale);
|
|
392
415
|
state.commands.set(`command:${agent}:${level}:${commandName}`, {
|
|
393
416
|
relativePath: relativeBase,
|
|
394
|
-
hash: null,
|
|
417
|
+
hash: resolvedCmd.error ? null : computeCommandContentHash(resolvedCmd.content),
|
|
395
418
|
size: null,
|
|
396
419
|
category: 'command',
|
|
397
420
|
sourcePath: null,
|
|
@@ -56,16 +56,30 @@ export function computeDiff(desired, actual, options = {}) {
|
|
|
56
56
|
const warnings = [];
|
|
57
57
|
const summary = { create: 0, update: 0, delete: 0, unchanged: 0, migrate_block: 0 };
|
|
58
58
|
|
|
59
|
+
// Files whose content on disk is byte-identical to what UDS ships (XSPEC-382 R6).
|
|
60
|
+
//
|
|
61
|
+
// These produce no action — there is nothing to install. They are collected
|
|
62
|
+
// so the caller can bring `manifest.fileHashes` back in line with a disk that
|
|
63
|
+
// is already correct. Without this there is no path back: a stale recorded
|
|
64
|
+
// hash makes `uds check` report a pristine file as modified forever, because
|
|
65
|
+
// an unchanged file is never re-hashed and the reconciler returns early when
|
|
66
|
+
// the action list is empty.
|
|
67
|
+
//
|
|
68
|
+
// Only ever recorded when disk === desired. Syncing the record to whatever is
|
|
69
|
+
// on disk would silently absorb a hand edit and empty the integrity check of
|
|
70
|
+
// its purpose — it exists to tell you somebody changed a standard.
|
|
71
|
+
const verifiedPristine = [];
|
|
72
|
+
|
|
59
73
|
// Diff standards
|
|
60
74
|
diffCategory(
|
|
61
75
|
desired.standards, actual.standards,
|
|
62
|
-
'standard', actions, warnings, summary, force
|
|
76
|
+
'standard', actions, warnings, summary, force, verifiedPristine
|
|
63
77
|
);
|
|
64
78
|
|
|
65
79
|
// Diff options
|
|
66
80
|
diffCategory(
|
|
67
81
|
desired.options, actual.options,
|
|
68
|
-
'option', actions, warnings, summary, force
|
|
82
|
+
'option', actions, warnings, summary, force, verifiedPristine
|
|
69
83
|
);
|
|
70
84
|
|
|
71
85
|
// Diff integrations (special: use migrate_block)
|
|
@@ -77,22 +91,22 @@ export function computeDiff(desired, actual, options = {}) {
|
|
|
77
91
|
// Diff skills
|
|
78
92
|
diffCategory(
|
|
79
93
|
desired.skills, actual.skills,
|
|
80
|
-
'skill', actions, warnings, summary, force
|
|
94
|
+
'skill', actions, warnings, summary, force, verifiedPristine
|
|
81
95
|
);
|
|
82
96
|
|
|
83
97
|
// Diff commands
|
|
84
98
|
diffCategory(
|
|
85
99
|
desired.commands, actual.commands,
|
|
86
|
-
'command', actions, warnings, summary, force
|
|
100
|
+
'command', actions, warnings, summary, force, verifiedPristine
|
|
87
101
|
);
|
|
88
102
|
|
|
89
|
-
return { actions, summary, warnings };
|
|
103
|
+
return { actions, summary, warnings, verifiedPristine };
|
|
90
104
|
}
|
|
91
105
|
|
|
92
106
|
/**
|
|
93
107
|
* Diff a single category (standards, options, skills, commands).
|
|
94
108
|
*/
|
|
95
|
-
function diffCategory(desiredMap, actualMap, category, actions, warnings, summary, force) {
|
|
109
|
+
function diffCategory(desiredMap, actualMap, category, actions, warnings, summary, force, verifiedPristine = []) {
|
|
96
110
|
// Check desired entries against actual
|
|
97
111
|
for (const [key, desiredEntry] of desiredMap) {
|
|
98
112
|
const actualEntry = actualMap.get(key);
|
|
@@ -160,8 +174,31 @@ function diffCategory(desiredMap, actualMap, category, actions, warnings, summar
|
|
|
160
174
|
summary.unchanged++;
|
|
161
175
|
}
|
|
162
176
|
} else {
|
|
163
|
-
// Hashes match → unchanged
|
|
177
|
+
// Hashes match → unchanged.
|
|
178
|
+
//
|
|
179
|
+
// The file is byte-identical to what UDS ships, so there is nothing to
|
|
180
|
+
// install — but the manifest's recorded hash may still be stale (XSPEC-382
|
|
181
|
+
// R6: a pre-reconciler manifest written back over the reconciler's output
|
|
182
|
+
// reverted `fileHashes` along with `upstream.version`). Recording it here
|
|
183
|
+
// is safe precisely because we got here by proving disk === desired.
|
|
164
184
|
summary.unchanged++;
|
|
185
|
+
// Only file-backed categories. `manifest.fileHashes` is a FILE map that
|
|
186
|
+
// `uds check` validates with a `isFile()` test; skills are directories and
|
|
187
|
+
// commands live in `commandHashes`, so recording either there makes the
|
|
188
|
+
// integrity check report them as missing.
|
|
189
|
+
//
|
|
190
|
+
// This branch was unreachable for skills until R1 gave them hashes, so
|
|
191
|
+
// the two changes shipped together and the fault only appeared once both
|
|
192
|
+
// were in: 6.7.3 put 52 skill directory paths into `fileHashes` in every
|
|
193
|
+
// adopter repo and `uds check` reported 52 phantom missing files. Nothing
|
|
194
|
+
// was deleted — the record was wrong, not the disk. (XSPEC-382 R7)
|
|
195
|
+
if (actualEntry.hash && (category === 'standard' || category === 'option')) {
|
|
196
|
+
verifiedPristine.push({
|
|
197
|
+
path: desiredEntry.relativePath,
|
|
198
|
+
hash: actualEntry.hash,
|
|
199
|
+
size: actualEntry.size ?? null
|
|
200
|
+
});
|
|
201
|
+
}
|
|
165
202
|
}
|
|
166
203
|
}
|
|
167
204
|
|
|
@@ -369,11 +406,17 @@ export function formatPlan(plan) {
|
|
|
369
406
|
if (grouped.update.length > 0) {
|
|
370
407
|
// XSPEC-382 R3 — collapse the unconditional reinstalls.
|
|
371
408
|
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
409
|
+
// On a real upgrade this produced `Update (57)` where 55 rows carried one
|
|
410
|
+
// identical reason and only 2 were actual changes — the two a reviewer
|
|
411
|
+
// needs to see, buried under fifty-five that say nothing about this
|
|
412
|
+
// upgrade.
|
|
413
|
+
//
|
|
414
|
+
// R1 has since given SKILLS a content comparison, so those rows are gone;
|
|
415
|
+
// the same upgrade now reports `Update: 0, Unchanged: 127`. COMMANDS still
|
|
416
|
+
// hardcode `hash: null` on both sides, so this branch is live and still
|
|
417
|
+
// needed. It is deliberately not deleted along with the skills half: a
|
|
418
|
+
// collapse that silently stops applying looks exactly like a plan that
|
|
419
|
+
// never had anything to collapse.
|
|
377
420
|
//
|
|
378
421
|
// Collapsed, NOT hidden: the count is printed. A capped list that does not
|
|
379
422
|
// say it was capped reads as "these are all of them", which is the failure
|
|
@@ -388,7 +431,7 @@ export function formatPlan(plan) {
|
|
|
388
431
|
}
|
|
389
432
|
if (collapsed > 0) {
|
|
390
433
|
lines.push(
|
|
391
|
-
` i ${collapsed} UDS-managed
|
|
434
|
+
` i ${collapsed} UDS-managed item${collapsed === 1 ? '' : 's'} reinstalled unconditionally — no content comparison is implemented for them (XSPEC-382 R1 covers skills; commands do not have one yet)`
|
|
392
435
|
);
|
|
393
436
|
}
|
|
394
437
|
lines.push('');
|
package/src/reconciler/index.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* const rollbackResult = rollbackLast(projectPath);
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { readManifest, needsMigration } from '../core/manifest.js';
|
|
24
|
+
import { readManifest, writeManifest, needsMigration } from '../core/manifest.js';
|
|
25
25
|
import { migrateAndBackfill } from './manifest-migrator.js';
|
|
26
26
|
import { calculateDesiredState } from './desired-state-calculator.js';
|
|
27
27
|
import { scanActualState, legacyDiscovery } from './actual-state-scanner.js';
|
|
@@ -45,6 +45,58 @@ import { rollback } from './backup-manager.js';
|
|
|
45
45
|
* errors: string[]
|
|
46
46
|
* }>}
|
|
47
47
|
*/
|
|
48
|
+
/**
|
|
49
|
+
* Bring `manifest.fileHashes` in line with files proved byte-identical to what
|
|
50
|
+
* UDS ships (XSPEC-382 R6).
|
|
51
|
+
*
|
|
52
|
+
* Returns the manifest plus a count of entries corrected, or null when nothing
|
|
53
|
+
* needed correcting — so the caller can skip a pointless write.
|
|
54
|
+
*
|
|
55
|
+
* Deliberately narrow. It only ever writes a hash the reconciler computed from
|
|
56
|
+
* disk AFTER proving that disk matches the desired upstream content, so it
|
|
57
|
+
* cannot absorb a hand edit. Widening it to "sync the record to disk" would
|
|
58
|
+
* make `uds check` incapable of ever reporting a modified standard again, which
|
|
59
|
+
* is the one thing that check exists to do.
|
|
60
|
+
*/
|
|
61
|
+
function reconcileFileHashes(manifest, verifiedPristine) {
|
|
62
|
+
const current = manifest.fileHashes || {};
|
|
63
|
+
|
|
64
|
+
// Drop entries 6.7.3 wrote here that do not belong: skill directories and
|
|
65
|
+
// command files are tracked in `skillHashes` / `commandHashes`, and
|
|
66
|
+
// `uds check` validates everything in `fileHashes` with an `isFile()` test,
|
|
67
|
+
// so those entries surfaced as phantom "missing" reports — 52 of them per
|
|
68
|
+
// adopter repo. Fixing the writer does not remove what it already wrote, and
|
|
69
|
+
// there is no other path that would: a bad key is never revisited because
|
|
70
|
+
// nothing on disk corresponds to it. (XSPEC-382 R7)
|
|
71
|
+
const stale = Object.keys(current).filter(
|
|
72
|
+
(k) => k.startsWith('.claude/skills/') || k.startsWith('.claude/commands/')
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (!verifiedPristine?.length && stale.length === 0) return null;
|
|
76
|
+
|
|
77
|
+
const kept = Object.fromEntries(Object.entries(current).filter(([k]) => !stale.includes(k)));
|
|
78
|
+
const corrected = {};
|
|
79
|
+
let count = stale.length;
|
|
80
|
+
|
|
81
|
+
for (const entry of verifiedPristine || []) {
|
|
82
|
+
const key = entry.path.replace(/\\/g, '/');
|
|
83
|
+
const recorded = kept[key];
|
|
84
|
+
if (recorded && recorded.hash === entry.hash && recorded.size === entry.size) continue;
|
|
85
|
+
corrected[key] = {
|
|
86
|
+
hash: entry.hash,
|
|
87
|
+
size: entry.size,
|
|
88
|
+
installedAt: new Date().toISOString()
|
|
89
|
+
};
|
|
90
|
+
count++;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (count === 0) return null;
|
|
94
|
+
return {
|
|
95
|
+
manifest: { ...manifest, fileHashes: { ...kept, ...corrected } },
|
|
96
|
+
count
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
48
100
|
export async function reconcile(projectPath, options = {}) {
|
|
49
101
|
const { force = false, backup = true, onAction } = options;
|
|
50
102
|
const errors = [];
|
|
@@ -71,18 +123,31 @@ export async function reconcile(projectPath, options = {}) {
|
|
|
71
123
|
// Step 4: Compute diff
|
|
72
124
|
const reconciliationPlan = computeDiff(desired, actual, { force });
|
|
73
125
|
|
|
126
|
+
// Step 4b: correct any stale recorded hashes for files already pristine.
|
|
127
|
+
//
|
|
128
|
+
// This runs BEFORE the empty-plan early return on purpose. A manifest whose
|
|
129
|
+
// only problem is a stale hash produces zero actions, and the old early
|
|
130
|
+
// return meant it was never written — so `uds check` reported a byte-perfect
|
|
131
|
+
// file as modified with no path back through normal use. (XSPEC-382 R6)
|
|
132
|
+
// `manifest` above is destructured const, so carry the corrected copy in its
|
|
133
|
+
// own binding rather than reassigning it.
|
|
134
|
+
const hashFix = reconcileFileHashes(manifest, reconciliationPlan.verifiedPristine);
|
|
135
|
+
const effectiveManifest = hashFix ? hashFix.manifest : manifest;
|
|
136
|
+
if (hashFix) writeManifest(effectiveManifest, projectPath);
|
|
137
|
+
|
|
74
138
|
// Step 5: Execute plan
|
|
75
139
|
if (reconciliationPlan.actions.length === 0) {
|
|
76
140
|
return {
|
|
77
141
|
success: true,
|
|
78
142
|
plan: reconciliationPlan,
|
|
79
143
|
execution: null,
|
|
80
|
-
manifest,
|
|
144
|
+
manifest: effectiveManifest,
|
|
145
|
+
hashesCorrected: hashFix?.count ?? 0,
|
|
81
146
|
errors
|
|
82
147
|
};
|
|
83
148
|
}
|
|
84
149
|
|
|
85
|
-
const execution = await executePlan(projectPath, reconciliationPlan,
|
|
150
|
+
const execution = await executePlan(projectPath, reconciliationPlan, effectiveManifest, {
|
|
86
151
|
backup,
|
|
87
152
|
onAction
|
|
88
153
|
});
|
|
@@ -92,6 +157,7 @@ export async function reconcile(projectPath, options = {}) {
|
|
|
92
157
|
plan: reconciliationPlan,
|
|
93
158
|
execution,
|
|
94
159
|
manifest: execution.updatedManifest,
|
|
160
|
+
hashesCorrected: hashFix?.count ?? 0,
|
|
95
161
|
errors: [
|
|
96
162
|
...errors,
|
|
97
163
|
...execution.results.filter(r => !r.success).map(r => r.error || 'Unknown error')
|
|
@@ -71,8 +71,19 @@ export async function executePlan(projectPath, plan, manifest, options = {}) {
|
|
|
71
71
|
// Create backup
|
|
72
72
|
if (backup && !dryRun) {
|
|
73
73
|
const backupResult = createBackup(projectPath, plan);
|
|
74
|
-
if
|
|
75
|
-
|
|
74
|
+
// Abort if ANY planned path could not be backed up — not only if every one
|
|
75
|
+
// failed.
|
|
76
|
+
//
|
|
77
|
+
// The old condition required `backedUp.length === 0`, so a run that backed
|
|
78
|
+
// up 74 paths and failed on 55 proceeded silently and overwrote all 55 with
|
|
79
|
+
// no rollback point. One success was enough to hide any number of failures:
|
|
80
|
+
// the same aggregate-masks-partial-failure shape this repo keeps finding.
|
|
81
|
+
// Those 55 were the skill directories, which failed on every platform
|
|
82
|
+
// because the backup called `copyFileSync` on a directory. With that fixed
|
|
83
|
+
// this branch should be rare — and when it does fire, refusing to overwrite
|
|
84
|
+
// a file we could not copy first is the whole point of taking a backup.
|
|
85
|
+
// (XSPEC-382 R6)
|
|
86
|
+
if (backupResult.errors.length > 0) {
|
|
76
87
|
return {
|
|
77
88
|
success: false,
|
|
78
89
|
backupId: null,
|
package/src/utils/registry.js
CHANGED
|
@@ -273,6 +273,37 @@ export function resolveSelectedOptionSources(manifestOptions, format = 'ai') {
|
|
|
273
273
|
* @param {string} format - Content format: 'ai' or 'human'
|
|
274
274
|
* @returns {string|null} Basename as installed, or null if unresolvable
|
|
275
275
|
*/
|
|
276
|
+
/**
|
|
277
|
+
* Resolve a manifest `standards` entry to the source path it was installed from.
|
|
278
|
+
*
|
|
279
|
+
* Manifests have stored IDs (`commit-message`) rather than paths since 3.4.0,
|
|
280
|
+
* and four places in the CLI needed to turn one back into a path. Three of them
|
|
281
|
+
* grew their own copy of the lookup; the fourth — the one `uds check --restore`
|
|
282
|
+
* depends on — never did, and matched `entry.endsWith(fileName)` instead. That
|
|
283
|
+
* cannot match an ID, so restore failed for **64 of 72** tracked standards with
|
|
284
|
+
* "Could not determine source". Only the eight `options/` entries worked,
|
|
285
|
+
* because those are still stored as paths. (XSPEC-382 R6)
|
|
286
|
+
*
|
|
287
|
+
* Paired with `resolveStandardFilename` on purpose: same input handling, same
|
|
288
|
+
* registry lookup, one returns the path and the other its basename. Two
|
|
289
|
+
* separately-written resolvers would answer differently the first time an entry
|
|
290
|
+
* form changed, and neither would be obviously wrong.
|
|
291
|
+
*
|
|
292
|
+
* @param {string} entry - Manifest standards entry: an ID or a legacy path
|
|
293
|
+
* @param {string} format - 'ai' or 'human'
|
|
294
|
+
* @returns {string|null} Source path, or null when unresolvable
|
|
295
|
+
*/
|
|
296
|
+
export function resolveStandardSourcePath(entry, format = 'ai') {
|
|
297
|
+
if (typeof entry !== 'string' || entry.length === 0) return null;
|
|
298
|
+
|
|
299
|
+
// A path (option files, and legacy pre-3.4.0 manifests) already names itself.
|
|
300
|
+
if (entry.includes('/') || entry.includes('.')) return entry;
|
|
301
|
+
|
|
302
|
+
const found = getAllStandards().find((s) => s.id === entry);
|
|
303
|
+
if (!found) return null;
|
|
304
|
+
return getStandardSource(found, format) || null;
|
|
305
|
+
}
|
|
306
|
+
|
|
276
307
|
export function resolveStandardFilename(entry, format = 'ai') {
|
|
277
308
|
if (typeof entry !== 'string' || entry.length === 0) return null;
|
|
278
309
|
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { mkdirSync, writeFileSync, existsSync, readFileSync, readdirSync, copyFileSync, statSync, rmSync, unlinkSync } from 'fs';
|
|
11
11
|
import { dirname, join, basename } from 'path';
|
|
12
|
+
import { createHash } from 'crypto';
|
|
12
13
|
import { fileURLToPath } from 'url';
|
|
13
14
|
import {
|
|
14
15
|
getAgentConfig,
|
|
@@ -399,33 +400,161 @@ function mergeSkillFrontmatter(enSourceDir, targetDir) {
|
|
|
399
400
|
* @param {string} locale - Locale for skill content (default: 'en')
|
|
400
401
|
* @returns {{success: boolean, skillName: string, path?: string, error?: string, fallbackToEn?: boolean}} Result
|
|
401
402
|
*/
|
|
402
|
-
|
|
403
|
+
/**
|
|
404
|
+
* Resolve what installing a skill would put on disk, without writing anything.
|
|
405
|
+
*
|
|
406
|
+
* This is THE definition of a skill's installed content, and it exists as one
|
|
407
|
+
* function because both the installer and the reconciler need that answer.
|
|
408
|
+
* XSPEC-382 R1 originally proposed hashing the source directory on both sides
|
|
409
|
+
* and letting the existing comparison work. Measurement killed that: installing
|
|
410
|
+
* is not a verbatim copy. Three things sit in between, any one of them fatal to
|
|
411
|
+
* a source-directory hash —
|
|
412
|
+
*
|
|
413
|
+
* 1. a localized SKILL.md gets English frontmatter fields merged in, so the
|
|
414
|
+
* installed file matches no source file (measured: brainstorm-assistant is
|
|
415
|
+
* 23,866 bytes installed against a 23,753-byte zh-TW source);
|
|
416
|
+
* 2. the source directory is chosen at runtime by locale with fallback to
|
|
417
|
+
* English per skill, so there is no single path to hash;
|
|
418
|
+
* 3. subdirectories are skipped, making the install a strict subset.
|
|
419
|
+
*
|
|
420
|
+
* The alternative was a second copy of this logic inside the planner. That is
|
|
421
|
+
* the arrangement this file has already been bitten by twice; two copies answer
|
|
422
|
+
* differently the first time one changes, and a planner that disagrees with the
|
|
423
|
+
* installer reports every skill as changed on every upgrade — worse than the
|
|
424
|
+
* no-signal it replaced, because a false signal is indistinguishable from a
|
|
425
|
+
* real one.
|
|
426
|
+
*
|
|
427
|
+
* All 514 installable files are UTF-8 text (508 .md, 5 .yaml, 1 .json), checked
|
|
428
|
+
* by decoding every one of them, so reading content as a string is lossless.
|
|
429
|
+
*
|
|
430
|
+
* @param {string} skillName
|
|
431
|
+
* @param {string} locale
|
|
432
|
+
* @returns {{files: Array<{name: string, content: string}>, fallbackToEn: boolean, error: string|null}}
|
|
433
|
+
*/
|
|
434
|
+
export function resolveSkillFiles(skillName, locale = 'en') {
|
|
403
435
|
const enSourceDir = join(SKILLS_LOCAL_DIR, skillName);
|
|
404
|
-
const targetDir = join(targetBaseDir, skillName);
|
|
405
436
|
|
|
406
|
-
// Determine the actual source directory based on locale
|
|
407
437
|
let sourceDir = enSourceDir;
|
|
408
438
|
let needsFrontmatterMerge = false;
|
|
409
439
|
let fallbackToEn = false;
|
|
410
440
|
|
|
411
441
|
if (isLocalizedLocale(locale)) {
|
|
412
|
-
const
|
|
413
|
-
const localizedSkillDir = join(localizedDir, skillName);
|
|
442
|
+
const localizedSkillDir = join(getLocalizedSkillsSourceDir(locale), skillName);
|
|
414
443
|
if (existsSync(localizedSkillDir)) {
|
|
415
444
|
sourceDir = localizedSkillDir;
|
|
416
445
|
needsFrontmatterMerge = true;
|
|
417
446
|
} else {
|
|
418
|
-
// Locale requested but missing for this skill — fall back to English source.
|
|
419
|
-
// Flag the result so the caller can aggregate a WARN.
|
|
420
447
|
fallbackToEn = true;
|
|
421
448
|
}
|
|
422
449
|
}
|
|
423
450
|
|
|
424
451
|
if (!existsSync(sourceDir)) {
|
|
452
|
+
return { files: [], fallbackToEn, error: `Skill not found: ${skillName}` };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Per-FILE fallback to English, not per-skill.
|
|
456
|
+
//
|
|
457
|
+
// A locale pack that has SKILL.md but not its companion files used to install
|
|
458
|
+
// just the SKILL.md, leaving the companions uninstalled entirely. Measured:
|
|
459
|
+
// 4 of 59 localized skills in zh-TW and 5 of 59 in zh-CN are short of the
|
|
460
|
+
// English source, and two of those gaps are REFERENCED — the zh-TW
|
|
461
|
+
// `dev-workflow-guide/SKILL.md` points at `workflow-phases.md` three times
|
|
462
|
+
// and `testing-guide/SKILL.md` at `test-skeleton-templates.md` three times,
|
|
463
|
+
// neither of which the locale pack ships. Those installs shipped a document
|
|
464
|
+
// pointing at files that were never going to be there.
|
|
465
|
+
//
|
|
466
|
+
// Taking the union — locale file where it exists, English where it does not —
|
|
467
|
+
// is also what makes the content comparison usable: without it those skills
|
|
468
|
+
// would report as changed on every upgrade forever. (XSPEC-382 R7)
|
|
469
|
+
const names = new Map(); // fileName -> absolute source path
|
|
470
|
+
const collect = (dir) => {
|
|
471
|
+
if (!existsSync(dir)) return;
|
|
472
|
+
for (const fileName of readdirSync(dir)) {
|
|
473
|
+
const sourcePath = join(dir, fileName);
|
|
474
|
+
// Subdirectories are skipped — matching what the installer has always done.
|
|
475
|
+
if (statSync(sourcePath).isDirectory()) continue;
|
|
476
|
+
if (!names.has(fileName)) names.set(fileName, sourcePath);
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
collect(sourceDir); // locale first, so it wins
|
|
480
|
+
if (sourceDir !== enSourceDir) collect(enSourceDir);
|
|
481
|
+
|
|
482
|
+
const files = [];
|
|
483
|
+
for (const [name, sourcePath] of names) {
|
|
484
|
+
files.push({ name, content: readFileSync(sourcePath, 'utf-8') });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (needsFrontmatterMerge) {
|
|
488
|
+
const skill = files.find((f) => f.name === 'SKILL.md');
|
|
489
|
+
if (skill) {
|
|
490
|
+
const merged = mergeFrontmatterContent(enSourceDir, skill.content);
|
|
491
|
+
if (merged !== null) skill.content = merged;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Sorted so a hash over this list depends on content, not on directory order.
|
|
496
|
+
files.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
497
|
+
return { files, fallbackToEn, error: null };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Content hash of a resolved skill, over names and contents.
|
|
502
|
+
*
|
|
503
|
+
* Names are included: two skills with the same bodies under different filenames
|
|
504
|
+
* are different installs, and a hash over contents alone would call a rename
|
|
505
|
+
* "unchanged".
|
|
506
|
+
*
|
|
507
|
+
* @param {Array<{name: string, content: string}>} files
|
|
508
|
+
* @returns {string|null} `sha256:<hex>`, or null for an empty resolution
|
|
509
|
+
*/
|
|
510
|
+
export function computeSkillContentHash(files) {
|
|
511
|
+
if (!files?.length) return null;
|
|
512
|
+
const h = createHash('sha256');
|
|
513
|
+
for (const f of files) {
|
|
514
|
+
h.update(f.name);
|
|
515
|
+
h.update('\0');
|
|
516
|
+
h.update(f.content);
|
|
517
|
+
h.update('\0');
|
|
518
|
+
}
|
|
519
|
+
return `sha256:${h.digest('hex')}`;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* The in-memory half of `mergeSkillFrontmatter`, so resolution and installation
|
|
524
|
+
* agree by construction rather than by both being kept up to date.
|
|
525
|
+
*
|
|
526
|
+
* @returns {string|null} merged content, or null when there is nothing to merge
|
|
527
|
+
*/
|
|
528
|
+
function mergeFrontmatterContent(enSourceDir, targetContent) {
|
|
529
|
+
const enSkillPath = join(enSourceDir, 'SKILL.md');
|
|
530
|
+
if (!existsSync(enSkillPath)) return null;
|
|
531
|
+
|
|
532
|
+
const enParsed = parseFrontmatter(readFileSync(enSkillPath, 'utf-8'));
|
|
533
|
+
if (!enParsed) return null;
|
|
534
|
+
|
|
535
|
+
const REQUIRED_FIELDS = ['name', 'allowed-tools', 'scope', 'argument-hint', 'disable-model-invocation'];
|
|
536
|
+
const fieldsToMerge = {};
|
|
537
|
+
for (const field of REQUIRED_FIELDS) {
|
|
538
|
+
if (enParsed.frontmatter[field] !== undefined) {
|
|
539
|
+
fieldsToMerge[field] = enParsed.frontmatter[field];
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (Object.keys(fieldsToMerge).length === 0) return null;
|
|
543
|
+
|
|
544
|
+
return rebuildWithFrontmatter(targetContent, fieldsToMerge);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function installSingleSkill(skillName, targetBaseDir, locale = 'en') {
|
|
548
|
+
const targetDir = join(targetBaseDir, skillName);
|
|
549
|
+
|
|
550
|
+
const resolved = resolveSkillFiles(skillName, locale);
|
|
551
|
+
const { fallbackToEn } = resolved;
|
|
552
|
+
|
|
553
|
+
if (resolved.error) {
|
|
425
554
|
return {
|
|
426
555
|
success: false,
|
|
427
556
|
skillName,
|
|
428
|
-
error:
|
|
557
|
+
error: resolved.error
|
|
429
558
|
};
|
|
430
559
|
}
|
|
431
560
|
|
|
@@ -435,23 +564,39 @@ function installSingleSkill(skillName, targetBaseDir, locale = 'en') {
|
|
|
435
564
|
}
|
|
436
565
|
|
|
437
566
|
try {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
// Skip directories for now (could be extended to handle subdirs)
|
|
444
|
-
if (statSync(sourcePath).isDirectory()) continue;
|
|
445
|
-
|
|
446
|
-
copyFileSync(sourcePath, targetPath);
|
|
567
|
+
// Write what `resolveSkillFiles` says this install is. The planner asks the
|
|
568
|
+
// same function what it WOULD be, so the two cannot disagree — which is the
|
|
569
|
+
// whole point of routing installation through it. (XSPEC-382 R1)
|
|
570
|
+
for (const file of resolved.files) {
|
|
571
|
+
writeFileSync(join(targetDir, file.name), file.content, 'utf-8');
|
|
447
572
|
}
|
|
448
573
|
|
|
449
|
-
//
|
|
450
|
-
|
|
451
|
-
|
|
574
|
+
// Remove top-level files UDS does not ship for this skill.
|
|
575
|
+
//
|
|
576
|
+
// Without this, a file UDS once shipped (or that arrived some other way)
|
|
577
|
+
// stays forever, and the content comparison reports the skill as changed on
|
|
578
|
+
// every upgrade with no way to make it stop — the same permanently-true
|
|
579
|
+
// false positive that made `uds check` useless before R6. Found in vibeops:
|
|
580
|
+
// `deploy-assistant/guide.md`, which is in no UDS skills tree and never was.
|
|
581
|
+
//
|
|
582
|
+
// Scoped to files directly inside a directory whose name UDS itself ships,
|
|
583
|
+
// which is the same provenance test that keeps an adopter's OWN skill
|
|
584
|
+
// directories out of every code path here. Subdirectories are left alone —
|
|
585
|
+
// the installer has never written them, so it has no claim on them.
|
|
586
|
+
// (XSPEC-382 R7)
|
|
587
|
+
const shipped = new Set(resolved.files.map((f) => f.name));
|
|
588
|
+
const pruned = [];
|
|
589
|
+
for (const entry of readdirSync(targetDir, { withFileTypes: true })) {
|
|
590
|
+
if (!entry.isFile() || shipped.has(entry.name)) continue;
|
|
591
|
+
try {
|
|
592
|
+
unlinkSync(join(targetDir, entry.name));
|
|
593
|
+
pruned.push(entry.name);
|
|
594
|
+
} catch {
|
|
595
|
+
// Leave it; a stale file is a smaller problem than a failed install.
|
|
596
|
+
}
|
|
452
597
|
}
|
|
453
598
|
|
|
454
|
-
return { success: true, skillName, path: targetDir, fallbackToEn };
|
|
599
|
+
return { success: true, skillName, path: targetDir, fallbackToEn, pruned };
|
|
455
600
|
} catch (error) {
|
|
456
601
|
return {
|
|
457
602
|
success: false,
|
|
@@ -587,44 +732,77 @@ export async function installCommandsForAgent(agent, level = 'project', commandN
|
|
|
587
732
|
* @param {string} locale - Locale for command content (default: 'en')
|
|
588
733
|
* @returns {Object} Result
|
|
589
734
|
*/
|
|
590
|
-
|
|
735
|
+
/**
|
|
736
|
+
* Resolve what installing a command would put on disk, without writing anything.
|
|
737
|
+
*
|
|
738
|
+
* The command-side twin of `resolveSkillFiles`, and for the same reason: what
|
|
739
|
+
* gets installed is not what is in the source tree. The source is chosen at
|
|
740
|
+
* runtime by locale with fallback to English, and `transformCommandForAgent`
|
|
741
|
+
* rewrites the content per agent. Hashing the source file would report every
|
|
742
|
+
* command as changed on every upgrade for any agent that transforms.
|
|
743
|
+
*
|
|
744
|
+
* XSPEC-382 R1 covered skills and left commands on `hash: null`, which kept the
|
|
745
|
+
* unconditional-reinstall branch alive for them. R7 closes it the same way —
|
|
746
|
+
* one function the installer writes from and the planner hashes, rather than a
|
|
747
|
+
* second copy of the resolution in the planner.
|
|
748
|
+
*
|
|
749
|
+
* @param {string} cmdName
|
|
750
|
+
* @param {string} agent
|
|
751
|
+
* @param {string} locale
|
|
752
|
+
* @returns {{content: string|null, error: string|null}}
|
|
753
|
+
*/
|
|
754
|
+
export function resolveCommandContent(cmdName, agent, locale = 'en') {
|
|
591
755
|
let sourcePath = join(COMMANDS_LOCAL_DIR, `${cmdName}.md`);
|
|
592
756
|
|
|
593
|
-
// Check for localized command file
|
|
594
757
|
if (isLocalizedLocale(locale)) {
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
if (existsSync(localizedPath)) {
|
|
598
|
-
sourcePath = localizedPath;
|
|
599
|
-
}
|
|
758
|
+
const localizedPath = join(getLocalizedCommandsSourceDir(locale), `${cmdName}.md`);
|
|
759
|
+
if (existsSync(localizedPath)) sourcePath = localizedPath;
|
|
600
760
|
// else: fall back to English source
|
|
601
761
|
}
|
|
602
762
|
|
|
603
|
-
const targetExt = getCommandFileExtension(agent);
|
|
604
|
-
const targetPath = join(targetDir, `${cmdName}${targetExt}`);
|
|
605
|
-
|
|
606
763
|
if (!existsSync(sourcePath)) {
|
|
764
|
+
return { content: null, error: `Command not found: ${cmdName}` };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
try {
|
|
607
768
|
return {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
error: `Command not found: ${cmdName}`
|
|
769
|
+
content: transformCommandForAgent(readFileSync(sourcePath, 'utf-8'), cmdName, agent),
|
|
770
|
+
error: null
|
|
611
771
|
};
|
|
772
|
+
} catch (error) {
|
|
773
|
+
return { content: null, error: error.message };
|
|
612
774
|
}
|
|
775
|
+
}
|
|
613
776
|
|
|
614
|
-
|
|
615
|
-
|
|
777
|
+
/**
|
|
778
|
+
* Content hash of a resolved command.
|
|
779
|
+
*
|
|
780
|
+
* Same `sha256:` shape as `computeSkillContentHash` so the two sides of the
|
|
781
|
+
* diff can be compared without caring which category an entry came from.
|
|
782
|
+
*
|
|
783
|
+
* @param {string|null} content
|
|
784
|
+
* @returns {string|null}
|
|
785
|
+
*/
|
|
786
|
+
export function computeCommandContentHash(content) {
|
|
787
|
+
if (content === null || content === undefined) return null;
|
|
788
|
+
return `sha256:${createHash('sha256').update(content).digest('hex')}`;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function installSingleCommand(cmdName, targetDir, agent, locale = 'en') {
|
|
792
|
+
const targetPath = join(targetDir, `${cmdName}${getCommandFileExtension(agent)}`);
|
|
616
793
|
|
|
617
|
-
|
|
618
|
-
|
|
794
|
+
// Write what `resolveCommandContent` says this install is; the planner asks
|
|
795
|
+
// the same function what it WOULD be. (XSPEC-382 R7)
|
|
796
|
+
const resolved = resolveCommandContent(cmdName, agent, locale);
|
|
797
|
+
if (resolved.error) {
|
|
798
|
+
return { success: false, command: cmdName, error: resolved.error };
|
|
799
|
+
}
|
|
619
800
|
|
|
620
|
-
|
|
801
|
+
try {
|
|
802
|
+
writeFileSync(targetPath, resolved.content);
|
|
621
803
|
return { success: true, command: cmdName, path: targetPath };
|
|
622
804
|
} catch (error) {
|
|
623
|
-
return {
|
|
624
|
-
success: false,
|
|
625
|
-
command: cmdName,
|
|
626
|
-
error: error.message
|
|
627
|
-
};
|
|
805
|
+
return { success: false, command: cmdName, error: error.message };
|
|
628
806
|
}
|
|
629
807
|
}
|
|
630
808
|
|
package/standards-registry.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"version": "6.7.
|
|
3
|
+
"version": "6.7.4",
|
|
4
4
|
"lastUpdated": "2026-05-13",
|
|
5
5
|
"description": "Standards registry for universal-dev-standards with integrated skills and AI-optimized formats",
|
|
6
6
|
"formats": {
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
"standards": {
|
|
59
59
|
"name": "universal-dev-standards",
|
|
60
60
|
"url": "https://github.com/AsiaOstrich/universal-dev-standards",
|
|
61
|
-
"version": "6.7.
|
|
61
|
+
"version": "6.7.4"
|
|
62
62
|
},
|
|
63
63
|
"skills": {
|
|
64
64
|
"name": "universal-dev-standards",
|
|
65
65
|
"url": "https://github.com/AsiaOstrich/universal-dev-standards",
|
|
66
66
|
"localPath": "skills",
|
|
67
67
|
"rawUrl": "https://raw.githubusercontent.com/AsiaOstrich/universal-dev-standards/main/skills",
|
|
68
|
-
"version": "6.7.
|
|
68
|
+
"version": "6.7.4",
|
|
69
69
|
"note": "Skills are now included in the main repository under skills/"
|
|
70
70
|
}
|
|
71
71
|
},
|
|
@@ -2260,7 +2260,7 @@
|
|
|
2260
2260
|
"id": "license-compliance",
|
|
2261
2261
|
"name": "License Compliance Standards",
|
|
2262
2262
|
"nameZh": "授權合規標準",
|
|
2263
|
-
"version": "6.7.
|
|
2263
|
+
"version": "6.7.4",
|
|
2264
2264
|
"source": {
|
|
2265
2265
|
"human": "core/license-compliance.md",
|
|
2266
2266
|
"ai": "ai/standards/license-compliance.ai.yaml"
|
|
@@ -2272,7 +2272,7 @@
|
|
|
2272
2272
|
"id": "verification-oracle",
|
|
2273
2273
|
"name": "Verification Oracle Standards",
|
|
2274
2274
|
"nameZh": "驗證 Oracle 標準",
|
|
2275
|
-
"version": "6.7.
|
|
2275
|
+
"version": "6.7.4",
|
|
2276
2276
|
"source": {
|
|
2277
2277
|
"human": "core/verification-oracle.md",
|
|
2278
2278
|
"ai": "ai/standards/verification-oracle.ai.yaml"
|
|
@@ -2284,7 +2284,7 @@
|
|
|
2284
2284
|
"id": "model-provenance",
|
|
2285
2285
|
"name": "Model Provenance Policy Standards",
|
|
2286
2286
|
"nameZh": "模型來源政策標準",
|
|
2287
|
-
"version": "6.7.
|
|
2287
|
+
"version": "6.7.4",
|
|
2288
2288
|
"source": {
|
|
2289
2289
|
"human": "core/model-provenance.md",
|
|
2290
2290
|
"ai": "ai/standards/model-provenance.ai.yaml"
|
|
@@ -2296,7 +2296,7 @@
|
|
|
2296
2296
|
"id": "resource-cost-boundary",
|
|
2297
2297
|
"name": "Resource / Cost Boundary Declaration Standards",
|
|
2298
2298
|
"nameZh": "資源/成本邊界宣告標準",
|
|
2299
|
-
"version": "6.7.
|
|
2299
|
+
"version": "6.7.4",
|
|
2300
2300
|
"source": {
|
|
2301
2301
|
"human": "core/resource-cost-boundary.md",
|
|
2302
2302
|
"ai": "ai/standards/resource-cost-boundary.ai.yaml"
|