universal-dev-standards 6.2.7 → 6.3.1
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/bin/uds.js +9 -0
- package/bundled/ai/standards/supply-chain-security-standards.ai.yaml +10 -2
- package/bundled/core/supply-chain-security-standards.md +48 -4
- package/bundled/locales/zh-CN/CHANGELOG.md +33 -3
- package/bundled/locales/zh-CN/README.md +1 -1
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/core/supply-chain-security-standards.md +35 -1
- package/bundled/locales/zh-TW/CHANGELOG.md +33 -3
- package/bundled/locales/zh-TW/README.md +1 -1
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/supply-chain-security-standards.md +35 -1
- package/package.json +3 -2
- package/src/commands/deps.js +112 -0
- package/src/utils/dependency-resolution.js +248 -0
- package/src/utils/github.js +26 -7
- package/standards-registry.json +7 -7
package/bin/uds.js
CHANGED
|
@@ -15,6 +15,7 @@ import { skillsCommand } from '../src/commands/skills.js';
|
|
|
15
15
|
import { agentListCommand, agentInstallCommand, agentInfoCommand } from '../src/commands/agent.js';
|
|
16
16
|
import { aiContextInitCommand, aiContextValidateCommand, aiContextGraphCommand } from '../src/commands/ai-context.js';
|
|
17
17
|
import { auditCommand } from '../src/commands/audit.js';
|
|
18
|
+
import { depsCommand } from '../src/commands/deps.js';
|
|
18
19
|
import { uninstallCommand } from '../src/commands/uninstall.js';
|
|
19
20
|
import { specCreateCommand, specListCommand, specShowCommand, specConfirmCommand, specArchiveCommand, specDeleteCommand, specSearchCommand } from '../src/commands/spec.js';
|
|
20
21
|
import { quickstartCommand } from '../src/commands/quickstart.js';
|
|
@@ -244,6 +245,14 @@ program
|
|
|
244
245
|
.option('--threshold <n>', 'Score threshold for CI mode (default: 75)', '75')
|
|
245
246
|
.action(auditCommand);
|
|
246
247
|
|
|
248
|
+
program
|
|
249
|
+
.command('deps')
|
|
250
|
+
.description('Compare what you test against what your users install (published packages ship no lockfile)')
|
|
251
|
+
.option('--path <dir>', 'Directory containing package.json (default: cwd)')
|
|
252
|
+
.option('--json', 'Output raw JSON')
|
|
253
|
+
.option('--concurrency <n>', 'Parallel registry lookups (default: 8)')
|
|
254
|
+
.action(depsCommand);
|
|
255
|
+
|
|
247
256
|
program
|
|
248
257
|
.command('uninstall')
|
|
249
258
|
.description('Remove UDS standards, integrations, skills, and hooks')
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
id: supply-chain-security-standards
|
|
2
2
|
meta:
|
|
3
|
-
version: "1.
|
|
4
|
-
updated: "2026-04
|
|
3
|
+
version: "1.1.0"
|
|
4
|
+
updated: "2026-08-04"
|
|
5
5
|
source: core/supply-chain-security-standards.md
|
|
6
|
+
# NOTE: this file is a STUB. It carries no machine-readable rules, so an
|
|
7
|
+
# agent reading the ai/ layer gets nothing for this standard — the same is
|
|
8
|
+
# true of design-document-standards, estimation-standards and
|
|
9
|
+
# privacy-standards (4 of 141 as of 2026-08-04). The version above tracks
|
|
10
|
+
# the .md it points at; it does not mean the .md's rules are represented
|
|
11
|
+
# here. XSPEC-366 R4 rewrote that .md's Lock Strategy section and
|
|
12
|
+
# deliberately did not add a single rule here: one rule in an otherwise
|
|
13
|
+
# empty file would make coverage look better than it is.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Supply Chain Security Standards
|
|
2
2
|
|
|
3
|
-
> **Language**: English | [繁體中文](../locales/zh-TW/core/supply-chain-security-standards.md)
|
|
3
|
+
> **Language**: English | [繁體中文](../locales/zh-TW/core/supply-chain-security-standards.md) | [简体中文](../locales/zh-CN/core/supply-chain-security-standards.md)
|
|
4
4
|
|
|
5
|
-
**Version**: 1.
|
|
6
|
-
**Last Updated**: 2026-04
|
|
5
|
+
**Version**: 1.1.0
|
|
6
|
+
**Last Updated**: 2026-08-04
|
|
7
7
|
**Applicability**: All software projects with external dependencies
|
|
8
8
|
**Scope**: universal
|
|
9
9
|
**Industry Standards**: SLSA, SPDX, CycloneDX, OpenSSF Scorecard
|
|
@@ -115,7 +115,7 @@ L1 (All projects) → L2 (CI/CD projects) → L3 (Security-critical) → L4 (Hig
|
|
|
115
115
|
| **Patch** (x.y.Z) | Auto-merge after CI passes | Fully automated (Dependabot/Renovate) |
|
|
116
116
|
| **Minor** (x.Y.0) | Auto-create PR, manual review | Semi-automated |
|
|
117
117
|
| **Major** (X.0.0) | Manual evaluation, migration plan | Manual with changelog review |
|
|
118
|
-
| **Lock Strategy** |
|
|
118
|
+
| **Lock Strategy** | Commit lock files — **and, if you publish, separately verify what consumers resolve to** (see below) | Lock file committed; consumer resolution checked at release |
|
|
119
119
|
|
|
120
120
|
### Update Rules
|
|
121
121
|
|
|
@@ -124,6 +124,50 @@ L1 (All projects) → L2 (CI/CD projects) → L3 (Security-critical) → L4 (Hig
|
|
|
124
124
|
- Security patches MUST be applied within 48 hours (Critical) or 7 days (High)
|
|
125
125
|
- Major version upgrades SHOULD be evaluated quarterly
|
|
126
126
|
|
|
127
|
+
### A lock file does not constrain your consumers
|
|
128
|
+
|
|
129
|
+
A committed lock file makes **your** builds reproducible. It does not reach
|
|
130
|
+
anyone who installs your package: a published npm tarball, a PyPI wheel, a
|
|
131
|
+
Ruby gem — none of them carry the lock file. Consumers resolve your declared
|
|
132
|
+
ranges themselves, at their install time, and get whatever satisfies them then.
|
|
133
|
+
|
|
134
|
+
**When those two differ, your test suite is green about a combination nobody
|
|
135
|
+
installs — and that green is indistinguishable from a real one.**
|
|
136
|
+
|
|
137
|
+
This is the failure mode, observed rather than hypothesised: a project declared
|
|
138
|
+
`"tree-sitter-c-sharp": "^0.23.1"`. Three published versions satisfied that
|
|
139
|
+
range and they did not share an API. npm resolves a caret to the newest match,
|
|
140
|
+
so every fresh install received the incompatible one, and **no C# file ever
|
|
141
|
+
parsed for anyone who installed from the registry** — while the lock file
|
|
142
|
+
pinned the working version and every test passed, for the entire life of that
|
|
143
|
+
feature. The project's own source comments even documented the incompatible
|
|
144
|
+
version; the caret quietly re-admitted it.
|
|
145
|
+
|
|
146
|
+
Projects that **publish a package** therefore MUST additionally:
|
|
147
|
+
|
|
148
|
+
- **Know what a consumer's install resolves to.** Compare, per runtime
|
|
149
|
+
dependency, the declared range, the locked version, and the version that
|
|
150
|
+
range resolves to today. Report only the differences — a table of agreeing
|
|
151
|
+
rows gets skimmed, and the ones that mattered go with it.
|
|
152
|
+
- **Declare native dependencies as exact versions.** Packages with a native
|
|
153
|
+
binding (an install-family script, or a dependency on node-gyp / prebuild /
|
|
154
|
+
node-addon-api / cmake-js and equivalents) MUST NOT be declared with a range.
|
|
155
|
+
semver makes no promise about native ABI compatibility, and this has been
|
|
156
|
+
broken inside a minor range in practice. Flag these **whether or not they are
|
|
157
|
+
currently drifting**: a range matching exactly one published version is safe
|
|
158
|
+
because upstream has not published again, not because anything guarantees it.
|
|
159
|
+
- **Run the test suite against consumer resolution before release.** Install
|
|
160
|
+
without the lock file and run the real suite against the result. This is the
|
|
161
|
+
only check in this section that does not need to be told in advance which
|
|
162
|
+
dependency will break.
|
|
163
|
+
- **Treat a lookup that fails as unknown, never as agreement.** A check whose
|
|
164
|
+
"everything is fine" and "I could not find out" look the same converts an
|
|
165
|
+
unknown into a reassurance.
|
|
166
|
+
|
|
167
|
+
> **Applies to published artifacts, not to applications.** A deployed service
|
|
168
|
+
> ships its lock file with it, so its build and its runtime see the same
|
|
169
|
+
> versions. This section is about packages other people install.
|
|
170
|
+
|
|
127
171
|
---
|
|
128
172
|
|
|
129
173
|
## CI/CD Integration
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
source: ../../CHANGELOG.md
|
|
3
|
-
source_version: 6.
|
|
4
|
-
translation_version: 6.
|
|
5
|
-
last_synced: 2026-
|
|
3
|
+
source_version: 6.3.1
|
|
4
|
+
translation_version: 6.3.1
|
|
5
|
+
last_synced: 2026-08-06
|
|
6
6
|
status: current
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -17,6 +17,36 @@ status: current
|
|
|
17
17
|
|
|
18
18
|
## [Unreleased]
|
|
19
19
|
|
|
20
|
+
## [6.3.1] - 2026-08-06
|
|
21
|
+
|
|
22
|
+
### 修复
|
|
23
|
+
|
|
24
|
+
- **6.3.0 从来没有到达 npm——而这一版的存在,正是因为它自己描述的那个失效。** 发版流程有跑,它的 clean-room job 在 `npm ci` 这一步失败,错误是 `EUSAGE … Missing: @emnapi/core@1.11.3 from lock file`,`Publish to npm` 被跳过。lock 文件是被 `npm install --save semver` 重新生成的,过程中掉了 `npm ci` 需要的传递依赖项;我在本机跑的检查接受了它,所以这个不一致要到发版 job 才显形——那时 tag 与 GitHub Release 都已经公开。改以最后一份 `npm ci` 确实能通过的 lock 文件为基础重建,只加进 semver 那一笔,并在整个 CI 矩阵上验证,而不是只在一台机器上。
|
|
25
|
+
- **`v6.3.0` 保留不删,其 release 说明已改为注明它从未发布。** 一个没有 npm 对应版本的 tag,正是 `uds deps` 被写出来要抓的那种不一致;删掉它移除的是证据,不是落差。**6.3.1 完整包含 6.3.0 的全部内容**,见下方。
|
|
26
|
+
|
|
27
|
+
## [6.3.0] - 2026-08-04
|
|
28
|
+
|
|
29
|
+
### 新增
|
|
30
|
+
|
|
31
|
+
- **`uds deps`——你测的东西,跟你用户装的是同一个吗?** 发布出去的软件包不带 lockfile:你的 CI 測的是 `package-lock.json` 鎖定的版本,你的用户拿到的是声明范围在他们安装当下解析出的版本。两者不同时,整套测试会对着一个没有人会安装的组合亮绿灯,而那个绿灯与真绿灯无从分辨。此命令逐一比对每个 runtime 依赖的三个数字,**只报告差异**并附上分母——一份大多一致的表格会被略过,而其中真正有问题的那几行也跟着被略过。
|
|
32
|
+
- **原生依赖适用更严格的规则。** 带原生绑定的软件包只要以范围声明就会被标出,**不论它今天是否正在漂移**。semver 对原生 ABI 兼容性没有任何承诺,而这在本生态已被在 minor 范围内打破过。一个只对应到单一已发布版本的范围,安全是因为上游还没再发布,不是因为有任何保障——等漂移,等于等到用户已经拿到为止。
|
|
33
|
+
- **查询失败绝不记为一致。** 它会成为 `unverifiable` 并使整次检查失败。一个「没问题」与「我查不到」长得一样的检查,会把未知转成安心。
|
|
34
|
+
- `--path`、`--json`、`--concurrency`。
|
|
35
|
+
|
|
36
|
+
### 变更
|
|
37
|
+
|
|
38
|
+
- **`supply-chain-security-standards` 1.0.0 → 1.1.0——Lock Strategy 条目是对的,但不完整。** 它写「使用 lock 文件,一律进版本控制」,读起来是完整的,因此照着做的人没有任何理由再往下查——而一份提交的 lock 文件约束的是**你自己的**构建,碰不到你任何一个用户。回头改写正文而非加但书,因为在一条未变动的规则下方补「但请注意……」,会让原文那一行继续误导只读那一行的人,而标准表格多半就是一行一行读的。新章节以产生它的那个案例陈述失效、对会发布软件包的项目给出四项要求,并明确限定于发布出去的产物——部署的服务会连同 lock 文件一起发布,不受影响。
|
|
39
|
+
|
|
40
|
+
### 备注
|
|
41
|
+
|
|
42
|
+
- 该标准的 `.ai.yaml` 仍是五行的壳、没有任何机器可读规则——**141 份中的四份之一**,另含 `design-document-standards`、`estimation-standards` 与 `privacy-standards`,因此读 `ai/` 层的 agent 对这四份得到的都是空的。本次发版刻意没有把新增的那一条规则加进去:一条规则躺在一个原本全空的文件里,会让覆盖率看起来比实际好。该缺口现已记在文件内部。
|
|
43
|
+
|
|
44
|
+
## [6.2.8] - 2026-07-31
|
|
45
|
+
|
|
46
|
+
### 修复
|
|
47
|
+
|
|
48
|
+
- **下载回来的标准,中文是坏的。** HTTPS 响应以 `data += chunk` 累积,而那会对每一个分块各自解码——于是任何字节跨在分块边界上的字符都变成替换字符(`日期` → `日�期`)。单字节的拉丁文不受影响;三字节的中日韩文字受影响。**过程中没有任何一步失败**:传输完成、文件写入、动作报告成功,损坏只有读文字才看得见。在一台机器上实测,**11 个项目的已安装标准里共约 278 个替换字符**。损害集中在 `requirement-checklist.md`、`requirement-template.md`、`requirement-document-template.md` 与 locale 包——也就是发布包不出货的那些文件(`files` 不含 `templates/` 与 `extensions/`),它们只可能靠下载取得。**若你的标准里有 `�`,请在 6.2.8 以上跑 `uds update --force`——重新下载的内容是正确的,会覆盖掉它们。**
|
|
49
|
+
|
|
20
50
|
## [6.2.7] - 2026-07-31
|
|
21
51
|
|
|
22
52
|
### 修复
|
|
@@ -15,7 +15,7 @@ status: current
|
|
|
15
15
|
|
|
16
16
|
> **语言**: [English](../../README.md) | [繁體中文](../zh-TW/README.md) | 简体中文
|
|
17
17
|
|
|
18
|
-
**版本**: 6.
|
|
18
|
+
**版本**: 6.3.1 | **发布日期**: 2026-07-31 | **授权**: [双重授权](../../LICENSE) (CC BY 4.0 + MIT)
|
|
19
19
|
|
|
20
20
|
语言无关、框架无关的软件项目文档标准。通过 AI 原生工作流,确保不同技术栈之间的一致性、质量和可维护性。
|
|
21
21
|
|
|
@@ -8,7 +8,7 @@ status: current
|
|
|
8
8
|
|
|
9
9
|
# 供应链安全标准
|
|
10
10
|
|
|
11
|
-
> 版本: 1.
|
|
11
|
+
> 版本: 1.1.0 | 最后更新: 2026-08-04
|
|
12
12
|
|
|
13
13
|
## 概述
|
|
14
14
|
|
|
@@ -70,6 +70,40 @@ status: current
|
|
|
70
70
|
| **Patch** (x.y.Z) | CI 通过后自动合并 | 全自动 |
|
|
71
71
|
| **Minor** (x.Y.0) | 自动创建 PR,人工审查 | 半自动 |
|
|
72
72
|
| **Major** (X.0.0) | 人工评估,迁移计划 | 手动 |
|
|
73
|
+
| **锁定策略** | 提交 lock 文件——**若你发布软件包,另须验证消费端会解析到什么**(见下) | lock 文件进版本控制;发版时检查消费端解析 |
|
|
74
|
+
|
|
75
|
+
### lock 文件约束不了你的用户
|
|
76
|
+
|
|
77
|
+
提交 lock 文件让**你自己的**构建可重现。它到不了任何安装你软件包的人:发布出去的 npm
|
|
78
|
+
tarball、PyPI wheel、Ruby gem 都不带 lock 文件。消费端是**在他们安装的当下、自行解析你
|
|
79
|
+
声明的范围**,拿到当时满足该范围的任何版本。
|
|
80
|
+
|
|
81
|
+
**两者不同时,你的测试套件会对着一个没有人会安装的组合亮绿灯——而那个绿灯与真绿灯
|
|
82
|
+
无从分辨。**
|
|
83
|
+
|
|
84
|
+
这是观察到的失效,不是假设的:某项目声明 `"tree-sitter-c-sharp": "^0.23.1"`,该区间有
|
|
85
|
+
三个已发布版本而它们的 API 不同。npm 把 caret 解析到最新匹配版本,于是每一次全新安装
|
|
86
|
+
都拿到不兼容的那个,**任何从 registry 安装的人的 C# 文件从未解析成功过**——而 lock 文件
|
|
87
|
+
锁着能用的版本、每个测试都通过,横跨该功能的整个生命周期。该项目的源码注释甚至早就记录了
|
|
88
|
+
那个不兼容版本;caret 悄悄把它又放了回来。
|
|
89
|
+
|
|
90
|
+
因此**会发布软件包**的项目另须:
|
|
91
|
+
|
|
92
|
+
- **知道消费端安装会解析到什么。** 逐一比对每个 runtime 依赖的声明范围、锁定版本,以及
|
|
93
|
+
该范围今天会解析到的版本。**只报告差异**——一份大多一致的表格会被略过,而其中真正
|
|
94
|
+
有问题的那几行也跟着被略过。
|
|
95
|
+
- **原生依赖一律声明为精确版本。** 带原生绑定的软件包(有 install 家族脚本,或依赖
|
|
96
|
+
node-gyp/prebuild/node-addon-api/cmake-js 及同类者)**不得**以范围声明。semver 对
|
|
97
|
+
原生 ABI 兼容性没有任何承诺,而这在实践中已被在 minor 范围内打破过。**不论它今天是否
|
|
98
|
+
正在漂移都要标出**:一个只对应到单一已发布版本的范围,安全是因为上游还没再发布,
|
|
99
|
+
不是因为有任何保障。
|
|
100
|
+
- **发版前以消费端解析跑一次测试套件。** 在没有 lock 文件的情况下安装,再对结果跑真正的
|
|
101
|
+
测试。这是本节唯一**不需要事先知道哪个依赖会坏**的检查。
|
|
102
|
+
- **查不到的东西记为未知,绝不记为一致。** 一个「没问题」与「我查不到」长得一样的检查,
|
|
103
|
+
会把未知转成安心。
|
|
104
|
+
|
|
105
|
+
> **适用于发布出去的产物,不适用于应用程序。** 一个部署出去的服务会连同 lock 文件一起
|
|
106
|
+
> 发布,因此它的构建与运行看到的是同一组版本。本节谈的是**别人会安装的软件包**。
|
|
73
107
|
|
|
74
108
|
## 快速参考卡
|
|
75
109
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
source: ../../CHANGELOG.md
|
|
3
|
-
source_version: 6.
|
|
4
|
-
translation_version: 6.
|
|
5
|
-
last_synced: 2026-
|
|
3
|
+
source_version: 6.3.1
|
|
4
|
+
translation_version: 6.3.1
|
|
5
|
+
last_synced: 2026-08-06
|
|
6
6
|
status: current
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -17,6 +17,36 @@ status: current
|
|
|
17
17
|
|
|
18
18
|
## [Unreleased]
|
|
19
19
|
|
|
20
|
+
## [6.3.1] - 2026-08-06
|
|
21
|
+
|
|
22
|
+
### 修復
|
|
23
|
+
|
|
24
|
+
- **6.3.0 從來沒有到達 npm——而這一版的存在,正是因為它自己描述的那個失效。** 發版流程有跑,它的 clean-room job 在 `npm ci` 這一步失敗,錯誤是 `EUSAGE … Missing: @emnapi/core@1.11.3 from lock file`,`Publish to npm` 被跳過。lock 檔是被 `npm install --save semver` 重新產生的,過程中掉了 `npm ci` 需要的傳遞相依項目;我在本機跑的檢查接受了它,所以這個不一致要到發版 job 才顯形——那時 tag 與 GitHub Release 都已經公開。改以最後一份 `npm ci` 確實能通過的 lock 檔為基礎重建,只加進 semver 那一筆,並在整個 CI 矩陣上驗證,而不是只在一台機器上。
|
|
25
|
+
- **`v6.3.0` 保留不刪,其 release 說明已改為註明它從未發布。** 一個沒有 npm 對應版本的 tag,正是 `uds deps` 被寫出來要抓的那種不一致;刪掉它移除的是證據,不是落差。**6.3.1 完整包含 6.3.0 的全部內容**,見下方。
|
|
26
|
+
|
|
27
|
+
## [6.3.0] - 2026-08-04
|
|
28
|
+
|
|
29
|
+
### 新增
|
|
30
|
+
|
|
31
|
+
- **`uds deps`——你測的東西,跟你使用者裝的是同一個嗎?** 發布出去的套件不帶 lockfile:你的 CI 測的是 `package-lock.json` 鎖定的版本,你的使用者拿到的是宣告範圍在他們安裝當下解析出的版本。兩者不同時,整套測試會對著一個沒有人會安裝的組合亮綠燈,而那個綠燈與真綠燈無從分辨。此指令逐一比對每個 runtime 相依的三個數字,**只回報差異**並附上分母——一份大多一致的表格會被略過,而其中真正有問題的那幾列也跟著被略過。
|
|
32
|
+
- **原生相依適用更嚴格的規則。** 帶原生綁定的套件只要以範圍宣告就會被標出,**不論它今天是否正在漂移**。semver 對原生 ABI 相容性沒有任何承諾,而這在本生態已被在 minor 範圍內打破過。一個只對應到單一已發布版本的範圍,安全是因為上游還沒再發布,不是因為有任何保障——等漂移,等於等到使用者已經拿到為止。
|
|
33
|
+
- **查詢失敗絕不記為一致。** 它會成為 `unverifiable` 並使整次檢查失敗。一個「沒問題」與「我查不到」長得一樣的檢查,會把未知轉成安心。
|
|
34
|
+
- `--path`、`--json`、`--concurrency`。
|
|
35
|
+
|
|
36
|
+
### 變更
|
|
37
|
+
|
|
38
|
+
- **`supply-chain-security-standards` 1.0.0 → 1.1.0——Lock Strategy 條目是對的,但不完整。** 它寫「使用 lock 檔,一律進版控」,讀起來是完整的,因此照著做的人沒有任何理由再往下查——而一份提交的 lock 檔約束的是**你自己的**建置,碰不到你任何一個使用者。回頭改寫本文而非加但書,因為在一條未變動的規則下方補「但請注意……」,會讓原文那一行繼續誤導只讀那一行的人,而標準表格多半就是一行一行讀的。新章節以產生它的那個案例陳述失效、對會發布套件的專案給出四項要求,並明確限定於發布出去的產物——部署的服務會連同 lock 檔一起出貨,不受影響。
|
|
39
|
+
|
|
40
|
+
### 備註
|
|
41
|
+
|
|
42
|
+
- 該標準的 `.ai.yaml` 仍是五行的殼、沒有任何機器可讀規則——**141 份中的四份之一**,另含 `design-document-standards`、`estimation-standards` 與 `privacy-standards`,因此讀 `ai/` 層的 agent 對這四份得到的都是空的。本次發版刻意沒有把新增的那一條規則加進去:一條規則躺在一個原本全空的檔案裡,會讓覆蓋率看起來比實際好。該缺口現已記在檔案內部。
|
|
43
|
+
|
|
44
|
+
## [6.2.8] - 2026-07-31
|
|
45
|
+
|
|
46
|
+
### 修復
|
|
47
|
+
|
|
48
|
+
- **下載回來的標準,中文是壞的。** HTTPS 回應以 `data += chunk` 累積,而那會對每一個分塊各自解碼——於是任何位元組跨在分塊邊界上的字元都變成替換字元(`日期` → `日�期`)。單位元組的拉丁文不受影響;三位元組的中日韓文字受影響。**過程中沒有任何一步失敗**:傳輸完成、檔案寫入、動作回報成功,損壞只有讀文字才看得見。在一台機器上實測,**11 個專案的已安裝標準裡共約 278 個替換字元**。損害集中在 `requirement-checklist.md`、`requirement-template.md`、`requirement-document-template.md` 與 locale 包——也就是發布包不出貨的那些檔(`files` 不含 `templates/` 與 `extensions/`),它們只可能靠下載取得。**若你的標準裡有 `�`,請在 6.2.8 以上跑 `uds update --force`——重新下載的內容是正確的,會覆蓋掉它們。**
|
|
49
|
+
|
|
20
50
|
## [6.2.7] - 2026-07-31
|
|
21
51
|
|
|
22
52
|
### 修復
|
|
@@ -15,7 +15,7 @@ status: current
|
|
|
15
15
|
|
|
16
16
|
> **語言**: [English](../../README.md) | 繁體中文 | [简体中文](../zh-CN/README.md)
|
|
17
17
|
|
|
18
|
-
**版本**: 6.
|
|
18
|
+
**版本**: 6.3.1 | **發布日期**: 2026-07-31 | **授權**: [雙重授權](../../LICENSE) (CC BY 4.0 + MIT)
|
|
19
19
|
|
|
20
20
|
語言無關、框架無關的軟體專案文件標準。透過 AI 原生工作流,確保不同技術堆疊之間的一致性、品質和可維護性。
|
|
21
21
|
|
|
@@ -8,7 +8,7 @@ status: current
|
|
|
8
8
|
|
|
9
9
|
# 供應鏈安全標準
|
|
10
10
|
|
|
11
|
-
> 版本: 1.
|
|
11
|
+
> 版本: 1.1.0 | 最後更新: 2026-08-04
|
|
12
12
|
|
|
13
13
|
## 概述
|
|
14
14
|
|
|
@@ -70,6 +70,40 @@ status: current
|
|
|
70
70
|
| **Patch** (x.y.Z) | CI 通過後自動合併 | 全自動 |
|
|
71
71
|
| **Minor** (x.Y.0) | 自動建立 PR,人工審查 | 半自動 |
|
|
72
72
|
| **Major** (X.0.0) | 人工評估,遷移計畫 | 手動 |
|
|
73
|
+
| **鎖定策略** | 提交 lock 檔——**若你發布套件,另須驗證消費端會解析到什麼**(見下) | lock 檔進版控;發版時檢查消費端解析 |
|
|
74
|
+
|
|
75
|
+
### lock 檔約束不了你的使用者
|
|
76
|
+
|
|
77
|
+
提交 lock 檔讓**你自己的**建置可重現。它到不了任何安裝你套件的人:發布出去的 npm
|
|
78
|
+
tarball、PyPI wheel、Ruby gem 都不帶 lock 檔。消費端是**在他們安裝的當下、自行解析你
|
|
79
|
+
宣告的範圍**,拿到當時滿足該範圍的任何版本。
|
|
80
|
+
|
|
81
|
+
**兩者不同時,你的測試套件會對著一個沒有人會安裝的組合亮綠燈——而那個綠燈與真綠燈
|
|
82
|
+
無從分辨。**
|
|
83
|
+
|
|
84
|
+
這是觀察到的失效,不是假設的:某專案宣告 `"tree-sitter-c-sharp": "^0.23.1"`,該區間有
|
|
85
|
+
三個已發布版本而它們的 API 不同。npm 把 caret 解析到最新相符版本,於是每一次全新安裝
|
|
86
|
+
都拿到不相容的那個,**任何從 registry 安裝的人的 C# 檔從未解析成功過**——而 lock 檔鎖著
|
|
87
|
+
能用的版本、每個測試都通過,橫跨該功能的整個生命期。該專案的原始碼註解甚至早就記載了
|
|
88
|
+
那個不相容版本;caret 悄悄把它又放了回來。
|
|
89
|
+
|
|
90
|
+
因此**會發布套件**的專案另須:
|
|
91
|
+
|
|
92
|
+
- **知道消費端安裝會解析到什麼。** 逐一比對每個 runtime 相依的宣告範圍、鎖定版本、以及
|
|
93
|
+
該範圍今天會解析到的版本。**只回報差異**——一份大多一致的表格會被略過,而其中真正
|
|
94
|
+
有問題的那幾列也跟著被略過。
|
|
95
|
+
- **原生相依一律宣告為精確版本。** 帶原生綁定的套件(有 install 家族腳本,或相依
|
|
96
|
+
node-gyp/prebuild/node-addon-api/cmake-js 及同類者)**不得**以範圍宣告。semver 對
|
|
97
|
+
原生 ABI 相容性沒有任何承諾,而這在實務上已被在 minor 範圍內打破過。**不論它今天是否
|
|
98
|
+
正在漂移都要標出**:一個只對應到單一已發布版本的範圍,安全是因為上游還沒再發布,
|
|
99
|
+
不是因為有任何保障。
|
|
100
|
+
- **發版前以消費端解析跑一次測試套件。** 在沒有 lock 檔的情況下安裝,再對結果跑真正的
|
|
101
|
+
測試。這是本節唯一**不需要事先知道哪個相依會壞**的檢查。
|
|
102
|
+
- **查不到的東西記為未知,絕不記為一致。** 一個「沒問題」與「我查不到」長得一樣的檢查,
|
|
103
|
+
會把未知轉成安心。
|
|
104
|
+
|
|
105
|
+
> **適用於發布出去的產物,不適用於應用程式。** 一個部署出去的服務會連同 lock 檔一起
|
|
106
|
+
> 出貨,因此它的建置與執行看到的是同一組版本。本節談的是**別人會安裝的套件**。
|
|
73
107
|
|
|
74
108
|
## 快速參考卡
|
|
75
109
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-dev-standards",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.1",
|
|
4
4
|
"description": "CLI tool for adopting Universal Development Standards",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"documentation",
|
|
@@ -63,7 +63,8 @@
|
|
|
63
63
|
"chalk": "^5.3.0",
|
|
64
64
|
"commander": "^15.0.0",
|
|
65
65
|
"js-yaml": "^5.2.1",
|
|
66
|
-
"ora": "^9.4.0"
|
|
66
|
+
"ora": "^9.4.0",
|
|
67
|
+
"semver": "^7.8.5"
|
|
67
68
|
},
|
|
68
69
|
"devDependencies": {
|
|
69
70
|
"@eslint/js": "^10.0.1",
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `uds deps` — does what you test match what your users install?
|
|
3
|
+
* // implements XSPEC-366 R1
|
|
4
|
+
*
|
|
5
|
+
* A published npm package does not carry its lockfile. This compares, per
|
|
6
|
+
* runtime dependency, the declared range, the version the lockfile pins, and
|
|
7
|
+
* the version a consumer's install would actually resolve to.
|
|
8
|
+
*
|
|
9
|
+
* See `utils/dependency-resolution.js` for why resolution goes through
|
|
10
|
+
* `npm view` and why a failed lookup is never reported as agreement.
|
|
11
|
+
*
|
|
12
|
+
* @module commands/deps
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import { measureResolutionDrift } from '../utils/dependency-resolution.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Render the human-readable report.
|
|
20
|
+
*
|
|
21
|
+
* **Only the delta is listed.** A table of every dependency, mostly agreeing,
|
|
22
|
+
* is skimmed and then ignored — and the two rows that mattered go with it. The
|
|
23
|
+
* denominator is still printed, because "no drift" and "nothing was checked"
|
|
24
|
+
* produce the same silence otherwise, and only one of them is good news.
|
|
25
|
+
*/
|
|
26
|
+
function render(result) {
|
|
27
|
+
const lines = [];
|
|
28
|
+
const label = result.packageName ? chalk.bold(result.packageName) : result.root;
|
|
29
|
+
|
|
30
|
+
lines.push('');
|
|
31
|
+
lines.push(`${label} — ${result.examined} runtime dependenc${result.examined === 1 ? 'y' : 'ies'} checked`);
|
|
32
|
+
|
|
33
|
+
if (!result.hasLockfile) {
|
|
34
|
+
lines.push(chalk.yellow(' no package-lock.json — nothing to compare the registry against'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (result.drifted.length > 0) {
|
|
38
|
+
lines.push('');
|
|
39
|
+
lines.push(chalk.yellow(` ${result.drifted.length} shipped ≠ tested:`));
|
|
40
|
+
for (const d of result.drifted) {
|
|
41
|
+
lines.push(
|
|
42
|
+
` ${chalk.bold(d.name)} ${chalk.dim(d.range)}` +
|
|
43
|
+
` tested=${chalk.cyan(d.locked)} users get=${chalk.yellow(d.resolved)}`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push(chalk.dim(' Your lockfile pins the tested column; consumers resolve the range'));
|
|
48
|
+
lines.push(chalk.dim(' themselves, because a published package does not ship a lockfile.'));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (result.unpinnedNative.length > 0) {
|
|
52
|
+
lines.push('');
|
|
53
|
+
lines.push(chalk.yellow(` ${result.unpinnedNative.length} native dependenc${result.unpinnedNative.length === 1 ? 'y is' : 'ies are'} behind a version range:`));
|
|
54
|
+
for (const n of result.unpinnedNative) {
|
|
55
|
+
lines.push(
|
|
56
|
+
` ${chalk.bold(n.name)} ${chalk.dim(n.range)}` +
|
|
57
|
+
` ${chalk.dim('(' + n.native.reasons.join('; ') + ')')}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
lines.push('');
|
|
61
|
+
lines.push(chalk.dim(' Flagged whether or not they are drifting today. semver makes no'));
|
|
62
|
+
lines.push(chalk.dim(' promise about native ABI compatibility, and this ecosystem has'));
|
|
63
|
+
lines.push(chalk.dim(' broken it inside a minor range. A range that matches one published'));
|
|
64
|
+
lines.push(chalk.dim(' version is safe because upstream has not published again — waiting'));
|
|
65
|
+
lines.push(chalk.dim(' for drift means waiting until your users already have it.'));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (result.unverifiable.length > 0) {
|
|
69
|
+
lines.push('');
|
|
70
|
+
lines.push(chalk.red(` ${result.unverifiable.length} could not be checked:`));
|
|
71
|
+
for (const u of result.unverifiable) {
|
|
72
|
+
const why = u.error ?? 'not present in package-lock.json';
|
|
73
|
+
lines.push(` ${chalk.bold(u.name)} ${chalk.dim(u.range)} ${chalk.red(why)}`);
|
|
74
|
+
}
|
|
75
|
+
lines.push('');
|
|
76
|
+
lines.push(chalk.dim(' These are unknowns, not agreements. Treating them as fine would'));
|
|
77
|
+
lines.push(chalk.dim(' turn "I could not find out" into "everything is fine".'));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (result.clean) {
|
|
81
|
+
lines.push(chalk.green(' ✓ every dependency resolves to the version you test against'));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
lines.push('');
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function depsCommand(options = {}) {
|
|
89
|
+
const root = options.path ?? process.cwd();
|
|
90
|
+
|
|
91
|
+
let result;
|
|
92
|
+
try {
|
|
93
|
+
result = await measureResolutionDrift(root, {
|
|
94
|
+
concurrency: options.concurrency ? Number(options.concurrency) : undefined,
|
|
95
|
+
});
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error(chalk.red(`uds deps: ${err.message}`));
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (options.json) {
|
|
103
|
+
console.log(JSON.stringify(result, null, 2));
|
|
104
|
+
} else {
|
|
105
|
+
console.log(render(result));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Drift and unverifiable both fail. Drift is a real divergence; unverifiable
|
|
109
|
+
// is an unknown, and a check that exits 0 on "I don't know" is a check that
|
|
110
|
+
// reports success for the case it was built to catch.
|
|
111
|
+
if (!result.clean) process.exitCode = 1;
|
|
112
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shipped dependency resolution integrity. // implements XSPEC-366 R1
|
|
3
|
+
*
|
|
4
|
+
* **The problem this measures.** A published npm package does not carry its
|
|
5
|
+
* lockfile. Your CI tests the versions `package-lock.json` pins; your users get
|
|
6
|
+
* whatever the declared ranges resolve to at their install time. When those two
|
|
7
|
+
* differ, the entire test suite is green about a combination nobody installs —
|
|
8
|
+
* and that green is indistinguishable from a real one.
|
|
9
|
+
*
|
|
10
|
+
* This is not hypothetical. `engramgraph` declared
|
|
11
|
+
* `"tree-sitter-c-sharp": "^0.23.1"`. Three published versions satisfy that
|
|
12
|
+
* range and they do not share an API: 0.23.1 exports `nodeTypeInfo`, 0.23.5
|
|
13
|
+
* does not. npm resolves a caret to the newest match, so every fresh install
|
|
14
|
+
* received the incompatible one and **no C# file ever parsed for anyone who
|
|
15
|
+
* installed from npm** — while the lockfile pinned the working version and
|
|
16
|
+
* every test passed. See XSPEC-365 / XSPEC-366.
|
|
17
|
+
*
|
|
18
|
+
* ## Three design choices worth knowing
|
|
19
|
+
*
|
|
20
|
+
* **Only `dependencies` and `optionalDependencies` are examined.**
|
|
21
|
+
* `devDependencies` are not installed by consumers, so a drift there cannot
|
|
22
|
+
* reach them.
|
|
23
|
+
*
|
|
24
|
+
* **Resolution goes through `npm view`, not a hand-rolled registry fetch.**
|
|
25
|
+
* The question being asked is "what would resolve *in this environment*", and
|
|
26
|
+
* `npm view` honours the local npm configuration — private registries, scoped
|
|
27
|
+
* registries, auth, proxies. A direct fetch of registry.npmjs.org would
|
|
28
|
+
* silently answer a different question for anyone who does not install from
|
|
29
|
+
* the public registry, and would answer it confidently.
|
|
30
|
+
*
|
|
31
|
+
* **A registry lookup that fails is never folded into "consistent".** It
|
|
32
|
+
* becomes `unverifiable` and makes the whole check non-zero. A tool whose
|
|
33
|
+
* "everything is fine" and "I could not find out" look the same is worse than
|
|
34
|
+
* no tool: it converts an unknown into a reassurance.
|
|
35
|
+
*
|
|
36
|
+
* ## Native dependencies are held to a stricter rule (XSPEC-366 R2)
|
|
37
|
+
*
|
|
38
|
+
* A package with a native binding is flagged when its declared version is a
|
|
39
|
+
* range rather than an exact version — **even when it is not currently
|
|
40
|
+
* drifting**. That is not general dependency hygiene, it is a response to
|
|
41
|
+
* measured behaviour: the tree-sitter ecosystem has broken ABI inside a minor
|
|
42
|
+
* range, and semver makes no promise about native ABI compatibility.
|
|
43
|
+
*
|
|
44
|
+
* The distinction matters because "not drifting" is a fact about today. Four
|
|
45
|
+
* tree-sitter ranges in one AsiaOstrich project currently match exactly one
|
|
46
|
+
* published version each, so nothing drifts — while the range that caused the
|
|
47
|
+
* incident next door matched two. They are safe because upstream has not
|
|
48
|
+
* published again, not because anything guarantees it, and waiting for drift
|
|
49
|
+
* means waiting until users already have it.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import { spawn } from 'node:child_process';
|
|
53
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
54
|
+
import { join } from 'node:path';
|
|
55
|
+
import semver from 'semver';
|
|
56
|
+
|
|
57
|
+
/** How many registry lookups to run at once. */
|
|
58
|
+
const DEFAULT_CONCURRENCY = 8;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Run `npm view <name>@<range> version --json` and return the highest version
|
|
62
|
+
* that satisfies the range.
|
|
63
|
+
*
|
|
64
|
+
* npm prints a bare JSON string when exactly one version matches and a JSON
|
|
65
|
+
* array when several do. The array is in publish order, **not** semver order —
|
|
66
|
+
* a backport released after a major bump appears last while being the lowest
|
|
67
|
+
* version — so the maximum is computed with semver rather than by taking the
|
|
68
|
+
* final element. Guessing here would produce a confident wrong answer, which
|
|
69
|
+
* is the failure mode this whole module exists to detect.
|
|
70
|
+
*/
|
|
71
|
+
async function resolveViaNpm(name, range, run) {
|
|
72
|
+
const { code, stdout, stderr } = await run(['view', `${name}@${range}`, 'version', '--json']);
|
|
73
|
+
|
|
74
|
+
if (code !== 0) {
|
|
75
|
+
const detail = (stderr || stdout || '').split('\n').find((l) => l.trim()) ?? '';
|
|
76
|
+
throw new Error(detail.trim() || `npm view exited ${code}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(stdout);
|
|
82
|
+
} catch {
|
|
83
|
+
throw new Error(`npm view returned output that is not JSON: ${stdout.slice(0, 120)}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (typeof parsed === 'string') return parsed;
|
|
87
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
88
|
+
const max = semver.maxSatisfying(parsed, range);
|
|
89
|
+
if (max) return max;
|
|
90
|
+
throw new Error(`no version in [${parsed.join(', ')}] satisfies ${range}`);
|
|
91
|
+
}
|
|
92
|
+
throw new Error('npm view returned no version');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Signals that a package carries a native binding, in the package's own
|
|
97
|
+
* manifest. Both are needed: `better-sqlite3@12.8.0` declares an `install`
|
|
98
|
+
* script *and* gyp-family dependencies, but its own 13.x dropped the install
|
|
99
|
+
* script while remaining native — a detector using only that signal would
|
|
100
|
+
* silently stop flagging it.
|
|
101
|
+
*/
|
|
102
|
+
const INSTALL_SCRIPTS = ['preinstall', 'install', 'postinstall'];
|
|
103
|
+
const NATIVE_BUILD_DEPS = /node-gyp|prebuild|node-addon-api|cmake-js|^bindings$/;
|
|
104
|
+
|
|
105
|
+
/** A declared version with no range operator at all. */
|
|
106
|
+
function isExactVersion(range) {
|
|
107
|
+
return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(range.trim());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Fetch the manifest of one exact version and decide whether it is native.
|
|
112
|
+
*
|
|
113
|
+
* **The version is always specified.** `npm view <name> scripts` answers about
|
|
114
|
+
* the *latest* published version, not the one being examined — measured on
|
|
115
|
+
* 2026-08-04: `better-sqlite3` at latest reports no install script while the
|
|
116
|
+
* 12.8.0 actually in use declares one. Querying the bare name would produce a
|
|
117
|
+
* confident answer about a different package version, which is the exact class
|
|
118
|
+
* of error this whole module exists to detect.
|
|
119
|
+
*
|
|
120
|
+
* The full manifest is requested rather than named fields, because npm
|
|
121
|
+
* unwraps the object when only one requested field exists — asking for
|
|
122
|
+
* `scripts dependencies` on a package with no dependencies returns the scripts
|
|
123
|
+
* map itself, with no `scripts` key to read it from.
|
|
124
|
+
*/
|
|
125
|
+
async function classifyNative(name, version, run) {
|
|
126
|
+
const { code, stdout, stderr } = await run(['view', `${name}@${version}`, '--json']);
|
|
127
|
+
if (code !== 0) {
|
|
128
|
+
const detail = (stderr || stdout || '').split('\n').find((l) => l.trim()) ?? '';
|
|
129
|
+
throw new Error(detail.trim() || `npm view exited ${code}`);
|
|
130
|
+
}
|
|
131
|
+
const manifest = JSON.parse(stdout);
|
|
132
|
+
const scripts = manifest.scripts ?? {};
|
|
133
|
+
const deps = { ...(manifest.dependencies ?? {}) };
|
|
134
|
+
|
|
135
|
+
const reasons = [];
|
|
136
|
+
const hookNames = INSTALL_SCRIPTS.filter((k) => typeof scripts[k] === 'string');
|
|
137
|
+
if (hookNames.length > 0) reasons.push(`${hookNames.join('/')} script`);
|
|
138
|
+
const buildDeps = Object.keys(deps).filter((d) => NATIVE_BUILD_DEPS.test(d));
|
|
139
|
+
if (buildDeps.length > 0) reasons.push(`depends on ${buildDeps.join(', ')}`);
|
|
140
|
+
|
|
141
|
+
return { native: reasons.length > 0, reasons };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Default runner: spawn npm and collect its output. */
|
|
145
|
+
function spawnNpm(cwd) {
|
|
146
|
+
return (args) =>
|
|
147
|
+
new Promise((resolve) => {
|
|
148
|
+
const child = spawn('npm', args, { cwd, shell: process.platform === 'win32' });
|
|
149
|
+
let stdout = '';
|
|
150
|
+
let stderr = '';
|
|
151
|
+
child.stdout.on('data', (d) => (stdout += d));
|
|
152
|
+
child.stderr.on('data', (d) => (stderr += d));
|
|
153
|
+
child.on('error', (err) => resolve({ code: -1, stdout: '', stderr: err.message }));
|
|
154
|
+
child.on('close', (code) => resolve({ code, stdout, stderr }));
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Map over `items` with a bounded number of concurrent workers. */
|
|
159
|
+
async function mapWithConcurrency(items, limit, worker) {
|
|
160
|
+
const results = new Array(items.length);
|
|
161
|
+
let next = 0;
|
|
162
|
+
const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
163
|
+
for (;;) {
|
|
164
|
+
const index = next++;
|
|
165
|
+
if (index >= items.length) return;
|
|
166
|
+
results[index] = await worker(items[index], index);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
await Promise.all(runners);
|
|
170
|
+
return results;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Compare, for every runtime dependency of the package at `root`: the declared
|
|
175
|
+
* range, the version the lockfile pins, and the version a consumer's install
|
|
176
|
+
* would resolve to.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} root Directory containing package.json.
|
|
179
|
+
* @param {object} [options]
|
|
180
|
+
* @param {number} [options.concurrency]
|
|
181
|
+
* @param {(args: string[]) => Promise<{code: number, stdout: string, stderr: string}>} [options.run]
|
|
182
|
+
* Injected for tests, so the unit tests do not depend on the network — the
|
|
183
|
+
* thing being tested is the comparison and the failure handling, not npm.
|
|
184
|
+
* @returns {Promise<object>} measurement result; see the shape below.
|
|
185
|
+
*/
|
|
186
|
+
export async function measureResolutionDrift(root, options = {}) {
|
|
187
|
+
const pkgPath = join(root, 'package.json');
|
|
188
|
+
if (!existsSync(pkgPath)) {
|
|
189
|
+
throw new Error(`no package.json at ${root}`);
|
|
190
|
+
}
|
|
191
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
192
|
+
|
|
193
|
+
const lockPath = join(root, 'package-lock.json');
|
|
194
|
+
const lock = existsSync(lockPath) ? JSON.parse(readFileSync(lockPath, 'utf8')) : null;
|
|
195
|
+
|
|
196
|
+
const declared = [
|
|
197
|
+
...Object.entries(pkg.dependencies ?? {}).map(([name, range]) => ({ name, range, kind: 'dependencies' })),
|
|
198
|
+
...Object.entries(pkg.optionalDependencies ?? {}).map(([name, range]) => ({ name, range, kind: 'optionalDependencies' })),
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const run = options.run ?? spawnNpm(root);
|
|
202
|
+
const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
|
|
203
|
+
|
|
204
|
+
const rows = await mapWithConcurrency(declared, concurrency, async (dep) => {
|
|
205
|
+
const locked = lock?.packages?.[`node_modules/${dep.name}`]?.version ?? null;
|
|
206
|
+
let resolved;
|
|
207
|
+
try {
|
|
208
|
+
resolved = await resolveViaNpm(dep.name, dep.range, run);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
return { ...dep, locked, resolved: null, native: null, error: err.message };
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const native = await classifyNative(dep.name, resolved, run);
|
|
214
|
+
return { ...dep, locked, resolved, native, error: null };
|
|
215
|
+
} catch (err) {
|
|
216
|
+
// The version resolved but the manifest did not. We cannot say whether
|
|
217
|
+
// this one needs pinning, and saying nothing would read as "it doesn't".
|
|
218
|
+
return { ...dep, locked, resolved, native: null, error: `could not classify: ${err.message}` };
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// A dependency with no lockfile entry is not "consistent" — it is unknown.
|
|
223
|
+
// Reporting it as fine because there was nothing to compare against would be
|
|
224
|
+
// the same class of mistake as reporting a failed lookup as fine.
|
|
225
|
+
const unverifiable = rows.filter((r) => r.error !== null || r.locked === null);
|
|
226
|
+
const drifted = rows.filter((r) => r.error === null && r.locked !== null && r.locked !== r.resolved);
|
|
227
|
+
|
|
228
|
+
// XSPEC-366 R2. Independent of drift on purpose: a native dependency behind
|
|
229
|
+
// a range is exposed whether or not upstream has published into it yet.
|
|
230
|
+
const unpinnedNative = rows.filter(
|
|
231
|
+
(r) => r.native?.native === true && !isExactVersion(r.range)
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const consistent = rows.length - unverifiable.length - drifted.length;
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
root,
|
|
238
|
+
packageName: pkg.name ?? null,
|
|
239
|
+
hasLockfile: lock !== null,
|
|
240
|
+
examined: rows.length,
|
|
241
|
+
consistent,
|
|
242
|
+
drifted,
|
|
243
|
+
unverifiable,
|
|
244
|
+
unpinnedNative,
|
|
245
|
+
/** True when every dependency was checked, agreed, and needs no pinning. */
|
|
246
|
+
clean: drifted.length === 0 && unverifiable.length === 0 && unpinnedNative.length === 0,
|
|
247
|
+
};
|
|
248
|
+
}
|
package/src/utils/github.js
CHANGED
|
@@ -90,13 +90,32 @@ function httpGet(url) {
|
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
// Collect Buffers and decode once at the end. `data += chunk` decoded each
|
|
94
|
+
// chunk on its own, so any character whose bytes straddled a chunk boundary
|
|
95
|
+
// became U+FFFD on both sides of the split. Latin text survived because its
|
|
96
|
+
// characters are one byte; Chinese standards are three bytes each and did
|
|
97
|
+
// not. `uds update --force` on one project downloaded four files and wrote
|
|
98
|
+
// 30 replacement characters into them — `日期` → `日�期`, `結構` → `結�構`
|
|
99
|
+
// — and reported every action as succeeded, because as far as the CLI was
|
|
100
|
+
// concerned the transfer completed and the file was written. (XSPEC-343)
|
|
101
|
+
const chunks = [];
|
|
102
|
+
res.on('data', chunk => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
103
|
+
res.on('end', () => {
|
|
104
|
+
// try/catch because a throw inside an event handler does not reject the
|
|
105
|
+
// enclosing promise — it escapes, and the caller's await hangs forever.
|
|
106
|
+
// The first draft of this fix passed strings to Buffer.concat and the
|
|
107
|
+
// test run simply stopped, with no error to read. A hang is a worse
|
|
108
|
+
// failure than an exception: there is nothing to grep for.
|
|
109
|
+
try {
|
|
110
|
+
resolve({
|
|
111
|
+
data: Buffer.concat(chunks).toString('utf8'),
|
|
112
|
+
statusCode: res.statusCode,
|
|
113
|
+
headers: res.headers
|
|
114
|
+
});
|
|
115
|
+
} catch (err) {
|
|
116
|
+
reject(err);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
100
119
|
res.on('error', reject);
|
|
101
120
|
}).on('error', reject);
|
|
102
121
|
});
|
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.
|
|
3
|
+
"version": "6.3.1",
|
|
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.
|
|
61
|
+
"version": "6.3.1"
|
|
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.
|
|
68
|
+
"version": "6.3.1",
|
|
69
69
|
"note": "Skills are now included in the main repository under skills/"
|
|
70
70
|
}
|
|
71
71
|
},
|
|
@@ -2236,7 +2236,7 @@
|
|
|
2236
2236
|
"id": "license-compliance",
|
|
2237
2237
|
"name": "License Compliance Standards",
|
|
2238
2238
|
"nameZh": "授權合規標準",
|
|
2239
|
-
"version": "6.
|
|
2239
|
+
"version": "6.3.1",
|
|
2240
2240
|
"source": {
|
|
2241
2241
|
"human": "core/license-compliance.md",
|
|
2242
2242
|
"ai": "ai/standards/license-compliance.ai.yaml"
|
|
@@ -2248,7 +2248,7 @@
|
|
|
2248
2248
|
"id": "verification-oracle",
|
|
2249
2249
|
"name": "Verification Oracle Standards",
|
|
2250
2250
|
"nameZh": "驗證 Oracle 標準",
|
|
2251
|
-
"version": "6.
|
|
2251
|
+
"version": "6.3.1",
|
|
2252
2252
|
"source": {
|
|
2253
2253
|
"human": "core/verification-oracle.md",
|
|
2254
2254
|
"ai": "ai/standards/verification-oracle.ai.yaml"
|
|
@@ -2260,7 +2260,7 @@
|
|
|
2260
2260
|
"id": "model-provenance",
|
|
2261
2261
|
"name": "Model Provenance Policy Standards",
|
|
2262
2262
|
"nameZh": "模型來源政策標準",
|
|
2263
|
-
"version": "6.
|
|
2263
|
+
"version": "6.3.1",
|
|
2264
2264
|
"source": {
|
|
2265
2265
|
"human": "core/model-provenance.md",
|
|
2266
2266
|
"ai": "ai/standards/model-provenance.ai.yaml"
|
|
@@ -2272,7 +2272,7 @@
|
|
|
2272
2272
|
"id": "resource-cost-boundary",
|
|
2273
2273
|
"name": "Resource / Cost Boundary Declaration Standards",
|
|
2274
2274
|
"nameZh": "資源/成本邊界宣告標準",
|
|
2275
|
-
"version": "6.
|
|
2275
|
+
"version": "6.3.1",
|
|
2276
2276
|
"source": {
|
|
2277
2277
|
"human": "core/resource-cost-boundary.md",
|
|
2278
2278
|
"ai": "ai/standards/resource-cost-boundary.ai.yaml"
|